"""Доп. 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)