feat: implement tenant OAuth flow and guest achievements
This commit is contained in:
126
resources/js/admin/auth/context.tsx
Normal file
126
resources/js/admin/auth/context.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user