140 lines
4.8 KiB
TypeScript
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;
|
|
}
|