feat: add create forms for servers, tariffs, payments in admin panel
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user