first commit
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
"""Web routes for browser-based UI using Jinja2 + HTMX."""
|
||||
|
||||
from fastapi import APIRouter
|
||||
from . import auth, dashboard, gateways, users, tenants, applications, connections, htmx
|
||||
from . import ca, vpn_servers, vpn_profiles
|
||||
|
||||
# Create web router
|
||||
web_router = APIRouter()
|
||||
|
||||
# Include web route modules
|
||||
web_router.include_router(auth.router, tags=["Web Auth"])
|
||||
web_router.include_router(dashboard.router, tags=["Web Dashboard"])
|
||||
web_router.include_router(gateways.router, tags=["Web Gateways"])
|
||||
web_router.include_router(users.router, tags=["Web Users"])
|
||||
web_router.include_router(tenants.router, tags=["Web Tenants"])
|
||||
web_router.include_router(applications.router, tags=["Web Applications"])
|
||||
web_router.include_router(connections.router, tags=["Web Connections"])
|
||||
web_router.include_router(htmx.router, prefix="/htmx", tags=["HTMX Partials"])
|
||||
|
||||
# PKI & VPN Management
|
||||
web_router.include_router(ca.router, tags=["Web CA"])
|
||||
web_router.include_router(vpn_servers.router, tags=["Web VPN Servers"])
|
||||
web_router.include_router(vpn_profiles.router, tags=["Web VPN Profiles"])
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Application template management web routes (Super Admin only)."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.user import User
|
||||
from ..models.endpoint import ApplicationTemplate, Protocol
|
||||
from .deps import require_super_admin_web, flash, get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/applications", response_class=HTMLResponse)
|
||||
async def list_applications(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""List all application templates."""
|
||||
applications = db.query(ApplicationTemplate).order_by(ApplicationTemplate.name).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"applications/list.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"applications": applications,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/applications/new", response_class=HTMLResponse)
|
||||
async def new_application_form(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""New application template form."""
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"applications/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"application": None,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/applications/new")
|
||||
async def create_application(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
default_port: int = Form(...),
|
||||
protocol: str = Form("tcp"),
|
||||
description: str = Form(None),
|
||||
icon: str = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""Create new application template."""
|
||||
# Check if name exists
|
||||
existing = db.query(ApplicationTemplate).filter(ApplicationTemplate.name == name).first()
|
||||
if existing:
|
||||
flash(request, "Anwendungsname bereits vergeben", "danger")
|
||||
return RedirectResponse(url="/applications/new", status_code=303)
|
||||
|
||||
application = ApplicationTemplate(
|
||||
name=name,
|
||||
default_port=default_port,
|
||||
protocol=Protocol(protocol),
|
||||
description=description or None,
|
||||
icon=icon or None
|
||||
)
|
||||
|
||||
db.add(application)
|
||||
db.commit()
|
||||
|
||||
flash(request, f"Anwendung '{name}' erstellt", "success")
|
||||
return RedirectResponse(url="/applications", status_code=303)
|
||||
|
||||
|
||||
@router.get("/applications/{app_id}/edit", response_class=HTMLResponse)
|
||||
async def edit_application_form(
|
||||
request: Request,
|
||||
app_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""Edit application template form."""
|
||||
application = db.query(ApplicationTemplate).filter(ApplicationTemplate.id == app_id).first()
|
||||
if not application:
|
||||
flash(request, "Anwendung nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/applications", status_code=303)
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"applications/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"application": application,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/applications/{app_id}/edit")
|
||||
async def update_application(
|
||||
request: Request,
|
||||
app_id: int,
|
||||
name: str = Form(...),
|
||||
default_port: int = Form(...),
|
||||
protocol: str = Form("tcp"),
|
||||
description: str = Form(None),
|
||||
icon: str = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""Update application template."""
|
||||
application = db.query(ApplicationTemplate).filter(ApplicationTemplate.id == app_id).first()
|
||||
if not application:
|
||||
flash(request, "Anwendung nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/applications", status_code=303)
|
||||
|
||||
application.name = name
|
||||
application.default_port = default_port
|
||||
application.protocol = Protocol(protocol)
|
||||
application.description = description or None
|
||||
application.icon = icon or None
|
||||
|
||||
db.commit()
|
||||
|
||||
flash(request, "Anwendung aktualisiert", "success")
|
||||
return RedirectResponse(url="/applications", status_code=303)
|
||||
|
||||
|
||||
@router.post("/applications/{app_id}/delete")
|
||||
async def delete_application(
|
||||
request: Request,
|
||||
app_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""Delete application template."""
|
||||
application = db.query(ApplicationTemplate).filter(ApplicationTemplate.id == app_id).first()
|
||||
if not application:
|
||||
flash(request, "Anwendung nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/applications", status_code=303)
|
||||
|
||||
db.delete(application)
|
||||
db.commit()
|
||||
|
||||
flash(request, f"Anwendung '{application.name}' gelöscht", "warning")
|
||||
return RedirectResponse(url="/applications", status_code=303)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Web authentication routes (session-based)."""
|
||||
|
||||
from fastapi import APIRouter, Request, Form, Depends, HTTPException
|
||||
from fastapi.responses import RedirectResponse, HTMLResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..services.auth_service import AuthService
|
||||
from ..utils.security import verify_password
|
||||
from .deps import get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
async def login_page(request: Request):
|
||||
"""Show login page."""
|
||||
# If already logged in, redirect to dashboard
|
||||
if request.session.get("user_id"):
|
||||
return RedirectResponse(url="/dashboard", status_code=303)
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"auth/login.html",
|
||||
{"request": request, "error": None, "flash_messages": get_flashed_messages(request)}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/login", response_class=HTMLResponse)
|
||||
async def login_submit(
|
||||
request: Request,
|
||||
username: str = Form(...),
|
||||
password: str = Form(...),
|
||||
remember: bool = Form(False),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Process login form."""
|
||||
auth_service = AuthService(db)
|
||||
user = auth_service.authenticate_user(username, password)
|
||||
|
||||
if not user:
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"auth/login.html",
|
||||
{"request": request, "error": "Ungültige Anmeldedaten", "flash_messages": []}
|
||||
)
|
||||
|
||||
# Store user in session
|
||||
request.session["user_id"] = user.id
|
||||
request.session["username"] = user.username
|
||||
request.session["role"] = user.role.value
|
||||
request.session["tenant_id"] = user.tenant_id
|
||||
|
||||
return RedirectResponse(url="/dashboard", status_code=303)
|
||||
|
||||
|
||||
@router.get("/logout")
|
||||
async def logout(request: Request):
|
||||
"""Logout user."""
|
||||
request.session.clear()
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
@@ -0,0 +1,301 @@
|
||||
"""Certificate Authority management web routes."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form, UploadFile, File
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.user import User
|
||||
from ..models.certificate_authority import CertificateAuthority, CAStatus
|
||||
from ..services.certificate_service import CertificateService
|
||||
from .deps import require_admin_web, flash, get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/ca", response_class=HTMLResponse)
|
||||
async def list_cas(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""List all Certificate Authorities."""
|
||||
if current_user.is_super_admin:
|
||||
cas = db.query(CertificateAuthority).all()
|
||||
else:
|
||||
cas = db.query(CertificateAuthority).filter(
|
||||
(CertificateAuthority.tenant_id == current_user.tenant_id) |
|
||||
(CertificateAuthority.tenant_id == None)
|
||||
).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"ca/list.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"cas": cas,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ca/new", response_class=HTMLResponse)
|
||||
async def new_ca_form(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""New CA form."""
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"ca/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"ca": None,
|
||||
"mode": "create",
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ca/new")
|
||||
async def create_ca(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
description: str = Form(None),
|
||||
key_size: int = Form(4096),
|
||||
validity_days: int = Form(3650),
|
||||
organization: str = Form("mGuard VPN"),
|
||||
country: str = Form("DE"),
|
||||
state: str = Form("NRW"),
|
||||
city: str = Form("Dortmund"),
|
||||
is_default: bool = Form(False),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Create new CA."""
|
||||
try:
|
||||
service = CertificateService(db)
|
||||
|
||||
tenant_id = None if current_user.is_super_admin else current_user.tenant_id
|
||||
|
||||
ca = service.create_ca(
|
||||
name=name,
|
||||
key_size=key_size,
|
||||
validity_days=validity_days,
|
||||
organization=organization,
|
||||
country=country,
|
||||
state=state,
|
||||
city=city,
|
||||
tenant_id=tenant_id,
|
||||
created_by_id=current_user.id,
|
||||
is_default=is_default
|
||||
)
|
||||
|
||||
flash(request, f"CA '{name}' wird erstellt. DH-Parameter werden im Hintergrund generiert...", "success")
|
||||
return RedirectResponse(url=f"/ca/{ca.id}", status_code=303)
|
||||
|
||||
except Exception as e:
|
||||
flash(request, f"Fehler beim Erstellen der CA: {str(e)}", "danger")
|
||||
return RedirectResponse(url="/ca/new", status_code=303)
|
||||
|
||||
|
||||
@router.get("/ca/import", response_class=HTMLResponse)
|
||||
async def import_ca_form(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Import CA form."""
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"ca/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"ca": None,
|
||||
"mode": "import",
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ca/import")
|
||||
async def import_ca(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
description: str = Form(None),
|
||||
ca_cert: UploadFile = File(...),
|
||||
ca_key: UploadFile = File(...),
|
||||
dh_params: Optional[UploadFile] = File(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Import existing CA from files."""
|
||||
try:
|
||||
service = CertificateService(db)
|
||||
|
||||
ca_cert_pem = (await ca_cert.read()).decode('utf-8')
|
||||
ca_key_pem = (await ca_key.read()).decode('utf-8')
|
||||
dh_params_pem = (await dh_params.read()).decode('utf-8') if dh_params else None
|
||||
|
||||
tenant_id = None if current_user.is_super_admin else current_user.tenant_id
|
||||
|
||||
ca = service.import_ca(
|
||||
name=name,
|
||||
ca_cert_pem=ca_cert_pem,
|
||||
ca_key_pem=ca_key_pem,
|
||||
dh_params_pem=dh_params_pem,
|
||||
tenant_id=tenant_id,
|
||||
created_by_id=current_user.id
|
||||
)
|
||||
|
||||
if dh_params_pem:
|
||||
flash(request, f"CA '{name}' erfolgreich importiert", "success")
|
||||
else:
|
||||
flash(request, f"CA '{name}' importiert. DH-Parameter werden generiert...", "success")
|
||||
|
||||
return RedirectResponse(url=f"/ca/{ca.id}", status_code=303)
|
||||
|
||||
except Exception as e:
|
||||
flash(request, f"Fehler beim Importieren: {str(e)}", "danger")
|
||||
return RedirectResponse(url="/ca/import", status_code=303)
|
||||
|
||||
|
||||
@router.get("/ca/{ca_id}", response_class=HTMLResponse)
|
||||
async def ca_detail(
|
||||
request: Request,
|
||||
ca_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""CA detail page."""
|
||||
ca = db.query(CertificateAuthority).filter(
|
||||
CertificateAuthority.id == ca_id
|
||||
).first()
|
||||
|
||||
if not ca:
|
||||
flash(request, "CA nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/ca", status_code=303)
|
||||
|
||||
# Check access
|
||||
if not current_user.is_super_admin and ca.tenant_id != current_user.tenant_id and ca.tenant_id is not None:
|
||||
flash(request, "Zugriff verweigert", "danger")
|
||||
return RedirectResponse(url="/ca", status_code=303)
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"ca/detail.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"ca": ca,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ca/{ca_id}/set-default")
|
||||
async def set_ca_default(
|
||||
request: Request,
|
||||
ca_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Set CA as default."""
|
||||
ca = db.query(CertificateAuthority).filter(
|
||||
CertificateAuthority.id == ca_id
|
||||
).first()
|
||||
|
||||
if not ca:
|
||||
flash(request, "CA nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/ca", status_code=303)
|
||||
|
||||
# Unset other defaults for same tenant
|
||||
db.query(CertificateAuthority).filter(
|
||||
CertificateAuthority.tenant_id == ca.tenant_id,
|
||||
CertificateAuthority.id != ca.id
|
||||
).update({"is_default": False})
|
||||
|
||||
ca.is_default = True
|
||||
db.commit()
|
||||
|
||||
flash(request, f"'{ca.name}' ist jetzt die Standard-CA", "success")
|
||||
return RedirectResponse(url=f"/ca/{ca_id}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/ca/{ca_id}/download/cert")
|
||||
async def download_ca_cert(
|
||||
ca_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Download CA certificate."""
|
||||
ca = db.query(CertificateAuthority).filter(
|
||||
CertificateAuthority.id == ca_id
|
||||
).first()
|
||||
|
||||
if not ca or not ca.ca_cert:
|
||||
return Response(status_code=404)
|
||||
|
||||
return Response(
|
||||
content=ca.ca_cert,
|
||||
media_type="application/x-pem-file",
|
||||
headers={"Content-Disposition": f"attachment; filename={ca.name}-ca.crt"}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/ca/{ca_id}/download/crl")
|
||||
async def download_crl(
|
||||
ca_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Download Certificate Revocation List."""
|
||||
ca = db.query(CertificateAuthority).filter(
|
||||
CertificateAuthority.id == ca_id
|
||||
).first()
|
||||
|
||||
if not ca:
|
||||
return Response(status_code=404)
|
||||
|
||||
service = CertificateService(db)
|
||||
crl = service.get_crl(ca)
|
||||
|
||||
return Response(
|
||||
content=crl,
|
||||
media_type="application/x-pem-file",
|
||||
headers={"Content-Disposition": f"attachment; filename={ca.name}-crl.pem"}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/ca/{ca_id}/delete")
|
||||
async def delete_ca(
|
||||
request: Request,
|
||||
ca_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Delete CA."""
|
||||
ca = db.query(CertificateAuthority).filter(
|
||||
CertificateAuthority.id == ca_id
|
||||
).first()
|
||||
|
||||
if not ca:
|
||||
flash(request, "CA nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/ca", status_code=303)
|
||||
|
||||
# Check if CA has servers or profiles
|
||||
if ca.vpn_servers:
|
||||
flash(request, "CA wird noch von VPN-Servern verwendet. Bitte zuerst löschen.", "danger")
|
||||
return RedirectResponse(url=f"/ca/{ca_id}", status_code=303)
|
||||
|
||||
if ca.vpn_profiles:
|
||||
flash(request, "CA wird noch von VPN-Profilen verwendet. Bitte zuerst löschen.", "danger")
|
||||
return RedirectResponse(url=f"/ca/{ca_id}", status_code=303)
|
||||
|
||||
name = ca.name
|
||||
db.delete(ca)
|
||||
db.commit()
|
||||
|
||||
flash(request, f"CA '{name}' gelöscht", "warning")
|
||||
return RedirectResponse(url="/ca", status_code=303)
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Connection log web routes."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.user import User
|
||||
from .deps import get_current_user_web, get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/connections", response_class=HTMLResponse)
|
||||
async def connection_log(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Connection log page."""
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"connections/log.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Dashboard web routes."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func
|
||||
from ..database import get_db
|
||||
from ..models.user import User
|
||||
from ..models.gateway import Gateway
|
||||
from ..models.endpoint import Endpoint
|
||||
from ..models.access import ConnectionLog
|
||||
from .deps import get_current_user_web, get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def root(request: Request):
|
||||
"""Root redirect to dashboard."""
|
||||
if request.session.get("user_id"):
|
||||
return RedirectResponse(url="/dashboard", status_code=303)
|
||||
return RedirectResponse(url="/login", status_code=303)
|
||||
|
||||
|
||||
@router.get("/dashboard", response_class=HTMLResponse)
|
||||
async def dashboard(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Dashboard with overview."""
|
||||
# Get statistics
|
||||
gateways_total = db.query(Gateway).count()
|
||||
gateways_online = db.query(Gateway).filter(Gateway.is_online == True).count()
|
||||
endpoints_total = db.query(Endpoint).count()
|
||||
users_total = db.query(User).filter(User.is_active == True).count()
|
||||
active_connections = db.query(ConnectionLog).filter(
|
||||
ConnectionLog.disconnected_at.is_(None)
|
||||
).count()
|
||||
|
||||
stats = {
|
||||
"gateways_total": gateways_total,
|
||||
"gateways_online": gateways_online,
|
||||
"endpoints_total": endpoints_total,
|
||||
"users_total": users_total,
|
||||
"active_connections": active_connections
|
||||
}
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"dashboard/index.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"stats": stats,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Web route dependencies."""
|
||||
|
||||
from fastapi import Request, HTTPException, Depends
|
||||
from fastapi.responses import RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.user import User, UserRole
|
||||
|
||||
|
||||
async def get_current_user_web(request: Request, db: Session = Depends(get_db)) -> User:
|
||||
"""Get current user from session for web routes."""
|
||||
user_id = request.session.get("user_id")
|
||||
|
||||
if not user_id:
|
||||
raise HTTPException(status_code=303, headers={"Location": "/login"})
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
|
||||
if not user or not user.is_active:
|
||||
request.session.clear()
|
||||
raise HTTPException(status_code=303, headers={"Location": "/login"})
|
||||
|
||||
return user
|
||||
|
||||
|
||||
async def require_user_web(
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
) -> User:
|
||||
"""Require any authenticated user for web routes."""
|
||||
return current_user
|
||||
|
||||
|
||||
async def require_admin_web(
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
) -> User:
|
||||
"""Require admin role for web routes."""
|
||||
if not current_user.is_admin:
|
||||
raise HTTPException(status_code=403, detail="Admin-Rechte erforderlich")
|
||||
return current_user
|
||||
|
||||
|
||||
async def require_super_admin_web(
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
) -> User:
|
||||
"""Require super admin role for web routes."""
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
raise HTTPException(status_code=403, detail="Super-Admin-Rechte erforderlich")
|
||||
return current_user
|
||||
|
||||
|
||||
def get_flashed_messages(request: Request) -> list[dict]:
|
||||
"""Get and clear flash messages from session."""
|
||||
messages = request.session.pop("flash_messages", [])
|
||||
return messages
|
||||
|
||||
|
||||
def flash(request: Request, message: str, category: str = "info"):
|
||||
"""Add a flash message to session."""
|
||||
if "flash_messages" not in request.session:
|
||||
request.session["flash_messages"] = []
|
||||
request.session["flash_messages"].append({"category": category, "message": message})
|
||||
@@ -0,0 +1,250 @@
|
||||
"""Gateway web routes."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form, Response
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.user import User, UserRole
|
||||
from ..models.gateway import Gateway, RouterType, ProvisioningMethod
|
||||
from ..models.endpoint import ApplicationTemplate
|
||||
from ..models.tenant import Tenant
|
||||
from ..models.access import UserGatewayAccess
|
||||
from .deps import get_current_user_web, require_admin_web, flash, get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def get_accessible_gateways(db: Session, user: User):
|
||||
"""Get gateways accessible by user."""
|
||||
if user.role == UserRole.SUPER_ADMIN:
|
||||
return db.query(Gateway).all()
|
||||
elif user.role == UserRole.ADMIN:
|
||||
return db.query(Gateway).filter(Gateway.tenant_id == user.tenant_id).all()
|
||||
else:
|
||||
return db.query(Gateway).join(
|
||||
UserGatewayAccess,
|
||||
UserGatewayAccess.gateway_id == Gateway.id
|
||||
).filter(UserGatewayAccess.user_id == user.id).all()
|
||||
|
||||
|
||||
@router.get("/gateways", response_class=HTMLResponse)
|
||||
async def list_gateways(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""List gateways page."""
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"gateways/list.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/gateways/new", response_class=HTMLResponse)
|
||||
async def new_gateway_form(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""New gateway form."""
|
||||
tenants = db.query(Tenant).filter(Tenant.is_active == True).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"gateways/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"gateway": None,
|
||||
"tenants": tenants,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/gateways/new", response_class=HTMLResponse)
|
||||
async def create_gateway(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
router_type: str = Form(...),
|
||||
firmware_version: str = Form(None),
|
||||
serial_number: str = Form(None),
|
||||
location: str = Form(None),
|
||||
vpn_subnet: str = Form(None),
|
||||
description: str = Form(None),
|
||||
tenant_id: int = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Create new gateway."""
|
||||
# Determine tenant
|
||||
if current_user.role == UserRole.SUPER_ADMIN and tenant_id:
|
||||
gateway_tenant_id = tenant_id
|
||||
else:
|
||||
gateway_tenant_id = current_user.tenant_id
|
||||
|
||||
# Generate VPN cert CN
|
||||
vpn_cert_cn = f"gateway-{name.lower().replace(' ', '-')}"
|
||||
|
||||
gateway = Gateway(
|
||||
name=name,
|
||||
router_type=RouterType(router_type),
|
||||
firmware_version=firmware_version or None,
|
||||
serial_number=serial_number or None,
|
||||
location=location or None,
|
||||
vpn_subnet=vpn_subnet or None,
|
||||
description=description or None,
|
||||
tenant_id=gateway_tenant_id,
|
||||
vpn_cert_cn=vpn_cert_cn
|
||||
)
|
||||
|
||||
db.add(gateway)
|
||||
db.commit()
|
||||
db.refresh(gateway)
|
||||
|
||||
flash(request, f"Gateway '{name}' erfolgreich erstellt", "success")
|
||||
return RedirectResponse(url=f"/gateways/{gateway.id}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}", response_class=HTMLResponse)
|
||||
async def gateway_detail(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Gateway detail page."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
|
||||
if not gateway:
|
||||
flash(request, "Gateway nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
# Check access
|
||||
if current_user.role == UserRole.TECHNICIAN:
|
||||
access = db.query(UserGatewayAccess).filter(
|
||||
UserGatewayAccess.user_id == current_user.id,
|
||||
UserGatewayAccess.gateway_id == gateway_id
|
||||
).first()
|
||||
if not access:
|
||||
flash(request, "Kein Zugriff auf dieses Gateway", "danger")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
templates = db.query(ApplicationTemplate).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"gateways/detail.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"gateway": gateway,
|
||||
"templates": templates,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/edit", response_class=HTMLResponse)
|
||||
async def edit_gateway_form(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Edit gateway form."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if not gateway:
|
||||
flash(request, "Gateway nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
tenants = db.query(Tenant).filter(Tenant.is_active == True).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"gateways/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"gateway": gateway,
|
||||
"tenants": tenants,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/edit")
|
||||
async def update_gateway(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
name: str = Form(...),
|
||||
router_type: str = Form(...),
|
||||
firmware_version: str = Form(None),
|
||||
serial_number: str = Form(None),
|
||||
location: str = Form(None),
|
||||
vpn_subnet: str = Form(None),
|
||||
description: str = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Update gateway."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if not gateway:
|
||||
flash(request, "Gateway nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
gateway.name = name
|
||||
gateway.router_type = RouterType(router_type)
|
||||
gateway.firmware_version = firmware_version or None
|
||||
gateway.serial_number = serial_number or None
|
||||
gateway.location = location or None
|
||||
gateway.vpn_subnet = vpn_subnet or None
|
||||
gateway.description = description or None
|
||||
|
||||
db.commit()
|
||||
|
||||
flash(request, "Gateway aktualisiert", "success")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/delete")
|
||||
async def delete_gateway(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Delete gateway."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if not gateway:
|
||||
flash(request, "Gateway nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
db.delete(gateway)
|
||||
db.commit()
|
||||
|
||||
flash(request, f"Gateway '{gateway.name}' gelöscht", "warning")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/provision")
|
||||
async def download_provisioning(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Redirect to VPN profiles for provisioning.
|
||||
|
||||
Provisioning is now done through VPN profiles.
|
||||
Each profile contains its own certificate and VPN server configuration.
|
||||
"""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
if not gateway:
|
||||
flash(request, "Gateway nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
# Redirect to profiles page
|
||||
flash(request, "Bitte erstellen Sie ein VPN-Profil für das Provisioning", "info")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
@@ -0,0 +1,698 @@
|
||||
"""HTMX partial routes for dynamic updates."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form
|
||||
from fastapi.responses import HTMLResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.user import User, UserRole
|
||||
from ..models.gateway import Gateway
|
||||
from ..models.endpoint import Endpoint
|
||||
from ..models.access import UserGatewayAccess, ConnectionLog
|
||||
from ..models.vpn_server import VPNServer
|
||||
from ..models.vpn_connection_log import VPNConnectionLog
|
||||
from ..services.vpn_server_service import VPNServerService
|
||||
from ..services.vpn_sync_service import VPNSyncService
|
||||
from .deps import get_current_user_web
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/dashboard/stats", response_class=HTMLResponse)
|
||||
async def dashboard_stats(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Dashboard statistics partial."""
|
||||
gateways_total = db.query(Gateway).count()
|
||||
gateways_online = db.query(Gateway).filter(Gateway.is_online == True).count()
|
||||
endpoints_total = db.query(Endpoint).count()
|
||||
users_total = db.query(User).filter(User.is_active == True).count()
|
||||
active_connections = db.query(ConnectionLog).filter(
|
||||
ConnectionLog.disconnected_at.is_(None)
|
||||
).count()
|
||||
|
||||
# Count VPN clients across all active servers
|
||||
vpn_clients_total = 0
|
||||
vpn_servers = db.query(VPNServer).filter(VPNServer.is_active == True).all()
|
||||
service = VPNServerService(db)
|
||||
for server in vpn_servers:
|
||||
try:
|
||||
clients = service.get_connected_clients(server)
|
||||
vpn_clients_total += len(clients)
|
||||
except:
|
||||
pass # Server might be offline
|
||||
|
||||
return f"""
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card stat-card bg-primary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="card-subtitle mb-2 text-white-50">Gateways</h6>
|
||||
<div class="stat-value">{gateways_online} / {gateways_total}</div>
|
||||
</div>
|
||||
<i class="bi bi-router stat-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card stat-card bg-success text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="card-subtitle mb-2 text-white-50">VPN-Clients</h6>
|
||||
<div class="stat-value">{vpn_clients_total}</div>
|
||||
</div>
|
||||
<i class="bi bi-shield-check stat-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card stat-card bg-info text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="card-subtitle mb-2 text-white-50">Sessions</h6>
|
||||
<div class="stat-value">{active_connections}</div>
|
||||
</div>
|
||||
<i class="bi bi-plug stat-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card stat-card bg-secondary text-white">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="card-subtitle mb-2 text-white-50">Endpunkte</h6>
|
||||
<div class="stat-value">{endpoints_total}</div>
|
||||
</div>
|
||||
<i class="bi bi-hdd-network stat-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2 mb-3">
|
||||
<div class="card stat-card bg-warning text-dark">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between">
|
||||
<div>
|
||||
<h6 class="card-subtitle mb-2">Benutzer</h6>
|
||||
<div class="stat-value">{users_total}</div>
|
||||
</div>
|
||||
<i class="bi bi-people stat-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
|
||||
@router.get("/gateways/list", response_class=HTMLResponse)
|
||||
@router.get("/gateways/search", response_class=HTMLResponse)
|
||||
@router.get("/gateways/filter", response_class=HTMLResponse)
|
||||
async def gateway_list_partial(
|
||||
request: Request,
|
||||
q: str = "",
|
||||
status: str = "",
|
||||
type: str = "",
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Gateway list partial for HTMX."""
|
||||
query = db.query(Gateway)
|
||||
|
||||
# Filter by tenant for non-super-admins
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if current_user.role == UserRole.ADMIN:
|
||||
query = query.filter(Gateway.tenant_id == current_user.tenant_id)
|
||||
else:
|
||||
query = query.join(
|
||||
UserGatewayAccess,
|
||||
UserGatewayAccess.gateway_id == Gateway.id
|
||||
).filter(UserGatewayAccess.user_id == current_user.id)
|
||||
|
||||
# Apply filters
|
||||
if q:
|
||||
query = query.filter(Gateway.name.ilike(f"%{q}%"))
|
||||
if status == "online":
|
||||
query = query.filter(Gateway.is_online == True)
|
||||
elif status == "offline":
|
||||
query = query.filter(Gateway.is_online == False)
|
||||
if type:
|
||||
query = query.filter(Gateway.router_type == type)
|
||||
|
||||
gateways = query.all()
|
||||
|
||||
if not gateways:
|
||||
return """
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Keine Gateways gefunden
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = '<div class="row">'
|
||||
for gw in gateways:
|
||||
status_class = "online" if gw.is_online else "offline"
|
||||
status_badge = '<span class="badge bg-success">Online</span>' if gw.is_online else '<span class="badge bg-secondary">Offline</span>'
|
||||
last_seen = gw.last_seen.strftime('%d.%m.%Y %H:%M') if gw.last_seen else 'Nie'
|
||||
|
||||
html += f"""
|
||||
<div class="col-md-6 col-lg-4 mb-3">
|
||||
<div class="card gateway-card {status_class}">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">
|
||||
<span class="status-indicator {status_class}"></span>
|
||||
{gw.name}
|
||||
</h5>
|
||||
<p class="card-text text-muted small mb-2">
|
||||
{gw.router_type} | {gw.location or 'Kein Standort'}
|
||||
</p>
|
||||
<p class="card-text small">
|
||||
{status_badge}
|
||||
<span class="text-muted ms-2">Zuletzt: {last_seen}</span>
|
||||
</p>
|
||||
<a href="/gateways/{gw.id}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-eye"></i> Details
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
html += '</div>'
|
||||
|
||||
return html
|
||||
|
||||
|
||||
@router.get("/gateways/status-list", response_class=HTMLResponse)
|
||||
async def gateway_status_list(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Gateway status list for dashboard."""
|
||||
query = db.query(Gateway)
|
||||
|
||||
if current_user.role != UserRole.SUPER_ADMIN:
|
||||
if current_user.role == UserRole.ADMIN:
|
||||
query = query.filter(Gateway.tenant_id == current_user.tenant_id)
|
||||
|
||||
gateways = query.limit(10).all()
|
||||
|
||||
if not gateways:
|
||||
return '<p class="text-muted">Keine Gateways vorhanden</p>'
|
||||
|
||||
html = '<table class="table table-sm table-hover mb-0"><tbody>'
|
||||
for gw in gateways:
|
||||
status = '<span class="status-indicator online"></span>' if gw.is_online else '<span class="status-indicator offline"></span>'
|
||||
html += f"""
|
||||
<tr onclick="window.location='/gateways/{gw.id}'" style="cursor:pointer">
|
||||
<td>{status} {gw.name}</td>
|
||||
<td class="text-muted">{gw.router_type}</td>
|
||||
<td class="text-end">
|
||||
<a href="/gateways/{gw.id}" class="btn btn-sm btn-link">
|
||||
<i class="bi bi-arrow-right"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
html += '</tbody></table>'
|
||||
|
||||
return html
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/endpoints", response_class=HTMLResponse)
|
||||
async def gateway_endpoints_partial(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Endpoints list partial."""
|
||||
endpoints = db.query(Endpoint).filter(Endpoint.gateway_id == gateway_id).all()
|
||||
|
||||
if not endpoints:
|
||||
return """
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="bi bi-inbox" style="font-size: 2rem;"></i>
|
||||
<p>Keine Endpunkte definiert</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = '<table class="table table-hover mb-0"><thead><tr><th>Name</th><th>Adresse</th><th>Protokoll</th><th>Anwendung</th><th></th></tr></thead><tbody>'
|
||||
|
||||
for ep in endpoints:
|
||||
protocol_badge = f'<span class="badge badge-{ep.protocol.value}">{ep.protocol.value.upper()}</span>'
|
||||
html += f"""
|
||||
<tr>
|
||||
<td><strong>{ep.name}</strong></td>
|
||||
<td><code>{ep.internal_ip}:{ep.port}</code></td>
|
||||
<td>{protocol_badge}</td>
|
||||
<td>{ep.application_name or '-'}</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
hx-delete="/htmx/endpoints/{ep.id}"
|
||||
hx-confirm="Endpunkt '{ep.name}' löschen?"
|
||||
hx-target="#endpoints-list"
|
||||
hx-swap="innerHTML">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
html += '</tbody></table>'
|
||||
|
||||
return html
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/endpoints", response_class=HTMLResponse)
|
||||
async def create_endpoint_htmx(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
name: str = Form(...),
|
||||
internal_ip: str = Form(...),
|
||||
port: int = Form(...),
|
||||
protocol: str = Form("tcp"),
|
||||
application_template_id: int = Form(None),
|
||||
description: str = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Create endpoint via HTMX."""
|
||||
from ..models.endpoint import Protocol
|
||||
|
||||
endpoint = Endpoint(
|
||||
gateway_id=gateway_id,
|
||||
name=name,
|
||||
internal_ip=internal_ip,
|
||||
port=port,
|
||||
protocol=Protocol(protocol),
|
||||
application_template_id=application_template_id if application_template_id else None,
|
||||
description=description
|
||||
)
|
||||
|
||||
db.add(endpoint)
|
||||
db.commit()
|
||||
|
||||
# Return updated list
|
||||
return await gateway_endpoints_partial(request, gateway_id, db, current_user)
|
||||
|
||||
|
||||
@router.delete("/endpoints/{endpoint_id}", response_class=HTMLResponse)
|
||||
async def delete_endpoint_htmx(
|
||||
request: Request,
|
||||
endpoint_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Delete endpoint via HTMX."""
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == endpoint_id).first()
|
||||
if endpoint:
|
||||
gateway_id = endpoint.gateway_id
|
||||
db.delete(endpoint)
|
||||
db.commit()
|
||||
return await gateway_endpoints_partial(request, gateway_id, db, current_user)
|
||||
return ""
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/access", response_class=HTMLResponse)
|
||||
async def gateway_access_partial(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""User access list partial for gateway."""
|
||||
access_list = db.query(UserGatewayAccess).filter(
|
||||
UserGatewayAccess.gateway_id == gateway_id
|
||||
).all()
|
||||
|
||||
if not access_list:
|
||||
return """
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="bi bi-people" style="font-size: 2rem;"></i>
|
||||
<p>Keine Benutzer haben Zugriff</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = '<table class="table table-hover mb-0"><thead><tr><th>Benutzer</th><th>Rolle</th><th>Gewährt am</th><th>Gewährt von</th><th></th></tr></thead><tbody>'
|
||||
|
||||
for access in access_list:
|
||||
user = db.query(User).filter(User.id == access.user_id).first()
|
||||
granted_by = db.query(User).filter(User.id == access.granted_by_id).first() if access.granted_by_id else None
|
||||
granted_at = access.granted_at.strftime('%d.%m.%Y') if access.granted_at else '-'
|
||||
|
||||
html += f"""
|
||||
<tr>
|
||||
<td><strong>{user.username if user else '-'}</strong></td>
|
||||
<td><span class="badge bg-secondary">{user.role.value if user else '-'}</span></td>
|
||||
<td>{granted_at}</td>
|
||||
<td>{granted_by.username if granted_by else '-'}</td>
|
||||
<td class="text-end">
|
||||
<button class="btn btn-sm btn-outline-danger"
|
||||
hx-delete="/htmx/gateways/{gateway_id}/access/{access.user_id}"
|
||||
hx-confirm="Zugriff entziehen?"
|
||||
hx-target="#access-list"
|
||||
hx-swap="innerHTML">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
"""
|
||||
html += '</tbody></table>'
|
||||
|
||||
return html
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/access", response_class=HTMLResponse)
|
||||
async def grant_access_htmx(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
user_id: int = Form(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Grant user access to gateway via HTMX."""
|
||||
# Check if access already exists
|
||||
existing = db.query(UserGatewayAccess).filter(
|
||||
UserGatewayAccess.gateway_id == gateway_id,
|
||||
UserGatewayAccess.user_id == user_id
|
||||
).first()
|
||||
|
||||
if not existing:
|
||||
access = UserGatewayAccess(
|
||||
gateway_id=gateway_id,
|
||||
user_id=user_id,
|
||||
granted_by_id=current_user.id
|
||||
)
|
||||
db.add(access)
|
||||
db.commit()
|
||||
|
||||
return await gateway_access_partial(request, gateway_id, db, current_user)
|
||||
|
||||
|
||||
@router.delete("/gateways/{gateway_id}/access/{user_id}", response_class=HTMLResponse)
|
||||
async def revoke_access_htmx(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Revoke user access to gateway via HTMX."""
|
||||
access = db.query(UserGatewayAccess).filter(
|
||||
UserGatewayAccess.gateway_id == gateway_id,
|
||||
UserGatewayAccess.user_id == user_id
|
||||
).first()
|
||||
|
||||
if access:
|
||||
db.delete(access)
|
||||
db.commit()
|
||||
|
||||
return await gateway_access_partial(request, gateway_id, db, current_user)
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/vpn-log", response_class=HTMLResponse)
|
||||
async def gateway_vpn_log_partial(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""VPN connection log for gateway."""
|
||||
sync_service = VPNSyncService(db)
|
||||
logs = sync_service.get_gateway_connection_logs(gateway_id, limit=20)
|
||||
|
||||
if not logs:
|
||||
return """
|
||||
<div class="text-center text-muted py-4">
|
||||
<i class="bi bi-shield-x" style="font-size: 2rem;"></i>
|
||||
<p>Keine VPN-Verbindungen aufgezeichnet</p>
|
||||
</div>
|
||||
"""
|
||||
|
||||
html = '''<table class="table table-hover table-sm mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Profil</th>
|
||||
<th>Server</th>
|
||||
<th>Echte Adresse</th>
|
||||
<th>Verbunden</th>
|
||||
<th>Getrennt</th>
|
||||
<th>Dauer</th>
|
||||
<th>Traffic</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>'''
|
||||
|
||||
for log in logs:
|
||||
profile_name = log.vpn_profile.name if log.vpn_profile else '-'
|
||||
server_name = log.vpn_server.name if log.vpn_server else '-'
|
||||
real_addr = log.real_address or '-'
|
||||
connected = log.connected_at.strftime('%d.%m. %H:%M') if log.connected_at else '-'
|
||||
|
||||
if log.disconnected_at:
|
||||
disconnected = log.disconnected_at.strftime('%H:%M')
|
||||
duration = log.duration_seconds or 0
|
||||
if duration >= 3600:
|
||||
duration_str = f"{duration // 3600}h {(duration % 3600) // 60}m"
|
||||
elif duration >= 60:
|
||||
duration_str = f"{duration // 60}m"
|
||||
else:
|
||||
duration_str = f"{duration}s"
|
||||
status_badge = ''
|
||||
else:
|
||||
disconnected = '<span class="badge bg-success">Aktiv</span>'
|
||||
duration_str = '-'
|
||||
status_badge = ''
|
||||
|
||||
rx = log.bytes_received or 0
|
||||
tx = log.bytes_sent or 0
|
||||
rx_str = f"{rx / 1024 / 1024:.1f}" if rx > 1024*1024 else f"{rx / 1024:.0f}K"
|
||||
tx_str = f"{tx / 1024 / 1024:.1f}" if tx > 1024*1024 else f"{tx / 1024:.0f}K"
|
||||
traffic = f"↓{rx_str} ↑{tx_str}"
|
||||
|
||||
html += f'''
|
||||
<tr>
|
||||
<td>{profile_name}</td>
|
||||
<td>{server_name}</td>
|
||||
<td><code class="small">{real_addr}</code></td>
|
||||
<td>{connected}</td>
|
||||
<td>{disconnected}</td>
|
||||
<td>{duration_str}</td>
|
||||
<td class="small">{traffic}</td>
|
||||
</tr>'''
|
||||
|
||||
html += '</tbody></table>'
|
||||
return html
|
||||
|
||||
|
||||
@router.get("/connections/count", response_class=HTMLResponse)
|
||||
async def connections_count(
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
"""Active connections count."""
|
||||
count = db.query(ConnectionLog).filter(
|
||||
ConnectionLog.disconnected_at.is_(None)
|
||||
).count()
|
||||
|
||||
# Also count VPN clients
|
||||
vpn_clients = 0
|
||||
vpn_servers = db.query(VPNServer).filter(VPNServer.is_active == True).all()
|
||||
service = VPNServerService(db)
|
||||
for server in vpn_servers:
|
||||
try:
|
||||
clients = service.get_connected_clients(server)
|
||||
vpn_clients += len(clients)
|
||||
except:
|
||||
pass
|
||||
|
||||
return f'<i class="bi bi-shield-check"></i> {vpn_clients} VPN-Clients <i class="bi bi-plug"></i> {count} Sessions'
|
||||
|
||||
|
||||
@router.get("/connections/vpn-clients", response_class=HTMLResponse)
|
||||
async def vpn_clients_list(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""List of connected VPN clients (gateways)."""
|
||||
# Sync connections with database (updates gateway status, creates logs)
|
||||
sync_service = VPNSyncService(db)
|
||||
sync_service.sync_all_connections()
|
||||
|
||||
# Get active connections from database
|
||||
active_connections = sync_service.get_active_connections()
|
||||
|
||||
if not active_connections:
|
||||
return '<p class="text-muted text-center py-3">Keine VPN-Clients verbunden</p>'
|
||||
|
||||
html = '''<table class="table table-hover table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Gateway</th>
|
||||
<th>Profil</th>
|
||||
<th>VPN-Server</th>
|
||||
<th>Echte Adresse</th>
|
||||
<th>Empfangen</th>
|
||||
<th>Gesendet</th>
|
||||
<th>Verbunden seit</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>'''
|
||||
|
||||
for conn in active_connections:
|
||||
gateway_name = conn.gateway.name if conn.gateway else '-'
|
||||
gateway_id = conn.gateway.id if conn.gateway else ''
|
||||
profile_name = conn.vpn_profile.name if conn.vpn_profile else '-'
|
||||
server_name = conn.vpn_server.name if conn.vpn_server else '-'
|
||||
server_id = conn.vpn_server.id if conn.vpn_server else ''
|
||||
real_addr = conn.real_address or '-'
|
||||
rx = conn.bytes_received or 0
|
||||
tx = conn.bytes_sent or 0
|
||||
connected = conn.connected_at.strftime('%d.%m.%Y %H:%M') if conn.connected_at else '-'
|
||||
|
||||
# Format bytes
|
||||
rx_str = f"{rx / 1024 / 1024:.2f} MB" if rx > 1024*1024 else f"{rx / 1024:.1f} KB"
|
||||
tx_str = f"{tx / 1024 / 1024:.2f} MB" if tx > 1024*1024 else f"{tx / 1024:.1f} KB"
|
||||
|
||||
html += f'''
|
||||
<tr>
|
||||
<td><a href="/gateways/{gateway_id}"><strong>{gateway_name}</strong></a></td>
|
||||
<td>{profile_name}</td>
|
||||
<td><a href="/vpn-servers/{server_id}">{server_name}</a></td>
|
||||
<td><code>{real_addr}</code></td>
|
||||
<td>{rx_str}</td>
|
||||
<td>{tx_str}</td>
|
||||
<td>{connected}</td>
|
||||
</tr>'''
|
||||
|
||||
html += '</tbody></table>'
|
||||
return html
|
||||
|
||||
|
||||
@router.get("/connections/active", response_class=HTMLResponse)
|
||||
async def active_connections(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Active connections partial."""
|
||||
connections = db.query(ConnectionLog).filter(
|
||||
ConnectionLog.disconnected_at.is_(None)
|
||||
).all()
|
||||
|
||||
if not connections:
|
||||
return '<p class="text-muted text-center py-3">Keine aktiven Verbindungen</p>'
|
||||
|
||||
html = '<ul class="list-group list-group-flush">'
|
||||
for conn in connections:
|
||||
user = db.query(User).filter(User.id == conn.user_id).first()
|
||||
gateway = db.query(Gateway).filter(Gateway.id == conn.gateway_id).first()
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == conn.endpoint_id).first() if conn.endpoint_id else None
|
||||
|
||||
html += f"""
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>{user.username if user else 'Unknown'}</strong>
|
||||
<i class="bi bi-arrow-right mx-2"></i>
|
||||
{gateway.name if gateway else 'Unknown'}
|
||||
{f' / {endpoint.name}' if endpoint else ''}
|
||||
</div>
|
||||
<span class="badge bg-success"><i class="bi bi-broadcast"></i> Verbunden</span>
|
||||
</li>
|
||||
"""
|
||||
html += '</ul>'
|
||||
|
||||
return html
|
||||
|
||||
|
||||
@router.get("/connections/recent", response_class=HTMLResponse)
|
||||
async def recent_connections(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Recent connections for dashboard."""
|
||||
connections = db.query(ConnectionLog).order_by(
|
||||
ConnectionLog.connected_at.desc()
|
||||
).limit(5).all()
|
||||
|
||||
if not connections:
|
||||
return '<p class="text-muted text-center">Keine Verbindungen</p>'
|
||||
|
||||
html = '<ul class="list-group list-group-flush">'
|
||||
for conn in connections:
|
||||
user = db.query(User).filter(User.id == conn.user_id).first()
|
||||
gateway = db.query(Gateway).filter(Gateway.id == conn.gateway_id).first()
|
||||
time = conn.connected_at.strftime('%H:%M')
|
||||
|
||||
if conn.disconnected_at:
|
||||
status = '<span class="text-muted"><i class="bi bi-x-circle"></i></span>'
|
||||
else:
|
||||
status = '<span class="text-success"><i class="bi bi-check-circle"></i></span>'
|
||||
|
||||
html += f"""
|
||||
<li class="list-group-item d-flex justify-content-between align-items-center py-2">
|
||||
<span>
|
||||
{status}
|
||||
<span class="ms-2">{user.username if user else '?'}</span>
|
||||
<i class="bi bi-arrow-right text-muted mx-1"></i>
|
||||
<span class="text-muted">{gateway.name if gateway else '?'}</span>
|
||||
</span>
|
||||
<small class="text-muted">{time}</small>
|
||||
</li>
|
||||
"""
|
||||
html += '</ul>'
|
||||
|
||||
return html
|
||||
|
||||
|
||||
@router.get("/connections/list", response_class=HTMLResponse)
|
||||
async def connections_list(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user_web)
|
||||
):
|
||||
"""Connection history list."""
|
||||
connections = db.query(ConnectionLog).order_by(
|
||||
ConnectionLog.connected_at.desc()
|
||||
).limit(50).all()
|
||||
|
||||
if not connections:
|
||||
return '<p class="text-muted text-center py-4">Keine Verbindungshistorie</p>'
|
||||
|
||||
html = '<table class="table table-hover"><thead><tr><th>Benutzer</th><th>Gateway</th><th>Endpunkt</th><th>Verbunden</th><th>Getrennt</th><th>Dauer</th></tr></thead><tbody>'
|
||||
|
||||
for conn in connections:
|
||||
user = db.query(User).filter(User.id == conn.user_id).first()
|
||||
gateway = db.query(Gateway).filter(Gateway.id == conn.gateway_id).first()
|
||||
endpoint = db.query(Endpoint).filter(Endpoint.id == conn.endpoint_id).first() if conn.endpoint_id else None
|
||||
|
||||
connected = conn.connected_at.strftime('%d.%m.%Y %H:%M')
|
||||
disconnected = conn.disconnected_at.strftime('%H:%M') if conn.disconnected_at else '<span class="badge bg-success">Aktiv</span>'
|
||||
|
||||
duration = ""
|
||||
if conn.duration_seconds:
|
||||
mins = conn.duration_seconds // 60
|
||||
duration = f"{mins} Min."
|
||||
|
||||
html += f"""
|
||||
<tr>
|
||||
<td>{user.username if user else '-'}</td>
|
||||
<td>{gateway.name if gateway else '-'}</td>
|
||||
<td>{endpoint.name if endpoint else '-'}</td>
|
||||
<td>{connected}</td>
|
||||
<td>{disconnected}</td>
|
||||
<td>{duration}</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html += '</tbody></table>'
|
||||
return html
|
||||
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Tenant management web routes (Super Admin only)."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.user import User
|
||||
from ..models.tenant import Tenant
|
||||
from .deps import require_super_admin_web, flash, get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/tenants", response_class=HTMLResponse)
|
||||
async def list_tenants(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""List all tenants."""
|
||||
tenants = db.query(Tenant).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"tenants/list.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"tenants": tenants,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/tenants/new", response_class=HTMLResponse)
|
||||
async def new_tenant_form(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""New tenant form."""
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"tenants/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"tenant": None,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/tenants/new")
|
||||
async def create_tenant(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
description: str = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""Create new tenant."""
|
||||
# Check if name exists
|
||||
existing = db.query(Tenant).filter(Tenant.name == name).first()
|
||||
if existing:
|
||||
flash(request, "Mandantenname bereits vergeben", "danger")
|
||||
return RedirectResponse(url="/tenants/new", status_code=303)
|
||||
|
||||
tenant = Tenant(
|
||||
name=name,
|
||||
description=description or None
|
||||
)
|
||||
|
||||
db.add(tenant)
|
||||
db.commit()
|
||||
|
||||
flash(request, f"Mandant '{name}' erstellt", "success")
|
||||
return RedirectResponse(url="/tenants", status_code=303)
|
||||
|
||||
|
||||
@router.get("/tenants/{tenant_id}/edit", response_class=HTMLResponse)
|
||||
async def edit_tenant_form(
|
||||
request: Request,
|
||||
tenant_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""Edit tenant form."""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
flash(request, "Mandant nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/tenants", status_code=303)
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"tenants/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"tenant": tenant,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/edit")
|
||||
async def update_tenant(
|
||||
request: Request,
|
||||
tenant_id: int,
|
||||
name: str = Form(...),
|
||||
description: str = Form(None),
|
||||
is_active: bool = Form(True),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""Update tenant."""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
flash(request, "Mandant nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/tenants", status_code=303)
|
||||
|
||||
tenant.name = name
|
||||
tenant.description = description or None
|
||||
tenant.is_active = is_active
|
||||
|
||||
db.commit()
|
||||
|
||||
flash(request, "Mandant aktualisiert", "success")
|
||||
return RedirectResponse(url="/tenants", status_code=303)
|
||||
|
||||
|
||||
@router.post("/tenants/{tenant_id}/delete")
|
||||
async def delete_tenant(
|
||||
request: Request,
|
||||
tenant_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_super_admin_web)
|
||||
):
|
||||
"""Delete tenant."""
|
||||
tenant = db.query(Tenant).filter(Tenant.id == tenant_id).first()
|
||||
if not tenant:
|
||||
flash(request, "Mandant nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/tenants", status_code=303)
|
||||
|
||||
# Check if tenant has users or gateways
|
||||
if tenant.users or tenant.gateways:
|
||||
flash(request, "Mandant hat noch Benutzer oder Gateways. Bitte zuerst löschen.", "danger")
|
||||
return RedirectResponse(url="/tenants", status_code=303)
|
||||
|
||||
db.delete(tenant)
|
||||
db.commit()
|
||||
|
||||
flash(request, f"Mandant '{tenant.name}' gelöscht", "warning")
|
||||
return RedirectResponse(url="/tenants", status_code=303)
|
||||
@@ -0,0 +1,144 @@
|
||||
"""User management web routes."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from ..database import get_db
|
||||
from ..models.user import User, UserRole
|
||||
from ..utils.security import get_password_hash
|
||||
from .deps import get_current_user_web, require_admin_web, flash, get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/users", response_class=HTMLResponse)
|
||||
async def list_users(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""List users."""
|
||||
if current_user.role == UserRole.SUPER_ADMIN:
|
||||
users = db.query(User).all()
|
||||
else:
|
||||
users = db.query(User).filter(User.tenant_id == current_user.tenant_id).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"users/list.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"users": users,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/users/new", response_class=HTMLResponse)
|
||||
async def new_user_form(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""New user form."""
|
||||
from ..models.tenant import Tenant
|
||||
tenants = db.query(Tenant).filter(Tenant.is_active == True).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"users/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"user": None,
|
||||
"tenants": tenants,
|
||||
"roles": [r.value for r in UserRole if r != UserRole.SUPER_ADMIN or current_user.role == UserRole.SUPER_ADMIN],
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/users/new")
|
||||
async def create_user(
|
||||
request: Request,
|
||||
username: str = Form(...),
|
||||
email: str = Form(...),
|
||||
password: str = Form(...),
|
||||
role: str = Form(...),
|
||||
full_name: str = Form(None),
|
||||
tenant_id: int = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Create new user."""
|
||||
# Check if username exists
|
||||
existing = db.query(User).filter(User.username == username).first()
|
||||
if existing:
|
||||
flash(request, "Benutzername bereits vergeben", "danger")
|
||||
return RedirectResponse(url="/users/new", status_code=303)
|
||||
|
||||
# Check if email exists
|
||||
existing = db.query(User).filter(User.email == email).first()
|
||||
if existing:
|
||||
flash(request, "E-Mail bereits vergeben", "danger")
|
||||
return RedirectResponse(url="/users/new", status_code=303)
|
||||
|
||||
# Determine tenant
|
||||
if current_user.role == UserRole.SUPER_ADMIN and tenant_id:
|
||||
user_tenant_id = tenant_id
|
||||
else:
|
||||
user_tenant_id = current_user.tenant_id
|
||||
|
||||
user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
password_hash=get_password_hash(password),
|
||||
role=UserRole(role),
|
||||
full_name=full_name or None,
|
||||
tenant_id=user_tenant_id if UserRole(role) != UserRole.SUPER_ADMIN else None
|
||||
)
|
||||
|
||||
db.add(user)
|
||||
db.commit()
|
||||
|
||||
flash(request, f"Benutzer '{username}' erstellt", "success")
|
||||
return RedirectResponse(url="/users", status_code=303)
|
||||
|
||||
|
||||
@router.get("/users/{user_id}/access", response_class=HTMLResponse)
|
||||
async def user_access(
|
||||
request: Request,
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Manage user gateway access."""
|
||||
from ..models.gateway import Gateway
|
||||
from ..models.access import UserGatewayAccess
|
||||
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
flash(request, "Benutzer nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/users", status_code=303)
|
||||
|
||||
# Get all gateways and current access
|
||||
if current_user.role == UserRole.SUPER_ADMIN:
|
||||
gateways = db.query(Gateway).all()
|
||||
else:
|
||||
gateways = db.query(Gateway).filter(Gateway.tenant_id == current_user.tenant_id).all()
|
||||
|
||||
user_access = db.query(UserGatewayAccess).filter(
|
||||
UserGatewayAccess.user_id == user_id
|
||||
).all()
|
||||
access_gateway_ids = [a.gateway_id for a in user_access]
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"users/access.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"user": user,
|
||||
"gateways": gateways,
|
||||
"access_gateway_ids": access_gateway_ids,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,394 @@
|
||||
"""VPN Profile management web routes (nested under gateways)."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse, Response
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.user import User
|
||||
from ..models.gateway import Gateway
|
||||
from ..models.vpn_server import VPNServer
|
||||
from ..models.vpn_profile import VPNProfile
|
||||
from ..services.vpn_profile_service import VPNProfileService
|
||||
from .deps import require_admin_web, require_user_web, flash, get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/profiles", response_class=HTMLResponse)
|
||||
async def list_profiles(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_user_web)
|
||||
):
|
||||
"""List VPN profiles for a gateway."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
|
||||
if not gateway:
|
||||
flash(request, "Gateway nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
# Check access
|
||||
if not current_user.is_admin and not any(
|
||||
access.gateway_id == gateway_id for access in current_user.gateway_access
|
||||
):
|
||||
flash(request, "Zugriff verweigert", "danger")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
service = VPNProfileService(db)
|
||||
profiles = service.get_profiles_for_gateway(gateway_id)
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"gateways/profiles.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"gateway": gateway,
|
||||
"profiles": profiles,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/profiles/new", response_class=HTMLResponse)
|
||||
async def new_profile_form(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""New VPN profile form."""
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
|
||||
if not gateway:
|
||||
flash(request, "Gateway nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/gateways", status_code=303)
|
||||
|
||||
# Get available VPN servers
|
||||
if current_user.is_super_admin:
|
||||
servers = db.query(VPNServer).filter(VPNServer.is_active == True).all()
|
||||
else:
|
||||
servers = db.query(VPNServer).filter(
|
||||
(VPNServer.tenant_id == current_user.tenant_id) |
|
||||
(VPNServer.tenant_id == None),
|
||||
VPNServer.is_active == True
|
||||
).all()
|
||||
|
||||
# Calculate next priority
|
||||
existing_profiles = db.query(VPNProfile).filter(
|
||||
VPNProfile.gateway_id == gateway_id
|
||||
).count()
|
||||
next_priority = existing_profiles + 1
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"gateways/profile_form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"gateway": gateway,
|
||||
"profile": None,
|
||||
"vpn_servers": servers,
|
||||
"next_priority": next_priority,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/profiles/new")
|
||||
async def create_profile(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
name: str = Form(...),
|
||||
vpn_server_id: int = Form(...),
|
||||
priority: int = Form(1),
|
||||
description: str = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Create new VPN profile."""
|
||||
try:
|
||||
service = VPNProfileService(db)
|
||||
|
||||
profile = service.create_profile(
|
||||
gateway_id=gateway_id,
|
||||
vpn_server_id=vpn_server_id,
|
||||
name=name,
|
||||
priority=priority,
|
||||
description=description
|
||||
)
|
||||
|
||||
flash(request, f"VPN-Profil '{name}' erstellt", "success")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles/{profile.id}", status_code=303)
|
||||
|
||||
except ValueError as e:
|
||||
flash(request, str(e), "danger")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles/new", status_code=303)
|
||||
except Exception as e:
|
||||
flash(request, f"Fehler: {str(e)}", "danger")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles/new", status_code=303)
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/profiles/{profile_id}", response_class=HTMLResponse)
|
||||
async def profile_detail(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
profile_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_user_web)
|
||||
):
|
||||
"""VPN profile detail page."""
|
||||
profile = db.query(VPNProfile).filter(
|
||||
VPNProfile.id == profile_id,
|
||||
VPNProfile.gateway_id == gateway_id
|
||||
).first()
|
||||
|
||||
if not profile:
|
||||
flash(request, "Profil nicht gefunden", "danger")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"gateways/profile_detail.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"gateway": profile.gateway,
|
||||
"profile": profile,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/profiles/{profile_id}/edit", response_class=HTMLResponse)
|
||||
async def edit_profile_form(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
profile_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Edit VPN profile form."""
|
||||
profile = db.query(VPNProfile).filter(
|
||||
VPNProfile.id == profile_id,
|
||||
VPNProfile.gateway_id == gateway_id
|
||||
).first()
|
||||
|
||||
if not profile:
|
||||
flash(request, "Profil nicht gefunden", "danger")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
# Get available VPN servers
|
||||
if current_user.is_super_admin:
|
||||
servers = db.query(VPNServer).filter(VPNServer.is_active == True).all()
|
||||
else:
|
||||
servers = db.query(VPNServer).filter(
|
||||
(VPNServer.tenant_id == current_user.tenant_id) |
|
||||
(VPNServer.tenant_id == None),
|
||||
VPNServer.is_active == True
|
||||
).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"gateways/profile_form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"gateway": profile.gateway,
|
||||
"profile": profile,
|
||||
"vpn_servers": servers,
|
||||
"next_priority": profile.priority,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/profiles/{profile_id}/edit")
|
||||
async def update_profile(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
profile_id: int,
|
||||
name: str = Form(...),
|
||||
priority: int = Form(1),
|
||||
description: str = Form(None),
|
||||
is_active: str = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Update VPN profile."""
|
||||
profile = db.query(VPNProfile).filter(
|
||||
VPNProfile.id == profile_id,
|
||||
VPNProfile.gateway_id == gateway_id
|
||||
).first()
|
||||
|
||||
if not profile:
|
||||
flash(request, "Profil nicht gefunden", "danger")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
profile.name = name
|
||||
profile.description = description
|
||||
profile.priority = priority
|
||||
profile.is_active = is_active is not None # Checkbox sends "on" when checked, None when not
|
||||
db.commit()
|
||||
|
||||
flash(request, f"Profil '{name}' aktualisiert", "success")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles/{profile_id}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/profiles/{profile_id}/provision")
|
||||
async def provision_profile(
|
||||
gateway_id: int,
|
||||
profile_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_user_web)
|
||||
):
|
||||
"""Download OpenVPN config for a profile."""
|
||||
service = VPNProfileService(db)
|
||||
profile = service.get_profile_by_id(profile_id)
|
||||
|
||||
if not profile or profile.gateway_id != gateway_id:
|
||||
return Response(status_code=404, content="Profile not found")
|
||||
|
||||
if not profile.is_ready:
|
||||
return Response(status_code=400, content="Profile not ready for provisioning")
|
||||
|
||||
try:
|
||||
config = service.provision_profile(profile)
|
||||
|
||||
filename = f"{profile.gateway.name}-{profile.name}.ovpn"
|
||||
filename = filename.lower().replace(' ', '-')
|
||||
|
||||
return Response(
|
||||
content=config,
|
||||
media_type="application/x-openvpn-profile",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"}
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(status_code=500, content=str(e))
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/profiles/{profile_id}/priority")
|
||||
async def update_priority(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
profile_id: int,
|
||||
priority: int = Form(...),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Update profile priority."""
|
||||
service = VPNProfileService(db)
|
||||
profile = service.get_profile_by_id(profile_id)
|
||||
|
||||
if not profile or profile.gateway_id != gateway_id:
|
||||
flash(request, "Profil nicht gefunden", "danger")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
service.set_priority(profile, priority)
|
||||
|
||||
flash(request, "Priorität aktualisiert", "success")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/profiles/{profile_id}/revoke")
|
||||
async def revoke_profile(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
profile_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Revoke profile certificate."""
|
||||
service = VPNProfileService(db)
|
||||
profile = service.get_profile_by_id(profile_id)
|
||||
|
||||
if not profile or profile.gateway_id != gateway_id:
|
||||
flash(request, "Profil nicht gefunden", "danger")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
service.revoke_profile(profile)
|
||||
|
||||
flash(request, f"Zertifikat für '{profile.name}' widerrufen", "warning")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/profiles/{profile_id}/renew")
|
||||
async def renew_profile(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
profile_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Renew profile certificate."""
|
||||
service = VPNProfileService(db)
|
||||
profile = service.get_profile_by_id(profile_id)
|
||||
|
||||
if not profile or profile.gateway_id != gateway_id:
|
||||
flash(request, "Profil nicht gefunden", "danger")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
service.renew_profile(profile)
|
||||
|
||||
flash(request, f"Zertifikat für '{profile.name}' erneuert", "success")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles/{profile_id}", status_code=303)
|
||||
|
||||
|
||||
@router.post("/gateways/{gateway_id}/profiles/{profile_id}/delete")
|
||||
async def delete_profile(
|
||||
request: Request,
|
||||
gateway_id: int,
|
||||
profile_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Delete VPN profile."""
|
||||
service = VPNProfileService(db)
|
||||
profile = service.get_profile_by_id(profile_id)
|
||||
|
||||
if not profile or profile.gateway_id != gateway_id:
|
||||
flash(request, "Profil nicht gefunden", "danger")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
name = profile.name
|
||||
service.delete_profile(profile)
|
||||
|
||||
flash(request, f"Profil '{name}' gelöscht", "warning")
|
||||
return RedirectResponse(url=f"/gateways/{gateway_id}/profiles", status_code=303)
|
||||
|
||||
|
||||
@router.get("/gateways/{gateway_id}/provision-all")
|
||||
async def provision_all_profiles(
|
||||
gateway_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_user_web)
|
||||
):
|
||||
"""Download all active profiles as a ZIP file."""
|
||||
import io
|
||||
import zipfile
|
||||
|
||||
service = VPNProfileService(db)
|
||||
gateway = db.query(Gateway).filter(Gateway.id == gateway_id).first()
|
||||
|
||||
if not gateway:
|
||||
return Response(status_code=404, content="Gateway not found")
|
||||
|
||||
configs = service.generate_all_configs_for_gateway(gateway_id)
|
||||
|
||||
if not configs:
|
||||
return Response(status_code=400, content="No active profiles available")
|
||||
|
||||
# Create ZIP file in memory
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
|
||||
for filename, config in configs:
|
||||
zip_file.writestr(filename, config)
|
||||
|
||||
zip_buffer.seek(0)
|
||||
|
||||
zip_filename = f"{gateway.name}-vpn-profiles.zip"
|
||||
zip_filename = zip_filename.lower().replace(' ', '-')
|
||||
|
||||
return Response(
|
||||
content=zip_buffer.getvalue(),
|
||||
media_type="application/zip",
|
||||
headers={"Content-Disposition": f"attachment; filename={zip_filename}"}
|
||||
)
|
||||
@@ -0,0 +1,349 @@
|
||||
"""VPN Server management web routes."""
|
||||
|
||||
from fastapi import APIRouter, Request, Depends, Form
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
|
||||
from ..database import get_db
|
||||
from ..models.user import User
|
||||
from ..models.vpn_server import VPNServer, VPNProtocol, VPNCipher, VPNAuth, VPNCompression
|
||||
from ..models.certificate_authority import CertificateAuthority, CAStatus
|
||||
from ..services.vpn_server_service import VPNServerService
|
||||
from .deps import require_admin_web, flash, get_flashed_messages
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/vpn-servers", response_class=HTMLResponse)
|
||||
async def list_vpn_servers(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""List all VPN servers."""
|
||||
service = VPNServerService(db)
|
||||
|
||||
if current_user.is_super_admin:
|
||||
servers = db.query(VPNServer).all()
|
||||
else:
|
||||
servers = service.get_servers_for_tenant(current_user.tenant_id)
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"vpn_servers/list.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"servers": servers,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vpn-servers/new", response_class=HTMLResponse)
|
||||
async def new_vpn_server_form(
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""New VPN server form."""
|
||||
# Get available CAs
|
||||
if current_user.is_super_admin:
|
||||
cas = db.query(CertificateAuthority).filter(
|
||||
CertificateAuthority.status == CAStatus.ACTIVE
|
||||
).all()
|
||||
else:
|
||||
cas = db.query(CertificateAuthority).filter(
|
||||
(CertificateAuthority.tenant_id == current_user.tenant_id) |
|
||||
(CertificateAuthority.tenant_id == None),
|
||||
CertificateAuthority.status == CAStatus.ACTIVE
|
||||
).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"vpn_servers/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"server": None,
|
||||
"cas": cas,
|
||||
"protocols": VPNProtocol,
|
||||
"ciphers": VPNCipher,
|
||||
"auth_methods": VPNAuth,
|
||||
"compression_options": VPNCompression,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/vpn-servers/new")
|
||||
async def create_vpn_server(
|
||||
request: Request,
|
||||
name: str = Form(...),
|
||||
hostname: str = Form(...),
|
||||
ca_id: int = Form(...),
|
||||
port: int = Form(1194),
|
||||
protocol: str = Form("udp"),
|
||||
vpn_network: str = Form("10.8.0.0"),
|
||||
vpn_netmask: str = Form("255.255.255.0"),
|
||||
cipher: str = Form("AES-256-GCM"),
|
||||
auth: str = Form("SHA256"),
|
||||
tls_version_min: str = Form("1.2"),
|
||||
compression: str = Form("none"),
|
||||
max_clients: int = Form(100),
|
||||
keepalive_interval: int = Form(10),
|
||||
keepalive_timeout: int = Form(60),
|
||||
management_port: int = Form(7505),
|
||||
is_primary: bool = Form(False),
|
||||
description: str = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Create new VPN server."""
|
||||
try:
|
||||
service = VPNServerService(db)
|
||||
|
||||
tenant_id = None if current_user.is_super_admin else current_user.tenant_id
|
||||
|
||||
server = service.create_server(
|
||||
name=name,
|
||||
hostname=hostname,
|
||||
ca_id=ca_id,
|
||||
port=port,
|
||||
protocol=VPNProtocol(protocol),
|
||||
vpn_network=vpn_network,
|
||||
vpn_netmask=vpn_netmask,
|
||||
cipher=VPNCipher(cipher),
|
||||
auth=VPNAuth(auth),
|
||||
tls_version_min=tls_version_min,
|
||||
compression=VPNCompression(compression),
|
||||
max_clients=max_clients,
|
||||
keepalive_interval=keepalive_interval,
|
||||
keepalive_timeout=keepalive_timeout,
|
||||
management_port=management_port,
|
||||
is_primary=is_primary,
|
||||
tenant_id=tenant_id,
|
||||
description=description
|
||||
)
|
||||
|
||||
flash(request, f"VPN-Server '{name}' erstellt", "success")
|
||||
return RedirectResponse(url=f"/vpn-servers/{server.id}", status_code=303)
|
||||
|
||||
except ValueError as e:
|
||||
flash(request, str(e), "danger")
|
||||
return RedirectResponse(url="/vpn-servers/new", status_code=303)
|
||||
except Exception as e:
|
||||
flash(request, f"Fehler beim Erstellen: {str(e)}", "danger")
|
||||
return RedirectResponse(url="/vpn-servers/new", status_code=303)
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}", response_class=HTMLResponse)
|
||||
async def vpn_server_detail(
|
||||
request: Request,
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""VPN server detail page."""
|
||||
service = VPNServerService(db)
|
||||
server = service.get_server_by_id(server_id)
|
||||
|
||||
if not server:
|
||||
flash(request, "VPN-Server nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/vpn-servers", status_code=303)
|
||||
|
||||
# Check access
|
||||
if not current_user.is_super_admin and server.tenant_id != current_user.tenant_id and server.tenant_id is not None:
|
||||
flash(request, "Zugriff verweigert", "danger")
|
||||
return RedirectResponse(url="/vpn-servers", status_code=303)
|
||||
|
||||
# Get status
|
||||
status = service.get_server_status(server)
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"vpn_servers/detail.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"server": server,
|
||||
"status": status,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/edit", response_class=HTMLResponse)
|
||||
async def edit_vpn_server_form(
|
||||
request: Request,
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Edit VPN server form."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
flash(request, "VPN-Server nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/vpn-servers", status_code=303)
|
||||
|
||||
# Get available CAs
|
||||
cas = db.query(CertificateAuthority).filter(
|
||||
CertificateAuthority.status == CAStatus.ACTIVE
|
||||
).all()
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"vpn_servers/form.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"server": server,
|
||||
"cas": cas,
|
||||
"protocols": VPNProtocol,
|
||||
"ciphers": VPNCipher,
|
||||
"auth_methods": VPNAuth,
|
||||
"compression_options": VPNCompression,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/vpn-servers/{server_id}/edit")
|
||||
async def update_vpn_server(
|
||||
request: Request,
|
||||
server_id: int,
|
||||
name: str = Form(...),
|
||||
hostname: str = Form(...),
|
||||
port: int = Form(1194),
|
||||
protocol: str = Form("udp"),
|
||||
vpn_network: str = Form("10.8.0.0"),
|
||||
vpn_netmask: str = Form("255.255.255.0"),
|
||||
cipher: str = Form("AES-256-GCM"),
|
||||
auth: str = Form("SHA256"),
|
||||
tls_version_min: str = Form("1.2"),
|
||||
compression: str = Form("none"),
|
||||
max_clients: int = Form(100),
|
||||
keepalive_interval: int = Form(10),
|
||||
keepalive_timeout: int = Form(60),
|
||||
management_port: int = Form(7505),
|
||||
is_primary: bool = Form(False),
|
||||
is_active: bool = Form(True),
|
||||
description: str = Form(None),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Update VPN server."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
flash(request, "VPN-Server nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/vpn-servers", status_code=303)
|
||||
|
||||
# Update fields
|
||||
server.name = name
|
||||
server.description = description
|
||||
server.hostname = hostname
|
||||
server.port = port
|
||||
server.protocol = VPNProtocol(protocol)
|
||||
server.vpn_network = vpn_network
|
||||
server.vpn_netmask = vpn_netmask
|
||||
server.cipher = VPNCipher(cipher)
|
||||
server.auth = VPNAuth(auth)
|
||||
server.tls_version_min = tls_version_min
|
||||
server.compression = VPNCompression(compression)
|
||||
server.max_clients = max_clients
|
||||
server.keepalive_interval = keepalive_interval
|
||||
server.keepalive_timeout = keepalive_timeout
|
||||
server.management_port = management_port
|
||||
server.is_primary = is_primary
|
||||
server.is_active = is_active
|
||||
|
||||
# If setting as primary, unset other primaries
|
||||
if is_primary:
|
||||
db.query(VPNServer).filter(
|
||||
VPNServer.tenant_id == server.tenant_id,
|
||||
VPNServer.id != server.id
|
||||
).update({"is_primary": False})
|
||||
|
||||
db.commit()
|
||||
|
||||
flash(request, "VPN-Server aktualisiert", "success")
|
||||
return RedirectResponse(url=f"/vpn-servers/{server_id}", status_code=303)
|
||||
|
||||
|
||||
@router.get("/vpn-servers/{server_id}/clients", response_class=HTMLResponse)
|
||||
async def vpn_server_clients(
|
||||
request: Request,
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Show connected clients for a VPN server."""
|
||||
service = VPNServerService(db)
|
||||
server = service.get_server_by_id(server_id)
|
||||
|
||||
if not server:
|
||||
flash(request, "VPN-Server nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/vpn-servers", status_code=303)
|
||||
|
||||
clients = service.get_connected_clients(server)
|
||||
|
||||
return request.app.state.templates.TemplateResponse(
|
||||
"vpn_servers/clients.html",
|
||||
{
|
||||
"request": request,
|
||||
"current_user": current_user,
|
||||
"server": server,
|
||||
"clients": clients,
|
||||
"flash_messages": get_flashed_messages(request)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/vpn-servers/{server_id}/disconnect/{common_name}")
|
||||
async def disconnect_client(
|
||||
request: Request,
|
||||
server_id: int,
|
||||
common_name: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Disconnect a client from VPN server."""
|
||||
service = VPNServerService(db)
|
||||
server = service.get_server_by_id(server_id)
|
||||
|
||||
if not server:
|
||||
flash(request, "VPN-Server nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/vpn-servers", status_code=303)
|
||||
|
||||
if service.disconnect_client(server, common_name):
|
||||
flash(request, f"Client '{common_name}' getrennt", "success")
|
||||
else:
|
||||
flash(request, f"Konnte Client '{common_name}' nicht trennen", "danger")
|
||||
|
||||
return RedirectResponse(url=f"/vpn-servers/{server_id}/clients", status_code=303)
|
||||
|
||||
|
||||
@router.post("/vpn-servers/{server_id}/delete")
|
||||
async def delete_vpn_server(
|
||||
request: Request,
|
||||
server_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_admin_web)
|
||||
):
|
||||
"""Delete VPN server."""
|
||||
server = db.query(VPNServer).filter(VPNServer.id == server_id).first()
|
||||
|
||||
if not server:
|
||||
flash(request, "VPN-Server nicht gefunden", "danger")
|
||||
return RedirectResponse(url="/vpn-servers", status_code=303)
|
||||
|
||||
# Check if server has profiles
|
||||
if server.vpn_profiles:
|
||||
flash(request, "Server hat noch VPN-Profile. Bitte zuerst löschen.", "danger")
|
||||
return RedirectResponse(url=f"/vpn-servers/{server_id}", status_code=303)
|
||||
|
||||
name = server.name
|
||||
db.delete(server)
|
||||
db.commit()
|
||||
|
||||
flash(request, f"VPN-Server '{name}' gelöscht", "warning")
|
||||
return RedirectResponse(url="/vpn-servers", status_code=303)
|
||||
Reference in New Issue
Block a user