Вход по почте (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:
111
app/main.py
111
app/main.py
@@ -66,6 +66,13 @@ SESSION_DAYS = 30
|
|||||||
VK_CLIENT_ID = os.getenv("VK_CLIENT_ID", "")
|
VK_CLIENT_ID = os.getenv("VK_CLIENT_ID", "")
|
||||||
VK_CLIENT_SECRET = os.getenv("VK_CLIENT_SECRET", "")
|
VK_CLIENT_SECRET = os.getenv("VK_CLIENT_SECRET", "")
|
||||||
VK_REDIRECT = f"{os.getenv('SITE_BASE_URL', 'https://pdf2scan.online')}/api/auth/vk/callback"
|
VK_REDIRECT = f"{os.getenv('SITE_BASE_URL', 'https://pdf2scan.online')}/api/auth/vk/callback"
|
||||||
|
# ── Вход по почте (magic-code через SMTP) ─────────────────────────────────────
|
||||||
|
SMTP_HOST = os.getenv("SMTP_HOST", "")
|
||||||
|
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
||||||
|
SMTP_USER = os.getenv("SMTP_USER", "")
|
||||||
|
SMTP_PASS = os.getenv("SMTP_PASS", "")
|
||||||
|
SMTP_FROM = os.getenv("SMTP_FROM", "") or SMTP_USER
|
||||||
|
EMAIL_CODE_TTL = 600 # 10 минут на ввод кода
|
||||||
|
|
||||||
# Чанковая загрузка (туннель схлопывает крупные передачи — грузим кусками ≤6 МБ)
|
# Чанковая загрузка (туннель схлопывает крупные передачи — грузим кусками ≤6 МБ)
|
||||||
UPLOAD_DIR = f"{BOT_TEMP}uploads/" # временные .part-файлы
|
UPLOAD_DIR = f"{BOT_TEMP}uploads/" # временные .part-файлы
|
||||||
@@ -280,6 +287,15 @@ def _db_ensure_auth_table():
|
|||||||
expires_at REAL
|
expires_at REAL
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
|
# Коды для входа по почте (magic-code).
|
||||||
|
con.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS WEB_EMAIL_CODES (
|
||||||
|
email TEXT PRIMARY KEY,
|
||||||
|
code TEXT,
|
||||||
|
created_at REAL,
|
||||||
|
attempts INTEGER DEFAULT 0
|
||||||
|
)
|
||||||
|
""")
|
||||||
# Платежи ЮKassa. status: pending/succeeded/canceled. purpose: donate/api_pro/file/sub.
|
# Платежи ЮKassa. status: pending/succeeded/canceled. purpose: donate/api_pro/file/sub.
|
||||||
con.execute("""
|
con.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS WEB_PAYMENTS (
|
CREATE TABLE IF NOT EXISTS WEB_PAYMENTS (
|
||||||
@@ -1996,6 +2012,101 @@ async def vk_callback(request: Request, code: str = "", state: str = "", device_
|
|||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
# ── Вход по почте (magic-code) ────────────────────────────────────────────────
|
||||||
|
import smtplib as _smtplib
|
||||||
|
import ssl as _ssl
|
||||||
|
from email.mime.text import MIMEText as _MIMEText
|
||||||
|
|
||||||
|
_EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
|
||||||
|
_email_rate: dict = {} # email → ts последней отправки (rate-limit)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_email(to: str, subject: str, body: str):
|
||||||
|
if not (SMTP_HOST and SMTP_USER and SMTP_PASS):
|
||||||
|
raise RuntimeError("SMTP не настроен")
|
||||||
|
msg = _MIMEText(body, "plain", "utf-8")
|
||||||
|
msg["Subject"] = subject
|
||||||
|
msg["From"] = SMTP_FROM
|
||||||
|
msg["To"] = to
|
||||||
|
ctx = _ssl.create_default_context()
|
||||||
|
if SMTP_PORT == 465:
|
||||||
|
with _smtplib.SMTP_SSL(SMTP_HOST, SMTP_PORT, context=ctx, timeout=20) as s:
|
||||||
|
s.login(SMTP_USER, SMTP_PASS)
|
||||||
|
s.sendmail(SMTP_FROM, [to], msg.as_string())
|
||||||
|
else:
|
||||||
|
with _smtplib.SMTP(SMTP_HOST, SMTP_PORT, timeout=20) as s:
|
||||||
|
s.starttls(context=ctx)
|
||||||
|
s.login(SMTP_USER, SMTP_PASS)
|
||||||
|
s.sendmail(SMTP_FROM, [to], msg.as_string())
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/auth/email/start")
|
||||||
|
async def email_start(payload: dict):
|
||||||
|
if not (SMTP_HOST and SMTP_USER):
|
||||||
|
raise HTTPException(status_code=503, detail="Вход по почте не настроен.")
|
||||||
|
email = (payload.get("email") or "").strip().lower()[:128] if isinstance(payload, dict) else ""
|
||||||
|
if not _EMAIL_RE.match(email):
|
||||||
|
raise HTTPException(status_code=400, detail="Некорректный email")
|
||||||
|
now = time.time()
|
||||||
|
if now - _email_rate.get(email, 0) < 60:
|
||||||
|
raise HTTPException(status_code=429, detail="Код уже отправлен, подождите минуту.")
|
||||||
|
code = f"{secrets.randbelow(1000000):06d}"
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(DB_PATH, timeout=10) as con:
|
||||||
|
con.execute(
|
||||||
|
"INSERT INTO WEB_EMAIL_CODES (email, code, created_at, attempts) VALUES (?,?,?,0) "
|
||||||
|
"ON CONFLICT(email) DO UPDATE SET code=excluded.code, created_at=excluded.created_at, attempts=0",
|
||||||
|
(email, code, now))
|
||||||
|
con.commit()
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=500, detail="Ошибка БД")
|
||||||
|
try:
|
||||||
|
await asyncio.to_thread(
|
||||||
|
_send_email, email, "Код входа pdf2scan",
|
||||||
|
f"Ваш код для входа на pdf2scan.online: {code}\n\nКод действует 10 минут.\nЕсли вы не запрашивали вход — проигнорируйте письмо.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"email send error: {e}")
|
||||||
|
raise HTTPException(status_code=502, detail="Не удалось отправить письмо. Попробуйте позже.")
|
||||||
|
_email_rate[email] = now
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/auth/email/verify")
|
||||||
|
async def email_verify(payload: dict):
|
||||||
|
email = (payload.get("email") or "").strip().lower()[:128] if isinstance(payload, dict) else ""
|
||||||
|
code = (payload.get("code") or "").strip()[:6]
|
||||||
|
if not _EMAIL_RE.match(email) or not code.isdigit():
|
||||||
|
raise HTTPException(status_code=400, detail="Некорректные данные")
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(DB_PATH, timeout=10) as con:
|
||||||
|
cur = con.execute("SELECT code, created_at, attempts FROM WEB_EMAIL_CODES WHERE email=?", (email,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
if not row:
|
||||||
|
raise HTTPException(status_code=400, detail="Код не найден. Запросите новый.")
|
||||||
|
saved, created, attempts = row
|
||||||
|
if time.time() - (created or 0) > EMAIL_CODE_TTL:
|
||||||
|
raise HTTPException(status_code=400, detail="Код истёк. Запросите новый.")
|
||||||
|
if (attempts or 0) >= 5:
|
||||||
|
raise HTTPException(status_code=429, detail="Слишком много попыток. Запросите новый код.")
|
||||||
|
if code != saved:
|
||||||
|
con.execute("UPDATE WEB_EMAIL_CODES SET attempts=attempts+1 WHERE email=?", (email,))
|
||||||
|
con.commit()
|
||||||
|
raise HTTPException(status_code=400, detail="Неверный код")
|
||||||
|
con.execute("DELETE FROM WEB_EMAIL_CODES WHERE email=?", (email,))
|
||||||
|
con.commit()
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
raise HTTPException(status_code=500, detail="Ошибка БД")
|
||||||
|
account_id = f"email:{email}"
|
||||||
|
_db_ensure_account(account_id)
|
||||||
|
token = _db_create_session(account_id, "email", email.split("@")[0], email)
|
||||||
|
resp = JSONResponse({"ok": True})
|
||||||
|
resp.set_cookie(SESSION_COOKIE, token, max_age=SESSION_DAYS * 86400,
|
||||||
|
httponly=True, secure=True, samesite="lax", path="/")
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
_API_DOCS_HTML = """<!DOCTYPE html>
|
_API_DOCS_HTML = """<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
|||||||
@@ -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 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>
|
<span data-i18n="login_via_vk">Через VK</span>
|
||||||
</button>
|
</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>
|
||||||
<div id="tg-user-info" style="display:none;">
|
<div id="tg-user-info" style="display:none;">
|
||||||
<img id="tg-user-photo" src="" alt="" onerror="this.style.display='none'">
|
<img id="tg-user-photo" src="" alt="" onerror="this.style.display='none'">
|
||||||
@@ -874,6 +878,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</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">×</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>
|
<main>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Description -->
|
||||||
@@ -1163,6 +1187,14 @@
|
|||||||
login_via_tg: 'Через Telegram',
|
login_via_tg: 'Через Telegram',
|
||||||
login_via_yandex: 'Через Яндекс',
|
login_via_yandex: 'Через Яндекс',
|
||||||
login_via_vk: 'Через VK',
|
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: 'Не удалось войти. Попробуйте снова.',
|
err_login: 'Не удалось войти. Попробуйте снова.',
|
||||||
logout_btn: 'Выйти',
|
logout_btn: 'Выйти',
|
||||||
page_description: '🧷 Выберите тип сшивки и при необходимости дополнительные настройки, затем загрузите файл — получите готовый скан.',
|
page_description: '🧷 Выберите тип сшивки и при необходимости дополнительные настройки, затем загрузите файл — получите готовый скан.',
|
||||||
@@ -1277,6 +1309,14 @@
|
|||||||
login_via_tg: 'Via Telegram',
|
login_via_tg: 'Via Telegram',
|
||||||
login_via_yandex: 'Via Yandex',
|
login_via_yandex: 'Via Yandex',
|
||||||
login_via_vk: 'Via VK',
|
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.',
|
err_login: 'Login failed. Please try again.',
|
||||||
logout_btn: 'Logout',
|
logout_btn: 'Logout',
|
||||||
page_description: '🧷 Select binding type and optional settings, then upload your file — get a ready scan.',
|
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_tg: 'Über Telegram',
|
||||||
login_via_yandex: 'Über Yandex',
|
login_via_yandex: 'Über Yandex',
|
||||||
login_via_vk: 'Über VK',
|
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.',
|
err_login: 'Anmeldung fehlgeschlagen. Bitte erneut versuchen.',
|
||||||
logout_btn: 'Abmelden',
|
logout_btn: 'Abmelden',
|
||||||
page_description: '🧷 Wählen Sie den Bindungstyp und optionale Einstellungen, laden Sie dann Ihre Datei hoch — erhalten Sie einen fertigen Scan.',
|
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_tg: 'Via Telegram',
|
||||||
login_via_yandex: 'Via Yandex',
|
login_via_yandex: 'Via Yandex',
|
||||||
login_via_vk: 'Via VK',
|
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.',
|
err_login: 'Échec de connexion. Réessayez.',
|
||||||
logout_btn: 'Déconnexion',
|
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.',
|
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_tg: 'Via Telegram',
|
||||||
login_via_yandex: 'Via Yandex',
|
login_via_yandex: 'Via Yandex',
|
||||||
login_via_vk: 'Via VK',
|
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.',
|
err_login: 'Accesso non riuscito. Riprova.',
|
||||||
logout_btn: 'Esci',
|
logout_btn: 'Esci',
|
||||||
page_description: '🧷 Seleziona il tipo di rilegatura e le impostazioni opzionali, poi carica il file — ottieni una scansione pronta.',
|
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_tg: 'Con Telegram',
|
||||||
login_via_yandex: 'Con Yandex',
|
login_via_yandex: 'Con Yandex',
|
||||||
login_via_vk: 'Con VK',
|
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.',
|
err_login: 'Error de inicio de sesión. Inténtalo de nuevo.',
|
||||||
logout_btn: 'Cerrar sesión',
|
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.',
|
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_tg: 'Via Telegram',
|
||||||
login_via_yandex: 'Via Yandex',
|
login_via_yandex: 'Via Yandex',
|
||||||
login_via_vk: 'Via VK',
|
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.',
|
err_login: 'Falha no login. Tente novamente.',
|
||||||
logout_btn: 'Sair',
|
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.',
|
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();
|
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 _authPollInterval = null;
|
||||||
let _authToken = null;
|
let _authToken = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user