"use client"; import { useState, useEffect, useCallback, createContext, useContext } from "react"; import { api } from "@/lib/api"; import { hasNostrExtension, getPublicKey, signEvent, createAuthEvent, fetchNostrProfile, createBunkerSigner, type BunkerSignerInterface, type NostrProfile, } from "@/lib/nostr"; export interface User { pubkey: string; role: string; isSuperAdmin?: boolean; permissions?: string[]; username?: string; name?: string; picture?: string; about?: string; nip05?: string; displayName?: string; } interface AuthContextType { user: User | null; loading: boolean; login: () => Promise; loginWithBunker: (input: string) => Promise; loginWithConnectedSigner: (signer: BunkerSignerInterface) => Promise; logout: () => void; isAdmin: boolean; isModerator: boolean; isSuperAdmin: boolean; permissions: string[]; can: (permission: string) => boolean; refreshAccess: () => Promise; } export const AuthContext = createContext({ user: null, loading: true, login: async () => ({ pubkey: "", role: "guest" }), loginWithBunker: async () => ({ pubkey: "", role: "guest" }), loginWithConnectedSigner: async () => ({ pubkey: "", role: "guest" }), logout: () => {}, isAdmin: false, isModerator: false, isSuperAdmin: false, permissions: [], can: () => false, refreshAccess: async () => {}, }); export function useAuth() { return useContext(AuthContext); } export function useAuthProvider(): AuthContextType { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const refreshAccess = useCallback(async () => { const token = localStorage.getItem("bbe_token"); if (!token) return; try { const me = await api.getMe(); setUser((prev) => { if (!prev) return prev; const next: User = { ...prev, role: me.role, isSuperAdmin: me.isSuperAdmin, permissions: me.permissions, username: me.username ?? prev.username, }; localStorage.setItem("bbe_user", JSON.stringify(next)); return next; }); } catch { // Best-effort refresh. The stored snapshot remains usable. } }, []); useEffect(() => { const stored = localStorage.getItem("bbe_user"); const token = localStorage.getItem("bbe_token"); if (stored && token) { try { setUser(JSON.parse(stored)); } catch { localStorage.removeItem("bbe_user"); localStorage.removeItem("bbe_token"); } // Refresh effective role and permissions live so a stale JWT snapshot does // not drive what the user can see or do. void refreshAccess(); } setLoading(false); }, [refreshAccess]); const completeAuth = useCallback( async ( getPubKey: () => Promise, sign: (event: any) => Promise ): Promise => { const pubkey = await getPubKey(); const { challenge } = await api.getChallenge(pubkey); const event = createAuthEvent(pubkey, challenge); const signedEvent = await sign(event); const { token, user: userData } = await api.verify(pubkey, signedEvent); let profile: NostrProfile = {}; try { profile = await fetchNostrProfile(pubkey); } catch { // Profile fetch is best-effort } const fullUser: User = { ...userData, isSuperAdmin: userData.isSuperAdmin ?? false, permissions: userData.permissions ?? [], name: profile.name, displayName: profile.displayName, picture: profile.picture, about: profile.about, nip05: profile.nip05, username: userData.username, }; localStorage.setItem("bbe_token", token); localStorage.setItem("bbe_user", JSON.stringify(fullUser)); setUser(fullUser); return fullUser; }, [] ); const login = useCallback(async (): Promise => { if (!hasNostrExtension()) { throw new Error("Please install a Nostr extension (e.g., Alby, nos2x)"); } return completeAuth(getPublicKey, signEvent); }, [completeAuth]); const loginWithConnectedSigner = useCallback( async (signer: BunkerSignerInterface): Promise => { return completeAuth( () => signer.getPublicKey(), (event) => signer.signEvent(event) ); }, [completeAuth] ); const loginWithBunker = useCallback( async (input: string): Promise => { const { signer } = await createBunkerSigner(input); try { return await loginWithConnectedSigner(signer); } finally { await signer.close().catch(() => {}); } }, [loginWithConnectedSigner] ); const logout = useCallback(() => { localStorage.removeItem("bbe_token"); localStorage.removeItem("bbe_user"); setUser(null); }, []); const permissions = user?.permissions ?? []; const isSuperAdmin = user?.isSuperAdmin ?? false; const can = useCallback( (permission: string) => isSuperAdmin || permissions.includes(permission), [isSuperAdmin, permissions] ); return { user, loading, login, loginWithBunker, loginWithConnectedSigner, logout, isSuperAdmin, permissions, can, refreshAccess, // Compatibility shims for any remaining role-name checks. isAdmin: isSuperAdmin || user?.role === "admin", isModerator: isSuperAdmin || user?.role === "admin" || user?.role === "moderator", }; }