Мобильная шапка + оплата подписки через Telegram (deep-link)
- Мобильная шапка: скрыт подзаголовок, короткая метка «Войти», бейдж ★ — не вылезает за край - /subscribe?token=…: проверка HMAC-подписи из бота → сессия tg:<id> → авто-открытие оплаты - Фронт: ?subscribe=1 открывает окно подписки после загрузки сессии Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
35
app/main.py
35
app/main.py
@@ -69,6 +69,9 @@ VK_REDIRECT = f"{os.getenv('SITE_BASE_URL', 'https://pdf2scan.online')}/api
|
||||
# ── Админ-панель ──────────────────────────────────────────────────────────────
|
||||
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", "")
|
||||
|
||||
# ── Подписка через Telegram-бота (подписанные deep-link) ──────────────────────
|
||||
SUB_LINK_SECRET = os.getenv("SUB_LINK_SECRET", "") # общий секрет бота и веба
|
||||
|
||||
# ── Вход по почте (magic-code через SMTP) ─────────────────────────────────────
|
||||
SMTP_HOST = os.getenv("SMTP_HOST", "")
|
||||
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
||||
@@ -2131,6 +2134,38 @@ async def yandex_callback(request: Request, code: str = "", state: str = ""):
|
||||
return resp
|
||||
|
||||
|
||||
def _verify_sub_token(token: str):
|
||||
"""Проверяет подписанный deep-link из бота: '<tgid>.<exp>.<sig>'. Возвращает tgid (int) или None."""
|
||||
if not SUB_LINK_SECRET or not token:
|
||||
return None
|
||||
try:
|
||||
tgid_s, exp_s, sig = token.split(".", 2)
|
||||
if float(exp_s) < time.time():
|
||||
return None
|
||||
expect = hmac.new(SUB_LINK_SECRET.encode(), f"{tgid_s}.{exp_s}".encode(), hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(sig, expect):
|
||||
return None
|
||||
return int(tgid_s)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/subscribe")
|
||||
async def subscribe_via_bot(request: Request, token: str = "", name: str = ""):
|
||||
"""Deep-link из Telegram-бота: проверяем подпись, логиним как tg:<id> (сессия),
|
||||
ведём на главную с авто-открытием окна оплаты подписки."""
|
||||
tgid = _verify_sub_token(token)
|
||||
if not tgid:
|
||||
return RedirectResponse(f"{SITE_BASE_URL}/?login=error", status_code=302)
|
||||
account_id = f"tg:{tgid}"
|
||||
_db_ensure_account(account_id)
|
||||
sess = _db_create_session(account_id, "telegram", (name or "").strip()[:64] or "Telegram", "")
|
||||
resp = RedirectResponse(f"{SITE_BASE_URL}/?subscribe=1", status_code=302)
|
||||
resp.set_cookie(SESSION_COOKIE, sess, max_age=SESSION_DAYS * 86400,
|
||||
httponly=True, secure=True, samesite="lax", path="/")
|
||||
return resp
|
||||
|
||||
|
||||
@app.get("/api/me")
|
||||
async def me(request: Request):
|
||||
sess = _db_get_session(request.cookies.get(SESSION_COOKIE))
|
||||
|
||||
@@ -241,9 +241,12 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#tg-login-header-btn:hover { background: var(--blue-hover); }
|
||||
/* Короткий вариант метки кнопки входа — только на узких экранах */
|
||||
.login-mini { display: none; }
|
||||
|
||||
/* ── Main card ── */
|
||||
main {
|
||||
@@ -746,6 +749,21 @@
|
||||
.bs { font-size: 12px; padding: 3px 8px; }
|
||||
}
|
||||
|
||||
/* ── Компактная шапка на телефонах ── */
|
||||
@media (max-width: 500px) {
|
||||
header { padding: 8px 12px; gap: 8px; flex-wrap: wrap; }
|
||||
.logo-subtitle { display: none; } /* длинный подзаголовок распирает шапку — прячем на телефоне */
|
||||
.logo-text { font-size: 18px; }
|
||||
#header-auth { gap: 8px; }
|
||||
#tg-login-header-btn { padding: 7px 12px; font-size: 12.5px; }
|
||||
.login-full { display: none; }
|
||||
.login-mini { display: inline; }
|
||||
#tg-user-info { font-size: 13px; gap: 6px; }
|
||||
#tg-user-name { display: inline-block; max-width: 84px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: middle; }
|
||||
#sub-badge { padding: 4px 8px; }
|
||||
#sub-badge-text { display: none; } /* дата — только в карточке ниже; в шапке оставляем ★ */
|
||||
}
|
||||
|
||||
/* ── SEO section ── */
|
||||
#seo-section {
|
||||
width: 100%;
|
||||
@@ -851,7 +869,8 @@
|
||||
<div id="header-auth" style="position:relative;">
|
||||
<button id="tg-login-header-btn" onclick="toggleLoginMenu(event)" style="display:none;">
|
||||
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/><path d="M10 17l5-5-5-5"/><path d="M15 12H3"/></svg>
|
||||
<span data-i18n="login_btn">Войти / Регистрация</span>
|
||||
<span class="login-full" data-i18n="login_btn">Войти / Регистрация</span>
|
||||
<span class="login-mini" data-i18n="login_btn_mini">Войти</span>
|
||||
</button>
|
||||
<div id="login-menu" style="display:none;position:absolute;right:0;top:calc(100% + 6px);background:#fff;border:1px solid var(--border,#d0d7e2);border-radius:10px;box-shadow:0 6px 20px rgba(0,0,0,.12);padding:6px;z-index:50;min-width:190px;">
|
||||
<button onclick="startTgAuth();closeLoginMenu();" 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;">
|
||||
@@ -1224,6 +1243,7 @@
|
||||
page_title: 'pdf2scan — онлайн сканирование PDF',
|
||||
logo_subtitle: 'Создание растровых pdf с эффектом сканирования из электронных pdf документов',
|
||||
login_btn: 'Войти / Регистрация',
|
||||
login_btn_mini: 'Войти',
|
||||
login_via_tg: 'Через Telegram',
|
||||
login_via_yandex: 'Через Яндекс',
|
||||
login_via_vk: 'Через VK',
|
||||
@@ -1366,6 +1386,7 @@
|
||||
page_title: 'pdf2scan — online PDF scan',
|
||||
logo_subtitle: 'Convert PDF to scanned document online',
|
||||
login_btn: 'Log in / Sign up',
|
||||
login_btn_mini: 'Log in',
|
||||
login_via_tg: 'Via Telegram',
|
||||
login_via_yandex: 'Via Yandex',
|
||||
login_via_vk: 'Via VK',
|
||||
@@ -1509,6 +1530,7 @@
|
||||
page_title: 'pdf2scan — Online-PDF-Scan',
|
||||
logo_subtitle: 'PDF online in gescannte Dokumente umwandeln',
|
||||
login_btn: 'Anmelden / Registrieren',
|
||||
login_btn_mini: 'Anmelden',
|
||||
login_via_tg: 'Über Telegram',
|
||||
login_via_yandex: 'Über Yandex',
|
||||
login_via_vk: 'Über VK',
|
||||
@@ -1652,6 +1674,7 @@
|
||||
page_title: 'pdf2scan — scan PDF en ligne',
|
||||
logo_subtitle: 'Convertir des PDF en documents scannés en ligne',
|
||||
login_btn: 'Connexion / Inscription',
|
||||
login_btn_mini: 'Connexion',
|
||||
login_via_tg: 'Via Telegram',
|
||||
login_via_yandex: 'Via Yandex',
|
||||
login_via_vk: 'Via VK',
|
||||
@@ -1795,6 +1818,7 @@
|
||||
page_title: 'pdf2scan — scansione PDF online',
|
||||
logo_subtitle: 'Converti PDF in documenti scansionati online',
|
||||
login_btn: 'Accedi / Registrati',
|
||||
login_btn_mini: 'Accedi',
|
||||
login_via_tg: 'Via Telegram',
|
||||
login_via_yandex: 'Via Yandex',
|
||||
login_via_vk: 'Via VK',
|
||||
@@ -1938,6 +1962,7 @@
|
||||
page_title: 'pdf2scan — escaneo PDF en línea',
|
||||
logo_subtitle: 'Convierte PDF en documentos escaneados en línea',
|
||||
login_btn: 'Entrar / Registro',
|
||||
login_btn_mini: 'Entrar',
|
||||
login_via_tg: 'Con Telegram',
|
||||
login_via_yandex: 'Con Yandex',
|
||||
login_via_vk: 'Con VK',
|
||||
@@ -2081,6 +2106,7 @@
|
||||
page_title: 'pdf2scan — digitalização PDF online',
|
||||
logo_subtitle: 'Converta PDF em documentos digitalizados online',
|
||||
login_btn: 'Entrar / Registrar',
|
||||
login_btn_mini: 'Entrar',
|
||||
login_via_tg: 'Via Telegram',
|
||||
login_via_yandex: 'Via Yandex',
|
||||
login_via_vk: 'Via VK',
|
||||
@@ -2590,13 +2616,18 @@
|
||||
catch (_) { return new Date(ts * 1000).toLocaleDateString(); }
|
||||
}
|
||||
async function loadSubscription() {
|
||||
const badge = document.getElementById('sub-badge');
|
||||
const card = document.getElementById('sub-card');
|
||||
if (!tgUser && !sessionUser) { renderSubStatus({ active: false }); return; }
|
||||
try {
|
||||
const uid = tgUser ? (tgUser.user_id || tgUser.id || 0) : 0;
|
||||
const r = await fetch('/api/subscription' + (uid ? ('?user_id=' + encodeURIComponent(uid)) : ''));
|
||||
renderSubStatus(await r.json());
|
||||
const d = await r.json();
|
||||
renderSubStatus(d);
|
||||
// Пришли по ссылке подписки из бота — открываем оплату (если ещё не подписан)
|
||||
if (window._openSubAfterLoad) {
|
||||
window._openSubAfterLoad = false;
|
||||
if (!d.active) showPaywall(null, paywallFilePrice, (d.price || paywallSubPrice), 'renew');
|
||||
else scrollToSubCard();
|
||||
}
|
||||
} catch (_) { renderSubStatus({ active: false }); }
|
||||
}
|
||||
function renderSubStatus(d) {
|
||||
@@ -2796,6 +2827,12 @@
|
||||
try { history.replaceState({}, '', location.pathname); } catch (_) {}
|
||||
if (v === 'error') showError(t('err_login'));
|
||||
}
|
||||
// Приход по ссылке подписки из Telegram-бота (/subscribe → /?subscribe=1):
|
||||
// после загрузки сессии авто-открываем окно оплаты подписки.
|
||||
if (p.has('subscribe')) {
|
||||
window._openSubAfterLoad = true;
|
||||
try { history.replaceState({}, '', location.pathname); } catch (_) {}
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Drop zone ─────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user