152 lines
3.9 KiB
TypeScript
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",
|
|
};
|
|
}
|