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 from app.services import vpn as vpn_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 servers = (await db.execute(select(VpnServer).order_by(VpnServer.id))).scalars().all() clients_total = await db.scalar(select(func.count()).select_from(VpnClient)) or 0 clients_active = await db.scalar( select(func.count()).select_from(VpnClient).where(VpnClient.is_enabled.is_(True)) ) or 0 return request.app.state.templates.TemplateResponse( "admin/dashboard.html", { "request": request, "admin": admin, "servers": servers, "clients_total": clients_total, "clients_active": clients_active, "app_name": request.app.state.settings.app_name, }, ) @router.get("/servers", response_class=HTMLResponse) async def servers_page(request: Request, db: AsyncSession = Depends(get_db), admin=Depends(require_admin)): if _is_redirect(admin): return admin servers = ( await db.execute( select(VpnServer).options(selectinload(VpnServer.clients)).order_by(VpnServer.id) ) ).scalars().all() probes = await vpn_service.probe_servers(list(servers)) return request.app.state.templates.TemplateResponse( "admin/servers.html", { "request": request, "admin": admin, "servers": servers, "probes": probes, "protocols": Protocol, "app_name": request.app.state.settings.app_name, "flash": request.query_params.get("flash"), }, ) @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, protocol: str | None = None, db: AsyncSession = Depends(get_db), admin=Depends(require_admin), ): if _is_redirect(admin): return admin query = select(VpnClient).options(selectinload(VpnClient.server)).order_by(VpnClient.id.desc()) if protocol: query = query.join(VpnServer).where(VpnServer.protocol == protocol) clients = (await db.execute(query)).scalars().all() servers = (await db.execute(select(VpnServer).order_by(VpnServer.id))).scalars().all() return request.app.state.templates.TemplateResponse( "admin/clients.html", { "request": request, "admin": admin, "clients": clients, "servers": servers, "filter_protocol": protocol, "protocols": Protocol, "app_name": request.app.state.settings.app_name, "flash": request.query_params.get("flash"), }, ) @router.post("/clients") async def create_client( request: Request, name: str = Form(...), server_id: int = Form(...), notes: str = Form(""), db: AsyncSession = Depends(get_db), admin=Depends(require_admin), ): if _is_redirect(admin): return admin try: await vpn_service.create_client(db, server_id=server_id, name=name, notes=notes or None) 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")