import { html, useState, useEffect, useRef, useCallback, } from './vendor/htm-preact.js'; import { t } from './i18n.js'; import { hubFetch, navigate, session, HUB, loadAuth, _storeRecoveryKey, } from './hub-client.js'; import * as platform from './platform.js'; const PASSWORD_MIN_BITS = 60; const PASSWORD_MIN_LEN = 12; // ── reCAPTCHA v2 helper ────────────────────────────────────────────────────── let _captchaSiteKey = null; let _captchaKeyFetched = false; async function fetchCaptchaSiteKey() { if (_captchaKeyFetched) return _captchaSiteKey; try { const info = await hubFetch('/v1/hub/info'); _captchaSiteKey = info.captcha_site_key || null; } catch { _captchaSiteKey = null; } _captchaKeyFetched = true; return _captchaSiteKey; } function loadRecaptchaScript() { if (document.getElementById('recaptcha-script')) return; const s = document.createElement('script'); s.id = 'recaptcha-script'; s.src = 'https://www.google.com/recaptcha/api.js?render=explicit'; s.async = true; s.defer = true; document.head.appendChild(s); } function useCaptcha() { const [siteKey, setSiteKey] = useState(_captchaSiteKey); const [token, setToken] = useState(null); const containerRef = useRef(null); const widgetId = useRef(null); useEffect(() => { fetchCaptchaSiteKey().then(k => { if (k) { setSiteKey(k); loadRecaptchaScript(); } }); }, []); useEffect(() => { if (!siteKey || !containerRef.current) return; const poll = setInterval(() => { if (window.grecaptcha && window.grecaptcha.render && widgetId.current === null) { clearInterval(poll); widgetId.current = window.grecaptcha.render(containerRef.current, { sitekey: siteKey, callback: (tk) => setToken(tk), 'expired-callback': () => setToken(null), theme: document.documentElement.getAttribute('data-theme') === 'dark' ? 'dark' : 'light', }); } }, 100); return () => clearInterval(poll); }, [siteKey]); const reset = useCallback(() => { if (widgetId.current !== null && window.grecaptcha) { window.grecaptcha.reset(widgetId.current); setToken(null); } }, []); const widget = siteKey ? html`
` : null; return { token, widget, reset, enabled: !!siteKey }; } 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.hint')}
${error && html`${t('firstrun.note')}
${t('register.recovery_intro')}
${recoveryMnemonic}
${emailRecovery ? t('register.recovery_emailed') : t('register.recovery_not_emailed')}
${t('register.recovery_warning')}
${t('register.success_msg')}
${t('reset.working')} ${progress && progress.total ? ` (${progress.done}/${progress.total})` : ''}
${t('reset.signin_restored')}
${error && html`${t('reset.no_recovery')}
`} ${phase === 'done' && !error && stragglers.length === 0 && html`${t('reset.groups_restored')}
`} ${stragglers.length > 0 && html`${t('reset.needs_operator')}
${t('reset.request_intro')}
`} ${phase === 'form' && html`${t('reset.form_intro')}
`}