first commit

This commit is contained in:
Stefan Hacker
2026-02-02 09:46:35 +01:00
commit 6901dc369b
98 changed files with 13030 additions and 0 deletions
@@ -0,0 +1,73 @@
{% extends "base.html" %}
{% block title %}{% if application %}Anwendung bearbeiten{% else %}Neue Anwendung{% endif %} - mGuard VPN{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="bi bi-app-indicator"></i>
{% if application %}Anwendung bearbeiten{% else %}Neue Anwendung{% endif %}
</h4>
</div>
<div class="card-body">
<form method="post" action="{% if application %}/applications/{{ application.id }}/edit{% else %}/applications/new{% endif %}">
<div class="mb-3">
<label for="name" class="form-label">Name *</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ application.name if application else '' }}" required
placeholder="z.B. CoDeSys Runtime">
</div>
<div class="row">
<div class="col-6 mb-3">
<label for="default_port" class="form-label">Standard-Port *</label>
<input type="number" class="form-control" id="default_port" name="default_port"
value="{{ application.default_port if application else '' }}" required
min="1" max="65535" placeholder="11740">
</div>
<div class="col-6 mb-3">
<label for="protocol" class="form-label">Protokoll</label>
<select class="form-select" id="protocol" name="protocol">
<option value="tcp" {% if not application or application.protocol.value == 'tcp' %}selected{% endif %}>TCP</option>
<option value="udp" {% if application and application.protocol.value == 'udp' %}selected{% endif %}>UDP</option>
</select>
</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Beschreibung</label>
<input type="text" class="form-control" id="description" name="description"
value="{{ application.description if application and application.description else '' }}"
placeholder="z.B. CoDeSys Runtime/Gateway Service">
</div>
<div class="mb-3">
<label for="icon" class="form-label">Icon (Bootstrap Icon Name)</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-{{ application.icon if application and application.icon else 'app' }}"></i></span>
<input type="text" class="form-control" id="icon" name="icon"
value="{{ application.icon if application and application.icon else '' }}"
placeholder="z.B. hdd-network, terminal, globe">
</div>
<div class="form-text">
<a href="https://icons.getbootstrap.com/" target="_blank">Bootstrap Icons</a> - nur den Namen ohne "bi-"
</div>
</div>
<div class="d-flex justify-content-between">
<a href="/applications" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Abbrechen
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Speichern
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Anwendungen - mGuard VPN{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-app-indicator"></i> Anwendungstemplates</h1>
<a href="/applications/new" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Neue Anwendung
</a>
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Port</th>
<th>Protokoll</th>
<th>Beschreibung</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for app in applications %}
<tr>
<td>
{% if app.icon %}<i class="bi bi-{{ app.icon }} me-2"></i>{% endif %}
<strong>{{ app.name }}</strong>
</td>
<td><code>{{ app.default_port }}</code></td>
<td><span class="badge bg-{{ 'primary' if app.protocol.value == 'tcp' else 'info' }}">{{ app.protocol.value|upper }}</span></td>
<td class="text-muted">{{ app.description or '-' }}</td>
<td>
<a href="/applications/{{ app.id }}/edit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</a>
<form action="/applications/{{ app.id }}/delete" method="post" class="d-inline"
onsubmit="return confirm('Anwendung wirklich löschen?');">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center text-muted">Keine Anwendungen vorhanden</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Hinweis</h5>
</div>
<div class="card-body">
<p class="mb-0">
Anwendungstemplates definieren Standard-Ports und Protokolle für bekannte Industrieanwendungen.
Beim Anlegen eines Endpunkts wird automatisch der Port ausgefüllt, wenn eine Anwendung ausgewählt wird.
</p>
</div>
</div>
{% endblock %}
+64
View File
@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - mGuard VPN Manager</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
<link href="/static/css/custom.css" rel="stylesheet">
</head>
<body>
<div class="login-container">
<div class="login-box">
<div class="text-center mb-4">
<i class="bi bi-shield-lock login-logo"></i>
<h2 class="mt-2">mGuard VPN Manager</h2>
<p class="text-muted">Bitte anmelden</p>
</div>
{% if error %}
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i> {{ error }}
</div>
{% endif %}
<form method="post" action="/login">
<div class="mb-3">
<label for="username" class="form-label">Benutzername</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<input type="text" class="form-control" id="username" name="username"
placeholder="Benutzername eingeben" required autofocus>
</div>
</div>
<div class="mb-3">
<label for="password" class="form-label">Passwort</label>
<div class="input-group">
<span class="input-group-text"><i class="bi bi-lock"></i></span>
<input type="password" class="form-control" id="password" name="password"
placeholder="Passwort eingeben" required>
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="remember" name="remember">
<label class="form-check-label" for="remember">Angemeldet bleiben</label>
</div>
<button type="submit" class="btn btn-primary w-100">
<i class="bi bi-box-arrow-in-right"></i> Anmelden
</button>
</form>
<hr class="my-4">
<p class="text-center text-muted small mb-0">
mGuard VPN Manager v1.0<br>
&copy; 2024
</p>
</div>
</div>
</body>
</html>
+142
View File
@@ -0,0 +1,142 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}mGuard VPN Manager{% endblock %}</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.css" rel="stylesheet">
<!-- Custom CSS -->
<link href="/static/css/custom.css" rel="stylesheet">
<!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
{% block head %}{% endblock %}
</head>
<body hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
{% if current_user %}
<!-- Navigation -->
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="/dashboard">
<i class="bi bi-shield-lock"></i> mGuard VPN
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if request.url.path == '/dashboard' %}active{% endif %}" href="/dashboard">
<i class="bi bi-speedometer2"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/gateways' in request.url.path %}active{% endif %}" href="/gateways">
<i class="bi bi-router"></i> Gateways
</a>
</li>
{% if current_user.is_admin %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if '/vpn-servers' in request.url.path or '/ca' in request.url.path %}active{% endif %}"
href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-hdd-network"></i> VPN
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item" href="/vpn-servers">
<i class="bi bi-server"></i> VPN-Server
</a>
</li>
<li>
<a class="dropdown-item" href="/ca">
<i class="bi bi-file-earmark-lock"></i> Zertifizierungsstellen
</a>
</li>
</ul>
</li>
{% endif %}
{% if current_user.is_admin %}
<li class="nav-item">
<a class="nav-link {% if '/users' in request.url.path %}active{% endif %}" href="/users">
<i class="bi bi-people"></i> Benutzer
</a>
</li>
{% endif %}
{% if current_user.is_super_admin %}
<li class="nav-item">
<a class="nav-link {% if '/tenants' in request.url.path %}active{% endif %}" href="/tenants">
<i class="bi bi-building"></i> Mandanten
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if '/applications' in request.url.path %}active{% endif %}" href="/applications">
<i class="bi bi-app-indicator"></i> Anwendungen
</a>
</li>
{% endif %}
<li class="nav-item">
<a class="nav-link {% if '/connections' in request.url.path %}active{% endif %}" href="/connections">
<i class="bi bi-plug"></i> Verbindungen
</a>
</li>
</ul>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="bi bi-person-circle"></i> {{ current_user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><span class="dropdown-item-text text-muted">{{ current_user.role.value }}</span></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="/logout"><i class="bi bi-box-arrow-right"></i> Logout</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
{% endif %}
<!-- Main Content -->
<main class="{% if current_user %}py-4{% endif %}">
<div class="container-fluid">
<!-- Flash Messages -->
{% if flash_messages %}
{% for msg in flash_messages %}
<div class="alert alert-{{ msg.category }} alert-dismissible fade show" role="alert">
{{ msg.message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
</div>
</main>
<!-- Footer -->
{% if current_user %}
<footer class="footer mt-auto py-3 bg-light">
<div class="container-fluid">
<span class="text-muted">mGuard VPN Manager v1.0 |
<span hx-get="/htmx/connections/count" hx-trigger="load, every 30s" hx-swap="innerHTML">
<i class="bi bi-hourglass-split"></i>
</span>
</span>
</div>
</footer>
{% endif %}
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Custom JS -->
<script src="/static/js/app.js"></script>
{% block scripts %}{% endblock %}
</body>
</html>
+64
View File
@@ -0,0 +1,64 @@
{% extends "base.html" %}
{% block title %}Verbindungen - mGuard VPN Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-plug"></i> Verbindungen</h1>
<div>
<span class="badge bg-success me-2" id="active-count"
hx-get="/htmx/connections/count" hx-trigger="load, every 30s">
...
</span>
<button class="btn btn-outline-primary"
hx-get="/htmx/connections/list" hx-target="#connection-log" hx-swap="innerHTML">
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
</button>
</div>
</div>
<!-- VPN Clients (Gateways) -->
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-shield-check"></i> VPN-Clients (Gateways)</h5>
</div>
<div class="card-body" id="vpn-clients"
hx-get="/htmx/connections/vpn-clients" hx-trigger="load, every 10s" hx-swap="innerHTML">
<div class="text-center py-3">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
</div>
</div>
<!-- Active User Sessions -->
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0"><i class="bi bi-broadcast"></i> Aktive Benutzer-Sessions</h5>
</div>
<div class="card-body" id="active-connections"
hx-get="/htmx/connections/active" hx-trigger="load, every 10s" hx-swap="innerHTML">
<div class="text-center py-3">
<div class="spinner-border text-success" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
</div>
</div>
<!-- Connection History -->
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Verbindungshistorie</h5>
</div>
<div class="card-body" id="connection-log"
hx-get="/htmx/connections/list" hx-trigger="load" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
</div>
</div>
{% endblock %}
+139
View File
@@ -0,0 +1,139 @@
{% extends "base.html" %}
{% block title %}Dashboard - mGuard VPN Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-speedometer2"></i> Dashboard</h1>
<button class="btn btn-outline-primary" hx-get="/htmx/dashboard/stats" hx-target="#stats-row" hx-swap="innerHTML">
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
</button>
</div>
<!-- Stats Cards -->
<div class="row mb-4" id="stats-row">
<div class="col-md-3 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 Online</h6>
<div class="stat-value">{{ stats.gateways_online }} / {{ stats.gateways_total }}</div>
</div>
<i class="bi bi-router stat-icon"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 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">Aktive Verbindungen</h6>
<div class="stat-value">{{ stats.active_connections }}</div>
</div>
<i class="bi bi-plug stat-icon"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 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">Endpunkte</h6>
<div class="stat-value">{{ stats.endpoints_total }}</div>
</div>
<i class="bi bi-hdd-network stat-icon"></i>
</div>
</div>
</div>
</div>
<div class="col-md-3 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">{{ stats.users_total }}</div>
</div>
<i class="bi bi-people stat-icon"></i>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Gateway Status -->
<div class="col-lg-8 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-router"></i> Gateway Status</h5>
<a href="/gateways" class="btn btn-sm btn-outline-primary">Alle anzeigen</a>
</div>
<div class="card-body" hx-get="/htmx/gateways/status-list" hx-trigger="load, every 30s" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Connections -->
<div class="col-lg-4 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Letzte Verbindungen</h5>
<a href="/connections" class="btn btn-sm btn-outline-primary">Alle</a>
</div>
<div class="card-body" hx-get="/htmx/connections/recent" hx-trigger="load, every 60s" hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-lightning"></i> Schnellaktionen</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-2">
<a href="/gateways/new" class="btn btn-outline-primary w-100">
<i class="bi bi-plus-circle"></i> Neues Gateway
</a>
</div>
<div class="col-md-3 mb-2">
<a href="/users/new" class="btn btn-outline-success w-100">
<i class="bi bi-person-plus"></i> Neuer Benutzer
</a>
</div>
<div class="col-md-3 mb-2">
<a href="/connections" class="btn btn-outline-info w-100">
<i class="bi bi-list-check"></i> Verbindungs-Log
</a>
</div>
<div class="col-md-3 mb-2">
<a href="/api/docs" target="_blank" class="btn btn-outline-secondary w-100">
<i class="bi bi-code-slash"></i> API Docs
</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
+262
View File
@@ -0,0 +1,262 @@
{% extends "base.html" %}
{% block title %}{{ gateway.name }} - mGuard VPN Manager{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/gateways">Gateways</a></li>
<li class="breadcrumb-item active">{{ gateway.name }}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1>
<span class="status-indicator {{ 'online' if gateway.is_online else 'offline' }}"></span>
{{ gateway.name }}
</h1>
<p class="text-muted mb-0">
{{ gateway.router_type }} |
{{ gateway.location or 'Kein Standort' }} |
{% if gateway.last_seen %}
Zuletzt gesehen: <span data-relative-time="{{ gateway.last_seen }}">{{ gateway.last_seen }}</span>
{% else %}
Nie verbunden
{% endif %}
</p>
</div>
<div>
<a href="/gateways/{{ gateway.id }}/provision" class="btn btn-success">
<i class="bi bi-download"></i> Provisioning
</a>
<a href="/gateways/{{ gateway.id }}/edit" class="btn btn-outline-primary">
<i class="bi bi-pencil"></i> Bearbeiten
</a>
<button class="btn btn-outline-danger" onclick="confirmDelete('Gateway wirklich löschen?', 'delete-form')">
<i class="bi bi-trash"></i> Löschen
</button>
<form id="delete-form" action="/gateways/{{ gateway.id }}/delete" method="post" style="display:none;">
</form>
</div>
</div>
<div class="row">
<!-- Gateway Info -->
<div class="col-lg-4 mb-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Gateway Details</h5>
</div>
<div class="card-body">
<table class="table table-sm">
<tr>
<th>Status</th>
<td>
{% if gateway.is_online %}
<span class="badge bg-success">Online</span>
{% else %}
<span class="badge bg-secondary">Offline</span>
{% endif %}
</td>
</tr>
<tr>
<th>Typ</th>
<td>{{ gateway.router_type }}</td>
</tr>
<tr>
<th>Firmware</th>
<td>{{ gateway.firmware_version or '-' }}</td>
</tr>
<tr>
<th>Seriennummer</th>
<td>{{ gateway.serial_number or '-' }}</td>
</tr>
<tr>
<th>VPN IP</th>
<td>{{ gateway.vpn_ip or 'Nicht zugewiesen' }}</td>
</tr>
<tr>
<th>VPN Subnetz</th>
<td>{{ gateway.vpn_subnet or '-' }}</td>
</tr>
<tr>
<th>Provisioniert</th>
<td>
{% if gateway.is_provisioned %}
<span class="badge bg-success">Ja</span>
{% else %}
<span class="badge bg-warning">Nein</span>
{% endif %}
</td>
</tr>
<tr>
<th>Standort</th>
<td>{{ gateway.location or '-' }}</td>
</tr>
</table>
{% if gateway.description %}
<hr>
<p class="mb-0 text-muted">{{ gateway.description }}</p>
{% endif %}
</div>
</div>
</div>
<!-- Endpoints -->
<div class="col-lg-8 mb-4">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-hdd-network"></i> Endpunkte</h5>
<button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addEndpointModal">
<i class="bi bi-plus"></i> Endpunkt hinzufügen
</button>
</div>
<div class="card-body" id="endpoints-list"
hx-get="/htmx/gateways/{{ gateway.id }}/endpoints"
hx-trigger="load"
hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- VPN Connection Log -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0"><i class="bi bi-shield-check"></i> VPN-Verbindungslog</h5>
</div>
<div class="card-body" id="vpn-log"
hx-get="/htmx/gateways/{{ gateway.id }}/vpn-log"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- User Access -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-person-check"></i> Benutzerzugriff</h5>
<button class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#addAccessModal">
<i class="bi bi-plus"></i> Zugriff gewähren
</button>
</div>
<div class="card-body" id="access-list"
hx-get="/htmx/gateways/{{ gateway.id }}/access"
hx-trigger="load"
hx-swap="innerHTML">
<div class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Add Endpoint Modal -->
<div class="modal fade" id="addEndpointModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form hx-post="/htmx/gateways/{{ gateway.id }}/endpoints"
hx-target="#endpoints-list"
hx-swap="innerHTML"
hx-on::after-request="bootstrap.Modal.getInstance(document.getElementById('addEndpointModal')).hide(); this.reset()">
<div class="modal-header">
<h5 class="modal-title">Endpunkt hinzufügen</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Name</label>
<input type="text" class="form-control" name="name" required placeholder="z.B. HMI Türsteuerung">
</div>
<div class="row">
<div class="col-8 mb-3">
<label class="form-label">IP-Adresse</label>
<input type="text" class="form-control" name="internal_ip" required placeholder="10.0.0.3">
</div>
<div class="col-4 mb-3">
<label class="form-label">Port</label>
<input type="number" class="form-control" name="port" required placeholder="11740">
</div>
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">Protokoll</label>
<select class="form-select" name="protocol">
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
</select>
</div>
<div class="col-6 mb-3">
<label class="form-label">Anwendung</label>
<select class="form-select" name="application_template_id" id="application-select"
onchange="applyApplicationTemplate()">
<option value="" data-port="" data-protocol="">Benutzerdefiniert</option>
{% for template in templates %}
<option value="{{ template.id }}"
data-port="{{ template.default_port }}"
data-protocol="{{ template.protocol }}">
{{ template.name }} (:{{ template.default_port }})
</option>
{% endfor %}
</select>
</div>
</div>
<div class="mb-3">
<label class="form-label">Beschreibung (optional)</label>
<textarea class="form-control" name="description" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Abbrechen</button>
<button type="submit" class="btn btn-primary">Hinzufügen</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function applyApplicationTemplate() {
const select = document.getElementById('application-select');
const option = select.options[select.selectedIndex];
const port = option.dataset.port;
const protocol = option.dataset.protocol;
if (port) {
document.querySelector('#addEndpointModal input[name="port"]').value = port;
}
if (protocol) {
document.querySelector('#addEndpointModal select[name="protocol"]').value = protocol;
}
}
function confirmDelete(message, formId) {
if (confirm(message)) {
document.getElementById(formId).submit();
}
}
</script>
{% endblock %}
+106
View File
@@ -0,0 +1,106 @@
{% extends "base.html" %}
{% block title %}{{ 'Gateway bearbeiten' if gateway else 'Neues Gateway' }} - mGuard VPN Manager{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/gateways">Gateways</a></li>
<li class="breadcrumb-item active">{{ 'Bearbeiten' if gateway else 'Neu' }}</li>
</ol>
</nav>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-{{ 'pencil' if gateway else 'plus-circle' }}"></i>
{{ 'Gateway bearbeiten' if gateway else 'Neues Gateway' }}
</h5>
</div>
<div class="card-body">
<form method="post" action="{{ '/gateways/' ~ gateway.id ~ '/edit' if gateway else '/gateways/new' }}">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Name *</label>
<input type="text" class="form-control" name="name" required
value="{{ gateway.name if gateway else '' }}"
placeholder="z.B. Kunde ABC - Türsteuerung">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Router-Typ *</label>
<select class="form-select" name="router_type" required>
<option value="">Bitte wählen</option>
<option value="FL_MGUARD_2000" {{ 'selected' if gateway and gateway.router_type.value == 'FL_MGUARD_2000' }}>FL MGUARD 2000</option>
<option value="FL_MGUARD_4000" {{ 'selected' if gateway and gateway.router_type.value == 'FL_MGUARD_4000' }}>FL MGUARD 4000</option>
<option value="FL_MGUARD_RS4000" {{ 'selected' if gateway and gateway.router_type.value == 'FL_MGUARD_RS4000' }}>FL MGUARD RS4000</option>
<option value="FL_MGUARD_1000" {{ 'selected' if gateway and gateway.router_type.value == 'FL_MGUARD_1000' }}>FL MGUARD 1000</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Firmware-Version</label>
<input type="text" class="form-control" name="firmware_version"
value="{{ gateway.firmware_version if gateway else '' }}"
placeholder="z.B. 10.5.1">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Seriennummer</label>
<input type="text" class="form-control" name="serial_number"
value="{{ gateway.serial_number if gateway else '' }}"
placeholder="Optional">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Standort</label>
<input type="text" class="form-control" name="location"
value="{{ gateway.location if gateway else '' }}"
placeholder="z.B. Halle 1, Raum 102">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">VPN Subnetz</label>
<input type="text" class="form-control" name="vpn_subnet"
value="{{ gateway.vpn_subnet if gateway else '' }}"
placeholder="z.B. 10.0.0.0/24">
<small class="text-muted">Netzwerk hinter dem Gateway</small>
</div>
</div>
<div class="mb-3">
<label class="form-label">Beschreibung</label>
<textarea class="form-control" name="description" rows="3"
placeholder="Optionale Beschreibung">{{ gateway.description if gateway else '' }}</textarea>
</div>
{% if current_user.is_super_admin and not gateway %}
<div class="mb-3">
<label class="form-label">Mandant</label>
<select class="form-select" name="tenant_id">
{% for tenant in tenants %}
<option value="{{ tenant.id }}">{{ tenant.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
<hr>
<div class="d-flex justify-content-between">
<a href="/gateways{{ '/' ~ gateway.id if gateway else '' }}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Abbrechen
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> {{ 'Speichern' if gateway else 'Gateway anlegen' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+60
View File
@@ -0,0 +1,60 @@
{% extends "base.html" %}
{% block title %}Gateways - mGuard VPN Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-router"></i> Gateways</h1>
<a href="/gateways/new" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Neues Gateway
</a>
</div>
<!-- Filter -->
<div class="card mb-4">
<div class="card-body">
<div class="row align-items-center">
<div class="col-md-4">
<input type="text" class="form-control" placeholder="Suchen..."
hx-get="/htmx/gateways/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#gateway-list"
name="q">
</div>
<div class="col-md-3">
<select class="form-select" hx-get="/htmx/gateways/filter"
hx-trigger="change" hx-target="#gateway-list" name="status">
<option value="">Alle Status</option>
<option value="online">Online</option>
<option value="offline">Offline</option>
</select>
</div>
<div class="col-md-3">
<select class="form-select" hx-get="/htmx/gateways/filter"
hx-trigger="change" hx-target="#gateway-list" name="type">
<option value="">Alle Typen</option>
<option value="FL_MGUARD_2000">FL MGUARD 2000</option>
<option value="FL_MGUARD_4000">FL MGUARD 4000</option>
<option value="FL_MGUARD_RS4000">FL MGUARD RS4000</option>
</select>
</div>
<div class="col-md-2">
<button class="btn btn-outline-secondary w-100"
hx-get="/htmx/gateways/list"
hx-target="#gateway-list">
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
</div>
</div>
</div>
</div>
<!-- Gateway List -->
<div id="gateway-list" hx-get="/htmx/gateways/list" hx-trigger="load" hx-swap="innerHTML">
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Laden...</span>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,231 @@
{% extends "base.html" %}
{% block title %}{{ profile.name }} - {{ gateway.name }} - mGuard VPN{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/gateways">Gateways</a></li>
<li class="breadcrumb-item"><a href="/gateways/{{ gateway.id }}">{{ gateway.name }}</a></li>
<li class="breadcrumb-item"><a href="/gateways/{{ gateway.id }}/profiles">VPN-Profile</a></li>
<li class="breadcrumb-item active">{{ profile.name }}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>
<i class="bi bi-shield-lock"></i> {{ profile.name }}
{% if profile.priority == 1 %}
<span class="badge bg-success">Primär</span>
{% else %}
<span class="badge bg-secondary">Priorität {{ profile.priority }}</span>
{% endif %}
</h1>
<div>
<a href="/gateways/{{ gateway.id }}/profiles" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Zurück
</a>
<a href="/gateways/{{ gateway.id }}/profiles/{{ profile.id }}/edit" class="btn btn-outline-primary">
<i class="bi bi-pencil"></i> Bearbeiten
</a>
<a href="/gateways/{{ gateway.id }}/profiles/{{ profile.id }}/provision" class="btn btn-success">
<i class="bi bi-download"></i> Herunterladen
</a>
</div>
</div>
<div class="row">
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-info-circle"></i> Profil-Informationen
</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th style="width: 40%;">Name</th>
<td>{{ profile.name }}</td>
</tr>
<tr>
<th>Beschreibung</th>
<td>{{ profile.description or '-' }}</td>
</tr>
<tr>
<th>Status</th>
<td>
{% if profile.status.value == 'active' %}
<span class="badge bg-success">Aktiv</span>
{% elif profile.status.value == 'provisioned' %}
<span class="badge bg-info">Provisioniert</span>
{% elif profile.status.value == 'pending' %}
<span class="badge bg-warning text-dark">Ausstehend</span>
{% elif profile.status.value == 'expired' %}
<span class="badge bg-danger">Abgelaufen</span>
{% elif profile.status.value == 'revoked' %}
<span class="badge bg-dark">Widerrufen</span>
{% else %}
<span class="badge bg-secondary">{{ profile.status.value }}</span>
{% endif %}
</td>
</tr>
<tr>
<th>Priorität</th>
<td>{{ profile.priority }}</td>
</tr>
<tr>
<th>Aktiv</th>
<td>
{% if profile.is_active %}
<span class="text-success"><i class="bi bi-check-circle"></i> Ja</span>
{% else %}
<span class="text-danger"><i class="bi bi-x-circle"></i> Nein</span>
{% endif %}
</td>
</tr>
<tr>
<th>Erstellt</th>
<td>{{ profile.created_at.strftime('%d.%m.%Y %H:%M') }}</td>
</tr>
{% if profile.provisioned_at %}
<tr>
<th>Zuletzt provisioniert</th>
<td>{{ profile.provisioned_at.strftime('%d.%m.%Y %H:%M') }}</td>
</tr>
{% endif %}
</table>
</div>
</div>
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-hdd-network"></i> VPN-Server
</div>
<div class="card-body">
{% if profile.vpn_server %}
<table class="table table-borderless">
<tr>
<th style="width: 40%;">Server</th>
<td>
<a href="/vpn-servers/{{ profile.vpn_server.id }}">
{{ profile.vpn_server.name }}
</a>
</td>
</tr>
<tr>
<th>Hostname</th>
<td><code>{{ profile.vpn_server.hostname }}</code></td>
</tr>
<tr>
<th>Port / Protokoll</th>
<td>{{ profile.vpn_server.port }} / {{ profile.vpn_server.protocol.value|upper }}</td>
</tr>
<tr>
<th>VPN-Netzwerk</th>
<td><code>{{ profile.vpn_server.vpn_network }}/{{ profile.vpn_server.vpn_netmask }}</code></td>
</tr>
</table>
{% else %}
<p class="text-muted mb-0">Kein VPN-Server zugewiesen</p>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card mb-4">
<div class="card-header">
<i class="bi bi-file-earmark-lock"></i> Zertifikat
</div>
<div class="card-body">
<table class="table table-borderless">
<tr>
<th style="width: 40%;">Common Name</th>
<td><code>{{ profile.cert_cn }}</code></td>
</tr>
<tr>
<th>Gültig von</th>
<td>{{ profile.valid_from.strftime('%d.%m.%Y') if profile.valid_from else '-' }}</td>
</tr>
<tr>
<th>Gültig bis</th>
<td>
{% if profile.valid_until %}
{{ profile.valid_until.strftime('%d.%m.%Y') }}
{% if profile.is_expired %}
<span class="badge bg-danger ms-2">Abgelaufen</span>
{% elif profile.days_until_expiry <= 30 %}
<span class="badge bg-warning text-dark ms-2">{{ profile.days_until_expiry }} Tage</span>
{% endif %}
{% else %}
-
{% endif %}
</td>
</tr>
<tr>
<th>CA</th>
<td>
{% if profile.certificate_authority %}
<a href="/ca/{{ profile.certificate_authority.id }}">
{{ profile.certificate_authority.name }}
</a>
{% else %}
-
{% endif %}
</td>
</tr>
</table>
{% if profile.client_cert %}
<hr>
<details>
<summary class="btn btn-sm btn-outline-secondary">
<i class="bi bi-eye"></i> Zertifikat anzeigen
</summary>
<pre class="mt-3 p-3 bg-light" style="max-height: 200px; overflow: auto; font-size: 0.8rem;">{{ profile.client_cert }}</pre>
</details>
{% endif %}
</div>
</div>
<div class="card mb-4">
<div class="card-header bg-success text-white">
<i class="bi bi-download"></i> Provisioning
</div>
<div class="card-body">
<p>
Laden Sie die OpenVPN-Konfigurationsdatei herunter und importieren Sie sie auf dem mGuard-Router.
</p>
<a href="/gateways/{{ gateway.id }}/profiles/{{ profile.id }}/provision"
class="btn btn-success btn-lg w-100">
<i class="bi bi-download"></i> {{ profile.name }}.ovpn herunterladen
</a>
{% if profile.provisioned_at %}
<div class="text-muted mt-2 text-center">
<small>Zuletzt heruntergeladen: {{ profile.provisioned_at.strftime('%d.%m.%Y %H:%M') }}</small>
</div>
{% endif %}
</div>
</div>
{% if profile.status.value not in ['revoked', 'expired'] %}
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<i class="bi bi-exclamation-triangle"></i> Gefahrenzone
</div>
<div class="card-body">
<p class="mb-3">
Durch das Widerrufen des Zertifikats wird der Zugang zum VPN-Server gesperrt.
Diese Aktion kann nicht rückgängig gemacht werden.
</p>
<form action="/gateways/{{ gateway.id }}/profiles/{{ profile.id }}/revoke" method="post"
onsubmit="return confirm('Sind Sie sicher? Das Zertifikat wird unwiderruflich gesperrt.');">
<button type="submit" class="btn btn-outline-danger">
<i class="bi bi-shield-x"></i> Zertifikat widerrufen
</button>
</form>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,151 @@
{% extends "base.html" %}
{% block title %}
{% if profile %}Profil bearbeiten{% else %}Neues VPN-Profil{% endif %} - {{ gateway.name }} - mGuard VPN
{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/gateways">Gateways</a></li>
<li class="breadcrumb-item"><a href="/gateways/{{ gateway.id }}">{{ gateway.name }}</a></li>
<li class="breadcrumb-item"><a href="/gateways/{{ gateway.id }}/profiles">VPN-Profile</a></li>
<li class="breadcrumb-item active">{% if profile %}Bearbeiten{% else %}Neu{% endif %}</li>
</ol>
</nav>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="bi bi-shield-lock"></i>
{% if profile %}Profil bearbeiten{% else %}Neues VPN-Profil{% endif %}
</h4>
</div>
<div class="card-body">
{% if error %}
<div class="alert alert-danger">
<i class="bi bi-exclamation-triangle"></i> {{ error }}
</div>
{% endif %}
<form method="post">
<div class="mb-3">
<label for="name" class="form-label">Name *</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ profile.name if profile else '' }}" required
placeholder="z.B. Produktion, Fallback, Migration">
<div class="form-text">Ein beschreibender Name für dieses Profil</div>
</div>
<div class="mb-3">
<label for="description" class="form-label">Beschreibung</label>
<textarea class="form-control" id="description" name="description" rows="2"
placeholder="Optionale Beschreibung">{{ profile.description if profile else '' }}</textarea>
</div>
<div class="mb-3">
<label for="vpn_server_id" class="form-label">VPN-Server *</label>
{% if profile %}
<input type="text" class="form-control" readonly disabled
value="{{ profile.vpn_server.name }} ({{ profile.vpn_server.hostname }}:{{ profile.vpn_server.port }})">
<div class="form-text text-muted">
Der VPN-Server kann nicht geändert werden (Zertifikat ist an den Server gebunden).
</div>
{% else %}
<select class="form-select" id="vpn_server_id" name="vpn_server_id" required>
<option value="">-- Server auswählen --</option>
{% for server in vpn_servers %}
<option value="{{ server.id }}">
{{ server.name }} ({{ server.hostname }}:{{ server.port }}/{{ server.protocol.value }})
</option>
{% endfor %}
</select>
<div class="form-text">
Der VPN-Server, mit dem sich das Gateway verbinden soll.
{% if not vpn_servers %}
<span class="text-warning">
<i class="bi bi-exclamation-triangle"></i>
Keine VPN-Server verfügbar. <a href="/vpn-servers/new">Erstellen Sie zuerst einen Server.</a>
</span>
{% endif %}
</div>
{% endif %}
</div>
<div class="mb-3">
<label for="priority" class="form-label">Priorität *</label>
<input type="number" class="form-control" id="priority" name="priority"
value="{{ profile.priority if profile else (existing_profiles|length + 1) }}"
min="1" max="99" required style="max-width: 120px;">
<div class="form-text">
1 = Höchste Priorität (Primärer Server). Bei Verbindungsproblemen wird das Profil
mit der nächsthöheren Priorität verwendet.
</div>
</div>
{% if not profile %}
<div class="mb-3">
<label for="cert_cn" class="form-label">Common Name (CN)</label>
<input type="text" class="form-control" id="cert_cn" name="cert_cn"
placeholder="Automatisch generiert wenn leer"
value="">
<div class="form-text">
Der Common Name für das Client-Zertifikat. Wenn leer, wird automatisch
"<code>{{ gateway.name|lower|replace(' ', '-') }}-[profilname]</code>" verwendet.
</div>
</div>
<div class="mb-3">
<label for="validity_days" class="form-label">Gültigkeit (Tage)</label>
<input type="number" class="form-control" id="validity_days" name="validity_days"
value="365" min="30" max="3650" style="max-width: 150px;">
<div class="form-text">Wie lange das Zertifikat gültig sein soll (Standard: 365 Tage)</div>
</div>
{% endif %}
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_active" name="is_active"
{% if not profile or profile.is_active %}checked{% endif %}>
<label class="form-check-label" for="is_active">
Profil ist aktiv
</label>
</div>
<div class="form-text">Inaktive Profile werden beim Provisioning nicht berücksichtigt</div>
</div>
<hr>
<div class="d-flex justify-content-between">
<a href="/gateways/{{ gateway.id }}/profiles" class="btn btn-outline-secondary">
<i class="bi bi-x-lg"></i> Abbrechen
</a>
<button type="submit" class="btn btn-primary" {% if not vpn_servers %}disabled{% endif %}>
<i class="bi bi-check-lg"></i>
{% if profile %}Speichern{% else %}Profil erstellen{% endif %}
</button>
</div>
</form>
</div>
</div>
{% if not profile %}
<div class="card mt-4">
<div class="card-header">
<i class="bi bi-info-circle"></i> Was passiert beim Erstellen?
</div>
<div class="card-body">
<ol class="mb-0">
<li>Ein Client-Zertifikat wird aus der CA des VPN-Servers generiert</li>
<li>Das Zertifikat wird mit dem Gateway verknüpft</li>
<li>Nach dem Erstellen können Sie die OpenVPN-Konfigurationsdatei (.ovpn) herunterladen</li>
<li>Die Konfiguration kann dann auf dem mGuard-Router importiert werden</li>
</ol>
</div>
</div>
{% endif %}
</div>
</div>
{% endblock %}
+155
View File
@@ -0,0 +1,155 @@
{% extends "base.html" %}
{% block title %}VPN-Profile - {{ gateway.name }} - mGuard VPN{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/gateways">Gateways</a></li>
<li class="breadcrumb-item"><a href="/gateways/{{ gateway.id }}">{{ gateway.name }}</a></li>
<li class="breadcrumb-item active">VPN-Profile</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>
<i class="bi bi-shield-lock"></i> VPN-Profile
<span class="badge bg-primary">{{ profiles|length }}</span>
</h1>
<div>
<a href="/gateways/{{ gateway.id }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Zurück
</a>
<a href="/gateways/{{ gateway.id }}/profiles/new" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Neues Profil
</a>
</div>
</div>
{% if profiles %}
<div class="card">
<div class="card-body">
<div class="table-responsive">
<table class="table table-hover align-middle">
<thead>
<tr>
<th style="width: 60px;">Priorität</th>
<th>Name</th>
<th>VPN-Server</th>
<th>Common Name</th>
<th>Status</th>
<th>Gültigkeit</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for profile in profiles %}
<tr>
<td>
<span class="badge bg-secondary fs-6">{{ profile.priority }}</span>
</td>
<td>
<a href="/gateways/{{ gateway.id }}/profiles/{{ profile.id }}" class="text-decoration-none">
<strong>{{ profile.name }}</strong>
</a>
{% if profile.priority == 1 %}
<span class="badge bg-success ms-1">Primär</span>
{% endif %}
</td>
<td>
{% if profile.vpn_server %}
<a href="/vpn-servers/{{ profile.vpn_server.id }}">
{{ profile.vpn_server.name }}
</a>
<br>
<small class="text-muted">
{{ profile.vpn_server.hostname }}:{{ profile.vpn_server.port }}/{{ profile.vpn_server.protocol.value }}
</small>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
<code>{{ profile.cert_cn }}</code>
</td>
<td>
{% if profile.status.value == 'active' %}
<span class="badge bg-success">Aktiv</span>
{% elif profile.status.value == 'provisioned' %}
<span class="badge bg-info">Provisioniert</span>
{% elif profile.status.value == 'pending' %}
<span class="badge bg-warning text-dark">Ausstehend</span>
{% elif profile.status.value == 'expired' %}
<span class="badge bg-danger">Abgelaufen</span>
{% elif profile.status.value == 'revoked' %}
<span class="badge bg-dark">Widerrufen</span>
{% else %}
<span class="badge bg-secondary">{{ profile.status.value }}</span>
{% endif %}
</td>
<td>
{% if profile.valid_until %}
<small>
bis {{ profile.valid_until.strftime('%d.%m.%Y') }}
{% if profile.days_until_expiry is defined %}
{% if profile.days_until_expiry <= 30 %}
<br><span class="text-warning">{{ profile.days_until_expiry }} Tage</span>
{% endif %}
{% endif %}
</small>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="/gateways/{{ gateway.id }}/profiles/{{ profile.id }}"
class="btn btn-outline-primary" title="Details">
<i class="bi bi-eye"></i>
</a>
<a href="/gateways/{{ gateway.id }}/profiles/{{ profile.id }}/edit"
class="btn btn-outline-secondary" title="Bearbeiten">
<i class="bi bi-pencil"></i>
</a>
<a href="/gateways/{{ gateway.id }}/profiles/{{ profile.id }}/provision"
class="btn btn-outline-success" title="Konfiguration herunterladen">
<i class="bi bi-download"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">
<i class="bi bi-info-circle"></i> Hinweis zur Priorität
</div>
<div class="card-body">
<p class="mb-0">
Profile mit niedrigerer Prioritätsnummer werden bevorzugt verwendet.
Bei Verbindungsproblemen mit dem primären Server (Priorität 1) versucht der Client
automatisch, sich mit dem nächsten Profil zu verbinden (Failover).
</p>
</div>
</div>
{% else %}
<div class="card">
<div class="card-body text-center py-5">
<i class="bi bi-shield-lock" style="font-size: 3rem;" class="text-muted"></i>
<h4 class="mt-3">Keine VPN-Profile vorhanden</h4>
<p class="text-muted">
Erstellen Sie ein VPN-Profil, um dieses Gateway mit einem VPN-Server zu verbinden.
</p>
<a href="/gateways/{{ gateway.id }}/profiles/new" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Erstes Profil erstellen
</a>
</div>
</div>
{% endif %}
{% endblock %}
+51
View File
@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block title %}{% if tenant %}Mandant bearbeiten{% else %}Neuer Mandant{% endif %} - mGuard VPN{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
<i class="bi bi-building"></i>
{% if tenant %}Mandant bearbeiten{% else %}Neuer Mandant{% endif %}
</h4>
</div>
<div class="card-body">
<form method="post" action="{% if tenant %}/tenants/{{ tenant.id }}/edit{% else %}/tenants/new{% endif %}">
<div class="mb-3">
<label for="name" class="form-label">Name *</label>
<input type="text" class="form-control" id="name" name="name"
value="{{ tenant.name if tenant else '' }}" required>
</div>
<div class="mb-3">
<label for="description" class="form-label">Beschreibung</label>
<textarea class="form-control" id="description" name="description" rows="3">{{ tenant.description if tenant and tenant.description else '' }}</textarea>
</div>
{% if tenant %}
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="is_active" name="is_active" value="true"
{% if tenant.is_active %}checked{% endif %}>
<label class="form-check-label" for="is_active">Aktiv</label>
</div>
</div>
{% endif %}
<div class="d-flex justify-content-between">
<a href="/tenants" class="btn btn-secondary">
<i class="bi bi-arrow-left"></i> Abbrechen
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i> Speichern
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+63
View File
@@ -0,0 +1,63 @@
{% extends "base.html" %}
{% block title %}Mandanten - mGuard VPN{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-building"></i> Mandanten</h1>
<a href="/tenants/new" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Neuer Mandant
</a>
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>Name</th>
<th>Beschreibung</th>
<th>Benutzer</th>
<th>Gateways</th>
<th>Status</th>
<th>Erstellt</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for tenant in tenants %}
<tr>
<td><strong>{{ tenant.name }}</strong></td>
<td>{{ tenant.description or '-' }}</td>
<td>{{ tenant.users|length }}</td>
<td>{{ tenant.gateways|length }}</td>
<td>
{% if tenant.is_active %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-secondary">Inaktiv</span>
{% endif %}
</td>
<td>{{ tenant.created_at.strftime('%d.%m.%Y') }}</td>
<td>
<a href="/tenants/{{ tenant.id }}/edit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</a>
<form action="/tenants/{{ tenant.id }}/delete" method="post" class="d-inline"
onsubmit="return confirm('Mandant wirklich löschen?');">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-trash"></i>
</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center text-muted">Keine Mandanten vorhanden</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
+134
View File
@@ -0,0 +1,134 @@
{% extends "base.html" %}
{% block title %}Zugriffe für {{ user.username }} - mGuard VPN Manager{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/users">Benutzer</a></li>
<li class="breadcrumb-item"><a href="/users/{{ user.id }}/edit">{{ user.username }}</a></li>
<li class="breadcrumb-item active">Zugriffe</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1><i class="bi bi-key"></i> Zugriffe für {{ user.username }}</h1>
<p class="text-muted mb-0">{{ user.email }} | Rolle: {{ user.role.value }}</p>
</div>
<a href="/users" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Zurück
</a>
</div>
<div class="row">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-router"></i> Gateway-Zugriffe</h5>
</div>
<div class="card-body">
<form method="post" action="/users/{{ user.id }}/access">
<table class="table table-hover">
<thead>
<tr>
<th style="width: 50px;">Zugriff</th>
<th>Gateway</th>
<th>Standort</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for gateway in gateways %}
<tr>
<td>
<div class="form-check">
<input class="form-check-input" type="checkbox"
name="gateway_ids" value="{{ gateway.id }}"
id="gw_{{ gateway.id }}"
{{ 'checked' if gateway.id in access_gateway_ids }}>
</div>
</td>
<td>
<label for="gw_{{ gateway.id }}" class="mb-0" style="cursor: pointer;">
{{ gateway.name }}
</label>
</td>
<td class="text-muted">{{ gateway.location or '-' }}</td>
<td>
{% if gateway.is_online %}
<span class="badge bg-success">Online</span>
{% else %}
<span class="badge bg-secondary">Offline</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="4" class="text-center text-muted py-4">
Keine Gateways vorhanden
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if gateways %}
<div class="d-flex justify-content-between">
<div>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectAll()">
Alle auswählen
</button>
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="selectNone()">
Keine auswählen
</button>
</div>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> Speichern
</button>
</div>
{% endif %}
</form>
</div>
</div>
</div>
<div class="col-lg-4">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Info</h5>
</div>
<div class="card-body">
<p class="mb-2"><strong>Benutzer:</strong> {{ user.username }}</p>
<p class="mb-2"><strong>Rolle:</strong>
<span class="badge badge-role-{{ user.role.value }}">{{ user.role.value }}</span>
</p>
<hr>
<h6>Rollen-Erklärung:</h6>
<ul class="small text-muted">
<li><strong>technician:</strong> Kann nur zugewiesene Gateways sehen und verbinden</li>
<li><strong>admin:</strong> Kann alle Gateways des Mandanten verwalten</li>
<li><strong>super_admin:</strong> Voller Zugriff auf alle Mandanten</li>
</ul>
<div class="alert alert-info small mb-0">
<i class="bi bi-lightbulb"></i>
Admins haben automatisch Zugriff auf alle Gateways ihres Mandanten.
Diese Zuweisungen gelten nur für Techniker.
</div>
</div>
</div>
</div>
</div>
<script>
function selectAll() {
document.querySelectorAll('input[name="gateway_ids"]').forEach(cb => cb.checked = true);
}
function selectNone() {
document.querySelectorAll('input[name="gateway_ids"]').forEach(cb => cb.checked = false);
}
</script>
{% endblock %}
+99
View File
@@ -0,0 +1,99 @@
{% extends "base.html" %}
{% block title %}{{ 'Benutzer bearbeiten' if user else 'Neuer Benutzer' }} - mGuard VPN Manager{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/users">Benutzer</a></li>
<li class="breadcrumb-item active">{{ 'Bearbeiten' if user else 'Neu' }}</li>
</ol>
</nav>
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-{{ 'pencil' if user else 'person-plus' }}"></i>
{{ 'Benutzer bearbeiten' if user else 'Neuer Benutzer' }}
</h5>
</div>
<div class="card-body">
<form method="post" action="{{ '/users/' ~ user.id ~ '/edit' if user else '/users/new' }}">
<div class="mb-3">
<label class="form-label">Benutzername *</label>
<input type="text" class="form-control" name="username" required
value="{{ user.username if user else '' }}"
placeholder="benutzername"
{{ 'readonly' if user else '' }}>
</div>
<div class="mb-3">
<label class="form-label">E-Mail *</label>
<input type="email" class="form-control" name="email" required
value="{{ user.email if user else '' }}"
placeholder="user@example.com">
</div>
<div class="mb-3">
<label class="form-label">Vollständiger Name</label>
<input type="text" class="form-control" name="full_name"
value="{{ user.full_name if user else '' }}"
placeholder="Max Mustermann">
</div>
<div class="mb-3">
<label class="form-label">Passwort {{ '(leer lassen = unverändert)' if user else '*' }}</label>
<input type="password" class="form-control" name="password"
{{ '' if user else 'required' }}
placeholder="{{ '••••••••' if user else 'Mindestens 8 Zeichen' }}">
</div>
<div class="mb-3">
<label class="form-label">Rolle *</label>
<select class="form-select" name="role" required>
{% for role in roles %}
<option value="{{ role }}" {{ 'selected' if user and user.role.value == role }}>
{{ role }}
</option>
{% endfor %}
</select>
</div>
{% if current_user.is_super_admin and not user %}
<div class="mb-3">
<label class="form-label">Mandant</label>
<select class="form-select" name="tenant_id">
<option value="">Kein Mandant (Super Admin)</option>
{% for tenant in tenants %}
<option value="{{ tenant.id }}">{{ tenant.name }}</option>
{% endfor %}
</select>
</div>
{% endif %}
{% if user %}
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" name="is_active" id="is_active"
{{ 'checked' if user.is_active }}>
<label class="form-check-label" for="is_active">Aktiv</label>
</div>
{% endif %}
<hr>
<div class="d-flex justify-content-between">
<a href="/users" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Abbrechen
</a>
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-circle"></i> {{ 'Speichern' if user else 'Benutzer anlegen' }}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+83
View File
@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block title %}Benutzer - mGuard VPN Manager{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-people"></i> Benutzer</h1>
<a href="/users/new" class="btn btn-primary">
<i class="bi bi-person-plus"></i> Neuer Benutzer
</a>
</div>
<div class="card">
<div class="card-body">
<table class="table table-hover">
<thead>
<tr>
<th>Benutzername</th>
<th>E-Mail</th>
<th>Rolle</th>
<th>Status</th>
<th>Letzter Login</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>
<i class="bi bi-person-circle"></i>
{{ user.username }}
{% if user.full_name %}
<small class="text-muted d-block">{{ user.full_name }}</small>
{% endif %}
</td>
<td>{{ user.email }}</td>
<td>
<span class="badge badge-role-{{ user.role.value }}">{{ user.role.value }}</span>
</td>
<td>
{% if user.is_active %}
<span class="badge bg-success">Aktiv</span>
{% else %}
<span class="badge bg-danger">Inaktiv</span>
{% endif %}
</td>
<td>
{% if user.last_login %}
<span data-relative-time="{{ user.last_login }}">{{ user.last_login.strftime('%d.%m.%Y %H:%M') }}</span>
{% else %}
<span class="text-muted">Nie</span>
{% endif %}
</td>
<td>
<a href="/users/{{ user.id }}/edit" class="btn btn-sm btn-outline-primary">
<i class="bi bi-pencil"></i>
</a>
<a href="/users/{{ user.id }}/access" class="btn btn-sm btn-outline-info">
<i class="bi bi-key"></i> Zugriffe
</a>
{% if user.id != current_user.id %}
<button class="btn btn-sm btn-outline-danger"
hx-delete="/htmx/users/{{ user.id }}"
hx-confirm="Benutzer '{{ user.username }}' wirklich löschen?"
hx-target="closest tr"
hx-swap="outerHTML">
<i class="bi bi-trash"></i>
</button>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="6" class="text-center text-muted py-4">
Keine Benutzer vorhanden
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}
@@ -0,0 +1,70 @@
{% extends "base.html" %}
{% block title %}Verbundene Clients - {{ server.name }} - mGuard VPN{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/vpn-servers">VPN-Server</a></li>
<li class="breadcrumb-item"><a href="/vpn-servers/{{ server.id }}">{{ server.name }}</a></li>
<li class="breadcrumb-item active">Clients</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>
<i class="bi bi-people"></i> Verbundene Clients
<span class="badge bg-primary">{{ clients|length }}</span>
</h1>
<a href="/vpn-servers/{{ server.id }}" class="btn btn-outline-secondary">
<i class="bi bi-arrow-left"></i> Zurück zum Server
</a>
</div>
<div class="card">
<div class="card-body">
{% if clients %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th>Common Name</th>
<th>Echte Adresse</th>
<th>Empfangen</th>
<th>Gesendet</th>
<th>Verbunden seit</th>
<th>Aktionen</th>
</tr>
</thead>
<tbody>
{% for client in clients %}
<tr>
<td>
<strong>{{ client.common_name }}</strong>
</td>
<td><code>{{ client.real_address }}</code></td>
<td>{{ (client.bytes_received / 1024 / 1024)|round(2) }} MB</td>
<td>{{ (client.bytes_sent / 1024 / 1024)|round(2) }} MB</td>
<td>{{ client.connected_since }}</td>
<td>
<form action="/vpn-servers/{{ server.id }}/disconnect/{{ client.common_name }}" method="post"
onsubmit="return confirm('Client wirklich trennen?');">
<button type="submit" class="btn btn-sm btn-outline-danger">
<i class="bi bi-x-circle"></i> Trennen
</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center py-5 text-muted">
<i class="bi bi-people" style="font-size: 3rem;"></i>
<p class="mt-3">Keine Clients verbunden</p>
</div>
{% endif %}
</div>
</div>
{% endblock %}
@@ -0,0 +1,260 @@
{% extends "base.html" %}
{% block title %}{{ server.name }} - VPN-Server - mGuard VPN{% endblock %}
{% block content %}
<nav aria-label="breadcrumb" class="mb-3">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/vpn-servers">VPN-Server</a></li>
<li class="breadcrumb-item active">{{ server.name }}</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<h1>
{% if server.status.value == 'running' %}
<span class="status-indicator online"></span>
{% else %}
<span class="status-indicator offline"></span>
{% endif %}
{{ server.name }}
{% if server.is_primary %}
<span class="badge bg-primary">Primär</span>
{% endif %}
{% if server.protocol.value == 'udp' %}
<span class="badge badge-udp">UDP</span>
{% else %}
<span class="badge badge-tcp">TCP</span>
{% endif %}
</h1>
<div>
<a href="/vpn-servers/{{ server.id }}/clients" class="btn btn-outline-primary">
<i class="bi bi-people"></i> Clients ({{ status.connected_clients or 0 }})
</a>
<a href="/vpn-servers/{{ server.id }}/edit" class="btn btn-outline-secondary">
<i class="bi bi-pencil"></i> Bearbeiten
</a>
<form action="/vpn-servers/{{ server.id }}/delete" method="post" class="d-inline"
onsubmit="return confirm('VPN-Server wirklich löschen?');">
<button type="submit" class="btn btn-outline-danger">
<i class="bi bi-trash"></i> Löschen
</button>
</form>
</div>
</div>
<div class="row">
<!-- Status Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-activity"></i> Status</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-5">Status</dt>
<dd class="col-sm-7">
{% if server.status.value == 'running' %}
<span class="badge bg-success">Läuft</span>
{% elif server.status.value == 'stopped' %}
<span class="badge bg-secondary">Gestoppt</span>
{% elif server.status.value == 'starting' %}
<span class="badge bg-warning">Startet...</span>
{% elif server.status.value == 'error' %}
<span class="badge bg-danger">Fehler</span>
{% else %}
<span class="badge bg-light text-dark">{{ server.status.value }}</span>
{% endif %}
</dd>
<dt class="col-sm-5">Verbundene Clients</dt>
<dd class="col-sm-7">{{ status.connected_clients or 0 }}</dd>
<dt class="col-sm-5">Letzte Prüfung</dt>
<dd class="col-sm-7">
{{ server.last_status_check.strftime('%d.%m.%Y %H:%M') if server.last_status_check else '-' }}
</dd>
<dt class="col-sm-5">Container</dt>
<dd class="col-sm-7"><code>{{ server.docker_container_name or '-' }}</code></dd>
<dt class="col-sm-5">Management-Port</dt>
<dd class="col-sm-7">{{ server.management_port }}</dd>
</dl>
</div>
</div>
</div>
<!-- Network Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-hdd-network"></i> Netzwerk</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-5">Adresse</dt>
<dd class="col-sm-7"><code>{{ server.hostname }}:{{ server.port }}</code></dd>
<dt class="col-sm-5">Protokoll</dt>
<dd class="col-sm-7">{{ server.protocol.value.upper() }}</dd>
<dt class="col-sm-5">VPN-Netzwerk</dt>
<dd class="col-sm-7">{{ server.vpn_network }}/{{ server.vpn_netmask }}</dd>
<dt class="col-sm-5">Max Clients</dt>
<dd class="col-sm-7">{{ server.max_clients }}</dd>
<dt class="col-sm-5">Keepalive</dt>
<dd class="col-sm-7">{{ server.keepalive_interval }}s / {{ server.keepalive_timeout }}s</dd>
</dl>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Security Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-shield-lock"></i> Sicherheit</h5>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-5">CA</dt>
<dd class="col-sm-7">
<a href="/ca/{{ server.certificate_authority.id }}">
{{ server.certificate_authority.name }}
</a>
</dd>
<dt class="col-sm-5">Cipher</dt>
<dd class="col-sm-7">{{ server.cipher.value }}</dd>
<dt class="col-sm-5">Auth</dt>
<dd class="col-sm-7">{{ server.auth.value }}</dd>
<dt class="col-sm-5">TLS Version</dt>
<dd class="col-sm-7">>= {{ server.tls_version_min }}</dd>
<dt class="col-sm-5">TLS-Auth</dt>
<dd class="col-sm-7">
{% if server.tls_auth_enabled %}
<span class="badge bg-success">Aktiviert</span>
{% else %}
<span class="badge bg-secondary">Deaktiviert</span>
{% endif %}
</dd>
<dt class="col-sm-5">Kompression</dt>
<dd class="col-sm-7">{{ server.compression.value }}</dd>
</dl>
</div>
</div>
</div>
<!-- Profiles Card -->
<div class="col-md-6 mb-4">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-person-vcard"></i> VPN-Profile</h5>
</div>
<div class="card-body">
{% if server.vpn_profiles %}
<ul class="list-group list-group-flush">
{% for profile in server.vpn_profiles[:10] %}
<li class="list-group-item d-flex justify-content-between align-items-center px-0">
<span>
<a href="/gateways/{{ profile.gateway_id }}/profiles/{{ profile.id }}">
{{ profile.gateway.name }} - {{ profile.name }}
</a>
</span>
{% if profile.status.value == 'active' %}
<span class="badge bg-success">Aktiv</span>
{% elif profile.status.value == 'provisioned' %}
<span class="badge bg-info">Provisioniert</span>
{% else %}
<span class="badge bg-secondary">{{ profile.status.value }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% if server.vpn_profiles|length > 10 %}
<p class="text-muted small mt-2 mb-0">... und {{ server.vpn_profiles|length - 10 }} weitere</p>
{% endif %}
{% else %}
<p class="text-muted mb-0">Keine Profile verwenden diesen Server.</p>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Server Log -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="bi bi-terminal"></i> Server-Log</h5>
<div>
<button class="btn btn-sm btn-outline-secondary" onclick="refreshLog()">
<i class="bi bi-arrow-clockwise"></i> Aktualisieren
</button>
<a href="/api/internal/vpn-servers/{{ server.id }}/logs/raw?lines=500"
class="btn btn-sm btn-outline-primary" target="_blank">
<i class="bi bi-download"></i> Download
</a>
</div>
</div>
<div class="card-body p-0">
<div id="server-log" class="bg-dark text-light p-3"
style="max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 12px;">
<div class="text-center py-4">
<div class="spinner-border spinner-border-sm text-light" role="status">
<span class="visually-hidden">Laden...</span>
</div>
<span class="ms-2">Log wird geladen...</span>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function refreshLog() {
const container = document.getElementById('server-log');
try {
const response = await fetch('/api/internal/vpn-servers/{{ server.id }}/logs?lines=100');
const data = await response.json();
if (data.lines && data.lines.length > 0) {
container.innerHTML = data.lines.map(line => {
// Color-code log levels
let lineClass = '';
if (line.includes('ERROR') || line.includes('error')) lineClass = 'text-danger';
else if (line.includes('WARN') || line.includes('warning')) lineClass = 'text-warning';
else if (line.includes('INFO')) lineClass = 'text-info';
return `<div class="${lineClass}">${escapeHtml(line)}</div>`;
}).join('');
container.scrollTop = container.scrollHeight;
} else {
container.innerHTML = '<div class="text-muted">Keine Log-Einträge vorhanden</div>';
}
} catch (error) {
container.innerHTML = `<div class="text-danger">Fehler beim Laden: ${error.message}</div>`;
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Load log on page load
document.addEventListener('DOMContentLoaded', refreshLog);
// Auto-refresh every 10 seconds
setInterval(refreshLog, 10000);
</script>
{% endblock %}
+204
View File
@@ -0,0 +1,204 @@
{% extends "base.html" %}
{% block title %}{% if server %}VPN-Server bearbeiten{% else %}Neuer VPN-Server{% endif %} - mGuard VPN{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h4 class="mb-0">
{% if server %}
<i class="bi bi-pencil"></i> VPN-Server bearbeiten
{% else %}
<i class="bi bi-plus-lg"></i> Neuer VPN-Server
{% endif %}
</h4>
</div>
<div class="card-body">
<form action="{% if server %}/vpn-servers/{{ server.id }}/edit{% else %}/vpn-servers/new{% endif %}" method="post">
<!-- Basic Settings -->
<h6 class="text-muted mb-3">Grundeinstellungen</h6>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Name *</label>
<input type="text" name="name" class="form-control" required
value="{{ server.name if server else '' }}"
placeholder="z.B. Produktion UDP">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Certificate Authority *</label>
{% if server %}
<input type="text" class="form-control" disabled
value="{{ server.certificate_authority.name }}">
<input type="hidden" name="ca_id" value="{{ server.ca_id }}">
{% else %}
<select name="ca_id" class="form-select" required>
<option value="">-- CA auswählen --</option>
{% for ca in cas %}
<option value="{{ ca.id }}">{{ ca.name }}</option>
{% endfor %}
</select>
{% endif %}
</div>
</div>
<div class="mb-3">
<label class="form-label">Beschreibung</label>
<textarea name="description" class="form-control" rows="2"
placeholder="Optionale Beschreibung">{{ server.description if server else '' }}</textarea>
</div>
<hr>
<h6 class="text-muted mb-3">Netzwerk</h6>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Hostname/IP *</label>
<input type="text" name="hostname" class="form-control" required
value="{{ server.hostname if server else '' }}"
placeholder="vpn.meinefirma.de">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Port *</label>
<input type="number" name="port" class="form-control" required
value="{{ server.port if server else 1194 }}"
min="1" max="65535">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Protokoll *</label>
<select name="protocol" class="form-select">
<option value="udp" {% if not server or server.protocol.value == 'udp' %}selected{% endif %}>UDP</option>
<option value="tcp" {% if server and server.protocol.value == 'tcp' %}selected{% endif %}>TCP</option>
</select>
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">VPN-Netzwerk</label>
<input type="text" name="vpn_network" class="form-control"
value="{{ server.vpn_network if server else '10.8.0.0' }}"
placeholder="10.8.0.0">
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Netzmaske</label>
<input type="text" name="vpn_netmask" class="form-control"
value="{{ server.vpn_netmask if server else '255.255.255.0' }}"
placeholder="255.255.255.0">
</div>
</div>
<hr>
<h6 class="text-muted mb-3">Sicherheit</h6>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label">Cipher</label>
<select name="cipher" class="form-select">
{% for c in ciphers %}
<option value="{{ c.value }}" {% if server and server.cipher.value == c.value %}selected{% elif not server and c.value == 'AES-256-GCM' %}selected{% endif %}>
{{ c.value }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">Auth</label>
<select name="auth" class="form-select">
{% for a in auth_methods %}
<option value="{{ a.value }}" {% if server and server.auth.value == a.value %}selected{% elif not server and a.value == 'SHA256' %}selected{% endif %}>
{{ a.value }}
</option>
{% endfor %}
</select>
</div>
<div class="col-md-4 mb-3">
<label class="form-label">TLS Version min</label>
<select name="tls_version_min" class="form-select">
<option value="1.2" {% if not server or server.tls_version_min == '1.2' %}selected{% endif %}>1.2</option>
<option value="1.3" {% if server and server.tls_version_min == '1.3' %}selected{% endif %}>1.3</option>
</select>
</div>
</div>
<hr>
<h6 class="text-muted mb-3">Performance</h6>
<div class="row">
<div class="col-md-3 mb-3">
<label class="form-label">Max Clients</label>
<input type="number" name="max_clients" class="form-control"
value="{{ server.max_clients if server else 100 }}"
min="1" max="1000">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Keepalive (s)</label>
<input type="number" name="keepalive_interval" class="form-control"
value="{{ server.keepalive_interval if server else 10 }}"
min="1" max="60">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Timeout (s)</label>
<input type="number" name="keepalive_timeout" class="form-control"
value="{{ server.keepalive_timeout if server else 60 }}"
min="10" max="300">
</div>
<div class="col-md-3 mb-3">
<label class="form-label">Kompression</label>
<select name="compression" class="form-select">
{% for co in compression_options %}
<option value="{{ co.value }}" {% if server and server.compression.value == co.value %}selected{% elif not server and co.value == 'none' %}selected{% endif %}>
{{ co.value }}
</option>
{% endfor %}
</select>
</div>
</div>
<hr>
<h6 class="text-muted mb-3">Docker</h6>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Management-Port</label>
<input type="number" name="management_port" class="form-control"
value="{{ server.management_port if server else 7505 }}"
min="1" max="65535">
</div>
<div class="col-md-6 mb-3 d-flex align-items-end">
<div class="form-check">
<input type="checkbox" name="is_primary" class="form-check-input" id="isPrimary"
{% if server and server.is_primary %}checked{% endif %}>
<label class="form-check-label" for="isPrimary">Primärer Server</label>
</div>
</div>
</div>
{% if server %}
<div class="row">
<div class="col-md-6 mb-3 d-flex align-items-end">
<div class="form-check">
<input type="checkbox" name="is_active" class="form-check-input" id="isActive"
{% if server.is_active %}checked{% endif %}>
<label class="form-check-label" for="isActive">Server aktiv</label>
</div>
</div>
</div>
{% endif %}
<div class="d-grid gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg"></i>
{% if server %}Speichern{% else %}VPN-Server erstellen{% endif %}
</button>
<a href="/vpn-servers" class="btn btn-outline-secondary">Abbrechen</a>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
@@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block title %}VPN-Server - mGuard VPN{% endblock %}
{% block content %}
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="bi bi-server"></i> VPN-Server</h1>
<a href="/vpn-servers/new" class="btn btn-primary">
<i class="bi bi-plus-lg"></i> Neuer VPN-Server
</a>
</div>
<div class="row">
{% for server in servers %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card h-100 {% if server.is_primary %}border-primary{% endif %}">
<div class="card-header d-flex justify-content-between align-items-center">
<span>
{% if server.status.value == 'running' %}
<span class="status-indicator online"></span>
{% else %}
<span class="status-indicator offline"></span>
{% endif %}
{{ server.name }}
</span>
<div>
{% if server.is_primary %}
<span class="badge bg-primary">Primär</span>
{% endif %}
{% if server.protocol.value == 'udp' %}
<span class="badge badge-udp">UDP</span>
{% else %}
<span class="badge badge-tcp">TCP</span>
{% endif %}
</div>
</div>
<div class="card-body">
<p class="text-muted small">{{ server.description or 'Keine Beschreibung' }}</p>
<!-- Connection Info -->
<div class="mb-3">
<code>{{ server.hostname }}:{{ server.port }}</code>
</div>
<!-- Status -->
<div class="mb-2">
{% if server.status.value == 'running' %}
<span class="badge bg-success">Läuft</span>
<span class="badge bg-light text-dark">{{ server.connected_clients }} Clients</span>
{% elif server.status.value == 'stopped' %}
<span class="badge bg-secondary">Gestoppt</span>
{% elif server.status.value == 'starting' %}
<span class="badge bg-warning">Startet...</span>
{% elif server.status.value == 'error' %}
<span class="badge bg-danger">Fehler</span>
{% else %}
<span class="badge bg-light text-dark">{{ server.status.value }}</span>
{% endif %}
</div>
<!-- Details -->
<ul class="list-unstyled small mb-0">
<li><strong>Netzwerk:</strong> {{ server.vpn_network }}/{{ server.vpn_netmask }}</li>
<li><strong>Cipher:</strong> {{ server.cipher.value }}</li>
<li><strong>CA:</strong> {{ server.certificate_authority.name if server.certificate_authority else '-' }}</li>
</ul>
</div>
<div class="card-footer">
<a href="/vpn-servers/{{ server.id }}" class="btn btn-sm btn-primary">
<i class="bi bi-eye"></i> Details
</a>
<a href="/vpn-servers/{{ server.id }}/clients" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-people"></i> Clients
</a>
<a href="/vpn-servers/{{ server.id }}/edit" class="btn btn-sm btn-outline-secondary">
<i class="bi bi-pencil"></i>
</a>
</div>
</div>
</div>
{% else %}
<div class="col-12">
<div class="alert alert-info">
<i class="bi bi-info-circle"></i>
Keine VPN-Server vorhanden.
<a href="/vpn-servers/new">Erstellen Sie einen neuen VPN-Server</a>.
<br>
<small class="text-muted">Hinweis: Sie benötigen zuerst eine <a href="/ca">Certificate Authority</a>.</small>
</div>
</div>
{% endfor %}
</div>
{% endblock %}