first commit
This commit is contained in:
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user