From 7497e6b3e5d278970db328dd7d4ade79e6852c6c Mon Sep 17 00:00:00 2001 From: Padreug Date: Thu, 8 Oct 2026 20:43:47 +0200 Subject: [PATCH] fix(auth): don't require a Nostr pubkey to be considered logged in isFullyAuthed() keyed the "server confirmed this session" check on currentUser.pubkey. Every account has an id; a pubkey is optional, so Lightning-only accounts were locked out of every strict-guard app: login succeeded, isAuthenticated flipped true, pubkey stayed empty, isFullyAuthed returned false, and the guard redirected back to /login -- a loop with no way out. Observed on atio, where 9 of 58 accounts have a Nostr identity and the other 49 do not. The pubkey was only ever a proxy for "getCurrentUser() came back", added for issue #36 to stop a bare localStorage token counting as a session. Keying on id preserves that protection exactly and drops the incidental identity requirement. Also gates the genuine requirement where it belongs: chat is Nostr end-to-end and now opts in via installStrictAuthGuard(router, { requirePubkey: true }). Wallet and accounting are payment/ledger surfaces -- per ADR-0001 (aiolabs/lnbits) LNbits is a liquidity service, not an identity provider, so core payment flows must not be gated on having an identity. Verified the wallet module never reads the user's pubkey; its only nostr-tools import is nip19.encodeBytes for LNURL bech32 in the receive QR, unrelated to identity. Known gap, not addressed here: a pubkey-less user opening chat still loops at /login rather than seeing an "identity required" screen. The loop is now correct behaviour instead of a bug, but it deserves a real message. vue-tsc -b passes; all three call sites typecheck (the new parameter is optional, so wallet and accounting are unchanged). --- src/chat-app/app.ts | 3 ++- src/lib/router-helpers.ts | 35 +++++++++++++++++++++++++++-------- 2 files changed, 29 insertions(+), 9 deletions(-) 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 }