refactor(frontend): create centralised auth manager (#24)
- Resolves #17 Reviewed-on: #24
This commit was merged in pull request #24.
This commit is contained in:
20
frontend/src/auth/AuthContext.ts
Normal file
20
frontend/src/auth/AuthContext.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { createContext } from "react";
|
||||
import type { User } from "../utils/types.ts";
|
||||
|
||||
type AuthContextValue = {
|
||||
/** The currently authenticated user, or null when logged out. */
|
||||
user: User | null;
|
||||
/** True until the initial "who am I" request has resolved. */
|
||||
loading: boolean;
|
||||
/** Exchange a Google credential for a session, then load the user. */
|
||||
login: (googleCredential: string) => Promise<void>;
|
||||
/** End the session and clear the user. */
|
||||
logout: () => Promise<void>;
|
||||
/** Re-fetch the current user (e.g. after a profile change). */
|
||||
refresh: () => Promise<void>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
||||
export { AuthContext };
|
||||
export type { AuthContextValue };
|
||||
46
frontend/src/auth/AuthProvider.tsx
Normal file
46
frontend/src/auth/AuthProvider.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import type { User } from "../utils/types.ts";
|
||||
import { login as apiLogin, logout as apiLogout, getUser } from "../utils/api/users.ts";
|
||||
import { AuthContext } from "./AuthContext.ts";
|
||||
|
||||
/**
|
||||
* Owns the single source of truth for the authenticated user. Everything that
|
||||
* needs to know who is logged in — the layout, the top bar, protected pages —
|
||||
* reads it through the `useAuth` hook rather than fetching the user itself.
|
||||
*/
|
||||
const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
setUser(await getUser());
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch user:", error);
|
||||
setUser(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh().finally(() => setLoading(false));
|
||||
}, [refresh]);
|
||||
|
||||
const login = useCallback(async (googleCredential: string) => {
|
||||
await apiLogin(googleCredential);
|
||||
await refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
await apiLogout();
|
||||
setUser(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, loading, login, logout, refresh }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthProvider;
|
||||
14
frontend/src/auth/useAuth.ts
Normal file
14
frontend/src/auth/useAuth.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useContext } from "react";
|
||||
import { AuthContext } from "./AuthContext.ts";
|
||||
import type { AuthContextValue } from "./AuthContext.ts";
|
||||
|
||||
/** Access the centralised auth state. Must be used within an <AuthProvider>. */
|
||||
const useAuth = (): AuthContextValue => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error("useAuth must be used within an AuthProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export { useAuth };
|
||||
Reference in New Issue
Block a user