@@ -0,0 +1,250 @@
|
||||
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()
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"admin/servers.html",
|
||||
{
|
||||
"request": request,
|
||||
"admin": admin,
|
||||
"servers": servers,
|
||||
"app_name": request.app.state.settings.app_name,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/servers/{server_id}/sync")
|
||||
async def sync_server(
|
||||
server_id: int,
|
||||
request: Request,
|
||||
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}")
|
||||
async def update_server(
|
||||
server_id: int,
|
||||
public_host: str = Form(...),
|
||||
public_port: int = Form(...),
|
||||
dns: str = Form("1.1.1.1"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
admin=Depends(require_admin),
|
||||
):
|
||||
if _is_redirect(admin):
|
||||
return admin
|
||||
server = await db.get(VpnServer, server_id)
|
||||
if server:
|
||||
server.public_host = public_host.strip()
|
||||
server.public_port = public_port
|
||||
server.dns = dns.strip() or "1.1.1.1"
|
||||
await db.commit()
|
||||
await vpn_service.sync_server_config(db, server_id)
|
||||
return RedirectResponse("/admin/servers", 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")
|
||||
Reference in New Issue
Block a user