Вход по почте (magic-code через SMTP)

- /api/auth/email/{start,verify}: 6-значный код на email (TTL 10мин, лимит
  1/мин, до 5 попыток), аккаунт email:<addr> + cookie-сессия
- _send_email (smtplib SSL/STARTTLS), таблица WEB_EMAIL_CODES
- Фронт: пункт «Через почту» в меню + модалка (email → код → вход), i18n

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
root
2026-07-03 09:08:08 +00:00
parent 836784e56f
commit b0d2580c37
2 changed files with 231 additions and 0 deletions

View File

@@ -865,6 +865,10 @@
<span style="display:inline-flex;width:22px;height:18px;background:#07f;color:#fff;border-radius:4px;align-items:center;justify-content:center;font-weight:700;font-size:11px;">VK</span>
<span data-i18n="login_via_vk">Через VK</span>
</button>
<button onclick="closeLoginMenu();openEmailLogin();" style="display:flex;align-items:center;gap:8px;width:100%;padding:9px 12px;background:none;border:none;border-radius:8px;cursor:pointer;font-family:inherit;font-size:14px;color:#1c2b45;text-align:left;">
<span style="display:inline-flex;width:22px;height:18px;background:#64748b;color:#fff;border-radius:4px;align-items:center;justify-content:center;font-weight:700;font-size:12px;"></span>
<span data-i18n="login_via_email">Через почту</span>
</button>
</div>
<div id="tg-user-info" style="display:none;">
<img id="tg-user-photo" src="" alt="" onerror="this.style.display='none'">
@@ -874,6 +878,26 @@
</div>
</header>
<!-- Модалка входа по почте -->
<div id="email-login-overlay" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:100;align-items:center;justify-content:center;padding:16px;">
<div style="background:#fff;border-radius:14px;max-width:340px;width:100%;padding:22px;box-shadow:0 10px 40px rgba(0,0,0,.25);">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<strong style="font-size:17px;color:#1c2b45" data-i18n="email_login_title">Вход по почте</strong>
<button onclick="closeEmailLogin()" style="background:none;border:none;font-size:22px;line-height:1;cursor:pointer;color:#94a3b8">&times;</button>
</div>
<div id="email-step-1">
<input type="email" id="email-login-input" data-i18n-ph="email_ph" placeholder="ваш email" style="width:100%;padding:10px 12px;border:1px solid #d0d7e2;border-radius:8px;font-size:15px;font-family:inherit;margin-bottom:10px">
<button id="email-send-btn" onclick="emailSendCode()" style="width:100%;padding:11px;background:var(--blue,#007AFF);color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit" data-i18n="email_send_code">Получить код</button>
</div>
<div id="email-step-2" style="display:none">
<p style="font-size:13px;color:var(--text-secondary);margin:0 0 10px" data-i18n="email_code_sent">Код отправлен на почту. Введите его:</p>
<input type="text" id="email-code-input" inputmode="numeric" maxlength="6" placeholder="000000" style="width:100%;padding:10px 12px;border:1px solid #d0d7e2;border-radius:8px;font-size:20px;letter-spacing:6px;text-align:center;font-family:inherit;margin-bottom:10px">
<button id="email-verify-btn" onclick="emailVerifyCode()" style="width:100%;padding:11px;background:#10b981;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit" data-i18n="email_login_btn">Войти</button>
</div>
<div id="email-login-msg" style="font-size:12px;color:#e05555;margin-top:10px;min-height:16px"></div>
</div>
</div>
<main>
<!-- Description -->
@@ -1163,6 +1187,14 @@
login_via_tg: 'Через Telegram',
login_via_yandex: 'Через Яндекс',
login_via_vk: 'Через VK',
login_via_email: 'Через почту',
email_login_title: 'Вход по почте',
email_ph: 'ваш email',
email_send_code: 'Получить код',
email_code_sent: 'Код отправлен на почту. Введите его:',
email_login_btn: 'Войти',
email_sending: 'Отправляем код…',
email_bad_code: 'Неверный код',
err_login: 'Не удалось войти. Попробуйте снова.',
logout_btn: 'Выйти',
page_description: '🧷 Выберите тип сшивки и при необходимости дополнительные настройки, затем загрузите файл — получите готовый скан.',
@@ -1277,6 +1309,14 @@
login_via_tg: 'Via Telegram',
login_via_yandex: 'Via Yandex',
login_via_vk: 'Via VK',
login_via_email: 'Via email',
email_login_title: 'Email login',
email_ph: 'your email',
email_send_code: 'Get code',
email_code_sent: 'Code sent to your email. Enter it:',
email_login_btn: 'Log in',
email_sending: 'Sending code…',
email_bad_code: 'Invalid code',
err_login: 'Login failed. Please try again.',
logout_btn: 'Logout',
page_description: '🧷 Select binding type and optional settings, then upload your file — get a ready scan.',
@@ -1392,6 +1432,14 @@
login_via_tg: 'Über Telegram',
login_via_yandex: 'Über Yandex',
login_via_vk: 'Über VK',
login_via_email: 'Per E-Mail',
email_login_title: 'E-Mail-Anmeldung',
email_ph: 'Ihre E-Mail',
email_send_code: 'Code erhalten',
email_code_sent: 'Code an Ihre E-Mail gesendet. Eingeben:',
email_login_btn: 'Anmelden',
email_sending: 'Code wird gesendet…',
email_bad_code: 'Ungültiger Code',
err_login: 'Anmeldung fehlgeschlagen. Bitte erneut versuchen.',
logout_btn: 'Abmelden',
page_description: '🧷 Wählen Sie den Bindungstyp und optionale Einstellungen, laden Sie dann Ihre Datei hoch — erhalten Sie einen fertigen Scan.',
@@ -1507,6 +1555,14 @@
login_via_tg: 'Via Telegram',
login_via_yandex: 'Via Yandex',
login_via_vk: 'Via VK',
login_via_email: 'Par e-mail',
email_login_title: 'Connexion par e-mail',
email_ph: 'votre e-mail',
email_send_code: 'Recevoir le code',
email_code_sent: 'Code envoyé par e-mail. Saisissez-le :',
email_login_btn: 'Se connecter',
email_sending: 'Envoi du code…',
email_bad_code: 'Code invalide',
err_login: 'Échec de connexion. Réessayez.',
logout_btn: 'Déconnexion',
page_description: '🧷 Sélectionnez le type de reliure et les paramètres optionnels, puis téléchargez votre fichier — obtenez un scan prêt à l\'emploi.',
@@ -1622,6 +1678,14 @@
login_via_tg: 'Via Telegram',
login_via_yandex: 'Via Yandex',
login_via_vk: 'Via VK',
login_via_email: 'Via email',
email_login_title: 'Accesso via email',
email_ph: 'la tua email',
email_send_code: 'Ricevi codice',
email_code_sent: 'Codice inviato via email. Inseriscilo:',
email_login_btn: 'Accedi',
email_sending: 'Invio codice…',
email_bad_code: 'Codice non valido',
err_login: 'Accesso non riuscito. Riprova.',
logout_btn: 'Esci',
page_description: '🧷 Seleziona il tipo di rilegatura e le impostazioni opzionali, poi carica il file — ottieni una scansione pronta.',
@@ -1737,6 +1801,14 @@
login_via_tg: 'Con Telegram',
login_via_yandex: 'Con Yandex',
login_via_vk: 'Con VK',
login_via_email: 'Por correo',
email_login_title: 'Acceso por correo',
email_ph: 'tu correo',
email_send_code: 'Obtener código',
email_code_sent: 'Código enviado a tu correo. Ingrésalo:',
email_login_btn: 'Entrar',
email_sending: 'Enviando código…',
email_bad_code: 'Código inválido',
err_login: 'Error de inicio de sesión. Inténtalo de nuevo.',
logout_btn: 'Cerrar sesión',
page_description: '🧷 Selecciona el tipo de encuadernación y configuraciones opcionales, luego sube tu archivo — obtén un escaneo listo.',
@@ -1852,6 +1924,14 @@
login_via_tg: 'Via Telegram',
login_via_yandex: 'Via Yandex',
login_via_vk: 'Via VK',
login_via_email: 'Por e-mail',
email_login_title: 'Entrar por e-mail',
email_ph: 'seu e-mail',
email_send_code: 'Receber código',
email_code_sent: 'Código enviado ao seu e-mail. Digite:',
email_login_btn: 'Entrar',
email_sending: 'Enviando código…',
email_bad_code: 'Código inválido',
err_login: 'Falha no login. Tente novamente.',
logout_btn: 'Sair',
page_description: '🧷 Selecione o tipo de encadernação e configurações opcionais, depois faça upload do arquivo — obtenha uma digitalização pronta.',
@@ -2305,6 +2385,46 @@
if (sessionUser) loadHistory();
}
// ── Вход по почте (код) ───────────────────────────────────────────────────
let _emailForCode = '';
function openEmailLogin() {
document.getElementById('email-step-1').style.display = 'block';
document.getElementById('email-step-2').style.display = 'none';
document.getElementById('email-login-msg').textContent = '';
document.getElementById('email-login-input').value = '';
document.getElementById('email-code-input').value = '';
document.getElementById('email-login-overlay').style.display = 'flex';
}
function closeEmailLogin() { document.getElementById('email-login-overlay').style.display = 'none'; }
async function emailSendCode() {
const email = (document.getElementById('email-login-input').value || '').trim();
const msg = document.getElementById('email-login-msg');
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) { msg.textContent = t('pay_need_email'); return; }
const btn = document.getElementById('email-send-btn'); btn.disabled = true; msg.textContent = t('email_sending');
try {
const r = await fetch('/api/auth/email/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }) });
const d = await r.json();
if (r.ok) {
_emailForCode = email;
document.getElementById('email-step-1').style.display = 'none';
document.getElementById('email-step-2').style.display = 'block';
msg.textContent = ''; document.getElementById('email-code-input').focus();
} else { msg.textContent = d.detail || t('err_login'); btn.disabled = false; }
} catch (e) { msg.textContent = t('err_login'); btn.disabled = false; }
}
async function emailVerifyCode() {
const code = (document.getElementById('email-code-input').value || '').trim();
const msg = document.getElementById('email-login-msg');
if (!/^\d{6}$/.test(code)) { msg.textContent = t('email_bad_code'); return; }
const btn = document.getElementById('email-verify-btn'); btn.disabled = true; msg.textContent = '';
try {
const r = await fetch('/api/auth/email/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: _emailForCode, code }) });
const d = await r.json();
if (r.ok) { closeEmailLogin(); loadMe(); }
else { msg.textContent = d.detail || t('email_bad_code'); btn.disabled = false; }
} catch (e) { msg.textContent = t('err_login'); btn.disabled = false; }
}
// ── Авторизация через бота ────────────────────────────────────────────────
let _authPollInterval = null;
let _authToken = null;