182 lines
4.1 KiB
TypeScript
182 lines
4.1 KiB
TypeScript
"use client";
|
|
|
|
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
ReactNode,
|
|
} from "react";
|
|
import {
|
|
apiFetch,
|
|
authLogin,
|
|
authLogout,
|
|
authRegister,
|
|
clearTokens,
|
|
fetchMe,
|
|
getAccessToken,
|
|
getRefreshToken,
|
|
setTokens,
|
|
ACCESS_TOKEN_KEY,
|
|
REFRESH_TOKEN_KEY,
|
|
SESSION_ID_KEY,
|
|
} from "@/lib/api";
|
|
|
|
export interface AuthUser {
|
|
user_id: string;
|
|
email: string;
|
|
username?: string | null;
|
|
full_name: string | null;
|
|
[key: string]: any;
|
|
}
|
|
|
|
interface AuthState {
|
|
user: AuthUser | null;
|
|
isAuthenticated: boolean;
|
|
isLoading: boolean;
|
|
}
|
|
|
|
interface AuthContextValue extends AuthState {
|
|
login: (
|
|
email: string,
|
|
password: string,
|
|
mfaCode?: string,
|
|
) => Promise<{ mfaRequired?: boolean }>;
|
|
register: (payload: {
|
|
email: string;
|
|
password: string;
|
|
password_confirm: string;
|
|
full_name?: string;
|
|
username?: string;
|
|
}) => Promise<void>;
|
|
logout: () => Promise<void>;
|
|
refreshUser: () => Promise<void>;
|
|
apiFetch: typeof apiFetch;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [state, setState] = useState<AuthState>({
|
|
user: null,
|
|
isAuthenticated: false,
|
|
isLoading: true,
|
|
});
|
|
|
|
const refreshUser = useCallback(async () => {
|
|
const user = await fetchMe();
|
|
if (user) {
|
|
setState((prev) => ({
|
|
...prev,
|
|
user,
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
}));
|
|
} else {
|
|
clearTokens();
|
|
setState({ user: null, isAuthenticated: false, isLoading: false });
|
|
}
|
|
}, []);
|
|
|
|
// Bootstrap: if a token exists in storage, load the current user.
|
|
useEffect(() => {
|
|
let active = true;
|
|
if (typeof window === "undefined") {
|
|
setState((prev) => ({ ...prev, isLoading: false }));
|
|
return;
|
|
}
|
|
if (getAccessToken()) {
|
|
fetchMe().then((user) => {
|
|
if (!active) return;
|
|
if (user) {
|
|
setState({ user, isAuthenticated: true, isLoading: false });
|
|
} else {
|
|
clearTokens();
|
|
setState({ user: null, isAuthenticated: false, isLoading: false });
|
|
}
|
|
});
|
|
} else {
|
|
setState((prev) => ({ ...prev, isLoading: false }));
|
|
}
|
|
return () => {
|
|
active = false;
|
|
};
|
|
}, []);
|
|
|
|
const login = useCallback(
|
|
async (email: string, password: string, mfaCode?: string) => {
|
|
const { ok, data } = await authLogin(email, password, mfaCode);
|
|
if (!ok) {
|
|
if (data?.mfa_required) {
|
|
return { mfaRequired: true };
|
|
}
|
|
const err = new Error(data?.detail || "Login failed") as any;
|
|
err.status = data?.status;
|
|
throw err;
|
|
}
|
|
setTokens(data.access, data.refresh, data.session_id);
|
|
const user = await fetchMe();
|
|
setState({
|
|
user: user ?? null,
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
});
|
|
return {};
|
|
},
|
|
[],
|
|
);
|
|
|
|
const register = useCallback(
|
|
async (payload: {
|
|
email: string;
|
|
password: string;
|
|
password_confirm: string;
|
|
full_name?: string;
|
|
username?: string;
|
|
}) => {
|
|
const { ok, data } = await authRegister(payload);
|
|
if (!ok) {
|
|
const err = new Error(data?.detail || "Registration failed") as any;
|
|
err.errors = data;
|
|
err.status = data?.status;
|
|
throw err;
|
|
}
|
|
setTokens(data.access, data.refresh, data.session_id);
|
|
setState({
|
|
user: data.user ?? null,
|
|
isAuthenticated: true,
|
|
isLoading: false,
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const logout = useCallback(async () => {
|
|
await authLogout(getRefreshToken());
|
|
clearTokens();
|
|
setState({ user: null, isAuthenticated: false, isLoading: false });
|
|
}, []);
|
|
|
|
const value: AuthContextValue = {
|
|
...state,
|
|
login,
|
|
register,
|
|
logout,
|
|
refreshUser,
|
|
apiFetch,
|
|
};
|
|
|
|
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 within an AuthProvider");
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
export { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY, SESSION_ID_KEY };
|