This repository has been archived on 2026-05-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Nest/nest-front/src/contexts/AuthContext.tsx

140 lines
4.8 KiB
TypeScript

import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import type { User } from '../types';
import { authApi, usersApi, getToken, setToken, clearToken } from '../utils/api';
// ── Types ──────────────────────────────────────────────────────────────────────
interface AuthContextValue {
user: User | null;
isAuthenticated: boolean;
isStaff: boolean;
isAdmin: boolean;
isLoading: boolean;
login: (email: string, password: string) => Promise<{ success: boolean; error?: string }>;
register: (username: string, email: string, password: string) => Promise<{ success: boolean; error?: string }>;
logout: () => void;
updateUsername: (username: string) => Promise<{ success: boolean; error?: string }>;
}
// ── Context ────────────────────────────────────────────────────────────────────
const AuthContext = createContext<AuthContextValue | null>(null);
// ── Provider ───────────────────────────────────────────────────────────────────
const STORAGE_KEY = 'crowmate_auth_user';
function saveUserToStorage(user: User | null): void {
if (user) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(user));
} else {
localStorage.removeItem(STORAGE_KEY);
}
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [isLoading, setIsLoading] = useState(true);
// On mount: validate stored token and restore session
useEffect(() => {
const token = getToken();
if (!token) {
setIsLoading(false);
return;
}
authApi.me()
.then((freshUser) => {
setUser(freshUser);
saveUserToStorage(freshUser);
})
.catch(() => {
clearToken();
saveUserToStorage(null);
})
.finally(() => setIsLoading(false));
}, []);
const isAuthenticated = user !== null;
const isStaff = user?.role === 'dev' || user?.role === 'com';
const isAdmin = user?.isAdmin === true;
const login = useCallback(
async (email: string, password: string): Promise<{ success: boolean; error?: string }> => {
try {
const { token, user: loggedInUser } = await authApi.login(email, password);
setToken(token);
setUser(loggedInUser);
saveUserToStorage(loggedInUser);
return { success: true };
} catch (err) {
const message = err instanceof Error ? err.message : 'Login failed.';
return { success: false, error: message };
}
},
[]
);
const register = useCallback(
async (username: string, email: string, password: string): Promise<{ success: boolean; error?: string }> => {
try {
const { token, user: newUser } = await authApi.register(username, email, password);
setToken(token);
setUser(newUser);
saveUserToStorage(newUser);
return { success: true };
} catch (err) {
const message = err instanceof Error ? err.message : 'Registration failed.';
return { success: false, error: message };
}
},
[]
);
const logout = useCallback(() => {
clearToken();
setUser(null);
saveUserToStorage(null);
}, []);
const updateUsername = useCallback(
async (username: string): Promise<{ success: boolean; error?: string }> => {
try {
const updatedUser = await usersApi.updateUsername(username);
setUser(updatedUser);
saveUserToStorage(updatedUser);
return { success: true };
} catch (err) {
const message = err instanceof Error ? err.message : 'Failed to update username.';
return { success: false, error: message };
}
},
[]
);
const value = useMemo<AuthContextValue>(
() => ({ user, isAuthenticated, isStaff, isAdmin, isLoading, login, register, logout, updateUsername }),
[user, isAuthenticated, isStaff, isAdmin, isLoading, login, register, logout, updateUsername]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
// ── Hook ───────────────────────────────────────────────────────────────────────
export function useAuth(): AuthContextValue {
const ctx = useContext(AuthContext);
if (!ctx) {
throw new Error('useAuth must be used inside <AuthProvider>');
}
return ctx;
}