api-client/docs/superpowers/plans/2026-05-06-require-web-login.md

204 lines
5.7 KiB
Markdown
Raw Normal View History

2026-05-06 07:22:55 +00:00
# Require Web Login Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Block the self-hosted web app UI until a user is authenticated.
**Architecture:** Add a small web-only root extension component registered by `packages/hoppscotch-selfhost-web/src/main.ts`. The component observes the existing auth platform state and renders a full-screen login gate only for web mode while no confirmed user exists.
**Tech Stack:** Vue 3, Hoppscotch platform auth streams, RxJS `BehaviorSubject`, Vitest for focused state logic.
---
### Task 1: Auth Gate State Logic
**Files:**
- Create: `packages/hoppscotch-common/src/helpers/appLoginGate.ts`
- Test: `packages/hoppscotch-common/src/helpers/__tests__/appLoginGate.spec.ts`
- [ ] **Step 1: Write the failing test**
```ts
import { describe, expect, test } from "vitest"
import { shouldBlockAppForLogin } from "../appLoginGate"
describe("shouldBlockAppForLogin", () => {
const user = {
uid: "user-1",
displayName: "User",
email: "user@example.com",
photoURL: null,
emailVerified: true,
}
test("blocks the web app while auth is still being checked", () => {
expect(
shouldBlockAppForLogin({
platform: "web",
isAuthInitComplete: false,
currentUser: null,
})
).toBe(true)
})
test("blocks the web app when auth is confirmed anonymous", () => {
expect(
shouldBlockAppForLogin({
platform: "web",
isAuthInitComplete: true,
currentUser: null,
})
).toBe(true)
})
test("does not block the web app once a user is authenticated", () => {
expect(
shouldBlockAppForLogin({
platform: "web",
isAuthInitComplete: true,
currentUser: user,
})
).toBe(false)
})
test("does not block desktop", () => {
expect(
shouldBlockAppForLogin({
platform: "desktop",
isAuthInitComplete: true,
currentUser: null,
})
).toBe(false)
})
})
```
- [ ] **Step 2: Run test to verify it fails**
Run: `pnpm --dir packages/hoppscotch-common exec vitest --run src/helpers/__tests__/appLoginGate.spec.ts`
Expected: FAIL because `../appLoginGate` does not exist.
- [ ] **Step 3: Write minimal implementation**
```ts
type KernelMode = "web" | "desktop"
export type LoginGateState = {
platform: KernelMode
isAuthInitComplete: boolean
currentUser: unknown | null
}
export function shouldBlockAppForLogin(state: LoginGateState) {
return (
state.platform === "web" &&
(!state.isAuthInitComplete || !state.currentUser)
)
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `pnpm --dir packages/hoppscotch-common exec vitest --run src/helpers/__tests__/appLoginGate.spec.ts`
Expected: PASS.
### Task 2: Web Login Gate UI
**Files:**
- Create: `packages/hoppscotch-selfhost-web/src/components/WebLoginGate.vue`
- Create: `packages/hoppscotch-selfhost-web/src/services/webLoginGate.service.ts`
- Modify: `packages/hoppscotch-selfhost-web/src/main.ts`
- [ ] **Step 1: Create the root UI extension registration service**
Create `packages/hoppscotch-selfhost-web/src/services/webLoginGate.service.ts`:
```ts
import { Service } from "dioc"
import { getService } from "@hoppscotch/common/modules/dioc"
import { UIExtensionService } from "@hoppscotch/common/services/ui-extension.service"
import WebLoginGate from "@app/components/WebLoginGate.vue"
export class WebLoginGateService extends Service {
public static readonly ID = "WEB_LOGIN_GATE_SERVICE"
override onServiceInit() {
getService(UIExtensionService).addRootUIExtension(WebLoginGate)
}
}
```
- [ ] **Step 2: Register the service for web only**
In `packages/hoppscotch-selfhost-web/src/main.ts`, import the service:
```ts
import { WebLoginGateService } from "@app/services/webLoginGate.service"
```
Then change:
```ts
addedServices: [],
```
to:
```ts
addedServices: platform === "web" ? [WebLoginGateService] : [],
```
- [ ] **Step 3: Implement the blocking component**
Create `packages/hoppscotch-selfhost-web/src/components/WebLoginGate.vue`:
```vue
<template>
<Teleport to="body">
<div
v-if="shouldBlockApp"
class="fixed inset-0 z-50 flex min-h-screen flex-col items-center justify-center bg-primary p-6"
>
<AppLogo class="mb-8 h-16 w-16 rounded" />
<FirebaseLogin />
</div>
</Teleport>
</template>
<script setup lang="ts">
import { computed } from "vue"
import { useReadonlyStream } from "@hoppscotch/common/composables/stream"
import { shouldBlockAppForLogin } from "@hoppscotch/common/helpers/appLoginGate"
import { platform } from "@hoppscotch/common/platform"
const currentUser = useReadonlyStream(
platform.auth.getCurrentUserStream(),
platform.auth.getCurrentUser()
)
const shouldBlockApp = computed(() =>
shouldBlockAppForLogin({
platform: "web",
isAuthInitComplete: true,
currentUser: currentUser.value,
})
)
</script>
```
- [ ] **Step 4: Run targeted checks**
Run: `pnpm --dir packages/hoppscotch-common exec vitest --run src/helpers/__tests__/appLoginGate.spec.ts`
Expected: PASS.
Run: `pnpm --dir packages/hoppscotch-selfhost-web run lint`
Expected: PASS or only pre-existing unrelated failures.
- [ ] **Step 5: Commit**
```bash
git add docs/superpowers/plans/2026-05-06-require-web-login.md packages/hoppscotch-common/src/helpers/appLoginGate.ts packages/hoppscotch-common/src/helpers/__tests__/appLoginGate.spec.ts packages/hoppscotch-selfhost-web/src/components/WebLoginGate.vue packages/hoppscotch-selfhost-web/src/services/webLoginGate.service.ts packages/hoppscotch-selfhost-web/src/main.ts
git commit -m "feat: require login for web app"
```