first commit
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import secrets
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, HTTPException, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from auth import check_admin_credentials, get_current_admin, require_admin_page
|
||||
from config import get_settings
|
||||
from database import get_db
|
||||
from urlutil import public_base_url
|
||||
|
||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
|
||||
|
||||
def _now_iso() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
if require_admin_page(request):
|
||||
return RedirectResponse("/admin", status_code=303)
|
||||
settings = get_settings()
|
||||
return templates.TemplateResponse(
|
||||
"admin/login.html",
|
||||
{"request": request, "brand": settings.brand_name, "error": None},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login_submit(
|
||||
request: Request,
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
):
|
||||
settings = get_settings()
|
||||
if not check_admin_credentials(username, password):
|
||||
return templates.TemplateResponse(
|
||||
"admin/login.html",
|
||||
{
|
||||
"request": request,
|
||||
"brand": settings.brand_name,
|
||||
"error": "Неверный логин или пароль",
|
||||
},
|
||||
status_code=401,
|
||||
)
|
||||
request.session["admin"] = username
|
||||
return RedirectResponse("/admin", status_code=303)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(request: Request):
|
||||
request.session.clear()
|
||||
return RedirectResponse("/admin/login", status_code=303)
|
||||
|
||||
|
||||
@router.get("", response_class=HTMLResponse)
|
||||
async def admin_dashboard(request: Request):
|
||||
if not require_admin_page(request):
|
||||
return RedirectResponse("/admin/login", status_code=303)
|
||||
|
||||
settings = get_settings()
|
||||
db = await get_db()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
"""
|
||||
SELECT l.*,
|
||||
(SELECT COUNT(*) FROM guest_claims c WHERE c.link_id = l.id) AS claims
|
||||
FROM guest_links l
|
||||
ORDER BY l.id DESC
|
||||
"""
|
||||
)
|
||||
links = [dict(row) for row in await cursor.fetchall()]
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
site_base = public_base_url(request)
|
||||
return templates.TemplateResponse(
|
||||
"admin/dashboard.html",
|
||||
{
|
||||
"request": request,
|
||||
"brand": settings.brand_name,
|
||||
"links": links,
|
||||
"site_base": site_base,
|
||||
"flash": request.query_params.get("flash"),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/links")
|
||||
async def create_link(
|
||||
request: Request,
|
||||
link_type: str = Form(...),
|
||||
title: str = Form(""),
|
||||
value: int = Form(...),
|
||||
max_uses: int = Form(1),
|
||||
duration_days: int = Form(0),
|
||||
plan_traffic_gb: int = Form(0),
|
||||
expires_at: str = Form(""),
|
||||
note: str = Form(""),
|
||||
_admin: str = Depends(get_current_admin),
|
||||
):
|
||||
if link_type not in ("extend", "traffic"):
|
||||
raise HTTPException(400, "Неверный тип ссылки")
|
||||
if value < 1:
|
||||
raise HTTPException(400, "Значение должно быть >= 1")
|
||||
if max_uses < 1:
|
||||
raise HTTPException(400, "max_uses должен быть >= 1")
|
||||
|
||||
if link_type == "traffic":
|
||||
if duration_days < 1:
|
||||
raise HTTPException(
|
||||
400, "Для трафика укажите срок действия (дней) — трафик временный"
|
||||
)
|
||||
plan_traffic_gb = 0
|
||||
else:
|
||||
duration_days = 0
|
||||
if plan_traffic_gb < 1:
|
||||
raise HTTPException(
|
||||
400,
|
||||
"Для продления укажите трафик тарифа (ГБ) — лимит зафиксируется при активации",
|
||||
)
|
||||
|
||||
token = secrets.token_urlsafe(12)
|
||||
expires = expires_at.strip() or None
|
||||
if not title.strip():
|
||||
if link_type == "extend":
|
||||
title = f"Продление +{value} дн. / {plan_traffic_gb} ГБ"
|
||||
else:
|
||||
title = f"Трафик +{value} ГБ на {duration_days} дн."
|
||||
|
||||
db = await get_db()
|
||||
try:
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO guest_links
|
||||
(token, link_type, title, value, max_uses, expires_at, note,
|
||||
created_at, duration_days, plan_traffic_gb)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
token,
|
||||
link_type,
|
||||
title.strip(),
|
||||
value,
|
||||
max_uses,
|
||||
expires,
|
||||
note.strip(),
|
||||
_now_iso(),
|
||||
duration_days,
|
||||
plan_traffic_gb,
|
||||
),
|
||||
)
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
return RedirectResponse("/admin?flash=created", status_code=303)
|
||||
|
||||
|
||||
@router.post("/links/{link_id}/toggle")
|
||||
async def toggle_link(link_id: int, _admin: str = Depends(get_current_admin)):
|
||||
db = await get_db()
|
||||
try:
|
||||
await db.execute(
|
||||
"UPDATE guest_links SET is_active = CASE WHEN is_active = 1 THEN 0 ELSE 1 END WHERE id = ?",
|
||||
(link_id,),
|
||||
)
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
return RedirectResponse("/admin?flash=toggled", status_code=303)
|
||||
|
||||
|
||||
@router.post("/links/{link_id}/delete")
|
||||
async def delete_link(link_id: int, _admin: str = Depends(get_current_admin)):
|
||||
db = await get_db()
|
||||
try:
|
||||
await db.execute("DELETE FROM guest_links WHERE id = ?", (link_id,))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
return RedirectResponse("/admin?flash=deleted", status_code=303)
|
||||
|
||||
|
||||
@router.get("/links/{link_id}", response_class=HTMLResponse)
|
||||
async def link_detail(request: Request, link_id: int):
|
||||
if not require_admin_page(request):
|
||||
return RedirectResponse("/admin/login", status_code=303)
|
||||
|
||||
settings = get_settings()
|
||||
db = await get_db()
|
||||
try:
|
||||
cursor = await db.execute("SELECT * FROM guest_links WHERE id = ?", (link_id,))
|
||||
link = await cursor.fetchone()
|
||||
if not link:
|
||||
raise HTTPException(404, "Ссылка не найдена")
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM guest_claims WHERE link_id = ? ORDER BY id DESC",
|
||||
(link_id,),
|
||||
)
|
||||
claims = [dict(row) for row in await cursor.fetchall()]
|
||||
cursor = await db.execute(
|
||||
"""
|
||||
SELECT * FROM traffic_grants
|
||||
WHERE link_id = ?
|
||||
ORDER BY id DESC
|
||||
""",
|
||||
(link_id,),
|
||||
)
|
||||
grants = [dict(row) for row in await cursor.fetchall()]
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"admin/link_detail.html",
|
||||
{
|
||||
"request": request,
|
||||
"brand": settings.brand_name,
|
||||
"link": dict(link),
|
||||
"claims": claims,
|
||||
"grants": grants,
|
||||
"site_base": public_base_url(request),
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user