api-client/docs/superpowers/plans/2026-05-06-require-web-login.md
2026-05-06 09:22:55 +02:00

5.7 KiB

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

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
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:

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:

import { WebLoginGateService } from "@app/services/webLoginGate.service"

Then change:

addedServices: [],

to:

addedServices: platform === "web" ? [WebLoginGateService] : [],
  • Step 3: Implement the blocking component

Create packages/hoppscotch-selfhost-web/src/components/WebLoginGate.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
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"