Files
BelgianBitcoinEmbassy/frontend/hooks/useAuth.ts
Michilis 76210db03d first commit
Made-with: Cursor
2026-04-01 02:46:53 +00:00

152 lines
3.9 KiB
TypeScript

"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;
username?: string;
name?: string;
picture?: string;
about?: string;
nip05?: string;
displayName?: string;
}
interface AuthContextType {
user: User | null;
loading: boolean;
login: () => Promise<User>;
loginWithBunker: (input: string) => Promise<User>;
loginWithConnectedSigner: (signer: BunkerSignerInterface) => Promise<User>;
logout: () => void;
isAdmin: boolean;
isModerator: boolean;
}
export const AuthContext = createContext<AuthContextType>({
user: null,
loading: true,
login: async () => ({ pubkey: "", role: "USER" }),
loginWithBunker: async () => ({ pubkey: "", role: "USER" }),
loginWithConnectedSigner: async () => ({ pubkey: "", role: "USER" }),
logout: () => {},
isAdmin: false,
isModerator: false,
});
export function useAuth() {
return useContext(AuthContext);
}
export function useAuthProvider(): AuthContextType {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
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");
}
}
setLoading(false);
}, []);
const completeAuth = useCallback(
async (
getPubKey: () => Promise<string>,
sign: (event: any) => Promise<any>
): Promise<User> => {
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,
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<User> => {
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<User> => {
return completeAuth(
() => signer.getPublicKey(),
(event) => signer.signEvent(event)
);
},
[completeAuth]
);
const loginWithBunker = useCallback(
async (input: string): Promise<User> => {
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);
}, []);
return {
user,
loading,
login,
loginWithBunker,
loginWithConnectedSigner,
logout,
isAdmin: user?.role === "ADMIN",
isModerator: user?.role === "MODERATOR" || user?.role === "ADMIN",
};
}