Files
vpn/app/routers/admin.py
T

402 lines
13 KiB
Python

import io
import qrcode
from fastapi import APIRouter, Depends, Form, Request, status
from fastapi.responses import HTMLResponse, RedirectResponse, Response, StreamingResponse
from sqlalchemy import func, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from app.database import get_db
from app.models import AdminUser, Protocol, VpnClient, VpnServer, XuiPanel, XuiPanelInbound, XuiSiteClient
from app.services import vpn as vpn_service
from app.services import xui_panels as xui_service
router = APIRouter(prefix="/admin", tags=["admin"])
async def require_admin(request: Request, db: AsyncSession = Depends(get_db)) -> AdminUser | RedirectResponse:
admin_id = request.session.get("admin_id")
if not admin_id:
return RedirectResponse("/admin/login", status_code=status.HTTP_303_SEE_OTHER)
user = await db.get(AdminUser, admin_id)
if not user or not user.is_active:
request.session.clear()
return RedirectResponse("/admin/login", status_code=status.HTTP_303_SEE_OTHER)
return user
def _is_redirect(value: object) -> bool:
return isinstance(value, RedirectResponse)
@router.get("", response_class=HTMLResponse)
async def dashboard(request: Request, db: AsyncSession = Depends(get_db), admin=Depends(require_admin)):
if _is_redirect(admin):
return admin
xui_panels = await xui_service.list_panels(db)
xui_clients_total = await db.scalar(select(func.count()).select_from(XuiSiteClient)) or 0
user_inbounds_total = await db.scalar(
select(func.count())
.select_from(XuiPanelInbound)
.where(XuiPanelInbound.is_available_for_users.is_(True))
) or 0
user_inbounds_by_panel: dict[int, list] = {}
for panel in xui_panels:
rows = await xui_service.list_user_inbounds(db, panel.id)
user_inbounds_by_panel[panel.id] = [
{"inbound_id": r.inbound_id, "protocol": r.protocol, "remark": r.remark, "port": r.port}
for r in rows
]
return request.app.state.templates.TemplateResponse(
"admin/dashboard.html",
{
"request": request,
"admin": admin,
"xui_panels": xui_panels,
"xui_clients_total": xui_clients_total,
"user_inbounds_total": user_inbounds_total,
"user_inbounds_by_panel": user_inbounds_by_panel,
"app_name": request.app.state.settings.app_name,
},
)
@router.get("/servers", response_class=HTMLResponse)
async def servers_page(admin=Depends(require_admin)):
"""WG/AWG servers UI временно скрыт — редирект на клиентов 3x-ui."""
if _is_redirect(admin):
return admin
return RedirectResponse("/admin/clients", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/servers")
async def create_server(
name: str = Form(""),
protocol: str = Form(...),
ssh_host: str = Form(...),
ssh_port: int = Form(22),
ssh_username: str = Form(...),
ssh_password: str = Form(""),
ssh_private_key: str = Form(""),
public_host: str = Form(""),
public_port: str = Form(""),
dns: str = Form("1.1.1.1"),
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
try:
port_value = int(public_port) if public_port.strip() else None
await vpn_service.create_server(
db,
name=name,
protocol=protocol,
ssh_host=ssh_host,
ssh_port=ssh_port,
ssh_username=ssh_username,
ssh_password=ssh_password,
ssh_private_key=ssh_private_key,
public_host=public_host or None,
public_port=port_value,
dns=dns,
)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/servers?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse("/admin/servers?flash=created", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/servers/{server_id}/sync")
async def sync_server(
server_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
await vpn_service.sync_server_config(db, server_id)
return RedirectResponse("/admin/servers", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/servers/{server_id}/check-ssh")
async def check_ssh(
server_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
try:
await vpn_service.check_server_ssh(db, server_id)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/servers?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse("/admin/servers?flash=ssh_ok", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/servers/{server_id}/install-vpn")
async def install_vpn(
server_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
try:
await vpn_service.install_vpn_on_server(db, server_id)
return RedirectResponse(
"/admin/servers?flash=vpn_installed",
status_code=status.HTTP_303_SEE_OTHER,
)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/servers?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
@router.post("/servers/{server_id}/delete")
async def delete_server(
server_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
server = await db.get(VpnServer, server_id)
if server:
await db.delete(server)
await db.commit()
return RedirectResponse("/admin/servers?flash=deleted", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/servers/{server_id}")
async def update_server(
server_id: int,
name: str = Form(""),
public_host: str = Form(...),
public_port: int = Form(...),
dns: str = Form("1.1.1.1"),
ssh_host: str = Form(""),
ssh_port: int = Form(22),
ssh_username: str = Form(""),
ssh_password: str = Form(""),
ssh_private_key: str = Form(""),
clear_ssh_password: str = Form(""),
clear_ssh_private_key: str = Form(""),
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
try:
await vpn_service.update_server_settings(
db,
server_id,
name=name or None,
public_host=public_host,
public_port=public_port,
dns=dns,
ssh_host=ssh_host,
ssh_port=ssh_port,
ssh_username=ssh_username,
ssh_password=ssh_password,
ssh_private_key=ssh_private_key,
clear_ssh_password=clear_ssh_password == "1",
clear_ssh_private_key=clear_ssh_private_key == "1",
)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/servers?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse("/admin/servers?flash=saved", status_code=status.HTTP_303_SEE_OTHER)
@router.get("/clients", response_class=HTMLResponse)
async def clients_page(
request: Request,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
xui_panels = await xui_service.list_panels(db)
xui_clients = await xui_service.list_all_site_clients(db)
user_inbounds_by_panel: dict[int, list] = {}
for panel in xui_panels:
rows = await xui_service.list_user_inbounds(db, panel.id)
user_inbounds_by_panel[panel.id] = [
{
"inbound_id": r.inbound_id,
"protocol": r.protocol,
"remark": r.remark,
"port": r.port,
}
for r in rows
]
panels_with_inbounds = [p for p in xui_panels if user_inbounds_by_panel.get(p.id)]
return request.app.state.templates.TemplateResponse(
"admin/clients.html",
{
"request": request,
"admin": admin,
"xui_panels": xui_panels,
"xui_clients": xui_clients,
"user_inbounds_by_panel": user_inbounds_by_panel,
"panels_with_inbounds": panels_with_inbounds,
"app_name": request.app.state.settings.app_name,
"flash": request.query_params.get("flash"),
},
)
@router.post("/clients/xui")
async def create_xui_client_from_clients(
request: Request,
panel_id: int = Form(...),
protocol: str = Form(...),
email: str = Form(...),
inbound_id: int = Form(...),
total_gb: int = Form(0),
limit_ip: int = Form(0),
expiry_days: int = Form(30),
start_after_first_use: str = Form(""),
flow: str = Form(""),
comment: str = Form(""),
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
panel = await db.get(XuiPanel, panel_id)
if not panel:
return RedirectResponse(
"/admin/clients?flash=error:Панель не найдена",
status_code=status.HTTP_303_SEE_OTHER,
)
try:
await xui_service.add_xui_client(
db,
panel,
protocol=protocol,
email=email.strip(),
inbound_id=inbound_id,
total_gb=total_gb,
limit_ip=limit_ip,
expiry_days=expiry_days,
start_after_first_use=start_after_first_use == "1",
flow=flow.strip(),
comment=comment.strip(),
require_user_available=True,
)
except Exception as exc: # noqa: BLE001
return RedirectResponse(
f"/admin/clients?flash=error:{exc}",
status_code=status.HTTP_303_SEE_OTHER,
)
return RedirectResponse(
"/admin/clients?flash=created",
status_code=status.HTTP_303_SEE_OTHER,
)
@router.post("/clients/{client_id}/toggle")
async def toggle_client(
client_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
client = await db.get(VpnClient, client_id)
if client:
await vpn_service.set_client_enabled(db, client_id, not client.is_enabled)
return RedirectResponse("/admin/clients", status_code=status.HTTP_303_SEE_OTHER)
@router.post("/clients/{client_id}/delete")
async def remove_client(
client_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
await vpn_service.delete_client(db, client_id)
return RedirectResponse("/admin/clients?flash=deleted", status_code=status.HTTP_303_SEE_OTHER)
@router.get("/clients/{client_id}", response_class=HTMLResponse)
async def client_detail(
client_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
result = await db.execute(
select(VpnClient).options(selectinload(VpnClient.server)).where(VpnClient.id == client_id)
)
client = result.scalar_one_or_none()
if not client:
return RedirectResponse("/admin/clients", status_code=status.HTTP_303_SEE_OTHER)
config = await vpn_service.get_client_config(db, client_id)
return request.app.state.templates.TemplateResponse(
"admin/client_detail.html",
{
"request": request,
"admin": admin,
"client": client,
"config": config,
"app_name": request.app.state.settings.app_name,
},
)
@router.get("/clients/{client_id}/config")
async def download_config(
client_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
client = await db.get(VpnClient, client_id)
if not client:
return RedirectResponse("/admin/clients", status_code=status.HTTP_303_SEE_OTHER)
config = await vpn_service.get_client_config(db, client_id)
filename = f"{client.name}.conf"
return Response(
content=config,
media_type="text/plain",
headers={"Content-Disposition": f'attachment; filename="{filename}"'},
)
@router.get("/clients/{client_id}/qr")
async def client_qr(
client_id: int,
db: AsyncSession = Depends(get_db),
admin=Depends(require_admin),
):
if _is_redirect(admin):
return admin
config = await vpn_service.get_client_config(db, client_id)
img = qrcode.make(config)
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)
return StreamingResponse(buf, media_type="image/png")