Мобильная шапка + оплата подписки через 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", "")
|
ADMIN_TOKEN = os.getenv("ADMIN_TOKEN", "")
|
||||||
|
|
||||||
|
# ── Подписка через Telegram-бота (подписанные deep-link) ──────────────────────
|
||||||
|
SUB_LINK_SECRET = os.getenv("SUB_LINK_SECRET", "") # общий секрет бота и веба
|
||||||
|
|
||||||
# ── Вход по почте (magic-code через SMTP) ─────────────────────────────────────
|
# ── Вход по почте (magic-code через SMTP) ─────────────────────────────────────
|
||||||
SMTP_HOST = os.getenv("SMTP_HOST", "")
|
SMTP_HOST = os.getenv("SMTP_HOST", "")
|
||||||
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
SMTP_PORT = int(os.getenv("SMTP_PORT", "465"))
|
||||||
@@ -2131,6 +2134,38 @@ async def yandex_callback(request: Request, code: str = "", state: str = ""):
|
|||||||
return resp
|
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")
|
@app.get("/api/me")
|
||||||
async def me(request: Request):
|
async def me(request: Request):
|
||||||
sess = _db_get_session(request.cookies.get(SESSION_COOKIE))
|
sess = _db_get_session(request.cookies.get(SESSION_COOKIE))
|
||||||
|
|||||||
@@ -241,9 +241,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#tg-login-header-btn:hover { background: var(--blue-hover); }
|
#tg-login-header-btn:hover { background: var(--blue-hover); }
|
||||||
|
/* Короткий вариант метки кнопки входа — только на узких экранах */
|
||||||
|
.login-mini { display: none; }
|
||||||
|
|
||||||
/* ── Main card ── */
|
/* ── Main card ── */
|
||||||
main {
|
main {
|
||||||
@@ -746,6 +749,21 @@
|
|||||||
.bs { font-size: 12px; padding: 3px 8px; }
|
.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 ── */
|
||||||
#seo-section {
|
#seo-section {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -851,7 +869,8 @@
|
|||||||
<div id="header-auth" style="position:relative;">
|
<div id="header-auth" style="position:relative;">
|
||||||
<button id="tg-login-header-btn" onclick="toggleLoginMenu(event)" style="display:none;">
|
<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>
|
<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>
|
</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;">
|
<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;">
|
<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',
|
page_title: 'pdf2scan — онлайн сканирование PDF',
|
||||||
logo_subtitle: 'Создание растровых pdf с эффектом сканирования из электронных pdf документов',
|
logo_subtitle: 'Создание растровых pdf с эффектом сканирования из электронных pdf документов',
|
||||||
login_btn: 'Войти / Регистрация',
|
login_btn: 'Войти / Регистрация',
|
||||||
|
login_btn_mini: 'Войти',
|
||||||
login_via_tg: 'Через Telegram',
|
login_via_tg: 'Через Telegram',
|
||||||
login_via_yandex: 'Через Яндекс',
|
login_via_yandex: 'Через Яндекс',
|
||||||
login_via_vk: 'Через VK',
|
login_via_vk: 'Через VK',
|
||||||
@@ -1366,6 +1386,7 @@
|
|||||||
page_title: 'pdf2scan — online PDF scan',
|
page_title: 'pdf2scan — online PDF scan',
|
||||||
logo_subtitle: 'Convert PDF to scanned document online',
|
logo_subtitle: 'Convert PDF to scanned document online',
|
||||||
login_btn: 'Log in / Sign up',
|
login_btn: 'Log in / Sign up',
|
||||||
|
login_btn_mini: 'Log in',
|
||||||
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',
|
||||||
@@ -1509,6 +1530,7 @@
|
|||||||
page_title: 'pdf2scan — Online-PDF-Scan',
|
page_title: 'pdf2scan — Online-PDF-Scan',
|
||||||
logo_subtitle: 'PDF online in gescannte Dokumente umwandeln',
|
logo_subtitle: 'PDF online in gescannte Dokumente umwandeln',
|
||||||
login_btn: 'Anmelden / Registrieren',
|
login_btn: 'Anmelden / Registrieren',
|
||||||
|
login_btn_mini: 'Anmelden',
|
||||||
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',
|
||||||
@@ -1652,6 +1674,7 @@
|
|||||||
page_title: 'pdf2scan — scan PDF en ligne',
|
page_title: 'pdf2scan — scan PDF en ligne',
|
||||||
logo_subtitle: 'Convertir des PDF en documents scannés en ligne',
|
logo_subtitle: 'Convertir des PDF en documents scannés en ligne',
|
||||||
login_btn: 'Connexion / Inscription',
|
login_btn: 'Connexion / Inscription',
|
||||||
|
login_btn_mini: 'Connexion',
|
||||||
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',
|
||||||
@@ -1795,6 +1818,7 @@
|
|||||||
page_title: 'pdf2scan — scansione PDF online',
|
page_title: 'pdf2scan — scansione PDF online',
|
||||||
logo_subtitle: 'Converti PDF in documenti scansionati online',
|
logo_subtitle: 'Converti PDF in documenti scansionati online',
|
||||||
login_btn: 'Accedi / Registrati',
|
login_btn: 'Accedi / Registrati',
|
||||||
|
login_btn_mini: 'Accedi',
|
||||||
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',
|
||||||
@@ -1938,6 +1962,7 @@
|
|||||||
page_title: 'pdf2scan — escaneo PDF en línea',
|
page_title: 'pdf2scan — escaneo PDF en línea',
|
||||||
logo_subtitle: 'Convierte PDF en documentos escaneados en línea',
|
logo_subtitle: 'Convierte PDF en documentos escaneados en línea',
|
||||||
login_btn: 'Entrar / Registro',
|
login_btn: 'Entrar / Registro',
|
||||||
|
login_btn_mini: 'Entrar',
|
||||||
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',
|
||||||
@@ -2081,6 +2106,7 @@
|
|||||||
page_title: 'pdf2scan — digitalização PDF online',
|
page_title: 'pdf2scan — digitalização PDF online',
|
||||||
logo_subtitle: 'Converta PDF em documentos digitalizados online',
|
logo_subtitle: 'Converta PDF em documentos digitalizados online',
|
||||||
login_btn: 'Entrar / Registrar',
|
login_btn: 'Entrar / Registrar',
|
||||||
|
login_btn_mini: 'Entrar',
|
||||||
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',
|
||||||
@@ -2590,13 +2616,18 @@
|
|||||||
catch (_) { return new Date(ts * 1000).toLocaleDateString(); }
|
catch (_) { return new Date(ts * 1000).toLocaleDateString(); }
|
||||||
}
|
}
|
||||||
async function loadSubscription() {
|
async function loadSubscription() {
|
||||||
const badge = document.getElementById('sub-badge');
|
|
||||||
const card = document.getElementById('sub-card');
|
|
||||||
if (!tgUser && !sessionUser) { renderSubStatus({ active: false }); return; }
|
if (!tgUser && !sessionUser) { renderSubStatus({ active: false }); return; }
|
||||||
try {
|
try {
|
||||||
const uid = tgUser ? (tgUser.user_id || tgUser.id || 0) : 0;
|
const uid = tgUser ? (tgUser.user_id || tgUser.id || 0) : 0;
|
||||||
const r = await fetch('/api/subscription' + (uid ? ('?user_id=' + encodeURIComponent(uid)) : ''));
|
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 }); }
|
} catch (_) { renderSubStatus({ active: false }); }
|
||||||
}
|
}
|
||||||
function renderSubStatus(d) {
|
function renderSubStatus(d) {
|
||||||
@@ -2796,6 +2827,12 @@
|
|||||||
try { history.replaceState({}, '', location.pathname); } catch (_) {}
|
try { history.replaceState({}, '', location.pathname); } catch (_) {}
|
||||||
if (v === 'error') showError(t('err_login'));
|
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 ─────────────────────────────────────────────────────────────
|
// ── Drop zone ─────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user