402 lines
13 KiB
Python
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")
|