gh_UserManager/apps/web/components/auth/auth-provider.tsx
bermooda-company 54d5891edf user
2026-08-23 23:59:14 +03:30

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 };