105 lines
2.6 KiB
TypeScript
105 lines
2.6 KiB
TypeScript
import {
|
|
createContext,
|
|
useCallback,
|
|
useContext,
|
|
useEffect,
|
|
useMemo,
|
|
useState,
|
|
type ReactNode,
|
|
} from 'react';
|
|
import {
|
|
api,
|
|
hasStoredSession,
|
|
type LoginInput,
|
|
type Me,
|
|
type RegisterInput,
|
|
} from '../lib/api';
|
|
import { getSocket, disconnectSocket } from '../lib/socket';
|
|
|
|
type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
|
|
|
|
interface AuthContextValue {
|
|
status: AuthStatus;
|
|
user: Me | null;
|
|
login: (input: LoginInput) => Promise<void>;
|
|
register: (input: RegisterInput) => Promise<void>;
|
|
logout: () => Promise<void>;
|
|
refreshMe: () => Promise<void>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [status, setStatus] = useState<AuthStatus>('loading');
|
|
const [user, setUser] = useState<Me | null>(null);
|
|
|
|
// Bootstrap: if we have a refresh token on disk, try /auth/me
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
|
|
(async () => {
|
|
if (!(await hasStoredSession())) {
|
|
if (!cancelled) setStatus('unauthenticated');
|
|
return;
|
|
}
|
|
try {
|
|
const me = await api.me();
|
|
if (cancelled) return;
|
|
setUser(me);
|
|
setStatus('authenticated');
|
|
// Connect WebSocket on session restore
|
|
getSocket().catch(() => {});
|
|
} catch {
|
|
if (cancelled) return;
|
|
setUser(null);
|
|
setStatus('unauthenticated');
|
|
}
|
|
})();
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const login = useCallback(async (input: LoginInput) => {
|
|
await api.login(input);
|
|
const me = await api.me();
|
|
setUser(me);
|
|
setStatus('authenticated');
|
|
// Connect WebSocket after login
|
|
getSocket().catch(() => {});
|
|
}, []);
|
|
|
|
const register = useCallback(async (input: RegisterInput) => {
|
|
await api.register(input);
|
|
const me = await api.me();
|
|
setUser(me);
|
|
setStatus('authenticated');
|
|
}, []);
|
|
|
|
const logout = useCallback(async () => {
|
|
disconnectSocket();
|
|
await api.logout();
|
|
setUser(null);
|
|
setStatus('unauthenticated');
|
|
}, []);
|
|
|
|
const refreshMe = useCallback(async () => {
|
|
const me = await api.me();
|
|
setUser(me);
|
|
}, []);
|
|
|
|
const value = useMemo<AuthContextValue>(
|
|
() => ({ status, user, login, register, logout, refreshMe }),
|
|
[status, user, login, register, logout, refreshMe],
|
|
);
|
|
|
|
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 inside <AuthProvider>');
|
|
return ctx;
|
|
}
|