import { useEffect, useState, type FormEvent } from 'react' import { useNavigate, useSearch } from '@tanstack/react-router' import { useQueryClient } from '@tanstack/react-query' import { EyeIcon, EyeOffIcon, FingerprintIcon } from 'lucide-react' import { browserSupportsWebAuthn, browserSupportsWebAuthnAutofill, startAuthentication, WebAuthnAbortService, } from '@simplewebauthn/browser' import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/browser' import { buildSsoRedirectUrl, isPortalOidcAuthorizeUrl, isReturnToAllowed, type LoginResponse, } from '@authportal/shared' import { Button } from '@authportal/ui/components/button' import { Field, FieldGroup, FieldLabel } from '@authportal/ui/components/field' import { Input } from '@authportal/ui/components/input' import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, } from '@authportal/ui/components/input-group' import { Separator } from '@authportal/ui/components/separator' import { Alert, AlertDescription, AlertTitle, } from '@/components/reui/alert' import { ensureAuthConfig, setToken } from '@/lib/auth' import { ApiError } from '@/lib/api-client' import { login, meQueryKey } from '@/queries/auth' import { webauthnLogin, webauthnLoginOptions } from '@/queries/webauthn' import { AuthLogo } from '@/components/auth-logo' export function PortalLoginForm() { const navigate = useNavigate() const queryClient = useQueryClient() const search = useSearch({ from: '/' }) as { return_to?: string } const [showPassword, setShowPassword] = useState(false) const [error, setError] = useState(null) const [pending, setPending] = useState(false) const [passkeySupported, setPasskeySupported] = useState(false) async function applySession(res: LoginResponse) { setToken(res.access_token) queryClient.setQueryData(meQueryKey, res.user) const returnTo = search.return_to const { returnToAllowlist: allowlist, issuer } = await ensureAuthConfig() if (returnTo && isReturnToAllowed(returnTo, allowlist)) { if (isPortalOidcAuthorizeUrl(returnTo, issuer)) { window.location.href = returnTo return } window.location.href = buildSsoRedirectUrl( returnTo, res.access_token, res.expires_at, ) return } if (res.user.is_admin) { await navigate({ to: '/admin' }) } else { await navigate({ to: '/apps' }) } } async function runPasskeyLogin() { const { challenge_id, options } = await webauthnLoginOptions() const assertion = await startAuthentication({ optionsJSON: options as unknown as PublicKeyCredentialRequestOptionsJSON, }) const res = await webauthnLogin(challenge_id, assertion, search.return_to) await applySession(res) } useEffect(() => { if (!browserSupportsWebAuthn()) return setPasskeySupported(true) let cancelled = false async function startConditional() { if (!(await browserSupportsWebAuthnAutofill())) return try { const { challenge_id, options } = await webauthnLoginOptions() if (cancelled) return const assertion = await startAuthentication({ optionsJSON: options as unknown as PublicKeyCredentialRequestOptionsJSON, useBrowserAutofill: true, }) if (cancelled) return setPending(true) setError(null) const res = await webauthnLogin( challenge_id, assertion, search.return_to, ) await applySession(res) } catch { /* abort / unsupported / user dismissed */ } finally { if (!cancelled) setPending(false) } } void startConditional() return () => { cancelled = true WebAuthnAbortService.cancelCeremony() } // Login page mount only — return_to is stable for the visit. // eslint-disable-next-line react-hooks/exhaustive-deps }, []) async function handleSubmit(event: FormEvent) { event.preventDefault() WebAuthnAbortService.cancelCeremony() setError(null) setPending(true) const form = new FormData(event.currentTarget) const email = String(form.get('email') ?? '') const password = String(form.get('password') ?? '') try { const res = await login(email, password, search.return_to) await applySession(res) } catch (err) { setError(err instanceof ApiError ? err.message : 'Не удалось войти') } finally { setPending(false) } } async function handlePasskeyClick() { WebAuthnAbortService.cancelCeremony() setError(null) setPending(true) try { await runPasskeyLogin() } catch (err) { setError( err instanceof ApiError ? err.message : 'Не удалось войти с passkey', ) } finally { setPending(false) } } return (

Auth Portal

Единый вход в приложения shnt.top

{error ? ( Ошибка входа {error} ) : null}
Email Пароль setShowPassword((v) => !v)} > {showPassword ? : }
{passkeySupported ? (
или
) : null}
) }