码桶
发现社区成员的开源项目
Login.jsx36.8 KB
import React, { useState, useEffect } from 'react';
import { Globe, Server, User, Shield, Key, RefreshCw, Fingerprint, Languages, Zap } from 'lucide-react';
import { startAuthentication } from '@simplewebauthn/browser';
import { hashPassword, isPasswordStrong } from '../utils/auth.ts';
import SecurityBadges from './SecurityBadges.jsx';
const Login = ({ onLogin, t, lang, onLangChange }) => {
const [username, setUsername] = useState('admin');
const [password, setPassword] = useState('');
const [token, setToken] = useState('');
const [globalEmail, setGlobalEmail] = useState('');
const [globalKey, setGlobalKey] = useState('');
const [clientTokenType, setClientTokenType] = useState('api_token'); // 'api_token' | 'global_key'
const [loginTab, setLoginTab] = useState('server'); // 'server' | 'client' | 'setup' | 'register'
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const [successMsg, setSuccessMsg] = useState('');
const [remember, setRemember] = useState(false);
const [openRegistration, setOpenRegistration] = useState(false);
// TOTP 2FA state
const [totpStep, setTotpStep] = useState(false);
const [totpCode, setTotpCode] = useState('');
const [totpUsername, setTotpUsername] = useState('');
const [totpPassword, setTotpPassword] = useState('');
// Setup account fields
const [setupUsername, setSetupUsername] = useState('');
const [setupToken, setSetupToken] = useState('');
const [setupPassword, setSetupPassword] = useState('');
const [setupConfirm, setSetupConfirm] = useState('');
// Register fields
const [regUsername, setRegUsername] = useState('');
const [regPassword, setRegPassword] = useState('');
const [regConfirm, setRegConfirm] = useState('');
// Fetch public settings on mount
useEffect(() => {
fetch('/api/public-settings').then(r => r.json()).then(data => {
setOpenRegistration(!!data.openRegistration);
}).catch((err) => { console.error('Failed to fetch public settings:', err); });
}, []);
const supportsPasskey = typeof window !== 'undefined' && !!window.PublicKeyCredential;
const handlePasskeyLogin = async () => {
const passkeyUsername = username.trim().toLowerCase();
if (!passkeyUsername) {
setError(t('passkeyUsernameRequired'));
return;
}
setLoading(true);
setError('');
try {
const optRes = await fetch('/api/passkey/login-options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: passkeyUsername })
});
const optData = await optRes.json();
if (!optRes.ok) {
setError(optData.error || t('passkeyError'));
setLoading(false);
return;
}
const authResp = await startAuthentication({ optionsJSON: optData });
const verifyRes = await fetch('/api/passkey/login-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(authResp)
});
const verifyData = await verifyRes.json();
if (verifyRes.ok && verifyData.token) {
onLogin({
mode: 'server',
token: verifyData.token,
remember,
accounts: verifyData.accounts || [],
currentAccountIndex: verifyData.accounts?.[0]?.id || 0,
role: verifyData.role || 'user',
username: verifyData.username || passkeyUsername
});
} else {
setError(verifyData.error || t('passkeyError'));
}
} catch (err) {
if (err.name !== 'NotAllowedError') {
setError(t('passkeyError'));
}
} finally {
setLoading(false);
}
};
const handleLogin = async (e) => {
e.preventDefault();
const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1';
if (window.location.protocol === 'http:' && !isLocalhost) {
setError(t('httpWarning'));
return;
}
setLoading(true);
setError('');
setSuccessMsg('');
try {
if (loginTab === 'server') {
const hashedPassword = await hashPassword(password);
const res = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: username.trim() || 'admin', password: hashedPassword })
});
const data = await res.json();
if (res.ok) {
if (data.requiresTOTP) {
setTotpStep(true);
setTotpUsername(data.username);
setTotpPassword(hashedPassword);
setLoading(false);
return;
}
onLogin({
mode: 'server',
token: data.token,
refreshToken: data.refreshToken,
remember,
accounts: data.accounts || [],
currentAccountIndex: data.accounts?.[0]?.id || 0,
role: data.role || 'user',
username: data.username || 'admin'
});
} else {
let errMsg = data.error || t('loginFailed');
if (errMsg.includes('Invalid username or password')) errMsg = t('invalidPassword');
if (errMsg.includes('Server is not configured')) errMsg = t('serverNotConfigured');
if (data.lockedUntil || errMsg.includes('temporarily locked')) errMsg = t('accountLocked');
if (data.needsSetup) {
errMsg = t('needsSetup');
setLoginTab('setup');
setSetupUsername(username.trim());
}
setError(errMsg);
}
} else if (loginTab === 'client') {
const isGlobal = clientTokenType === 'global_key';
const verifyHeaders = isGlobal
? { 'X-Cloudflare-Token': globalKey, 'X-Cloudflare-Email': globalEmail }
: { 'X-Cloudflare-Token': token };
const res = await fetch('/api/verify-token', { headers: verifyHeaders });
const data = await res.json();
if (res.ok && data.success) {
const loginData = isGlobal
? { mode: 'client', token: globalKey, email: globalEmail, remember }
: { mode: 'client', token: token, remember };
onLogin(loginData);
} else {
let errMsg = data.message || t('loginFailed');
if (errMsg === 'Invalid token') errMsg = t('invalidToken');
if (errMsg === 'No token provided') errMsg = t('tokenRequired');
if (errMsg === 'Failed to verify token') errMsg = t('verifyFailed');
setError(errMsg);
}
}
} catch (err) {
setError(t('errorOccurred'));
} finally {
setLoading(false);
}
};
const handleSetupAccount = async (e) => {
e.preventDefault();
if (!setupUsername.trim() || !setupToken.trim() || !setupPassword.trim()) return;
if (setupPassword !== setupConfirm) {
setError(t('passwordMismatch'));
return;
}
if (!isPasswordStrong(setupPassword)) {
setError(t('passwordTooWeak'));
return;
}
setLoading(true);
setError('');
setSuccessMsg('');
try {
const hashedPwd = await hashPassword(setupPassword);
const res = await fetch('/api/setup-account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: setupUsername.trim().toLowerCase(), setupToken: setupToken.trim(), password: hashedPwd })
});
const data = await res.json();
if (res.ok && data.success) {
setSuccessMsg(t('setupSuccess'));
setSetupUsername('');
setSetupToken('');
setSetupPassword('');
setSetupConfirm('');
setTimeout(() => { setLoginTab('server'); setSuccessMsg(''); }, 2000);
} else {
setError(data.error || t('setupFailed'));
}
} catch (err) {
setError(t('errorOccurred'));
} finally {
setLoading(false);
}
};
const handleRegister = async (e) => {
e.preventDefault();
if (!regUsername.trim() || !regPassword.trim()) return;
if (regPassword !== regConfirm) {
setError(t('passwordMismatch'));
return;
}
if (!isPasswordStrong(regPassword)) {
setError(t('passwordTooWeak'));
return;
}
setLoading(true);
setError('');
setSuccessMsg('');
try {
const hashedPwd = await hashPassword(regPassword);
const res = await fetch('/api/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: regUsername.trim().toLowerCase(), password: hashedPwd })
});
const data = await res.json();
if (res.ok && data.success) {
setSuccessMsg(t('registerSuccess'));
setRegUsername('');
setRegPassword('');
setRegConfirm('');
setTimeout(() => { setLoginTab('server'); setSuccessMsg(''); }, 2000);
} else {
setError(data.error || t('registerFailed'));
}
} catch (err) {
setError(t('errorOccurred'));
} finally {
setLoading(false);
}
};
const handleTOTPVerify = async (e) => {
e.preventDefault();
if (!totpCode.trim()) return;
setLoading(true);
setError('');
try {
const res = await fetch('/api/verify-totp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: totpUsername, password: totpPassword, code: totpCode.trim() })
});
const data = await res.json();
if (res.ok && data.token) {
onLogin({
mode: 'server',
token: data.token,
refreshToken: data.refreshToken,
remember,
accounts: data.accounts || [],
currentAccountIndex: data.accounts?.[0]?.id || 0,
role: data.role || 'user',
username: data.username || totpUsername
});
} else {
setError(data.error || t('totpInvalid'));
}
} catch (err) {
setError(t('errorOccurred'));
} finally {
setLoading(false);
}
};
return (
<div className="container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', padding: '1rem' }}>
<div className="glass-card login-card fade-in">
<div style={{ position: 'absolute', top: '1rem', right: '1rem', zIndex: 10 }}>
<button
onClick={(e) => {
e.preventDefault();
const cycle = ['zh', 'en', 'ja', 'ko'];
const idx = cycle.indexOf(lang);
onLangChange(cycle[(idx + 1) % cycle.length]);
}}
style={{ border: 'none', background: 'transparent', padding: '4px 8px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '4px', color: 'var(--text-muted)', borderRadius: '8px', transition: 'background 0.2s', fontSize: '0.75rem', fontWeight: 600 }}
onMouseEnter={(e) => e.currentTarget.style.background = 'var(--hover-btn-bg)'}
onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
title={{ zh: 'English', en: '日本語', ja: '한국어', ko: '中文' }[lang]}
aria-label={{ zh: 'Switch to English', en: '日本語に切り替え', ja: '한국어로 전환', ko: '切换到中文' }[lang]}
>
<Languages size={18} />
<span>{lang.toUpperCase()}</span>
</button>
</div>
<div style={{ textAlign: 'center', marginBottom: '2rem' }}>
<div style={{ display: 'inline-flex', padding: '0.75rem', background: 'rgba(243, 128, 32, 0.1)', borderRadius: '12px', marginBottom: '1rem' }}>
<Zap size={32} color="var(--primary)" />
</div>
<h1 style={{ fontSize: '1.5rem', marginBottom: '0.25rem' }}>{t('title')}</h1>
<p style={{ color: 'var(--text-muted)', fontSize: '0.875rem' }}>{t('subtitle')}</p>
</div>
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.5rem', padding: '4px', background: 'var(--tab-bg)', borderRadius: '8px' }}>
<button
className={`btn ${loginTab === 'server' ? 'btn-primary' : 'btn-outline'}`}
style={{ flex: 1, padding: '0.4rem', border: 'none', fontSize: '0.8rem' }}
onClick={() => { setLoginTab('server'); setError(''); setSuccessMsg(''); setTotpStep(false); setTotpCode(''); }}
>
{t('serverMode')}
</button>
<button
className={`btn ${loginTab === 'client' ? 'btn-primary' : 'btn-outline'}`}
style={{ flex: 1, padding: '0.4rem', border: 'none', fontSize: '0.8rem' }}
onClick={() => { setLoginTab('client'); setError(''); setSuccessMsg(''); setTotpStep(false); setTotpCode(''); }}
>
{t('clientMode')}
</button>
{openRegistration ? (
<button
className={`btn ${loginTab === 'register' ? 'btn-primary' : 'btn-outline'}`}
style={{ flex: 1, padding: '0.4rem', border: 'none', fontSize: '0.8rem' }}
onClick={() => { setLoginTab('register'); setError(''); setSuccessMsg(''); setTotpStep(false); setTotpCode(''); }}
>
{t('register')}
</button>
) : (
<button
className={`btn ${loginTab === 'setup' ? 'btn-primary' : 'btn-outline'}`}
style={{ flex: 1, padding: '0.4rem', border: 'none', fontSize: '0.8rem' }}
onClick={() => { setLoginTab('setup'); setError(''); setSuccessMsg(''); setTotpStep(false); setTotpCode(''); }}
>
{t('setupAccount')}
</button>
)}
</div>
{totpStep ? (
<form onSubmit={handleTOTPVerify}>
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<div style={{ display: 'inline-flex', padding: '0.75rem', background: 'rgba(147, 51, 234, 0.1)', borderRadius: '12px', marginBottom: '0.75rem' }}>
<Shield size={28} color="#9333ea" />
</div>
<h3 style={{ fontSize: '1rem', marginBottom: '0.25rem' }}>{t('totpStep')}</h3>
<p style={{ fontSize: '0.8rem', color: 'var(--text-muted)' }}>{t('totpRequired')}</p>
</div>
<div className="input-group">
<div style={{ position: 'relative' }}>
<Shield size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={6}
placeholder={t('totpCodePlaceholder')}
value={totpCode}
onChange={(e) => setTotpCode(e.target.value.replace(/\D/g, '').slice(0, 6))}
style={{ paddingLeft: '38px', textAlign: 'center', fontSize: '1.2rem', letterSpacing: '0.3em' }}
autoFocus
required
/>
</div>
</div>
{error && <p style={{ color: 'var(--error)', fontSize: '0.75rem', marginBottom: '1rem', textAlign: 'center' }}>{error}</p>}
<button className="btn btn-primary" style={{ width: '100%', justifyContent: 'center' }} disabled={loading || totpCode.length !== 6}>
{loading ? <RefreshCw className="spin" size={18} /> : t('totpVerify')}
</button>
<button type="button" className="btn btn-outline" style={{ width: '100%', justifyContent: 'center', marginTop: '0.5rem' }}
onClick={() => { setTotpStep(false); setTotpCode(''); setError(''); }}>
{t('cancel') || 'Back'}
</button>
</form>
) : loginTab === 'register' ? (
<form onSubmit={handleRegister}>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '1rem', textAlign: 'center' }}>
{t('registerDesc')}
</p>
<div className="input-group">
<label>{t('usernameLabel')}</label>
<div style={{ position: 'relative' }}>
<User size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="text" placeholder={t('usernamePlaceholder')} value={regUsername}
onChange={(e) => setRegUsername(e.target.value)} style={{ paddingLeft: '38px' }} required />
</div>
</div>
<div className="input-group">
<label>{t('newPassword')}</label>
<div style={{ position: 'relative' }}>
<Key size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="password" placeholder={t('newPasswordPlaceholder2')} value={regPassword}
onChange={(e) => setRegPassword(e.target.value)} style={{ paddingLeft: '38px' }} required />
</div>
</div>
<div className="input-group">
<label>{t('confirmNewPassword')}</label>
<div style={{ position: 'relative' }}>
<Key size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="password" placeholder={t('confirmNewPasswordPlaceholder')} value={regConfirm}
onChange={(e) => setRegConfirm(e.target.value)} style={{ paddingLeft: '38px' }} required />
</div>
</div>
{error && <p style={{ color: 'var(--error)', fontSize: '0.75rem', marginBottom: '1rem', textAlign: 'center' }}>{error}</p>}
{successMsg && <p style={{ color: 'var(--success)', fontSize: '0.75rem', marginBottom: '1rem', textAlign: 'center' }}>{successMsg}</p>}
<button className="btn btn-primary" style={{ width: '100%', justifyContent: 'center' }} disabled={loading}>
{loading ? <RefreshCw className="spin" size={18} /> : t('register')}
</button>
</form>
) : loginTab === 'setup' ? (
<form onSubmit={handleSetupAccount}>
<p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: '1rem', textAlign: 'center' }}>
{t('setupAccountDesc')}
</p>
<div className="input-group">
<label>{t('usernameLabel')}</label>
<div style={{ position: 'relative' }}>
<User size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="text" placeholder={t('usernamePlaceholder')} value={setupUsername}
onChange={(e) => setSetupUsername(e.target.value)} style={{ paddingLeft: '38px' }} required />
</div>
</div>
<div className="input-group">
<label>{t('setupTokenLabel')}</label>
<div style={{ position: 'relative' }}>
<Shield size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="text" placeholder={t('setupTokenPlaceholder')} value={setupToken}
onChange={(e) => setSetupToken(e.target.value)} style={{ paddingLeft: '38px', fontFamily: 'monospace', fontSize: '0.8rem' }} required />
</div>
<p style={{ fontSize: '0.7rem', marginTop: '0.35rem', color: 'var(--text-muted)', fontStyle: 'italic' }}>
{t('tokenRequiredPermissions')}
</p>
</div>
<div className="input-group">
<label>{t('newPassword')}</label>
<div style={{ position: 'relative' }}>
<Key size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="password" placeholder={t('newPasswordPlaceholder2')} value={setupPassword}
onChange={(e) => setSetupPassword(e.target.value)} style={{ paddingLeft: '38px' }} required />
</div>
</div>
<div className="input-group">
<label>{t('confirmNewPassword')}</label>
<div style={{ position: 'relative' }}>
<Key size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="password" placeholder={t('confirmNewPasswordPlaceholder')} value={setupConfirm}
onChange={(e) => setSetupConfirm(e.target.value)} style={{ paddingLeft: '38px' }} required />
</div>
</div>
{error && <p style={{ color: 'var(--error)', fontSize: '0.75rem', marginBottom: '1rem', textAlign: 'center' }}>{error}</p>}
{successMsg && <p style={{ color: 'var(--success)', fontSize: '0.75rem', marginBottom: '1rem', textAlign: 'center' }}>{successMsg}</p>}
<button className="btn btn-primary" style={{ width: '100%', justifyContent: 'center' }} disabled={loading}>
{loading ? <RefreshCw className="spin" size={18} /> : t('setupAccount')}
</button>
</form>
) : (
<form onSubmit={handleLogin}>
{loginTab === 'server' ? (
<>
<div className="input-group">
<label>{t('usernameLabel')}</label>
<div style={{ position: 'relative' }}>
<User size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input
type="text"
placeholder={t('usernamePlaceholder')}
value={username}
onChange={(e) => setUsername(e.target.value)}
style={{ paddingLeft: '38px' }}
required
/>
</div>
</div>
<div className="input-group">
<label>{t('passwordLabel')}</label>
<div style={{ position: 'relative' }}>
<Key size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input
type="password"
placeholder={t('passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
style={{ paddingLeft: '38px' }}
required
/>
</div>
<p style={{ fontSize: '0.75rem', marginTop: '0.5rem', color: 'var(--text-muted)' }}>
{t('serverHint')}
</p>
</div>
</>
) : (
<div>
{/* API Token / Global API Key sub-tabs */}
<div className="auth-type-tabs" style={{ display: 'flex', gap: '0', marginBottom: '1rem', borderRadius: '6px', overflow: 'hidden', border: '1px solid var(--border)' }}>
<button type="button"
onClick={() => { setClientTokenType('api_token'); setError(''); }}
style={{
flex: 1, padding: '0.5rem 0.5rem', fontSize: '0.75rem', fontWeight: 600,
border: 'none', cursor: 'pointer',
background: clientTokenType === 'api_token' ? 'var(--primary)' : 'transparent',
color: clientTokenType === 'api_token' ? 'white' : 'var(--text-muted)',
}}>
{t('apiTokenTab')}
</button>
<button type="button"
onClick={() => { setClientTokenType('global_key'); setError(''); }}
style={{
flex: 1, padding: '0.5rem 0.5rem', fontSize: '0.75rem', fontWeight: 600,
border: 'none', borderLeft: '1px solid var(--border)', cursor: 'pointer',
background: clientTokenType === 'global_key' ? 'var(--primary)' : 'transparent',
color: clientTokenType === 'global_key' ? 'white' : 'var(--text-muted)',
}}>
{t('globalKeyTab')}
</button>
</div>
{clientTokenType === 'api_token' ? (
<div className="input-group">
<label>{t('tokenLabel')}</label>
<div style={{ position: 'relative' }}>
<Shield size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="password" placeholder={t('tokenPlaceholder')} value={token}
onChange={(e) => setToken(e.target.value)} style={{ paddingLeft: '38px' }} required />
</div>
<p style={{ fontSize: '0.7rem', marginTop: '0.35rem', color: 'var(--text-muted)', fontStyle: 'italic' }}>
{t('tokenRequiredPermissions')}
</p>
<p style={{ fontSize: '0.7rem', marginTop: '0.25rem', color: 'var(--text-muted)' }}>
{t('tokenHint')}
</p>
</div>
) : (
<>
<div style={{ padding: '0.5rem 0.75rem', borderRadius: '6px', background: 'rgba(245, 158, 11, 0.08)', border: '1px solid rgba(245, 158, 11, 0.25)', marginBottom: '0.75rem', fontSize: '0.7rem', color: '#b45309', lineHeight: 1.5 }}>
{t('globalKeyWarning')}
</div>
<div className="input-group">
<label>{t('globalKeyEmailLabel')}</label>
<div style={{ position: 'relative' }}>
<User size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="email" placeholder={t('globalKeyEmailPlaceholder')} value={globalEmail}
onChange={(e) => setGlobalEmail(e.target.value)} style={{ paddingLeft: '38px' }} required />
</div>
</div>
<div className="input-group">
<label>{t('globalKeyLabel')}</label>
<div style={{ position: 'relative' }}>
<Key size={16} style={{ position: 'absolute', left: '12px', top: '12px', color: 'var(--text-muted)' }} />
<input type="password" placeholder={t('globalKeyPlaceholder')} value={globalKey}
onChange={(e) => setGlobalKey(e.target.value)} style={{ paddingLeft: '38px' }} required />
</div>
</div>
<p style={{ fontSize: '0.7rem', color: 'var(--text-muted)', marginBottom: '0.5rem' }}>
{t('globalKeyHint')}{' '}
<a href="https://dash.cloudflare.com/profile/api-tokens" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--primary)' }}>
dash.cloudflare.com/profile/api-tokens
</a>
</p>
</>
)}
</div>
)}
{error && <p style={{ color: 'var(--error)', fontSize: '0.75rem', marginBottom: '1rem', textAlign: 'center' }}>{error}</p>}
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '1.5rem' }}>
<input
type="checkbox"
id="remember"
checked={remember}
onChange={(e) => setRemember(e.target.checked)}
style={{ width: '16px', height: '16px', cursor: 'pointer' }}
/>
<label htmlFor="remember" style={{ fontSize: '0.875rem', color: 'var(--text-muted)', cursor: 'pointer', userSelect: 'none' }}>
{loginTab === 'server' ? t('rememberMe') : t('rememberToken')}
</label>
</div>
<button className="btn btn-primary" style={{ width: '100%', justifyContent: 'center' }} disabled={loading}>
{loading ? <RefreshCw className="spin" size={18} /> : t('loginBtn')}
</button>
{loginTab === 'server' && supportsPasskey && (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', margin: '1rem 0' }}>
<div style={{ flex: 1, height: '1px', background: 'var(--border)' }}></div>
<span style={{ fontSize: '0.7rem', color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>or</span>
<div style={{ flex: 1, height: '1px', background: 'var(--border)' }}></div>
</div>
<button
type="button"
className="btn btn-outline"
style={{ width: '100%', justifyContent: 'center', display: 'flex', alignItems: 'center', gap: '8px' }}
disabled={loading}
onClick={handlePasskeyLogin}
>
<Fingerprint size={18} />
{t('passkeyLoginBtn')}
</button>
</>
)}
</form>
)}
{/* Security Badges */}
<div style={{ marginTop: '1.5rem', paddingTop: '1rem', borderTop: '1px solid var(--border)' }}>
<SecurityBadges t={t} />
<div style={{ textAlign: 'center', marginTop: '0.75rem' }}>
<a
href="https://github.com/C-NF/cloudflare-dns-manager"
target="_blank"
rel="noopener noreferrer"
style={{ display: 'inline-flex', alignItems: 'center', gap: '5px', fontSize: '0.7rem', color: 'var(--text-muted)', textDecoration: 'none', opacity: 0.6, transition: 'opacity 0.2s' }}
onMouseEnter={e => e.currentTarget.style.opacity = '1'}
onMouseLeave={e => e.currentTarget.style.opacity = '0.6'}
>
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
GitHub
</a>
</div>
</div>
</div>
</div>
);
};
export default Login;