import React, { useEffect, useState } from 'react' import { useAuthStore } from '../store/authStore' import { oauthApi } from '../api/client' import { SCOPE_GROUPS } from '../api/oauthScopes' import { Lock, ShieldCheck, AlertTriangle, Loader2, LogIn } from 'lucide-react' interface ValidateResult { valid: boolean error?: string error_description?: string client?: { name: string; allowed_scopes: string[] } scopes?: string[] consentRequired?: boolean loginRequired?: boolean } type PageState = 'loading' | 'login_required' | 'consent' | 'auto_approving' | 'error' | 'done' export default function OAuthAuthorizePage(): React.ReactElement { const { isAuthenticated, isLoading: authLoading, loadUser } = useAuthStore() const [pageState, setPageState] = useState('loading') const [validation, setValidation] = useState(null) const [submitting, setSubmitting] = useState(false) const [errorMsg, setErrorMsg] = useState(null) const params = new URLSearchParams(window.location.search) const clientId = params.get('client_id') || '' const redirectUri = params.get('redirect_uri') || '' const scope = params.get('scope') || '' const state = params.get('state') || '' const codeChallenge = params.get('code_challenge') || '' const ccMethod = params.get('code_challenge_method') || '' // Load auth state once, then validate useEffect(() => { loadUser({ silent: true }).catch(() => {}) }, [loadUser]) useEffect(() => { if (authLoading) return validateRequest() // eslint-disable-next-line react-hooks/exhaustive-deps }, [authLoading, isAuthenticated]) async function validateRequest() { setPageState('loading') try { const result = await oauthApi.validate({ client_id: clientId, redirect_uri: redirectUri, scope, state, code_challenge: codeChallenge, code_challenge_method: ccMethod, response_type: 'code', }) setValidation(result) if (!result.valid) { setPageState('error') setErrorMsg(result.error_description || result.error || 'Invalid authorization request') return } if (result.loginRequired) { setPageState('login_required') return } if (!result.consentRequired) { // Consent already on record — auto-approve silently setPageState('auto_approving') await submitConsent(true) return } setPageState('consent') } catch (err: unknown) { setPageState('error') setErrorMsg('Failed to validate authorization request. Please try again.') } } async function submitConsent(approved: boolean) { setSubmitting(true) try { const result = await oauthApi.authorize({ client_id: clientId, redirect_uri: redirectUri, scope, state, code_challenge: codeChallenge, code_challenge_method: ccMethod, approved, }) setPageState('done') window.location.href = result.redirect } catch { setPageState('error') setErrorMsg('Authorization failed. Please try again.') setSubmitting(false) } } function handleLoginRedirect() { const next = '/oauth/authorize?' + params.toString() window.location.href = '/login?redirect=' + encodeURIComponent(next) } // Group requested scopes by their human-readable group const scopesByGroup = React.useMemo(() => { const requested = validation?.scopes || [] const groups: Record = {} for (const s of requested) { const info = SCOPE_GROUPS[s] const group = info?.group || 'Other' if (!groups[group]) groups[group] = [] groups[group].push(s) } return groups }, [validation]) // ---- Render states ---- if (pageState === 'loading' || pageState === 'auto_approving') { return (

{pageState === 'auto_approving' ? 'Authorizing…' : 'Loading…'}

) } if (pageState === 'error') { return (

Authorization Error

{errorMsg}

) } if (pageState === 'login_required') { return (

Sign in to continue

{validation?.client?.name || clientId} wants access to your TREK account. Please sign in first.

) } // pageState === 'consent' return (
{/* Left panel — app identity + actions */}

Authorization Request

{validation?.client?.name || clientId}

This application is requesting access to your TREK account.

Only grant access to applications you trust. Your data stays on your server.

{/* Right panel — scopes */}

Permissions requested

{Object.entries(scopesByGroup).map(([group, groupScopes]) => (

{group}

{groupScopes.map(s => { const info = SCOPE_GROUPS[s] return (
{s.endsWith(':delete') ? '🗑️' : s.endsWith(':write') ? '✏️' : '👁️'}

{info?.label || s}

{info?.description || ''}

) })}
))}
) }