feat: add create forms for servers, tariffs, payments in admin panel

This commit is contained in:
2026-07-05 18:26:31 +07:00
parent 7f12e1d2ce
commit 4b33b883f1
4 changed files with 241 additions and 12 deletions

View File

@@ -1,4 +1,5 @@
from jinja2 import Environment, FileSystemLoader
from loguru import logger
from fastapi import APIRouter, Form, Request
from fastapi.responses import HTMLResponse, RedirectResponse
@@ -6,13 +7,15 @@ from sqlalchemy import func, select
from app.database import async_session_factory
from app.models.payment import Payment, PaymentStatus
from app.models.server import Server
from app.models.server import Server, ServerProtocol
from app.models.tariff import Tariff
from app.models.user import User
from app.repositories.payment import PaymentRepository
from app.repositories.server import ServerRepository
from app.repositories.tariff import TariffRepository
from app.repositories.user import UserRepository
from app.services.payment import PaymentService
from app.services.server import ServerService
from app.services.user import UserService
router = APIRouter(prefix="/admin", tags=["admin"])
@@ -110,20 +113,27 @@ async def users_create(
return RedirectResponse(url="/admin/users", status_code=303)
async def _get_payments_with_tariffs(session):
stmt = select(Payment).order_by(Payment.created_at.desc())
result = await session.execute(stmt)
payments = list(result.scalars().all())
tariff_repo = TariffRepository(session)
tariffs_map = {}
for p in payments:
if p.tariff_id not in tariffs_map:
t = await tariff_repo.get(p.tariff_id)
tariffs_map[p.tariff_id] = t.name if t else ""
return payments, tariffs_map
@router.get("/payments", response_class=HTMLResponse)
async def payments_page(request: Request):
async with async_session_factory() as session:
user_repo = UserRepository(session)
tariff_repo = TariffRepository(session)
stmt = select(Payment).order_by(Payment.created_at.desc())
result = await session.execute(stmt)
payments = list(result.scalars().all())
tariffs_map = {}
for p in payments:
if p.tariff_id not in tariffs_map:
t = await tariff_repo.get(p.tariff_id)
tariffs_map[p.tariff_id] = t.name if t else ""
payments, tariffs_map = await _get_payments_with_tariffs(session)
users = await user_repo.get_all()
tariffs = await tariff_repo.get_all()
return HTMLResponse(
_render(
@@ -131,10 +141,53 @@ async def payments_page(request: Request):
request=request,
payments=payments,
tariffs_map=tariffs_map,
users=users,
tariffs=tariffs,
)
)
@router.post("/payments/create", response_class=HTMLResponse)
async def payments_create(
request: Request,
user_id: int = Form(...),
tariff_id: int = Form(...),
amount: float = Form(...),
currency: str = Form("RUB"),
provider: str = Form("manual"),
):
async with async_session_factory() as session:
payment_repo = PaymentRepository(session)
user_repo = UserRepository(session)
tariff_repo = TariffRepository(session)
payment_service = PaymentService(payment_repo, user_repo, tariff_repo)
try:
payment = await payment_service.create_payment(
user_id=user_id,
tariff_id=tariff_id,
amount=amount,
currency=currency,
provider=provider,
)
logger.info("Admin created payment id={}", payment.id)
except ValueError as e:
payments, tariffs_map = await _get_payments_with_tariffs(session)
return HTMLResponse(
_render(
"payments.html",
request=request,
payments=payments,
tariffs_map=tariffs_map,
users=await user_repo.get_all(),
tariffs=await tariff_repo.get_all(),
error=str(e),
)
)
return RedirectResponse(url="/admin/payments", status_code=303)
@router.get("/servers", response_class=HTMLResponse)
async def servers_page(request: Request):
async with async_session_factory() as session:
@@ -142,10 +195,56 @@ async def servers_page(request: Request):
servers = await repo.get_all()
return HTMLResponse(
_render("servers.html", request=request, servers=servers)
_render(
"servers.html",
request=request,
servers=servers,
protocols=[p.value for p in ServerProtocol],
)
)
@router.post("/servers/create", response_class=HTMLResponse)
async def servers_create(
request: Request,
name: str = Form(...),
host: str = Form(...),
port: int = Form(...),
protocol: str = Form(...),
location: str = Form(...),
country_code: str = Form(...),
max_users: int = Form(100),
):
async with async_session_factory() as session:
server_repo = ServerRepository(session)
server_service = ServerService(server_repo)
try:
server = await server_service.create(
name=name,
host=host,
port=port,
protocol=ServerProtocol(protocol),
location=location,
country_code=country_code,
max_users=max_users,
)
logger.info("Admin created server id={}", server.id)
except ValueError as e:
repo = ServerRepository(session)
return HTMLResponse(
_render(
"servers.html",
request=request,
servers=await repo.get_all(),
protocols=[p.value for p in ServerProtocol],
error=str(e),
)
)
return RedirectResponse(url="/admin/servers", status_code=303)
@router.get("/tariffs", response_class=HTMLResponse)
async def tariffs_page(request: Request):
async with async_session_factory() as session:
@@ -155,3 +254,29 @@ async def tariffs_page(request: Request):
return HTMLResponse(
_render("tariffs.html", request=request, tariffs=tariffs)
)
@router.post("/tariffs/create", response_class=HTMLResponse)
async def tariffs_create(
request: Request,
name: str = Form(...),
duration_days: int = Form(...),
price: float = Form(...),
currency: str = Form("RUB"),
max_devices: int = Form(1),
traffic_gb: int = Form(0),
):
async with async_session_factory() as session:
tariff_repo = TariffRepository(session)
traffic = traffic_gb if traffic_gb > 0 else None
tariff = await tariff_repo.create(
name=name,
duration_days=duration_days,
price=price,
currency=currency,
max_devices=max_devices,
traffic_gb=traffic,
)
logger.info("Admin created tariff id={}", tariff.id)
return RedirectResponse(url="/admin/tariffs", status_code=303)

View File

@@ -2,6 +2,39 @@
{% block title %}Payments — VPN Control Panel{% endblock %}
{% block content %}
<h2>Payments</h2>
<div class="card" style="margin-bottom:20px;padding:16px;">
<form action="/admin/payments/create" method="post" style="display:flex;gap:8px;align-items:end;flex-wrap:wrap;">
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">User</label>
<select name="user_id" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
{% for u in users %}
<option value="{{ u.id }}">#{{ u.id }} @{{ u.username or u.telegram_id }}</option>
{% endfor %}
</select>
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Tariff</label>
<select name="tariff_id" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
{% for t in tariffs %}
<option value="{{ t.id }}">{{ t.name }} — {{ "%.2f"|format(t.price) }} {{ t.currency }}</option>
{% endfor %}
</select>
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Amount</label>
<input type="number" step="0.01" name="amount" required style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:90px;">
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Provider</label>
<input type="text" name="provider" value="manual" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:80px;">
</div>
<button type="submit" style="padding:7px 16px;background:#1a1a2e;color:#fff;border:none;border-radius:4px;font-size:13px;cursor:pointer;">+ Add Payment</button>
</form>
</div>
{% if error %}<div style="color:#c53030;font-size:13px;margin-bottom:12px;">{{ error }}</div>{% endif %}
<div class="count">{{ payments|length }} total</div>
<table>
<thead>

View File

@@ -2,6 +2,43 @@
{% block title %}Servers — VPN Control Panel{% endblock %}
{% block content %}
<h2>Servers</h2>
<div class="card" style="margin-bottom:20px;padding:16px;">
<form action="/admin/servers/create" method="post" style="display:flex;gap:8px;align-items:end;flex-wrap:wrap;">
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Name</label>
<input type="text" name="name" required style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Host</label>
<input type="text" name="host" required style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Port</label>
<input type="number" name="port" required value="443" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:70px;">
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Protocol</label>
<select name="protocol" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
{% for p in protocols %}
<option value="{{ p }}">{{ p }}</option>
{% endfor %}
</select>
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Location</label>
<input type="text" name="location" required style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:100px;">
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Country</label>
<input type="text" name="country_code" required maxlength="2" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:50px;">
</div>
<button type="submit" style="padding:7px 16px;background:#1a1a2e;color:#fff;border:none;border-radius:4px;font-size:13px;cursor:pointer;">+ Add Server</button>
</form>
</div>
{% if error %}<div style="color:#c53030;font-size:13px;margin-bottom:12px;">{{ error }}</div>{% endif %}
<div class="count">{{ servers|length }} total</div>
<table>
<thead>

View File

@@ -2,6 +2,40 @@
{% block title %}Tariffs — VPN Control Panel{% endblock %}
{% block content %}
<h2>Tariffs</h2>
<div class="card" style="margin-bottom:20px;padding:16px;">
<form action="/admin/tariffs/create" method="post" style="display:flex;gap:8px;align-items:end;flex-wrap:wrap;">
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Name</label>
<input type="text" name="name" required style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Days</label>
<input type="number" name="duration_days" required value="30" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:60px;">
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Price</label>
<input type="number" step="0.01" name="price" required value="0" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:80px;">
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Currency</label>
<select name="currency" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;">
<option value="RUB">RUB</option>
<option value="USD">USD</option>
</select>
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Devices</label>
<input type="number" name="max_devices" value="1" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:60px;">
</div>
<div>
<label style="font-size:11px;color:#888;display:block;margin-bottom:2px;">Traffic GB</label>
<input type="number" name="traffic_gb" value="0" placeholder="0=unlimited" style="padding:6px 10px;border:1px solid #ddd;border-radius:4px;font-size:13px;width:70px;">
</div>
<button type="submit" style="padding:7px 16px;background:#1a1a2e;color:#fff;border:none;border-radius:4px;font-size:13px;cursor:pointer;">+ Add Tariff</button>
</form>
</div>
<div class="count">{{ tariffs|length }} total</div>
<table>
<thead>