Compare commits

...

2 commits

Author SHA1 Message Date
54c1c990e1 Merge pull request 'fix(auth): don't require a Nostr pubkey to be considered logged in' (#180) from fix/auth-guard-no-pubkey-requirement into dev
Reviewed-on: #180
2026-10-08 18:46:03 +00:00
7497e6b3e5 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).
2026-10-08 20:43:47 +02:00
2 changed files with 29 additions and 9 deletions

View file

@ -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()

View file

@ -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
}