diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-08-31 01:06:06 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-08-31 01:06:06 +0200 |
| commit | 0c2754d2d4cb1905d5e324f56fbc64e4afae526f (patch) | |
| tree | c081859830e017af09af9d2f73c6ff8442aacce4 /packages/meshbay-hub/src/meshbay_hub/static/auth-page.js | |
| parent | f6e80064ff8c4869a6ba2cef908cc54326948463 (diff) | |
| download | meshbay-0c2754d2d4cb1905d5e324f56fbc64e4afae526f.tar.gz | |
refactor(ui): extract Explore, Login, Register and CreateGroup from app.js
ExplorePage → explore-page.js (static import), LoginPage/RegisterPage/
FirstRunPage → auth-page.js (static import, LoginPage receives onLogin
as a prop), CreateGroupPage/wizard → create-group-page.js (lazy-loaded
via dynamic import(), same pattern as AdminPage/NodePage).
app.js goes from 1803 to 914 lines. webapp.py _ASSETS extended with the
three new files and the previously missing extracted pages. Test fixtures
updated to follow the moved components.
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 | 201 |
1 files changed, 201 insertions, 0 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 new file mode 100644 index 0000000..1cca56f --- /dev/null +++ b/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js @@ -0,0 +1,201 @@ +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` + <div class="page-center"> + <div class="card login-card"> + <h2>${t('firstrun.title')}</h2> + <p class="settings-hint" style="margin-bottom:16px">${t('firstrun.hint')}</p> + <form onSubmit=${submit}> + <input type="text" placeholder="https://meshbay.org" required + autofocus value=${url} + onInput=${e => setUrl(e.target.value)} /> + <button type="submit" disabled=${busy}> + ${busy ? t('firstrun.checking') : t('firstrun.btn')} + </button> + </form> + ${error && html`<div class="error-msg">${error}</div>`} + <p class="settings-hint" style="margin-top:16px">${t('firstrun.note')}</p> + </div> + </div> + `; +} + +export function LoginPage({ onLogin }) { + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const onSubmit = async (e) => { + e.preventDefault(); + if (!username || !password) return; + setError(''); + setLoading(true); + try { + await onLogin(username, password); + navigate('/'); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + return html` + <div class="page-center"> + <div class="card login-card"> + <h2>${t('login.title')}</h2> + <form onSubmit=${onSubmit}> + <input type="text" placeholder="${t('login.username')}" value=${username} + onInput=${e => setUsername(e.target.value)} + autocomplete="username" required autofocus /> + <input type="password" placeholder="${t('login.password')}" value=${password} + onInput=${e => setPassword(e.target.value)} + autocomplete="current-password" required /> + ${error && html`<div class="error-msg">${error}</div>`} + <button type="submit" disabled=${loading}> + ${loading ? t('login.loading') : t('login.submit')} + </button> + </form> + <div class="login-footer"> + ${t('login.no_account')} <a href="#/register">${t('login.register_link')}</a> + </div> + </div> + </div> + `; +} + +export function RegisterPage() { + const [username, setUsername] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirm, setConfirm] = useState(''); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(false); + const [loading, setLoading] = 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: '' }, + }); + } + setSuccess(true); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + if (success) { + return html` + <div class="page-center"> + <div class="card login-card"> + <h2>${t('register.success_title')}</h2> + <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> + </div> + </div> + `; + } + + return html` + <div class="page-center"> + <div class="card login-card"> + <h2>${t('register.title')}</h2> + <form onSubmit=${onSubmit}> + <input type="text" placeholder="${t('register.username')}" value=${username} + onInput=${e => setUsername(e.target.value)} + autocomplete="username" required /> + <input type="email" placeholder="${t('register.email')}" value=${email} + onInput=${e => setEmail(e.target.value)} + autocomplete="email" required /> + <input type="password" placeholder="${t('register.password')}" value=${password} + onInput=${e => setPassword(e.target.value)} + autocomplete="new-password" required minlength="8" /> + ${password && html` + <div style="margin:-4px 0 10px"> + <div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden"> + <div style=${`height:100%;width:${Math.min(100, passwordBits(password) / 100 * 100)}%; + background:${passwordBits(password) < PASSWORD_MIN_BITS ? 'var(--error)' + : passwordBits(password) < 80 ? 'var(--yellow, #f59e0b)' : 'var(--success)'}`}></div> + </div> + <p style="font-size:0.8em;color:var(--text-dim);margin-top:4px"> + ${t('register.strength', { bits: passwordBits(password) })} + </p> + </div> + `} + <input type="password" placeholder="${t('register.confirm')}" value=${confirm} + onInput=${e => setConfirm(e.target.value)} + autocomplete="new-password" required /> + ${error && html`<div class="error-msg">${error}</div>`} + <button type="submit" disabled=${loading}> + ${loading ? t('register.loading') : t('register.submit')} + </button> + </form> + <div class="login-footer"> + ${t('register.has_account')} <a href="#/login">${t('register.login_link')}</a> + </div> + </div> + </div> + `; +} |