import { Button } from '@heroui/button'; import { CardBody, CardHeader } from '@heroui/card'; import { Image } from '@heroui/image'; import { Input } from '@heroui/input'; import { useLocalStorage } from '@uidotdev/usehooks'; import { useEffect, useState } from 'react'; import { toast } from 'react-hot-toast'; import { IoKeyOutline } from 'react-icons/io5'; import { useNavigate } from 'react-router-dom'; import key from '@/const/key'; import HoverEffectCard from '@/components/effect_card'; import { title } from '@/components/primitives'; import { ThemeSwitch } from '@/components/theme-switch'; import logo from '@/assets/images/logo.png'; import WebUIManager from '@/controllers/webui_manager'; import PureLayout from '@/layouts/pure'; export default function WebLoginPage () { const urlSearchParams = new URLSearchParams(window.location.search); const token = urlSearchParams.get('token'); const navigate = useNavigate(); const [tokenValue, setTokenValue] = useState(token || ''); const [isLoading, setIsLoading] = useState(false); const [isPasskeyLoading, setIsPasskeyLoading] = useState(true); // 初始为true,表示正在检查passkey const [, setLocalToken] = useLocalStorage(key.token, ''); // Helper function to decode base64url function base64UrlToUint8Array (base64Url: string): Uint8Array { const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); const binaryString = atob(base64); const bytes = new Uint8Array(binaryString.length); for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; } // Helper function to encode Uint8Array to base64url function uint8ArrayToBase64Url (uint8Array: Uint8Array): string { const base64 = btoa(String.fromCharCode(...uint8Array)); return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } // 自动检查并尝试passkey登录 const tryPasskeyLogin = async () => { try { // 检查是否有passkey const options = await WebUIManager.generatePasskeyAuthenticationOptions(); // 如果有passkey,自动进行认证 const credential = await navigator.credentials.get({ publicKey: { challenge: base64UrlToUint8Array(options.challenge) as BufferSource, allowCredentials: options.allowCredentials?.map((cred: any) => ({ id: base64UrlToUint8Array(cred.id) as BufferSource, type: cred.type, transports: cred.transports, })), userVerification: options.userVerification, }, }) as PublicKeyCredential; if (!credential) { throw new Error('Passkey authentication cancelled'); } // 准备响应进行验证 - 转换为base64url字符串格式 const authResponse = credential.response as AuthenticatorAssertionResponse; const response = { id: credential.id, rawId: uint8ArrayToBase64Url(new Uint8Array(credential.rawId)), response: { authenticatorData: uint8ArrayToBase64Url(new Uint8Array(authResponse.authenticatorData)), clientDataJSON: uint8ArrayToBase64Url(new Uint8Array(authResponse.clientDataJSON)), signature: uint8ArrayToBase64Url(new Uint8Array(authResponse.signature)), userHandle: authResponse.userHandle ? uint8ArrayToBase64Url(new Uint8Array(authResponse.userHandle)) : null, }, type: credential.type, }; // 验证认证 const data = await WebUIManager.verifyPasskeyAuthentication(response); if (data && data.Credential) { setLocalToken(data.Credential); navigate('/qq_login', { replace: true }); return true; // 登录成功 } } catch (error) { // passkey登录失败,继续显示token登录界面 console.log('Passkey login failed or not available:', error); } return false; // 登录失败 }; const onSubmit = async () => { if (!tokenValue) { toast.error('请输入token'); return; } setIsLoading(true); try { const data = await WebUIManager.loginWithToken(tokenValue); if (data) { setLocalToken(data); navigate('/qq_login', { replace: true }); } } catch (error) { toast.error((error as Error).message); } finally { setIsLoading(false); } }; // 处理全局键盘事件 const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Enter' && !isLoading && !isPasskeyLoading) { onSubmit(); } }; useEffect(() => { document.addEventListener('keydown', handleKeyDown); // 清理函数 return () => { document.removeEventListener('keydown', handleKeyDown); }; }, [tokenValue, isLoading, isPasskeyLoading]); // 依赖项包含用于登录的状态 useEffect(() => { // 如果URL中有token,直接登录 if (token) { onSubmit(); return; } // 否则尝试passkey自动登录 tryPasskeyLogin().finally(() => { setIsPasskeyLoading(false); }); }, []); return ( <> WebUI登录 - NapCat WebUI
logo
Web  Login 
{isPasskeyLoading && (
🔐 正在检查Passkey...
)}
{ e.preventDefault(); onSubmit(); }} > {/* 隐藏的用户名字段,帮助浏览器识别登录表单 */} } value={tokenValue} onChange={(e) => setTokenValue(e.target.value)} onClear={() => setTokenValue('')} />
💡 提示:请从 NapCat 启动日志中查看登录密钥
); }