'use client' import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react' import { TOKEN_KEY, apiFetch, apiUrl } from '../lib/api' export interface User { id: string username: string role: string email?: string | null display_name?: string | null avatar_url?: string | null } /** Response of `GET /api/auth/config` — decides which sign-in methods to offer. */ export interface AuthConfig { hosted: boolean platformConnected: boolean standardAgentsLogin: boolean localPassword: boolean } type LoginResult = { success: boolean; error?: string } interface AuthContextValue { isAuthenticated: boolean isLoading: boolean user: User | null config: AuthConfig | null login: (username: string, password: string) => Promise /** Local dev: mint a session from the project's platform key (cookie only). */ bootstrapStandardAgents: () => Promise /** Hosted: full-page redirect through Standard Agents sign-in. */ redirectToStandardAgents: () => void logout: () => void } const AuthContext = createContext(null) const DEFAULT_CONFIG: AuthConfig = { hosted: false, platformConnected: false, standardAgentsLogin: false, localPassword: true, } function clearStoredToken() { try { localStorage.removeItem(TOKEN_KEY) } catch { // Storage can be unavailable (private mode); the session cookie still applies. } } export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null) const [config, setConfig] = useState(null) const [isLoading, setIsLoading] = useState(true) const loadCurrentUser = useCallback(async (): Promise => { const res = await apiFetch('/api/auth/me') if (!res.ok) return false const data = await res.json() as { user: User } setUser(data.user) return true }, []) // One read at startup: the session may live in a bearer token (password // login) or in the HttpOnly cookie (Standard Agents sign-in). useEffect(() => { let cancelled = false async function init() { const [configRes, signedIn] = await Promise.all([ fetch(apiUrl('/api/auth/config'), { credentials: 'include' }) .then(res => res.ok ? res.json() as Promise> : {}) .catch(() => ({})), loadCurrentUser().catch(() => false), ]) if (cancelled) return setConfig({ ...DEFAULT_CONFIG, ...configRes }) if (!signedIn) clearStoredToken() setIsLoading(false) } init() return () => { cancelled = true } }, [loadCurrentUser]) const login = useCallback(async (username: string, password: string): Promise => { try { const res = await apiFetch('/api/auth/login', { method: 'POST', body: JSON.stringify({ username, password }), }) const data = await res.json().catch(() => ({})) as { token?: string; user?: User; error?: string } if (!res.ok || !data.user) { return { success: false, error: data.error || 'Invalid credentials' } } // The runtime also sets a session cookie; the token keeps bearer auth // working when the chat is served from a different origin. if (data.token) localStorage.setItem(TOKEN_KEY, data.token) setUser(data.user) return { success: true } } catch { return { success: false, error: 'Network error' } } }, []) const bootstrapStandardAgents = useCallback(async (): Promise => { try { const res = await apiFetch('/api/auth/bootstrap', { method: 'POST', headers: { Accept: 'application/json' }, }) if (res.status === 204) { return { success: false, error: 'Standard Agents sign-in is not available for this instance.' } } if (!res.ok) { const data = await res.json().catch(() => ({})) as { error?: string } return { success: false, error: data.error || 'Standard Agents sign-in failed.' } } const signedIn = await loadCurrentUser() return signedIn ? { success: true } : { success: false, error: 'Standard Agents sign-in failed.' } } catch { return { success: false, error: 'Network error' } } }, [loadCurrentUser]) const redirectToStandardAgents = useCallback(() => { const returnTo = encodeURIComponent(window.location.href) window.location.href = apiUrl(`/api/auth/sa/start?return_to=${returnTo}`) }, []) const logout = useCallback(() => { apiFetch('/api/auth/logout', { method: 'POST' }) .then(async res => { if (!res.ok) return const data = await res.json() as { redirect?: string } // Hosted instances also end the Standard Agents session. if (data.redirect) window.location.href = data.redirect }) .catch(() => {}) clearStoredToken() setUser(null) }, []) return ( {children} ) } export function useAuth() { const context = useContext(AuthContext) if (!context) { throw new Error('useAuth must be used within an AuthProvider') } return context }