Staff accounts use admin/curator roles and fine-grained flags; transitions connect source-to-target with color gradients and stream cutout masks so overlaps stay seamless. Co-authored-by: Cursor <cursoragent@cursor.com>
98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
import { useCallback, useContext, useEffect, useMemo, useState, type ReactNode, createContext } from 'react';
|
|
import {
|
|
getAuthMe,
|
|
loginCurator,
|
|
logoutCurator,
|
|
type AuthRole,
|
|
type StaffPermission,
|
|
} from '../api/client';
|
|
|
|
interface AuthContextValue {
|
|
role: AuthRole;
|
|
username?: string;
|
|
permissions: StaffPermission[];
|
|
isCurator: boolean;
|
|
isAdmin: boolean;
|
|
loading: boolean;
|
|
can: (permission: StaffPermission) => boolean;
|
|
login: (username: string, password: string) => Promise<void>;
|
|
logout: () => Promise<void>;
|
|
refresh: () => Promise<void>;
|
|
}
|
|
|
|
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
|
|
function applyAuthState(
|
|
me: { role: AuthRole; username?: string; permissions?: StaffPermission[] },
|
|
setRole: (r: AuthRole) => void,
|
|
setUsername: (u: string | undefined) => void,
|
|
setPermissions: (p: StaffPermission[]) => void
|
|
) {
|
|
setRole(me.role);
|
|
setUsername(me.username);
|
|
setPermissions(me.role === 'user' ? [] : me.permissions ?? []);
|
|
}
|
|
|
|
export function AuthProvider({ children }: { children: ReactNode }) {
|
|
const [role, setRole] = useState<AuthRole>('user');
|
|
const [username, setUsername] = useState<string | undefined>();
|
|
const [permissions, setPermissions] = useState<StaffPermission[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const refresh = useCallback(async () => {
|
|
const me = await getAuthMe();
|
|
applyAuthState(me, setRole, setUsername, setPermissions);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
refresh().finally(() => setLoading(false));
|
|
}, [refresh]);
|
|
|
|
const login = useCallback(async (user: string, password: string) => {
|
|
const me = await loginCurator(user, password);
|
|
applyAuthState(me, setRole, setUsername, setPermissions);
|
|
}, []);
|
|
|
|
const logout = useCallback(async () => {
|
|
await logoutCurator();
|
|
setRole('user');
|
|
setUsername(undefined);
|
|
setPermissions([]);
|
|
}, []);
|
|
|
|
const can = useCallback(
|
|
(permission: StaffPermission) => {
|
|
if (role === 'admin') return true;
|
|
if (role !== 'curator') return false;
|
|
return permissions.includes(permission);
|
|
},
|
|
[role, permissions]
|
|
);
|
|
|
|
const value = useMemo(
|
|
() => ({
|
|
role,
|
|
username,
|
|
permissions,
|
|
isCurator: role === 'admin' || role === 'curator',
|
|
isAdmin: role === 'admin',
|
|
loading,
|
|
can,
|
|
login,
|
|
logout,
|
|
refresh,
|
|
}),
|
|
[role, username, permissions, loading, can, login, logout, refresh]
|
|
);
|
|
|
|
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 within AuthProvider');
|
|
}
|
|
return ctx;
|
|
}
|