Files
spsays-training/components/auth/auth-provider.tsx
T
2026-06-14 00:18:31 +05:30

125 lines
3.5 KiB
TypeScript

"use client";
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { auth, type User } from "@/lib/auth";
import { feed } from "@/lib/social";
/**
* 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 () => {
// Drop the home-feed seen-set first, while the auth cookie is still set
// (the reset endpoint is auth-gated). Best-effort — a stale set would
// otherwise expire on its own TTL.
await feed.reset().catch(() => {});
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;
}