'use client'; import { useEffect, useState } from 'react'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Alert, AlertDescription } from '@/components/ui/alert'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { Separator } from '@/components/ui/separator'; import { authClient } from '@/lib/auth-client'; import { apiRequest, showErrorToast } from '@/lib/utils'; import { toast, Toaster } from 'sonner'; import { Shield, User, Mail, ChevronRight, AlertTriangle, Loader2 } from 'lucide-react'; import { isValidRedirectUri, parseRedirectUris } from '@/lib/utils/oauth-validation'; interface OAuthApplication { id: string; clientId: string; name: string; redirectURLs: string; type: string; } interface ConsentRequest { clientId: string; scope: string; state?: string; redirectUri?: string; } export default function ConsentPage() { const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); const [application, setApplication] = useState(null); const [scopes, setScopes] = useState([]); const [selectedScopes, setSelectedScopes] = useState>(new Set()); const [error, setError] = useState(null); useEffect(() => { loadConsentDetails(); }, []); const loadConsentDetails = async () => { try { const params = new URLSearchParams(window.location.search); const clientId = params.get('client_id'); const scope = params.get('scope'); const redirectUri = params.get('redirect_uri'); if (!clientId) { setError('Invalid authorization request: missing client ID'); return; } // Fetch application details const apps = await apiRequest('/sso/applications'); const app = apps.find(a => a.clientId === clientId); if (!app) { setError('Invalid authorization request: unknown application'); return; } // Validate redirect URI if provided if (redirectUri) { const authorizedUris = parseRedirectUris(app.redirectURLs); if (!isValidRedirectUri(redirectUri, authorizedUris)) { setError('Invalid authorization request: unauthorized redirect URI'); return; } } setApplication(app); // Parse requested scopes const requestedScopes = scope ? scope.split(' ').filter(s => s) : ['openid']; setScopes(requestedScopes); // By default, select all requested scopes setSelectedScopes(new Set(requestedScopes)); } catch (error) { console.error('Failed to load consent details:', error); setError('Failed to load authorization details'); } finally { setIsLoading(false); } }; const handleConsent = async (accept: boolean) => { setIsSubmitting(true); try { const result = await authClient.oauth2.consent({ accept, }); if (result.error) { throw new Error(result.error.message || 'Consent failed'); } // The consent method should handle the redirect if (!accept) { // If denied, redirect back to the application with error const params = new URLSearchParams(window.location.search); const redirectUri = params.get('redirect_uri'); if (redirectUri && application) { // Validate redirect URI against authorized URIs const authorizedUris = parseRedirectUris(application.redirectURLs); if (isValidRedirectUri(redirectUri, authorizedUris)) { try { // Parse and reconstruct the URL to ensure it's safe const url = new URL(redirectUri); url.searchParams.set('error', 'access_denied'); // Safe to redirect - URI has been validated and sanitized window.location.href = url.toString(); } catch (e) { console.error('Failed to parse redirect URI:', e); setError('Invalid redirect URI'); } } else { console.error('Unauthorized redirect URI:', redirectUri); setError('Invalid redirect URI'); } } } } catch (error) { showErrorToast(error, toast); } finally { setIsSubmitting(false); } }; const toggleScope = (scope: string) => { // openid scope is always required if (scope === 'openid') return; const newSelected = new Set(selectedScopes); if (newSelected.has(scope)) { newSelected.delete(scope); } else { newSelected.add(scope); } setSelectedScopes(newSelected); }; const getScopeDescription = (scope: string): { name: string; description: string; icon: any } => { const scopeDescriptions: Record = { openid: { name: 'Basic Information', description: 'Your user ID (required)', icon: User, }, profile: { name: 'Profile Information', description: 'Your name, username, and profile picture', icon: User, }, email: { name: 'Email Address', description: 'Your email address and verification status', icon: Mail, }, }; return scopeDescriptions[scope] || { name: scope, description: `Access to ${scope} information`, icon: Shield, }; }; if (isLoading) { return (
); } if (error) { return (
Authorization Error
{error}
); } return ( <>
Authorize {application?.name} This application is requesting access to your account

Requested permissions:

{scopes.map(scope => { const scopeInfo = getScopeDescription(scope); const Icon = scopeInfo.icon; const isRequired = scope === 'openid'; return (
toggleScope(scope)} disabled={isRequired || isSubmitting} />

{scopeInfo.description}

); })}

You'll be redirected to {application?.type === 'web' ? 'the website' : 'the application'}

You can revoke access at any time in your account settings

); }