373 Zeilen
16 KiB
HTML
373 Zeilen
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Administration{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<h1 class="h3">Administration</h1>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Account Forger Configuration -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header bg-primary text-white">
|
|
<h5 class="mb-0">Account Forger Konfiguration</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<form method="post" action="{{ url_for('admin.update_client_config') }}" class="row g-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Aktuelle Version</label>
|
|
<input type="text" class="form-control" name="current_version"
|
|
value="{{ client_config[4] if client_config else '1.0.0' }}"
|
|
pattern="^\d+\.\d+\.\d+$" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Minimum Version</label>
|
|
<input type="text" class="form-control" name="minimum_version"
|
|
value="{{ client_config[5] if client_config else '1.0.0' }}"
|
|
pattern="^\d+\.\d+\.\d+$" required>
|
|
</div>
|
|
<div class="col-12">
|
|
<button type="submit" class="btn btn-primary">Speichern</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<div class="card">
|
|
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">Aktive Sitzungen</h5>
|
|
<div>
|
|
<span class="badge bg-white text-dark" id="sessionCount">{{ active_sessions|length if active_sessions else 0 }}</span>
|
|
<a href="{{ url_for('admin.license_sessions') }}" class="btn btn-sm btn-light ms-2">Alle anzeigen</a>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="table-responsive">
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Kunde</th>
|
|
<th>Version</th>
|
|
<th>Letztes Heartbeat</th>
|
|
<th>Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="sessionTableBody">
|
|
{% if active_sessions %}
|
|
{% for session in active_sessions[:5] %}
|
|
<tr>
|
|
<td>{{ session[3] or 'Unbekannt' }}</td>
|
|
<td>{{ session[6] }}</td>
|
|
<td>{{ session[8].strftime('%H:%M:%S') }}</td>
|
|
<td>
|
|
{% if session[9] < 90 %}
|
|
<span class="badge bg-success">Aktiv</span>
|
|
{% else %}
|
|
<span class="badge bg-warning">Timeout</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted">Keine aktiven Sitzungen</td>
|
|
</tr>
|
|
{% endif %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- System API Key Section -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-header bg-warning text-dark">
|
|
<h5 class="mb-0"><i class="bi bi-key"></i> API Key für Account Forger</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if system_api_key %}
|
|
<div class="alert alert-info mb-3">
|
|
<i class="bi bi-info-circle"></i> Dies ist der einzige API Key, den Account Forger benötigt.
|
|
Verwenden Sie diesen Key im Header <code>X-API-Key</code> für alle API-Anfragen.
|
|
</div>
|
|
<div class="row mb-3">
|
|
<div class="col-md-12">
|
|
<label class="form-label fw-bold">Aktueller API Key:</label>
|
|
<div class="input-group">
|
|
<input type="text" class="form-control font-monospace" id="systemApiKey"
|
|
value="{{ system_api_key.api_key }}" readonly>
|
|
<button class="btn btn-outline-secondary" type="button" onclick="copySystemApiKey()">
|
|
<i class="bi bi-clipboard"></i> Kopieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<h6>Key Informationen:</h6>
|
|
<ul class="list-unstyled small">
|
|
<li><strong>Erstellt:</strong>
|
|
{% if system_api_key.created_at %}
|
|
{{ system_api_key.created_at.strftime('%d.%m.%Y %H:%M') }}
|
|
{% else %}
|
|
N/A
|
|
{% endif %}
|
|
</li>
|
|
<li><strong>Erstellt von:</strong> {{ system_api_key.created_by or 'System' }}</li>
|
|
{% if system_api_key.regenerated_at %}
|
|
<li><strong>Zuletzt regeneriert:</strong>
|
|
{{ system_api_key.regenerated_at.strftime('%d.%m.%Y %H:%M') }}
|
|
</li>
|
|
<li><strong>Regeneriert von:</strong> {{ system_api_key.regenerated_by }}</li>
|
|
{% else %}
|
|
<li><strong>Zuletzt regeneriert:</strong> Nie</li>
|
|
{% endif %}
|
|
</ul>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<h6>Nutzungsstatistiken:</h6>
|
|
<ul class="list-unstyled small">
|
|
<li><strong>Letzte Nutzung:</strong>
|
|
{% if system_api_key.last_used_at %}
|
|
{{ system_api_key.last_used_at.strftime('%d.%m.%Y %H:%M') }}
|
|
{% else %}
|
|
Noch nie genutzt
|
|
{% endif %}
|
|
</li>
|
|
<li><strong>Gesamte Anfragen:</strong> {{ system_api_key.usage_count or 0 }}</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div class="row">
|
|
<div class="col-md-12">
|
|
<form action="{{ url_for('admin.regenerate_api_key') }}" method="POST"
|
|
onsubmit="return confirmRegenerate()">
|
|
<button type="submit" class="btn btn-warning">
|
|
<i class="bi bi-arrow-clockwise"></i> API Key regenerieren
|
|
</button>
|
|
<span class="text-muted ms-2">
|
|
<i class="bi bi-exclamation-triangle"></i>
|
|
Dies wird den aktuellen Key ungültig machen!
|
|
</span>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<details>
|
|
<summary class="text-primary" style="cursor: pointer;">Verwendungsbeispiel anzeigen</summary>
|
|
<div class="mt-2">
|
|
<pre class="bg-light p-3"><code>import requests
|
|
|
|
headers = {
|
|
"X-API-Key": "{{ system_api_key.api_key }}",
|
|
"Content-Type": "application/json"
|
|
}
|
|
|
|
response = requests.post(
|
|
"{{ request.url_root }}api/license/verify",
|
|
headers=headers,
|
|
json={"license_key": "YOUR_LICENSE_KEY"}
|
|
)</code></pre>
|
|
</div>
|
|
</details>
|
|
</div>
|
|
{% else %}
|
|
<div class="alert alert-danger">
|
|
<i class="bi bi-exclamation-triangle"></i> Kein System API Key gefunden!
|
|
Bitte kontaktieren Sie den Administrator.
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Technical Settings (collapsible) -->
|
|
<div class="accordion mb-4" id="technicalSettings">
|
|
<!-- Feature Flags -->
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#featureFlags">
|
|
Feature Flags
|
|
</button>
|
|
</h2>
|
|
<div id="featureFlags" class="accordion-collapse collapse" data-bs-parent="#technicalSettings">
|
|
<div class="accordion-body">
|
|
<div class="table-responsive">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Feature</th>
|
|
<th>Beschreibung</th>
|
|
<th>Status</th>
|
|
<th>Aktion</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for flag in feature_flags %}
|
|
<tr>
|
|
<td><strong>{{ flag[1] }}</strong></td>
|
|
<td><small>{{ flag[2] }}</small></td>
|
|
<td>
|
|
{% if flag[3] %}
|
|
<span class="badge bg-success">Aktiv</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Inaktiv</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<form method="post" action="{{ url_for('admin.toggle_feature_flag', flag_id=flag[0]) }}" style="display: inline;">
|
|
<button type="submit" class="btn btn-sm {% if flag[3] %}btn-danger{% else %}btn-success{% endif %}">
|
|
{% if flag[3] %}Deaktivieren{% else %}Aktivieren{% endif %}
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted">Keine Feature Flags konfiguriert</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rate Limits -->
|
|
<div class="accordion-item">
|
|
<h2 class="accordion-header">
|
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#rateLimits">
|
|
Rate Limits
|
|
</button>
|
|
</h2>
|
|
<div id="rateLimits" class="accordion-collapse collapse" data-bs-parent="#technicalSettings">
|
|
<div class="accordion-body">
|
|
<div class="table-responsive">
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>API Key</th>
|
|
<th>Requests/Minute</th>
|
|
<th>Requests/Stunde</th>
|
|
<th>Requests/Tag</th>
|
|
<th>Burst Size</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for limit in rate_limits %}
|
|
<tr>
|
|
<td><code>{{ limit[1][:12] }}...</code></td>
|
|
<td>{{ limit[2] }}</td>
|
|
<td>{{ limit[3] }}</td>
|
|
<td>{{ limit[4] }}</td>
|
|
<td>{{ limit[5] }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="5" class="text-center text-muted">Keine Rate Limits konfiguriert</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function copyToClipboard(text) {
|
|
navigator.clipboard.writeText(text).then(function() {
|
|
// Show success message instead of alert
|
|
const button = event.target.closest('button');
|
|
const originalText = button.innerHTML;
|
|
button.innerHTML = '<i class="bi bi-check"></i> Kopiert!';
|
|
button.classList.remove('btn-outline-secondary');
|
|
button.classList.add('btn-success');
|
|
|
|
setTimeout(() => {
|
|
button.innerHTML = originalText;
|
|
button.classList.remove('btn-success');
|
|
button.classList.add('btn-outline-secondary');
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
function copySystemApiKey() {
|
|
const apiKeyInput = document.getElementById('systemApiKey');
|
|
apiKeyInput.select();
|
|
apiKeyInput.setSelectionRange(0, 99999);
|
|
|
|
navigator.clipboard.writeText(apiKeyInput.value).then(function() {
|
|
const button = event.currentTarget;
|
|
const originalHTML = button.innerHTML;
|
|
button.innerHTML = '<i class="bi bi-check"></i> Kopiert!';
|
|
button.classList.remove('btn-outline-secondary');
|
|
button.classList.add('btn-success');
|
|
|
|
setTimeout(() => {
|
|
button.innerHTML = originalHTML;
|
|
button.classList.remove('btn-success');
|
|
button.classList.add('btn-outline-secondary');
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
function confirmRegenerate() {
|
|
return confirm('Sind Sie sicher, dass Sie den API Key regenerieren möchten?\n\n' +
|
|
'Dies wird den aktuellen Key ungültig machen und alle bestehenden ' +
|
|
'Integrationen müssen mit dem neuen Key aktualisiert werden.');
|
|
}
|
|
|
|
// Auto-refresh sessions every 30 seconds
|
|
function refreshSessions() {
|
|
fetch('{{ url_for("admin.license_live_stats") }}')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
document.getElementById('sessionCount').textContent = data.active_licenses || 0;
|
|
|
|
// Update session table
|
|
const tbody = document.getElementById('sessionTableBody');
|
|
if (data.latest_sessions && data.latest_sessions.length > 0) {
|
|
tbody.innerHTML = data.latest_sessions.map(session => `
|
|
<tr>
|
|
<td>${session.customer_name || 'Unbekannt'}</td>
|
|
<td>${session.version}</td>
|
|
<td>${session.last_heartbeat}</td>
|
|
<td>
|
|
${session.seconds_since < 90
|
|
? '<span class="badge bg-success">Aktiv</span>'
|
|
: '<span class="badge bg-warning">Timeout</span>'}
|
|
</td>
|
|
</tr>
|
|
`).join('');
|
|
} else {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted">Keine aktiven Sitzungen</td></tr>';
|
|
}
|
|
})
|
|
.catch(error => console.error('Error refreshing sessions:', error));
|
|
}
|
|
|
|
// Refresh sessions every 30 seconds
|
|
setInterval(refreshSessions, 30000);
|
|
</script>
|
|
{% endblock %} |