fix(auth): don't require a Nostr pubkey to be considered logged in #180
2 changed files with 29 additions and 9 deletions
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).
commit
7497e6b3e5
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue