global update and global fix
This commit is contained in:
@@ -82,6 +82,16 @@ export default function LoginPage() {
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const finishLoginRedirect = useCallback(() => {
|
||||
const redirectAfterLogin =
|
||||
typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('redirect') : null;
|
||||
if (redirectAfterLogin && redirectAfterLogin.startsWith('/')) {
|
||||
router.push(redirectAfterLogin);
|
||||
return;
|
||||
}
|
||||
router.push('/');
|
||||
}, [router]);
|
||||
|
||||
const { identifyLogin, sendLoginOtp, verifyLoginOtp, loginWithPassword, loginWithLdap, beginTotpLogin, verifyTotpLogin, completePin, applyLoginAuth } = useAuth();
|
||||
|
||||
const { showToast } = useToast();
|
||||
@@ -174,7 +184,7 @@ export default function LoginPage() {
|
||||
|
||||
} else {
|
||||
|
||||
router.push('/');
|
||||
finishLoginRedirect();
|
||||
|
||||
}
|
||||
|
||||
@@ -596,7 +606,7 @@ export default function LoginPage() {
|
||||
|
||||
await completePin(pendingSessionId, pin);
|
||||
|
||||
router.push('/');
|
||||
finishLoginRedirect();
|
||||
|
||||
} catch (error) {
|
||||
|
||||
|
||||
150
apps/frontend/app/auth/oauth/authorize/page.tsx
Normal file
150
apps/frontend/app/auth/oauth/authorize/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
'use client';
|
||||
|
||||
import { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Loader2, ShieldCheck } from 'lucide-react';
|
||||
import { BrandLogo } from '@/components/id/brand-logo';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
function OAuthAuthorizeContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { user, token, isPinLocked, isLoading } = useAuth();
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const oauthQuery = useMemo(() => {
|
||||
const params = new URLSearchParams();
|
||||
searchParams.forEach((value, key) => params.set(key, value));
|
||||
return params;
|
||||
}, [searchParams]);
|
||||
|
||||
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';
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
if (!clientId || !redirectUri) return;
|
||||
if (user && token && !isPinLocked) return;
|
||||
const returnUrl = `/auth/oauth/authorize?${oauthQuery.toString()}`;
|
||||
router.replace(`/auth/login?redirect=${encodeURIComponent(returnUrl)}`);
|
||||
}, [clientId, isLoading, isPinLocked, oauthQuery, redirectUri, router, token, user]);
|
||||
|
||||
const approve = useCallback(async () => {
|
||||
if (!token || !user) return;
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
const apiBase = (process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3000').replace(/\/$/, '');
|
||||
const url = new URL(`${apiBase}/oauth/authorize`);
|
||||
oauthQuery.forEach((value, key) => url.searchParams.set(key, value));
|
||||
url.searchParams.set('userId', user.id);
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
Accept: 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = (await response.json().catch(() => null)) as { message?: string | string[] } | null;
|
||||
const message = Array.isArray(payload?.message) ? payload.message.join(', ') : payload?.message;
|
||||
throw new Error(message || 'Не удалось подтвердить доступ');
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { redirectUrl?: string };
|
||||
if (!data.redirectUrl) {
|
||||
throw new Error('Сервер не вернул redirect URL');
|
||||
}
|
||||
window.location.href = data.redirectUrl;
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Ошибка OAuth авторизации');
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}, [oauthQuery, token, user]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-[#667085]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!clientId || !redirectUri) {
|
||||
return (
|
||||
<div className="mx-auto max-w-md px-4 py-16 text-center">
|
||||
<h1 className="text-xl font-semibold">Некорректный OAuth запрос</h1>
|
||||
<p className="mt-3 text-sm text-[#667085]">Отсутствуют обязательные параметры client_id и redirect_uri.</p>
|
||||
<Link href="/" className="mt-6 inline-block text-[#3390ec] hover:underline">
|
||||
На главную
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user || !token || isPinLocked) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-[#667085]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex min-h-[70vh] max-w-lg flex-col justify-center px-4 py-12">
|
||||
<div className="mb-8 flex justify-center">
|
||||
<BrandLogo />
|
||||
</div>
|
||||
<div className="rounded-[24px] border border-[#eceef4] bg-white p-6 shadow-sm">
|
||||
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-2xl bg-[#eef4ff] text-[#3390ec]">
|
||||
<ShieldCheck className="h-6 w-6" />
|
||||
</div>
|
||||
<h1 className="text-2xl font-semibold">Разрешить доступ?</h1>
|
||||
<p className="mt-2 text-sm leading-relaxed text-[#667085]">
|
||||
Приложение <span className="font-medium text-[#1f2430]">{clientId}</span> запрашивает доступ к вашему аккаунту Lendry ID.
|
||||
</p>
|
||||
<div className="mt-4 space-y-2 rounded-2xl bg-[#f4f5f8] p-4 text-sm">
|
||||
<p>
|
||||
<span className="text-[#667085]">Пользователь:</span> {user.displayName}
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-[#667085]">Scopes:</span> {scope}
|
||||
</p>
|
||||
<p className="break-all">
|
||||
<span className="text-[#667085]">Redirect URI:</span> {redirectUri}
|
||||
</p>
|
||||
</div>
|
||||
{error ? <p className="mt-4 text-sm text-red-600">{error}</p> : null}
|
||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||
<Button className="flex-1 rounded-xl" disabled={submitting} onClick={() => void approve()}>
|
||||
{submitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
|
||||
Разрешить
|
||||
</Button>
|
||||
<Button variant="outline" className="flex-1 rounded-xl" disabled={submitting} onClick={() => router.push('/')}>
|
||||
Отмена
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function OAuthAuthorizePage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-[#667085]" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<OAuthAuthorizeContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user