diff --git a/src/chat-app/app.ts b/src/chat-app/app.ts index d4b2573..75db463 100644 --- a/src/chat-app/app.ts +++ b/src/chat-app/app.ts @@ -49,7 +49,8 @@ export async function createAppInstance() { }) // Chat has no public view — every non-login route requires auth. - installStrictAuthGuard(router) + // Chat is Nostr end-to-end — it cannot work without an identity. + installStrictAuthGuard(router, { requirePubkey: true }) const pinia = createPinia() diff --git a/src/lib/router-helpers.ts b/src/lib/router-helpers.ts index 84bec63..1b7d92a 100644 --- a/src/lib/router-helpers.ts +++ b/src/lib/router-helpers.ts @@ -14,12 +14,12 @@ import type { Router, RouteRecordRaw } from 'vue-router' * mismatch: guards register early but await this promise before reading * auth state. Phase 3 calls markAuthReady() once auth is initialized. */ -type AuthUserLike = { value: { pubkey?: string } | null } +type AuthUserLike = { value: { id?: string; pubkey?: string } | null } type AuthLike = { isAuthenticated: { value: boolean } // Populated after server-validated getCurrentUser() in auth.checkAuth(). - // Guards require BOTH isAuthenticated and a user with a pubkey — token - // presence alone is not enough (issue #36). + // Guards require BOTH isAuthenticated and a server-populated user — + // token presence alone is not enough (issue #36). currentUser: AuthUserLike } @@ -33,20 +33,39 @@ export function markAuthReady(auth: AuthLike): void { /** * Belt-and-suspenders auth check: token presence in localStorage isn't * sufficient — the server must have confirmed the token represents a real - * session, which is signalled by currentUser being populated with a pubkey. + * session, which is signalled by currentUser being populated. + * + * Keyed on `id`, never `pubkey`. Every account has an id; a Nostr pubkey + * is optional — Lightning-only accounts have none. Using pubkey as the + * "server answered" marker locked those accounts out of every + * strict-guard app: login succeeded, isAuthenticated flipped true, pubkey + * stayed empty, isFullyAuthed returned false, and the guard bounced them + * back to /login indefinitely. The #36 protection is unchanged — a bare + * token in localStorage still does not satisfy this. */ function isFullyAuthed(auth: AuthLike): boolean { - return auth.isAuthenticated.value && !!auth.currentUser?.value?.pubkey + return auth.isAuthenticated.value && !!auth.currentUser?.value?.id } /** * Strict guard — every non-/login route requires auth. - * Used by wallet, chat, libra (no public view). + * Used by wallet, chat, accounting (no public view). + * + * `requirePubkey` additionally demands a Nostr identity. Only chat sets + * it: chat is Nostr end-to-end and cannot function without a key. Wallet + * and accounting are payment/ledger surfaces and must stay reachable + * without one — per ADR-0001, LNbits is a liquidity service, not an + * identity provider, so core payment flows cannot be gated on identity. */ -export function installStrictAuthGuard(router: Router): void { +export function installStrictAuthGuard( + router: Router, + opts: { requirePubkey?: boolean } = {}, +): void { router.beforeEach(async (to) => { const auth = await authReady - const authed = isFullyAuthed(auth) + const authed = + isFullyAuthed(auth) && + (!opts.requirePubkey || !!auth.currentUser?.value?.pubkey) if (to.path === '/login') { return authed ? '/' : true }