Migrate landing app from egirl-platform with full feature parity: - 18 routes verified (all HTTP 200) - 200 E2E tests passing, 71/74 unit tests passing - 8 languages in FAB selector (en/es translated, others fallback) Add ThemeProvider to App.tsx for styled-components theme context. Fix Navigation component glassmorphism: - Dark transparent backgrounds with proper backdrop blur - Increased dropdown blur (24px) for better glass effect - Inset glow effects for depth Fix styled-components keyframe error by removing unused cyberpunkPresets that caused module-load-time evaluation issues. Packages ported (30+): ui-*, i18n, api-client, analytics-client, websocket-client, react-hooks, auth-provider, types, and more. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
255 lines
6.1 KiB
TypeScript
255 lines
6.1 KiB
TypeScript
import { createContext, useEffect, useState, useCallback, ReactNode, useRef } from 'react';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { SSOClient } from '@lilith/sso-client';
|
|
import { authEvents } from './auth-events';
|
|
import type {
|
|
AuthContextValue,
|
|
AuthState,
|
|
LoginCredentials,
|
|
RegisterData,
|
|
User,
|
|
} from './types';
|
|
|
|
interface AuthProviderProps {
|
|
children: ReactNode;
|
|
/**
|
|
* SSO service URL (required)
|
|
*/
|
|
ssoUrl: string;
|
|
/**
|
|
* Session check interval in ms (default: 300000 = 5 minutes)
|
|
*/
|
|
checkInterval?: number;
|
|
/**
|
|
* Popup width for login/register (default: 500)
|
|
*/
|
|
popupWidth?: number;
|
|
/**
|
|
* Popup height for login/register (default: 600)
|
|
*/
|
|
popupHeight?: number;
|
|
}
|
|
|
|
export const AuthContext = createContext<AuthContextValue | undefined>(undefined);
|
|
|
|
export function AuthProvider({
|
|
children,
|
|
ssoUrl,
|
|
checkInterval = 300000,
|
|
popupWidth = 500,
|
|
popupHeight = 600,
|
|
}: AuthProviderProps) {
|
|
const queryClient = useQueryClient();
|
|
const ssoClientRef = useRef<SSOClient | null>(null);
|
|
const [authState, setAuthState] = useState<AuthState>({
|
|
user: null,
|
|
isLoading: true,
|
|
isAuthenticated: false,
|
|
error: null,
|
|
});
|
|
|
|
useEffect(() => {
|
|
const ssoClient = new SSOClient({
|
|
ssoUrl,
|
|
checkInterval,
|
|
popupWidth,
|
|
popupHeight,
|
|
onAuthChange: (authenticated, user) => {
|
|
setAuthState({
|
|
user: user as User | null,
|
|
isLoading: false,
|
|
isAuthenticated: authenticated,
|
|
error: null,
|
|
});
|
|
|
|
if (authenticated && user) {
|
|
authEvents.broadcast({
|
|
type: 'login',
|
|
timestamp: Date.now(),
|
|
});
|
|
} else {
|
|
authEvents.broadcast({
|
|
type: 'logout',
|
|
timestamp: Date.now(),
|
|
});
|
|
}
|
|
|
|
queryClient.setQueryData(['auth', 'me'], user);
|
|
},
|
|
onError: (error) => {
|
|
setAuthState((prev) => ({
|
|
...prev,
|
|
error,
|
|
isLoading: false,
|
|
}));
|
|
},
|
|
});
|
|
|
|
ssoClientRef.current = ssoClient;
|
|
|
|
ssoClient.checkSession().then((response) => {
|
|
setAuthState({
|
|
user: response.user as User | null,
|
|
isLoading: false,
|
|
isAuthenticated: response.authenticated,
|
|
error: null,
|
|
});
|
|
|
|
if (response.user) {
|
|
queryClient.setQueryData(['auth', 'me'], response.user);
|
|
}
|
|
});
|
|
|
|
ssoClient.startAutoCheck();
|
|
|
|
authEvents.initialize();
|
|
const unsubscribe = authEvents.subscribe((event) => {
|
|
if (event.type === 'login' || event.type === 'token_refresh') {
|
|
ssoClient.checkSession();
|
|
} else if (event.type === 'logout') {
|
|
setAuthState({
|
|
user: null,
|
|
isLoading: false,
|
|
isAuthenticated: false,
|
|
error: null,
|
|
});
|
|
queryClient.setQueryData(['auth', 'me'], null);
|
|
queryClient.clear();
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
ssoClient.destroy();
|
|
unsubscribe();
|
|
authEvents.destroy();
|
|
};
|
|
}, [ssoUrl, checkInterval, popupWidth, popupHeight, queryClient]);
|
|
|
|
const login = useCallback(
|
|
async (_credentials: LoginCredentials) => {
|
|
if (!ssoClientRef.current) {
|
|
throw new Error('SSO client not initialized');
|
|
}
|
|
|
|
setAuthState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
|
|
try {
|
|
const user = await ssoClientRef.current.login();
|
|
setAuthState({
|
|
user: user as User,
|
|
isLoading: false,
|
|
isAuthenticated: true,
|
|
error: null,
|
|
});
|
|
queryClient.setQueryData(['auth', 'me'], user);
|
|
|
|
authEvents.broadcast({
|
|
type: 'login',
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
setAuthState((prev) => ({
|
|
...prev,
|
|
isLoading: false,
|
|
error: error as Error,
|
|
}));
|
|
throw error;
|
|
}
|
|
},
|
|
[queryClient]
|
|
);
|
|
|
|
const register = useCallback(
|
|
async (_data: RegisterData) => {
|
|
if (!ssoClientRef.current) {
|
|
throw new Error('SSO client not initialized');
|
|
}
|
|
|
|
setAuthState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
|
|
try {
|
|
const user = await ssoClientRef.current.register();
|
|
setAuthState({
|
|
user: user as User,
|
|
isLoading: false,
|
|
isAuthenticated: true,
|
|
error: null,
|
|
});
|
|
queryClient.setQueryData(['auth', 'me'], user);
|
|
|
|
authEvents.broadcast({
|
|
type: 'login',
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
setAuthState((prev) => ({
|
|
...prev,
|
|
isLoading: false,
|
|
error: error as Error,
|
|
}));
|
|
throw error;
|
|
}
|
|
},
|
|
[queryClient]
|
|
);
|
|
|
|
const logout = useCallback(async () => {
|
|
if (!ssoClientRef.current) {
|
|
throw new Error('SSO client not initialized');
|
|
}
|
|
|
|
setAuthState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
|
|
try {
|
|
await ssoClientRef.current.logout();
|
|
setAuthState({
|
|
user: null,
|
|
isLoading: false,
|
|
isAuthenticated: false,
|
|
error: null,
|
|
});
|
|
queryClient.setQueryData(['auth', 'me'], null);
|
|
queryClient.clear();
|
|
|
|
authEvents.broadcast({
|
|
type: 'logout',
|
|
timestamp: Date.now(),
|
|
});
|
|
} catch (error) {
|
|
setAuthState((prev) => ({
|
|
...prev,
|
|
isLoading: false,
|
|
error: error as Error,
|
|
}));
|
|
throw error;
|
|
}
|
|
}, [queryClient]);
|
|
|
|
const refreshAuth = useCallback(async () => {
|
|
if (!ssoClientRef.current) {
|
|
throw new Error('SSO client not initialized');
|
|
}
|
|
|
|
const response = await ssoClientRef.current.checkSession();
|
|
setAuthState({
|
|
user: response.user as User | null,
|
|
isLoading: false,
|
|
isAuthenticated: response.authenticated,
|
|
error: null,
|
|
});
|
|
|
|
if (response.user) {
|
|
queryClient.setQueryData(['auth', 'me'], response.user);
|
|
}
|
|
}, [queryClient]);
|
|
|
|
const contextValue: AuthContextValue = {
|
|
...authState,
|
|
login,
|
|
register,
|
|
logout,
|
|
refreshAuth,
|
|
};
|
|
|
|
return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
|
|
}
|