import { html, useState, } from './vendor/htm-preact.js'; import { t } from './i18n.js'; import { hubFetch, navigate } from './hub-client.js'; import * as platform from './platform.js'; const PASSWORD_MIN_BITS = 60; const PASSWORD_MIN_LEN = 12; function passwordBits(pw) { if (!pw) return 0; let pool = 0; if (/[a-z]/.test(pw)) pool += 26; if (/[A-Z]/.test(pw)) pool += 26; if (/[0-9]/.test(pw)) pool += 10; if (/[^A-Za-z0-9]/.test(pw)) pool += 32; let bits = pw.length * Math.log2(pool || 1); const unique = new Set(pw).size; if (unique < pw.length / 2) bits *= 0.6; if (/^[0-9]+$/.test(pw)) bits *= 0.5; if (/(password|motdepasse|azerty|qwerty|123456|meshbay)/i.test(pw)) bits *= 0.3; return Math.round(bits); } export function FirstRunPage({ onSet }) { const [url, setUrl] = useState(''); const [error, setError] = useState(''); const [busy, setBusy] = useState(false); const submit = async (e) => { e.preventDefault(); setError(''); setBusy(true); try { await window.meshbay.setHubBase(url.trim()); onSet(); } catch (err) { setError(platform.bridgeMessage(err)); setBusy(false); } }; return html`

${t('firstrun.title')}

${t('firstrun.hint')}

setUrl(e.target.value)} />
${error && html`
${error}
`}

${t('firstrun.note')}

`; } export function LoginPage({ onLogin }) { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [error, setError] = useState(''); const [loading, setLoading] = useState(false); const [pendingVerif, setPendingVerif] = useState(false); const onSubmit = async (e) => { e.preventDefault(); if (!username || !password) return; setError(''); setPendingVerif(false); setLoading(true); try { await onLogin(username, password); navigate('/'); } catch (err) { if (err.message === 'email_verification_required') { setPendingVerif(true); } else { setError(err.message); } } finally { setLoading(false); } }; return html`

${t('login.title')}

setUsername(e.target.value)} autocomplete="username" required autofocus /> setPassword(e.target.value)} autocomplete="current-password" required /> ${error && html`
${error}
`} ${pendingVerif && html`

${t('login.pending_verification')}

`}
`; } export function RegisterPage() { const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); const [error, setError] = useState(''); const [phase, setPhase] = useState('form'); // form | verify | done const [loading, setLoading] = useState(false); const [code, setCode] = useState(''); const [verifying, setVerifying] = useState(false); const [resent, setResent] = useState(false); const onSubmit = async (e) => { e.preventDefault(); if (password !== confirm) { setError(t('register.err_mismatch')); return; } if (password.length < PASSWORD_MIN_LEN) { setError(t('register.err_min_len', { n: PASSWORD_MIN_LEN })); return; } if (passwordBits(password) < PASSWORD_MIN_BITS) { setError(t('register.err_too_weak')); return; } setError(''); setLoading(true); try { if (window.MeshBayKeys) { await window.MeshBayKeys.registerUser(username, email, password); } else { await hubFetch('/v1/users/register', { method: 'POST', body: { username, email, password, pk_user_ed25519: '', pk_user_x25519: '' }, }); } setPhase('verify'); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const onVerify = async (e) => { e.preventDefault(); if (!code.trim()) return; setError(''); setVerifying(true); try { await hubFetch('/v1/users/verify-email', { method: 'POST', body: { email, code: code.trim() }, }); setPhase('done'); } catch (err) { setError(err.message); } finally { setVerifying(false); } }; const onResend = async () => { setError(''); setResent(false); try { await hubFetch('/v1/users/register', { method: 'POST', body: { username, email, password, pk_user_ed25519: '', pk_user_x25519: '' }, }); setResent(true); } catch (err) { setError(err.message); } }; if (phase === 'done') { return html`

${t('register.verified_title')}

${t('register.verified_msg')}

${t('register.go_login')}
`; } if (phase === 'verify') { return html`

${t('register.success_title')}

${t('register.success_msg')}

setCode(e.target.value)} autocomplete="one-time-code" inputmode="numeric" maxlength="6" required autofocus style="text-align:center;font-size:1.4em;letter-spacing:0.3em" /> ${error && html`
${error}
`}
`; } return html`

${t('register.title')}

setUsername(e.target.value)} autocomplete="username" required /> setEmail(e.target.value)} autocomplete="email" required /> setPassword(e.target.value)} autocomplete="new-password" required minlength="8" /> ${password && html`

${t('register.strength', { bits: passwordBits(password) })}

`} setConfirm(e.target.value)} autocomplete="new-password" required /> ${error && html`
${error}
`}
`; }