scMedia/public/assets/js/login.js
2026-01-16 22:53:04 +01:00

212 lines
6.4 KiB
JavaScript

// public/assets/login.js
/* English comments: login + MFA flow */
const qs = (id) => document.getElementById(id);
const i18n = window.I18N || {};
const t = (key, fallback) => {
if (key && Object.prototype.hasOwnProperty.call(i18n, key)) {
const val = i18n[key];
return val !== '' ? val : fallback;
}
return fallback;
};
const errorKeyByMessage = {
'Email and password required': 'auth.error.email_password_required',
'Invalid credentials': 'auth.error.invalid_credentials',
'Email required': 'auth.error.email_required',
'Request failed': 'auth.error.request_failed',
'Code required': 'auth.error.code_required',
'Invalid code': 'auth.error.invalid_code',
};
function normalizeErrorMessage(message, fallbackKey, fallbackText) {
if (message && Object.prototype.hasOwnProperty.call(errorKeyByMessage, message)) {
return t(errorKeyByMessage[message], message);
}
return message || t(fallbackKey, fallbackText);
}
function showError(msg) {
const el = qs('loginError');
if (!el) return;
el.textContent = msg;
el.classList.remove('is-hidden');
}
function clearError() {
if (retryTimer) {
clearInterval(retryTimer);
retryTimer = null;
}
const el = qs('loginError');
if (!el) return;
el.textContent = '';
el.classList.add('is-hidden');
}
let retryTimer = null;
let retryUntilTs = 0;
let retryShown = null;
function formatRetryMessage(secondsLeft) {
return t('auth.error.too_many_attempts', 'Too many attempts. Try again in {seconds}s.')
.replace('{seconds}', String(secondsLeft));
}
function showRetryError(seconds) {
const el = qs('loginError');
if (!el) return;
const now = Date.now();
retryUntilTs = now + Math.max(0, seconds) * 1000;
retryShown = null;
el.textContent = formatRetryMessage(Math.max(0, Math.ceil((retryUntilTs - now) / 1000)));
el.classList.remove('is-hidden');
if (retryTimer) {
clearInterval(retryTimer);
}
retryTimer = setInterval(() => {
const remaining = Math.max(0, Math.ceil((retryUntilTs - Date.now()) / 1000));
if (retryShown === null) {
retryShown = remaining;
} else if (retryShown > remaining) {
// Smoothly converge if timer drifts or tab throttles.
retryShown = Math.max(remaining, retryShown - 1);
} else {
retryShown = remaining;
}
el.textContent = formatRetryMessage(retryShown);
if (remaining <= 0) {
clearInterval(retryTimer);
retryTimer = null;
}
}, 250);
}
async function login() {
clearError();
const email = qs('loginEmail')?.value || '';
const password = qs('loginPassword')?.value || '';
const remember = !!qs('loginRemember')?.checked;
if (!email || !password) {
showError(t('auth.error.email_password_required', 'Email and password required'));
return;
}
const res = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password, client_type: 'web' }),
});
const data = await res.json();
if (!data?.ok) {
const retry = data?.error?.retry_after;
if (retry) {
showRetryError(retry);
} else {
const msg = normalizeErrorMessage(data?.error?.message || '', 'auth.error.login_failed', 'Login failed');
showError(msg);
}
return;
}
if (data?.data?.mfa_required) {
window.__mfaChallenge = data?.data?.challenge_token || '';
window.__rememberSession = remember;
qs('loginForm')?.classList.add('is-hidden');
qs('mfaForm')?.classList.remove('is-hidden');
return;
}
window.Auth.setTokens(data?.data || {}, 'web', remember);
window.location.href = '/';
}
function showForgot() {
qs('loginForm')?.classList.add('is-hidden');
qs('mfaForm')?.classList.add('is-hidden');
qs('forgotForm')?.classList.remove('is-hidden');
}
function showLoginForm() {
qs('forgotForm')?.classList.add('is-hidden');
qs('mfaForm')?.classList.add('is-hidden');
qs('loginForm')?.classList.remove('is-hidden');
}
async function sendForgot() {
clearError();
const email = qs('forgotEmail')?.value || '';
if (!email) {
showError(t('auth.error.email_required', 'Email required'));
return;
}
const res = await fetch('/api/auth/password/forgot', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json().catch(() => null);
if (!data?.ok) {
showError(normalizeErrorMessage(data?.error?.message || '', 'auth.error.request_failed', 'Request failed'));
return;
}
showError(t('auth.error.reset_sent', 'If this email exists, a reset link will be sent.'));
}
async function verifyMfa() {
clearError();
const code = qs('mfaCode')?.value || '';
const challenge = window.__mfaChallenge || '';
const remember = window.__rememberSession !== false;
if (!code || !challenge) {
showError(t('auth.error.code_required', 'Code required'));
return;
}
const res = await fetch('/api/auth/2fa/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ challenge_token: challenge, code, client_type: 'web' }),
});
const data = await res.json();
if (!data?.ok) {
const retry = data?.error?.retry_after;
if (retry) {
showRetryError(retry);
} else {
const msg = normalizeErrorMessage(data?.error?.message || '', 'auth.error.invalid_code', 'Invalid code');
showError(msg);
}
return;
}
window.Auth.setTokens(data?.data || {}, 'web', remember);
window.location.href = '/';
}
function init() {
const access = window.Auth?.getAccessToken?.() || '';
const refresh = window.Auth?.getRefreshToken?.() || '';
if (access && refresh && window.Auth?.refreshTokens) {
window.Auth.refreshTokens().then((ok) => {
if (ok) {
window.location.href = '/';
} else if (window.Auth?.clearTokens) {
window.Auth.clearTokens();
}
});
return;
}
if (access && window.Auth?.clearTokens) {
window.Auth.clearTokens();
}
const loginBtn = qs('btnLogin');
if (loginBtn) loginBtn.addEventListener('click', login);
const forgotBtn = qs('btnForgot');
if (forgotBtn) forgotBtn.addEventListener('click', showForgot);
const forgotSend = qs('btnForgotSend');
if (forgotSend) forgotSend.addEventListener('click', sendForgot);
const forgotBack = qs('btnForgotBack');
if (forgotBack) forgotBack.addEventListener('click', showLoginForm);
const verifyBtn = qs('btnMfaVerify');
if (verifyBtn) verifyBtn.addEventListener('click', verifyMfa);
}
init();