Files
fotospiel-app/resources/js/admin/auth/context.tsx

127 lines
3.4 KiB
TypeScript

import React from 'react';
import {
authorizedFetch,
clearOAuthSession,
clearTokens,
completeOAuthCallback,
isAuthError,
loadTokens,
registerAuthFailureHandler,
startOAuthFlow,
} from './tokens';
export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
export interface TenantProfile {
id: number;
tenant_id: number;
name?: string;
slug?: string;
email?: string | null;
event_credits_balance?: number;
[key: string]: unknown;
}
interface AuthContextValue {
status: AuthStatus;
user: TenantProfile | null;
login: (redirectPath?: string) => void;
logout: (options?: { redirect?: string }) => void;
completeLogin: (params: URLSearchParams) => Promise<string | null>;
refreshProfile: () => Promise<void>;
}
const AuthContext = React.createContext<AuthContextValue | undefined>(undefined);
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [status, setStatus] = React.useState<AuthStatus>('loading');
const [user, setUser] = React.useState<TenantProfile | null>(null);
const handleAuthFailure = React.useCallback(() => {
clearTokens();
setUser(null);
setStatus('unauthenticated');
}, []);
React.useEffect(() => {
const unsubscribe = registerAuthFailureHandler(handleAuthFailure);
return unsubscribe;
}, [handleAuthFailure]);
const refreshProfile = React.useCallback(async () => {
try {
const response = await authorizedFetch('/api/v1/tenant/me');
if (!response.ok) {
throw new Error('Failed to load profile');
}
const profile = (await response.json()) as TenantProfile;
setUser(profile);
setStatus('authenticated');
} catch (error) {
if (isAuthError(error)) {
handleAuthFailure();
} else {
console.error('[Auth] Failed to refresh profile', error);
}
throw error;
}
}, [handleAuthFailure]);
React.useEffect(() => {
const tokens = loadTokens();
if (!tokens) {
setStatus('unauthenticated');
return;
}
refreshProfile().catch(() => {
// refreshProfile already handled failures.
});
}, [refreshProfile]);
const login = React.useCallback((redirectPath?: string) => {
const target = redirectPath ?? window.location.pathname + window.location.search;
startOAuthFlow(target);
}, []);
const logout = React.useCallback(({ redirect }: { redirect?: string } = {}) => {
clearTokens();
clearOAuthSession();
setUser(null);
setStatus('unauthenticated');
if (redirect) {
window.location.href = redirect;
}
}, []);
const completeLogin = React.useCallback(
async (params: URLSearchParams) => {
setStatus('loading');
try {
const redirectTarget = await completeOAuthCallback(params);
await refreshProfile();
return redirectTarget;
} catch (error) {
handleAuthFailure();
throw error;
}
},
[handleAuthFailure, refreshProfile]
);
const value = React.useMemo<AuthContextValue>(
() => ({ status, user, login, logout, completeLogin, refreshProfile }),
[status, user, login, logout, completeLogin, refreshProfile]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export function useAuth(): AuthContextValue {
const context = React.useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
}