Пейвол больших файлов из бота: /pay deep-link + освобождение по подписке
- /pay?tg&job&exp&sig: HMAC-проверка → сессия tg:<id> → /?pay_job=<id> → пейволл по заданию - /api/held/release: освобождение придержанного файла владельцу с активной подпиской - Фронт: ?pay_job открывает оплату файла (99₽/подписка); при активной подписке — освобождает бесплатно Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
60
app/main.py
60
app/main.py
@@ -2166,6 +2166,66 @@ async def subscribe_via_bot(request: Request, token: str = "", name: str = ""):
|
||||
return resp
|
||||
|
||||
|
||||
def _verify_pay_link(tg, job, exp, sig):
|
||||
"""Проверяет подписанную ссылку оплаты файла из бота (раздельные параметры)."""
|
||||
if not SUB_LINK_SECRET:
|
||||
return None
|
||||
try:
|
||||
if float(exp) < time.time():
|
||||
return None
|
||||
expect = hmac.new(SUB_LINK_SECRET.encode(), f"{tg}:{exp}:{job}".encode(), hashlib.sha256).hexdigest()
|
||||
if not hmac.compare_digest(str(sig), expect):
|
||||
return None
|
||||
return int(tg)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
@app.get("/pay")
|
||||
async def pay_held_file(request: Request, tg: str = "", job: str = "", exp: str = "", sig: str = "", name: str = ""):
|
||||
"""Deep-link из бота на оплату придержанного файла: логиним tg:<id> + открываем пейволл по job."""
|
||||
tgid = _verify_pay_link(tg, job, exp, sig)
|
||||
if not tgid or not job:
|
||||
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}/?pay_job={_urlparse.quote(job)}", status_code=302)
|
||||
resp.set_cookie(SESSION_COOKIE, sess, max_age=SESSION_DAYS * 86400,
|
||||
httponly=True, secure=True, samesite="lax", path="/")
|
||||
return resp
|
||||
|
||||
|
||||
@app.post("/api/held/release")
|
||||
async def held_release(payload: dict, request: Request):
|
||||
"""Освободить придержанный файл БЕЗ оплаты — только владельцу с активной подпиской."""
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
acc = _resolve_account(request, payload.get("user_id"))
|
||||
job_id = (payload.get("job_id") or "").strip()[:128]
|
||||
if not acc or not job_id:
|
||||
raise HTTPException(status_code=400, detail="bad request")
|
||||
if not _db_sub_active(acc):
|
||||
raise HTTPException(status_code=403, detail="Нет активной подписки")
|
||||
|
||||
def _release():
|
||||
with sqlite3.connect(DB_PATH, timeout=10) as con:
|
||||
row = con.execute("SELECT user_id, url_res_f FROM FILES WHERE name_or_f=?", (job_id,)).fetchone()
|
||||
if not row:
|
||||
return False
|
||||
uid_owner, urf = row
|
||||
if (urf or "").strip() != "awaiting_payment":
|
||||
return False
|
||||
if acc.startswith("tg:") and str(uid_owner) != acc.split(":", 1)[1]:
|
||||
return False
|
||||
con.execute("UPDATE FILES SET url_res_f='-' WHERE name_or_f=?", (job_id,))
|
||||
con.commit()
|
||||
return True
|
||||
|
||||
ok = await asyncio.to_thread(_release)
|
||||
return {"released": bool(ok)}
|
||||
|
||||
|
||||
@app.get("/api/me")
|
||||
async def me(request: Request):
|
||||
sess = _db_get_session(request.cookies.get(SESSION_COOKIE))
|
||||
|
||||
Reference in New Issue
Block a user