Files
vpn-telegram-bot/services/webapp_growth_routes.py

834 lines
32 KiB
Python

"""Доп. API: рефералка, trial, промо, пароль, сессии, HWID, legal, бэкапы."""
from __future__ import annotations
import json
import logging
from pathlib import Path
from uuid import UUID
from aiohttp import web
from remnawave.models import HWIDDeleteRequest
from config import Settings, ROOT_DIR
from services.db import AuthError, BotUser, Database
from services.legal_pages import (
html_aup,
html_contacts,
html_cookies,
html_delivery,
html_help,
html_offer,
html_privacy,
html_refund,
html_site_rules,
html_status,
html_support,
html_terms,
html_vpn_usage,
)
from services.mailer import render_password_reset_email, send_email, smtp_configured
from services.pg_backup import (
delete_backup,
list_backups,
resolve_backup_path,
restore_from_backup_name,
restore_from_bytes,
run_pg_dump_backup,
save_uploaded_backup,
)
from services.rate_limit import allow, client_ip
from services.remnawave_users import find_panel_user
from services.trial import grant_trial, trial_days_setting, trial_enabled
logger = logging.getLogger(__name__)
def _require_user(request: web.Request) -> BotUser:
from webapp_server import _require_user as req
return req(request)
def _user_payload(user: BotUser) -> dict:
from webapp_server import _user_payload as payload
return payload(user)
def _session_token(request: web.Request) -> str | None:
from webapp_server import _session_token as tok
return tok(request)
def _set_session_cookie(resp, request, token: str) -> None:
from webapp_server import _set_session_cookie as setc
setc(resp, request, token)
def register_growth_routes(app: web.Application) -> None:
app.router.add_get("/api/referral", handle_referral_get)
app.router.add_post("/api/trial", handle_trial)
app.router.add_get("/api/account/notifications", handle_notify_get)
app.router.add_post("/api/account/notifications", handle_notify_set)
app.router.add_get("/api/account/sessions", handle_sessions_list)
app.router.add_post("/api/account/sessions/logout-all", handle_sessions_logout_all)
app.router.add_post("/api/auth/password/forgot", handle_password_forgot)
app.router.add_post("/api/auth/password/reset", handle_password_reset)
app.router.add_get("/api/devices/hwid", handle_hwid_list)
app.router.add_post("/api/devices/hwid/delete", handle_hwid_delete)
app.router.add_get("/terms", handle_terms_page)
app.router.add_get("/offer", handle_offer_page)
app.router.add_get("/privacy", handle_privacy_page)
app.router.add_get("/aup", handle_aup_page)
app.router.add_get("/vpn-usage", handle_vpn_usage_page)
app.router.add_get("/vpn_usage", handle_vpn_usage_page)
app.router.add_get("/rules", handle_rules_page)
app.router.add_get("/refund", handle_refund_page)
app.router.add_get("/delivery", handle_delivery_page)
app.router.add_get("/contacts", handle_contacts_page)
app.router.add_get("/help", handle_help_page)
app.router.add_get("/support", handle_support_page)
app.router.add_get("/cookies", handle_cookies_page)
app.router.add_get("/status", handle_status_page)
app.router.add_get("/api/admin/promos", handle_admin_promos_list)
app.router.add_post("/api/admin/promos", handle_admin_promos_create)
app.router.add_post("/api/admin/promos/{promo_id}/toggle", handle_admin_promos_toggle)
app.router.add_post("/api/admin/users/ban", handle_admin_user_ban)
app.router.add_post("/api/admin/users/delete", handle_admin_user_delete)
app.router.add_get("/api/admin/payments/journal", handle_admin_payment_journal)
app.router.add_get("/api/admin/backups", handle_admin_backups_list)
app.router.add_post("/api/admin/backups", handle_admin_backups_run)
app.router.add_get("/api/admin/backups/{name}/download", handle_admin_backups_download)
app.router.add_delete("/api/admin/backups/{name}", handle_admin_backups_delete)
app.router.add_post("/api/admin/backups/delete", handle_admin_backups_delete_post)
app.router.add_post("/api/admin/backups/restore", handle_admin_backups_restore)
app.router.add_post("/api/admin/backups/upload", handle_admin_backups_upload)
app.router.add_get("/api/admin/growth-settings", handle_admin_growth_get)
app.router.add_post("/api/admin/growth-settings", handle_admin_growth_set)
async def handle_referral_get(request: web.Request) -> web.Response:
settings: Settings = request.app["settings"]
db: Database = request.app["db"]
try:
user = _require_user(request)
except web.HTTPUnauthorized:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
code = db.ensure_referral_code(user.telegram_id)
bot = str(request.app.get("bot_username") or "").lstrip("@")
link = f"https://t.me/{bot}?start=ref_{code}" if bot else ""
web_link = f"{settings.webapp_url.rstrip('/')}/?ref={code}" if settings.webapp_url else ""
return web.json_response(
{
"ok": True,
"code": code,
"telegram_link": link,
"web_link": web_link,
"bonus_referrer": float(db.get_setting("referral_bonus_referrer") or "50"),
"bonus_referee": float(db.get_setting("referral_bonus_referee") or "50"),
}
)
async def handle_trial(request: web.Request) -> web.Response:
db: Database = request.app["db"]
sdk = request.app["sdk"]
try:
user = _require_user(request)
except web.HTTPUnauthorized:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
if not allow(request, "trial", limit=5, window_sec=3600):
return web.json_response({"ok": False, "error": "Слишком часто"}, status=429)
result = await grant_trial(
db=db, sdk=sdk, telegram_id=user.telegram_id, tariffs=request.app["tariffs"]
)
status = 200 if result.ok else 400
return web.json_response(
{
"ok": result.ok,
"error": None if result.ok else result.message,
"message": result.message,
"expire_at": result.expire_at.isoformat() if result.expire_at else None,
"trial_enabled": trial_enabled(db),
"trial_days": trial_days_setting(db),
},
status=status,
)
async def handle_notify_get(request: web.Request) -> web.Response:
try:
user = _require_user(request)
except web.HTTPUnauthorized:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
return web.json_response(
{
"ok": True,
"notify_telegram": bool(user.notify_telegram),
"notify_email": bool(user.notify_email),
}
)
async def handle_notify_set(request: web.Request) -> web.Response:
db: Database = request.app["db"]
try:
user = _require_user(request)
except web.HTTPUnauthorized:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
updated = db.set_notify_prefs(
user.telegram_id,
notify_telegram=body.get("notify_telegram"),
notify_email=body.get("notify_email"),
)
return web.json_response(
{
"ok": True,
"notify_telegram": updated.notify_telegram,
"notify_email": updated.notify_email,
"user": _user_payload(updated),
}
)
async def handle_sessions_list(request: web.Request) -> web.Response:
db: Database = request.app["db"]
try:
user = _require_user(request)
except web.HTTPUnauthorized:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
current = _session_token(request)
items = []
for s in db.list_sessions(user.telegram_id):
items.append(
{
"token_hint": (s["token"][:6] + "…") if s.get("token") else "",
"current": s.get("token") == current,
"created_at": s["created_at"].isoformat()
if hasattr(s["created_at"], "isoformat")
else str(s["created_at"]),
"expires_at": s["expires_at"].isoformat()
if hasattr(s["expires_at"], "isoformat")
else str(s["expires_at"]),
"user_agent": s.get("user_agent"),
"ip": s.get("ip"),
}
)
return web.json_response({"ok": True, "sessions": items})
async def handle_sessions_logout_all(request: web.Request) -> web.Response:
db: Database = request.app["db"]
try:
user = _require_user(request)
except web.HTTPUnauthorized:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
keep = _session_token(request)
deleted = db.clear_all_sessions(user.telegram_id, keep_token=keep)
return web.json_response({"ok": True, "deleted": deleted})
async def handle_password_forgot(request: web.Request) -> web.Response:
import os
from dotenv import load_dotenv
from config import get_settings
# Подтянуть .env (SMTP из админки) и обновить runtime settings
env_file = (os.getenv("ENV_FILE") or "").strip() or None
load_dotenv(env_file, override=True)
settings = get_settings(require_bot_token=False)
request.app["settings"] = settings
db: Database = request.app["db"]
if not allow(request, "pwd_forgot", limit=5, window_sec=600):
return web.json_response({"ok": False, "error": "Слишком часто"}, status=429)
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
email = str(body.get("email") or "").strip()
if not email or "@" not in email:
return web.json_response({"ok": False, "error": "Укажите корректный email"}, status=400)
if not smtp_configured(settings):
return web.json_response(
{
"ok": False,
"error": "Сброс пароля недоступен: SMTP не настроен (Админка → SMTP)",
},
status=503,
)
if not (settings.webapp_url or "").strip():
return web.json_response(
{
"ok": False,
"error": "Сброс пароля недоступен: не задан WEBAPP_URL",
},
status=503,
)
# Не раскрываем, есть ли email в базе
user = db.get_user_by_email(email)
if user and user.email:
token = db.create_password_reset_token(user.telegram_id)
link = f"{settings.webapp_url.rstrip('/')}/?reset={token}"
try:
await send_email(
settings,
to=user.email,
subject="Сброс пароля — VPN Service",
html=render_password_reset_email(
reset_url=link,
webapp_url=settings.webapp_url or "",
),
)
except Exception as exc: # noqa: BLE001
logger.exception("password reset mail failed")
return web.json_response(
{"ok": False, "error": f"Не удалось отправить письмо: {exc}"},
status=502,
)
return web.json_response(
{
"ok": True,
"message": "Если email найден — отправили ссылку для сброса. Проверьте почту и «Спам».",
}
)
async def handle_password_reset(request: web.Request) -> web.Response:
db: Database = request.app["db"]
if not allow(request, "pwd_reset", limit=10, window_sec=600):
return web.json_response({"ok": False, "error": "Слишком часто"}, status=429)
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
token = str(body.get("token") or "")
password = str(body.get("password") or "")
try:
tid = db.consume_password_reset_token(token)
db.set_password_hash(tid, password)
db.clear_all_sessions(tid)
session = db.create_session(
tid, user_agent=request.headers.get("User-Agent"), ip=client_ip(request)
)
except AuthError as exc:
return web.json_response({"ok": False, "error": str(exc)}, status=400)
user = db.get_user(tid)
assert user is not None
resp = web.json_response(
{"ok": True, "token": session, "user": _user_payload(user), "balance": user.balance}
)
_set_session_cookie(resp, request, session)
return resp
async def handle_hwid_list(request: web.Request) -> web.Response:
db: Database = request.app["db"]
sdk = request.app["sdk"]
try:
user = _require_user(request)
except web.HTTPUnauthorized:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
panel = await find_panel_user(sdk, user.telegram_id, login=user.login)
if panel is None:
return web.json_response({"ok": True, "devices": [], "total": 0})
try:
resp = await sdk.hwid.get_hwid_user(str(panel.uuid))
devices = []
for d in getattr(resp, "devices", None) or []:
devices.append(
{
"hwid": getattr(d, "hwid", ""),
"platform": getattr(d, "platform", None),
"os_version": getattr(d, "os_version", None),
"device_model": getattr(d, "device_model", None),
"user_agent": getattr(d, "user_agent", None),
"created_at": getattr(d, "created_at", None).isoformat()
if getattr(d, "created_at", None)
else None,
}
)
return web.json_response(
{
"ok": True,
"total": int(getattr(resp, "total", None) or len(devices)),
"devices": devices,
}
)
except Exception as exc: # noqa: BLE001
logger.exception("hwid list failed")
return web.json_response({"ok": False, "error": str(exc)}, status=500)
async def handle_hwid_delete(request: web.Request) -> web.Response:
db: Database = request.app["db"]
sdk = request.app["sdk"]
try:
user = _require_user(request)
except web.HTTPUnauthorized:
return web.json_response({"ok": False, "error": "unauthorized"}, status=401)
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
hwid = str(body.get("hwid") or "").strip()
if not hwid:
return web.json_response({"ok": False, "error": "hwid required"}, status=400)
panel = await find_panel_user(sdk, user.telegram_id, login=user.login)
if panel is None:
return web.json_response({"ok": False, "error": "Нет подписки"}, status=400)
try:
await sdk.hwid.delete_hwid_to_user(
HWIDDeleteRequest(user_uuid=UUID(str(panel.uuid)), hwid=hwid)
)
from services.remnawave_users import sync_device_policy
from services.extras_billing import effective_extra_devices
fresh = await find_panel_user(sdk, user.telegram_id, login=user.login)
if fresh is not None:
await sync_device_policy(
sdk,
fresh,
db=db,
telegram_id=user.telegram_id,
extra_paid=effective_extra_devices(user),
device_blocked=user.device_blocked,
)
except Exception as exc: # noqa: BLE001
logger.exception("hwid delete failed")
return web.json_response({"ok": False, "error": str(exc)}, status=500)
return web.json_response({"ok": True})
async def handle_terms_page(_: web.Request) -> web.Response:
return web.Response(text=html_terms(), content_type="text/html", charset="utf-8")
async def handle_offer_page(_: web.Request) -> web.Response:
return web.Response(text=html_offer(), content_type="text/html", charset="utf-8")
async def handle_privacy_page(_: web.Request) -> web.Response:
return web.Response(text=html_privacy(), content_type="text/html", charset="utf-8")
async def handle_aup_page(_: web.Request) -> web.Response:
return web.Response(text=html_aup(), content_type="text/html", charset="utf-8")
async def handle_vpn_usage_page(_: web.Request) -> web.Response:
return web.Response(text=html_vpn_usage(), content_type="text/html", charset="utf-8")
async def handle_rules_page(_: web.Request) -> web.Response:
return web.Response(text=html_site_rules(), content_type="text/html", charset="utf-8")
async def handle_refund_page(_: web.Request) -> web.Response:
return web.Response(text=html_refund(), content_type="text/html", charset="utf-8")
async def handle_delivery_page(_: web.Request) -> web.Response:
return web.Response(text=html_delivery(), content_type="text/html", charset="utf-8")
async def handle_contacts_page(_: web.Request) -> web.Response:
return web.Response(text=html_contacts(), content_type="text/html", charset="utf-8")
async def handle_help_page(_: web.Request) -> web.Response:
return web.Response(text=html_help(), content_type="text/html", charset="utf-8")
async def handle_support_page(_: web.Request) -> web.Response:
return web.Response(text=html_support(), content_type="text/html", charset="utf-8")
async def handle_cookies_page(_: web.Request) -> web.Response:
return web.Response(text=html_cookies(), content_type="text/html", charset="utf-8")
async def handle_status_page(_: web.Request) -> web.Response:
return web.Response(text=html_status(), content_type="text/html", charset="utf-8")
def _require_admin(request: web.Request):
from webapp_admin_routes import _require_admin as ra
return ra(request)
async def handle_admin_promos_list(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
db: Database = request.app["db"]
rows = db.list_promo_codes()
items = []
for r in rows:
items.append(
{
"id": r["id"],
"code": r["code"],
"kind": r["kind"],
"value": float(r["value"]),
"max_uses": r.get("max_uses"),
"used_count": int(r.get("used_count") or 0),
"tariff_id": r.get("tariff_id"),
"active": bool(r.get("active")),
"expires_at": r["expires_at"].isoformat()
if r.get("expires_at") and hasattr(r["expires_at"], "isoformat")
else None,
"note": r.get("note"),
}
)
return web.json_response({"ok": True, "promos": items})
async def handle_admin_promos_create(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
db: Database = request.app["db"]
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
try:
row = db.create_promo_code(
code=str(body.get("code") or ""),
kind=str(body.get("kind") or "percent"),
value=float(body.get("value") or 0),
max_uses=int(body["max_uses"]) if body.get("max_uses") not in (None, "") else None,
tariff_id=str(body.get("tariff_id") or "").strip() or None,
note=str(body.get("note") or "").strip() or None,
)
except (AuthError, ValueError) as exc:
return web.json_response({"ok": False, "error": str(exc)}, status=400)
return web.json_response({"ok": True, "promo": {"id": row["id"], "code": row["code"]}})
async def handle_admin_promos_toggle(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
db: Database = request.app["db"]
promo_id = int(request.match_info["promo_id"])
try:
body = await request.json()
except json.JSONDecodeError:
body = {}
db.set_promo_active(promo_id, bool(body.get("active", True)))
return web.json_response({"ok": True})
async def handle_admin_user_ban(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
db: Database = request.app["db"]
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
tid = int(body.get("telegram_id") or 0)
banned = bool(body.get("banned"))
user = db.set_banned(tid, banned)
return web.json_response({"ok": True, "user": _user_payload(user), "banned": user.banned})
async def handle_admin_user_delete(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
db: Database = request.app["db"]
settings: Settings = request.app["settings"]
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
try:
tid = int(body.get("telegram_id") or 0)
except (TypeError, ValueError):
return web.json_response({"ok": False, "error": "bad telegram_id"}, status=400)
if not tid:
return web.json_response({"ok": False, "error": "Укажите telegram_id"}, status=400)
if not body.get("confirm"):
return web.json_response(
{"ok": False, "error": "Нужно подтверждение (confirm=true)"},
status=400,
)
if tid in (settings.admin_ids or ()):
return web.json_response(
{"ok": False, "error": "Нельзя удалить пользователя из ADMIN_IDS"},
status=400,
)
user = db.get_user(tid)
if user is None:
return web.json_response({"ok": False, "error": "Пользователь не найден"}, status=404)
label = user.display_name
try:
ok = db.delete_user(tid)
except Exception as exc: # noqa: BLE001
logger.exception("admin delete user failed tid=%s", tid)
return web.json_response({"ok": False, "error": str(exc)}, status=500)
if not ok:
return web.json_response({"ok": False, "error": "Пользователь не найден"}, status=404)
return web.json_response(
{
"ok": True,
"telegram_id": tid,
"message": f"Удалён: {label} (ID {tid})",
}
)
async def handle_admin_payment_journal(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
db: Database = request.app["db"]
rows = db.list_payment_events(limit=100)
items = []
for r in rows:
items.append(
{
"id": r["id"],
"provider": r["provider"],
"event_type": r["event_type"],
"telegram_id": r.get("telegram_id"),
"amount": float(r["amount"]) if r.get("amount") is not None else None,
"invoice_ref": r.get("invoice_ref"),
"status": r.get("status"),
"meta": r.get("meta"),
"created_at": r["created_at"].isoformat()
if hasattr(r["created_at"], "isoformat")
else str(r["created_at"]),
}
)
return web.json_response({"ok": True, "events": items})
async def handle_admin_backups_list(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
return web.json_response({"ok": True, "backups": list_backups()})
async def handle_admin_backups_run(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
try:
path = await run_pg_dump_backup(compose_dir=ROOT_DIR)
except Exception as exc: # noqa: BLE001
logger.exception("backup failed")
return web.json_response({"ok": False, "error": str(exc)}, status=500)
return web.json_response({"ok": True, "file": path.name, "backups": list_backups()})
async def handle_admin_backups_download(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
name = request.match_info.get("name") or ""
try:
path = resolve_backup_path(name)
except (ValueError, FileNotFoundError) as exc:
return web.json_response({"ok": False, "error": str(exc)}, status=404)
return web.FileResponse(
path,
headers={
"Content-Type": "application/gzip",
"Content-Disposition": f'attachment; filename="{path.name}"',
},
)
async def handle_admin_backups_delete(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
name = request.match_info.get("name") or ""
try:
delete_backup(name)
except (ValueError, FileNotFoundError) as exc:
return web.json_response({"ok": False, "error": str(exc)}, status=404)
return web.json_response({"ok": True, "backups": list_backups()})
async def handle_admin_backups_delete_post(request: web.Request) -> web.Response:
"""Удаление через POST — надёжнее, чем DELETE за прокси."""
denied = _require_admin(request)
if denied:
return denied
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
name = str(body.get("name") or "").strip()
if not name:
return web.json_response({"ok": False, "error": "Укажите name"}, status=400)
try:
delete_backup(name)
except (ValueError, FileNotFoundError) as exc:
return web.json_response({"ok": False, "error": str(exc)}, status=404)
return web.json_response({"ok": True, "backups": list_backups(), "message": f"Удалён {name}"})
async def handle_admin_backups_restore(request: web.Request) -> web.Response:
"""Восстановить БД из сохранённого бэкапа или загруженного файла (multipart)."""
denied = _require_admin(request)
if denied:
return denied
content_type = (request.content_type or "").lower()
safety_name = None
try:
# Сначала страховочный бэкап текущего состояния
try:
safety = await run_pg_dump_backup(compose_dir=ROOT_DIR)
safety_name = safety.name
except Exception as exc: # noqa: BLE001
logger.warning("pre-restore safety backup failed: %s", exc)
if "multipart" in content_type:
reader = await request.multipart()
confirm = False
file_bytes: bytes | None = None
filename = "upload.sql.gz"
while True:
part = await reader.next()
if part is None:
break
if part.name == "confirm":
confirm = (await part.text()).strip().lower() in {"1", "true", "yes", "on"}
elif part.name == "file":
filename = Path(part.filename or filename).name
file_bytes = await part.read(decode=False)
if not confirm:
return web.json_response(
{"ok": False, "error": "Нужно подтверждение restore (confirm=true)"},
status=400,
)
if not file_bytes:
return web.json_response({"ok": False, "error": "Файл не передан"}, status=400)
await restore_from_bytes(file_bytes, filename=filename)
try:
await save_uploaded_backup(file_bytes, original_name=filename)
except Exception: # noqa: BLE001
pass
else:
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
if not body.get("confirm"):
return web.json_response(
{"ok": False, "error": "Нужно подтверждение restore (confirm=true)"},
status=400,
)
name = str(body.get("name") or "").strip()
if not name:
return web.json_response({"ok": False, "error": "Укажите name бэкапа"}, status=400)
await restore_from_backup_name(name)
except Exception as exc: # noqa: BLE001
logger.exception("restore failed")
return web.json_response(
{
"ok": False,
"error": str(exc),
"safety_backup": safety_name,
},
status=500,
)
return web.json_response(
{
"ok": True,
"message": "База восстановлена. Перезапустите bot при необходимости.",
"safety_backup": safety_name,
"backups": list_backups(),
}
)
async def handle_admin_backups_upload(request: web.Request) -> web.Response:
"""Только сохранить файл в список бэкапов (без restore)."""
denied = _require_admin(request)
if denied:
return denied
if "multipart" not in (request.content_type or "").lower():
return web.json_response({"ok": False, "error": "ожидается multipart"}, status=400)
reader = await request.multipart()
file_bytes: bytes | None = None
filename = "upload.sql.gz"
while True:
part = await reader.next()
if part is None:
break
if part.name == "file":
filename = Path(part.filename or filename).name
file_bytes = await part.read(decode=False)
if not file_bytes:
return web.json_response({"ok": False, "error": "Файл не передан"}, status=400)
try:
path = await save_uploaded_backup(file_bytes, original_name=filename)
except Exception as exc: # noqa: BLE001
return web.json_response({"ok": False, "error": str(exc)}, status=400)
return web.json_response({"ok": True, "file": path.name, "backups": list_backups()})
async def handle_admin_growth_get(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
db: Database = request.app["db"]
return web.json_response(
{
"ok": True,
"settings": {
"referral_bonus_referrer": db.get_setting("referral_bonus_referrer", "50"),
"referral_bonus_referee": db.get_setting("referral_bonus_referee", "50"),
"trial_days": db.get_setting("trial_days", "3"),
"trial_tariff_id": db.get_setting("trial_tariff_id", ""),
},
}
)
async def handle_admin_growth_set(request: web.Request) -> web.Response:
denied = _require_admin(request)
if denied:
return denied
db: Database = request.app["db"]
try:
body = await request.json()
except json.JSONDecodeError:
return web.json_response({"ok": False, "error": "invalid json"}, status=400)
mapping = {
"referral_bonus_referrer": "referral_bonus_referrer",
"referral_bonus_referee": "referral_bonus_referee",
"trial_days": "trial_days",
"trial_tariff_id": "trial_tariff_id",
}
data = {}
for k, sk in mapping.items():
if k in body and body[k] is not None:
data[sk] = str(body[k]).strip()
if data:
db.set_settings_map(data)
return await handle_admin_growth_get(request)