204 lines
5.7 KiB
Markdown
204 lines
5.7 KiB
Markdown
|
|
# 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"
|
||
|
|
```
|