120 lines
3.2 KiB
TypeScript
120 lines
3.2 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
} from "react";
|
|
import { auth, type User } from "@/lib/auth";
|
|
|
|
/**
|
|
* AuthProvider holds the current-user state and exposes the auth actions
|
|
* to the rest of the app.
|
|
*
|
|
* On mount it calls /auth/me to hydrate. Until that completes, `loading`
|
|
* is true and consumers should render a placeholder rather than assuming
|
|
* the user is logged out.
|
|
*
|
|
* After a successful login/signup/logout the provider re-fetches /auth/me
|
|
* to keep state in sync with the cookie.
|
|
*/
|
|
/**
|
|
* LogInResult is either a completed session (regular case) or a signal
|
|
* that the BFF wants a one-time code before completing the login. Callers
|
|
* should branch on `kind` and, for `otp_required`, present an OTP field
|
|
* and call `verifyLoginOTP`.
|
|
*/
|
|
export type LogInResult =
|
|
| { kind: "session"; user: User }
|
|
| { kind: "otp_required" };
|
|
|
|
type AuthContextValue = {
|
|
user: User | null;
|
|
loading: boolean;
|
|
signUp: (email: string, password: string, name: string) => Promise<User>;
|
|
logIn: (email: string, password: string) => Promise<LogInResult>;
|
|
verifyLoginOTP: (email: string, password: string, otp: string) => Promise<User>;
|
|
logOut: () => Promise<void>;
|
|
refresh: () => Promise<void>;
|
|
/** Replace the user state without a network round-trip (after profile edit). */
|
|
setUser: (u: User | null) => void;
|
|
};
|
|
|
|
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
|
|
export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|
const [user, setUser] = useState<User | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const refresh = useCallback(async () => {
|
|
try {
|
|
const me = await auth.me();
|
|
setUser(me);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void refresh();
|
|
}, [refresh]);
|
|
|
|
const signUp = useCallback(async (email: string, password: string, name: string) => {
|
|
const u = await auth.signUp(email, password, name);
|
|
setUser(u);
|
|
return u;
|
|
}, []);
|
|
|
|
const logIn = useCallback(async (email: string, password: string) => {
|
|
const res = await auth.logIn(email, password);
|
|
if (res.kind === "session") {
|
|
setUser(res.user);
|
|
}
|
|
// For `otp_required` we deliberately leave `user` null so the rest of
|
|
// the app keeps showing the signed-out state until VerifyLoginOTP
|
|
// completes.
|
|
return res;
|
|
}, []);
|
|
|
|
const verifyLoginOTP = useCallback(
|
|
async (email: string, password: string, otp: string) => {
|
|
const u = await auth.verifyLoginOTP(email, password, otp);
|
|
setUser(u);
|
|
return u;
|
|
},
|
|
[],
|
|
);
|
|
|
|
const logOut = useCallback(async () => {
|
|
await auth.logOut();
|
|
setUser(null);
|
|
}, []);
|
|
|
|
const value = useMemo<AuthContextValue>(
|
|
() => ({
|
|
user,
|
|
loading,
|
|
signUp,
|
|
logIn,
|
|
verifyLoginOTP,
|
|
logOut,
|
|
refresh,
|
|
setUser,
|
|
}),
|
|
[user, loading, signUp, logIn, verifyLoginOTP, logOut, refresh],
|
|
);
|
|
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
}
|
|
|
|
export function useAuth(): AuthContextValue {
|
|
const ctx = useContext(AuthContext);
|
|
if (!ctx) {
|
|
throw new Error("useAuth must be used inside <AuthProvider>");
|
|
}
|
|
return ctx;
|
|
}
|