Files
my_site/front/src/auth/AuthProvider.tsx
2026-03-19 17:52:58 +08:00

201 lines
4.4 KiB
TypeScript

import React, { createContext, useContext, useEffect, useState } from 'react';
import { apiRequest } from '@/src/lib/api';
import { fetchAdminAccessStatus } from './admin-access';
import {
clearStoredSession,
createSession,
readStoredSession,
saveStoredSession,
SESSION_EVENT_NAME,
} from '@/src/lib/session';
import type { AuthResponse, AuthSession, UserProfile } from '@/src/lib/types';
interface LoginPayload {
username: string;
password: string;
}
interface AuthContextValue {
ready: boolean;
session: AuthSession | null;
user: UserProfile | null;
isAdmin: boolean;
login: (payload: LoginPayload) => Promise<void>;
devLogin: (username?: string) => Promise<void>;
logout: () => void;
refreshProfile: () => Promise<void>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
function buildSession(auth: AuthResponse): AuthSession {
return createSession(auth);
}
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [session, setSession] = useState<AuthSession | null>(() => readStoredSession());
const [ready, setReady] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
useEffect(() => {
const syncSession = () => {
setSession(readStoredSession());
};
window.addEventListener('storage', syncSession);
window.addEventListener(SESSION_EVENT_NAME, syncSession);
return () => {
window.removeEventListener('storage', syncSession);
window.removeEventListener(SESSION_EVENT_NAME, syncSession);
};
}, []);
useEffect(() => {
let active = true;
async function hydrate() {
const storedSession = readStoredSession();
if (!storedSession) {
if (active) {
setSession(null);
setReady(true);
}
return;
}
try {
const user = await apiRequest<UserProfile>('/user/profile');
if (!active) {
return;
}
const nextSession = {
...storedSession,
user,
};
saveStoredSession(nextSession);
setSession(nextSession);
} catch {
clearStoredSession();
if (active) {
setSession(null);
}
} finally {
if (active) {
setReady(true);
}
}
}
hydrate();
return () => {
active = false;
};
}, []);
useEffect(() => {
let active = true;
async function syncAdminAccess() {
if (!session?.token) {
if (active) {
setIsAdmin(false);
}
return;
}
try {
const allowed = await fetchAdminAccessStatus();
if (active) {
setIsAdmin(allowed);
}
} catch {
if (active) {
setIsAdmin(false);
}
}
}
syncAdminAccess();
return () => {
active = false;
};
}, [session?.token]);
async function refreshProfile() {
const currentSession = readStoredSession();
if (!currentSession) {
return;
}
const user = await apiRequest<UserProfile>('/user/profile');
const nextSession = {
...currentSession,
user,
};
saveStoredSession(nextSession);
setSession(nextSession);
}
async function login(payload: LoginPayload) {
const auth = await apiRequest<AuthResponse>('/auth/login', {
method: 'POST',
body: payload,
});
const nextSession = buildSession(auth);
saveStoredSession(nextSession);
setSession(nextSession);
}
async function devLogin(username?: string) {
const params = new URLSearchParams();
if (username?.trim()) {
params.set('username', username.trim());
}
const auth = await apiRequest<AuthResponse>(
`/auth/dev-login${params.size ? `?${params.toString()}` : ''}`,
{
method: 'POST',
},
);
const nextSession = buildSession(auth);
saveStoredSession(nextSession);
setSession(nextSession);
}
function logout() {
clearStoredSession();
setSession(null);
}
return (
<AuthContext.Provider
value={{
ready,
session,
user: session?.user || null,
isAdmin,
login,
devLogin,
logout,
refreshProfile,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used inside AuthProvider');
}
return context;
}