'use client'; import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import Link from 'next/link'; import { useRouter, useSearchParams } from 'next/navigation'; import { CheckCircle2, Loader2, ShieldCheck } from 'lucide-react'; import { BrandLogo } from '@/components/id/brand-logo'; import { useAuth } from '@/components/id/auth-provider'; import { usePublicSettings } from '@/components/id/public-settings-provider'; import { Button } from '@/components/ui/button'; import { approveOAuthAuthorization, checkOAuthConsent, fetchOAuthClientPublicInfo, type OAuthConsentCheckResponse } from '@/lib/api'; import { deliverOAuthPopupResult, parsePopupOAuthParams, postOneTapResult } from '@/lib/oauth-popup-bridge'; function buildOAuthQuery(searchParams: URLSearchParams) { const params = new URLSearchParams(); searchParams.forEach((value, key) => { if (key !== 'userId') params.set(key, value); }); return params; } function OAuthAuthorizeContent() { const searchParams = useSearchParams(); const router = useRouter(); const { user, token, isPinLocked, isLoading } = useAuth(); const { projectName } = usePublicSettings(); const [submitting, setSubmitting] = useState(false); const [checkingConsent, setCheckingConsent] = useState(false); const [error, setError] = useState(null); const [consentInfo, setConsentInfo] = useState(null); const [clientName, setClientName] = useState(null); const autoApproveStartedRef = useRef(false); const oauthQuery = useMemo(() => buildOAuthQuery(searchParams), [searchParams]); const popupContext = useMemo(() => parsePopupOAuthParams(searchParams), [searchParams]); const isPopupMode = Boolean(popupContext); const clientId = searchParams.get('client_id') ?? searchParams.get('clientId'); const redirectUri = searchParams.get('redirect_uri') ?? searchParams.get('redirectUri'); const scope = searchParams.get('scope') ?? 'openid profile'; const state = searchParams.get('state'); const returnUrl = useMemo(() => `/auth/oauth/authorize?${oauthQuery.toString()}`, [oauthQuery]); const clientLabel = clientName ?? consentInfo?.client?.name ?? 'Приложение'; const approve = useCallback(async () => { if (!token || !user || isPinLocked) return; setSubmitting(true); setError(null); try { const data = await approveOAuthAuthorization(oauthQuery, token); if (!data.redirectUrl) { throw new Error('Сервер не вернул redirect URL'); } if (deliverOAuthPopupResult(popupContext, data.redirectUrl) === 'popup') { return; } window.location.href = data.redirectUrl; } catch (err) { setError(err instanceof Error ? err.message : 'Ошибка OAuth авторизации'); autoApproveStartedRef.current = false; } finally { setSubmitting(false); } }, [isPinLocked, oauthQuery, popupContext, token, user]); useEffect(() => { if (!searchParams.has('userId')) return; router.replace(returnUrl); }, [returnUrl, router, searchParams]); useEffect(() => { if (isLoading) return; if (!clientId || !redirectUri) return; if (isPinLocked) return; if (user && token) return; router.replace(`/auth/login?redirect=${encodeURIComponent(returnUrl)}`); }, [clientId, isLoading, isPinLocked, redirectUri, returnUrl, router, token, user]); useEffect(() => { if (isLoading || !token || !clientId || isPinLocked) return; let cancelled = false; setCheckingConsent(true); setError(null); void (async () => { try { try { const info = await fetchOAuthClientPublicInfo(clientId); if (!cancelled && info.name.trim()) { setClientName(info.name.trim()); } } catch { // название приложения необязательно для consent } const result = await checkOAuthConsent(oauthQuery, token); if (cancelled) return; setConsentInfo(result); if (result.client?.name?.trim()) { setClientName(result.client.name.trim()); } if (result.granted && !autoApproveStartedRef.current) { autoApproveStartedRef.current = true; void approve(); } } catch (err) { if (!cancelled) { setError(err instanceof Error ? err.message : 'Не удалось проверить согласие'); } } finally { if (!cancelled) setCheckingConsent(false); } })(); return () => { cancelled = true; }; }, [approve, clientId, isLoading, isPinLocked, oauthQuery, token]); const cancel = useCallback(() => { if (popupContext) { const delivered = postOneTapResult(popupContext.popupOrigin, { error: 'access_denied', errorDescription: 'Пользователь отклонил запрос' }); if (delivered) return; } if (!redirectUri) { router.push('/'); return; } try { const url = new URL(redirectUri); url.searchParams.set('error', 'access_denied'); url.searchParams.set('error_description', 'Пользователь отклонил запрос'); if (state) url.searchParams.set('state', state); window.location.href = url.toString(); } catch { router.push('/'); } }, [popupContext, redirectUri, router, state]); const authReady = Boolean(user && token && !isPinLocked && !isLoading); const scopeItems = consentInfo?.requestedScopes ?? []; if (isLoading || (clientId && redirectUri && !authReady && !isPinLocked)) { return (
); } if (isPinLocked) { return (

Подтвердите PIN-код, чтобы продолжить авторизацию приложения.

); } if (!clientId || !redirectUri) { return (

Некорректный OAuth запрос

Отсутствуют обязательные параметры client_id и redirect_uri.

На главную
); } if (checkingConsent || (consentInfo?.granted && submitting)) { return (

Продолжаем вход в {clientLabel}…

); } return (
{!isPopupMode ? (
) : null}

Разрешить доступ?

Приложение {clientLabel} запрашивает доступ к данным вашего аккаунта {projectName}.

Пользователь: {user?.displayName}

Запрашиваемые данные:

    {scopeItems.length > 0 ? ( scopeItems.map((item) => (
  • {item.name}
    {item.description ?
    {item.description}
    : null}
  • )) ) : (
  • {scope}
  • )}

После подтверждения доступ сохранится, и повторно спрашивать не будем. Отозвать его можно в разделе «Данные → Доступы к данным».

{error ?

{error}

: null}
); } export default function OAuthAuthorizePage() { return ( } > ); }