aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/auth-page.js
diff options
context:
space:
mode:
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.js201
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>
+ `;
+}