fix and update
This commit is contained in:
@@ -4,11 +4,11 @@
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { FormEvent, useCallback, useEffect, useState } from 'react';
|
||||
import { FormEvent, Suspense, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { ChevronLeft, Download, KeyRound, Mail, Network, Phone, QrCode, ShieldCheck, ShieldQuestion, UserRound } from 'lucide-react';
|
||||
import { ChevronLeft, Download, KeyRound, Loader2, Mail, Network, Phone, QrCode, ShieldCheck, ShieldQuestion, UserRound } from 'lucide-react';
|
||||
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
|
||||
@@ -33,10 +33,9 @@ import { PhoneInput, phoneCountries, toE164 } from '@/components/ui/phone-input'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
|
||||
import type { AuthTokens, LoginMethod, OtpChannel, OtpSendResponse } from '@/lib/api';
|
||||
import { AUTH_TOKEN_KEY, claimQrLoginSession, createQrLoginSession, fetchFedcmSessionStatus, pollQrLoginSession } from '@/lib/api';
|
||||
|
||||
import { createQrLoginSession, fetchFedcmSessionStatus, pollQrLoginSession } from '@/lib/api';
|
||||
|
||||
import { isFedcmLoginPopup } from '@/lib/fedcm-login-bridge';
|
||||
import { isFedcmLoginPopup, finalizeFedcmLogin } from '@/lib/fedcm-login-bridge';
|
||||
|
||||
import {
|
||||
clearOtpResendAvailableAt,
|
||||
@@ -48,7 +47,7 @@ import {
|
||||
|
||||
|
||||
|
||||
type Step = 'identify' | 'otp' | 'otpChannel' | 'password' | 'pin' | 'ldap' | 'qr' | 'totp';
|
||||
type Step = 'identify' | 'otp' | 'otpChannel' | 'password' | 'pin' | 'ldap' | 'qr' | 'qrLink' | 'totp';
|
||||
|
||||
|
||||
|
||||
@@ -83,8 +82,23 @@ const channelLabel: Record<string, string> = {
|
||||
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<main className="flex min-h-screen items-center justify-center bg-[#1f2128] text-white">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-[#b9bdc9]" />
|
||||
</main>
|
||||
}
|
||||
>
|
||||
<LoginPageContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function LoginPageContent() {
|
||||
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const finishLoginRedirect = useCallback(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -269,9 +283,18 @@ export default function LoginPage() {
|
||||
|
||||
void (async () => {
|
||||
const status = await fetchFedcmSessionStatus(publicApiUrl);
|
||||
if (cancelled || !status?.active || !status.requiresPin || !status.sessionId) return;
|
||||
setPendingSessionId(status.sessionId);
|
||||
setStep('pin');
|
||||
if (cancelled || !status?.active) return;
|
||||
|
||||
if (status.requiresPin && status.sessionId) {
|
||||
setPendingSessionId(status.sessionId);
|
||||
setStep('pin');
|
||||
return;
|
||||
}
|
||||
|
||||
const token = window.localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
if (token) {
|
||||
await finalizeFedcmLogin(token);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
@@ -279,6 +302,34 @@ export default function LoginPage() {
|
||||
};
|
||||
}, [publicApiUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
const qrLink = searchParams.get('qrLink');
|
||||
if (!qrLink || qrSessionId) return;
|
||||
|
||||
let cancelled = false;
|
||||
setQrLoading(true);
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const claimed = await claimQrLoginSession(qrLink);
|
||||
if (cancelled) return;
|
||||
setQrSessionId(qrLink);
|
||||
setQrExpiresAt(claimed.expiresAt);
|
||||
setStep('qrLink');
|
||||
} catch (error) {
|
||||
if (!cancelled) {
|
||||
showToast(error instanceof Error ? error.message : 'Не удалось подключить устройство по QR-коду');
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) setQrLoading(false);
|
||||
}
|
||||
})();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [qrSessionId, searchParams, showToast]);
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 'otp' || !recipient) return;
|
||||
const saved = readOtpResendAvailableAt(recipient);
|
||||
@@ -319,7 +370,7 @@ export default function LoginPage() {
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (step !== 'qr' || !qrSessionId) return;
|
||||
if ((step !== 'qr' && step !== 'qrLink') || !qrSessionId) return;
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
@@ -335,7 +386,13 @@ export default function LoginPage() {
|
||||
|
||||
if (session.status === 'EXPIRED') {
|
||||
|
||||
showToast('QR-код истёк, создайте новый');
|
||||
showToast(step === 'qrLink' ? 'QR-код истёк. Отсканируйте новый код на основном устройстве.' : 'QR-код истёк, создайте новый');
|
||||
|
||||
if (step === 'qrLink') {
|
||||
setStep('identify');
|
||||
setQrSessionId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setStep('identify');
|
||||
|
||||
@@ -1383,6 +1440,34 @@ export default function LoginPage() {
|
||||
|
||||
|
||||
|
||||
{step === 'qrLink' ? (
|
||||
|
||||
<div className="text-center">
|
||||
|
||||
<Loader2 className="mx-auto h-8 w-8 animate-spin text-white" />
|
||||
|
||||
<p className="mt-5 text-sm leading-relaxed text-[#b9bdc9]">
|
||||
|
||||
Подтверждаем подключение устройства. Дождитесь автоматического входа в аккаунт.
|
||||
|
||||
</p>
|
||||
|
||||
{qrExpiresAt ? (
|
||||
|
||||
<p className="mt-2 text-xs text-[#8f92a0]">
|
||||
|
||||
QR-код действует до {new Intl.DateTimeFormat('ru-RU', { hour: '2-digit', minute: '2-digit', second: '2-digit' }).format(new Date(qrExpiresAt))}
|
||||
|
||||
</p>
|
||||
|
||||
) : null}
|
||||
|
||||
</div>
|
||||
|
||||
) : null}
|
||||
|
||||
|
||||
|
||||
{step === 'pin' && pendingSessionId ? (
|
||||
|
||||
<form onSubmit={handlePinSubmit} className="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user