"""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"""
Gateways
{gateways_online} / {gateways_total}
VPN-Clients
{vpn_clients_total}
Sessions
{active_connections}
Endpunkte
{endpoints_total}
Benutzer
{users_total}
""" @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 """
Keine Gateways gefunden
""" html = '
' for gw in gateways: status_class = "online" if gw.is_online else "offline" status_badge = 'Online' if gw.is_online else 'Offline' last_seen = gw.last_seen.strftime('%d.%m.%Y %H:%M') if gw.last_seen else 'Nie' html += f"""
{gw.name}

{gw.router_type} | {gw.location or 'Kein Standort'}

{status_badge} Zuletzt: {last_seen}

Details
""" html += '
' 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 '

Keine Gateways vorhanden

' html = '' for gw in gateways: status = '' if gw.is_online else '' html += f""" """ html += '
{status} {gw.name} {gw.router_type}
' 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 """

Keine Endpunkte definiert

""" html = '' for ep in endpoints: protocol_badge = f'{ep.protocol.value.upper()}' html += f""" """ html += '
NameAdresseProtokollAnwendung
{ep.name} {ep.internal_ip}:{ep.port} {protocol_badge} {ep.application_name or '-'}
' 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 """

Keine Benutzer haben Zugriff

""" html = '' 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""" """ html += '
BenutzerRolleGewährt amGewährt von
{user.username if user else '-'} {user.role.value if user else '-'} {granted_at} {granted_by.username if granted_by else '-'}
' 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 """

Keine VPN-Verbindungen aufgezeichnet

""" html = '''''' 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 = 'Aktiv' 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''' ''' html += '
Profil Server Echte Adresse Verbunden Getrennt Dauer Traffic
{profile_name} {server_name} {real_addr} {connected} {disconnected} {duration_str} {traffic}
' 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' {vpn_clients} VPN-Clients   {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 '

Keine VPN-Clients verbunden

' html = '''''' 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''' ''' html += '
Gateway Profil VPN-Server Echte Adresse Empfangen Gesendet Verbunden seit
{gateway_name} {profile_name} {server_name} {real_addr} {rx_str} {tx_str} {connected}
' 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 '

Keine aktiven Verbindungen

' html = '' 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 '

Keine Verbindungen

' html = '' 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 '

Keine Verbindungshistorie

' html = '' 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 'Aktiv' duration = "" if conn.duration_seconds: mins = conn.duration_seconds // 60 duration = f"{mins} Min." html += f""" """ html += '
BenutzerGatewayEndpunktVerbundenGetrenntDauer
{user.username if user else '-'} {gateway.name if gateway else '-'} {endpoint.name if endpoint else '-'} {connected} {disconnected} {duration}
' return html