diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-31 17:19:17 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-31 17:19:17 +0200 |
| commit | c6fd7ea89b6e0a96eb1d81989de891b4768b1044 (patch) | |
| tree | 12e869044c80c889f83b588210cc0ac500cd7a6a /packages/meshbay-hub/src/meshbay_hub/static/auth-page.js | |
| parent | f4c6628c8e85513d9fd110ead95682368a15a0fd (diff) | |
| download | meshbay-c6fd7ea89b6e0a96eb1d81989de891b4768b1044.tar.gz | |
feat: email verification for registration, email change, and invitations
Registration now creates a pending account and sends a 6-digit code via
email; the account activates only after verification. Email changes on
the profile page follow the same flow. Group invitations send a
notification email to the invitee (without revealing their address to
the inviter) containing the invite code and hub link.
Backend: blind HMAC-SHA256 email index for uniqueness without decryption,
mail.py for localhost Postfix delivery, verification endpoints, cleanup
of expired codes and stale pending accounts, startup backfill of
email_hash for existing users.
Frontend: 3-phase register page, inline email change verification on
profile, invite-notify call with status display. All 10 locales updated.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/auth-page.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/auth-page.js | 86 |
1 files changed, 81 insertions, 5 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js index 1cca56f..860f890 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js @@ -68,16 +68,23 @@ export function LoginPage({ onLogin }) { 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) { - setError(err.message); + if (err.message === 'email_verification_required') { + setPendingVerif(true); + } else { + setError(err.message); + } } finally { setLoading(false); } @@ -95,6 +102,11 @@ export function LoginPage({ onLogin }) { onInput=${e => setPassword(e.target.value)} autocomplete="current-password" required /> ${error && html`<div class="error-msg">${error}</div>`} + ${pendingVerif && html` + <div class="error-msg" style="background:var(--bg-secondary);border-left:3px solid var(--yellow, #f59e0b)"> + <p>${t('login.pending_verification')}</p> + </div> + `} <button type="submit" disabled=${loading}> ${loading ? t('login.loading') : t('login.submit')} </button> @@ -113,8 +125,11 @@ export function RegisterPage() { const [password, setPassword] = useState(''); const [confirm, setConfirm] = useState(''); const [error, setError] = useState(''); - const [success, setSuccess] = useState(false); + 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(); @@ -136,7 +151,7 @@ export function RegisterPage() { body: { username, email, password, pk_user_ed25519: '', pk_user_x25519: '' }, }); } - setSuccess(true); + setPhase('verify'); } catch (err) { setError(err.message); } finally { @@ -144,7 +159,53 @@ export function RegisterPage() { } }; - if (success) { + 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` + <div class="page-center"> + <div class="card login-card"> + <h2>${t('register.verified_title')}</h2> + <p style="text-align:center; margin-bottom:16px; color:var(--text-secondary)"> + ${t('register.verified_msg')} + </p> + <a href="#/login" style="display:block; text-align:center">${t('register.go_login')}</a> + </div> + </div> + `; + } + + if (phase === 'verify') { return html` <div class="page-center"> <div class="card login-card"> @@ -152,7 +213,22 @@ export function RegisterPage() { <p style="text-align:center; margin-bottom:16px; color:var(--text-secondary)"> ${t('register.success_msg')} </p> - <a href="#/login" style="display:block; text-align:center">${t('register.go_login')}</a> + <form onSubmit=${onVerify}> + <input type="text" placeholder="${t('register.code_placeholder')}" + value=${code} onInput=${e => 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`<div class="error-msg">${error}</div>`} + <button type="submit" disabled=${verifying}> + ${verifying ? t('register.verifying') : t('register.verify_btn')} + </button> + </form> + <div class="login-footer"> + <button class="link-btn" onClick=${onResend}>${t('register.resend')}</button> + ${resent && html`<span style="color:var(--success);margin-left:8px"> + ${t('register.resend_sent')}</span>`} + </div> </div> </div> `; |