Donut Ding weg + Toggle mit Aktiv

Dieser Commit ist enthalten in:
2025-06-08 21:38:53 +02:00
Ursprung ecd621c435
Commit 37ab3601c0
5 geänderte Dateien mit 471 neuen und 61 gelöschten Zeilen

Datei anzeigen

@@ -2162,5 +2162,153 @@ def clear_attempts():
return redirect(url_for('blocked_ips')) return redirect(url_for('blocked_ips'))
# API Endpoints for License Management
@app.route("/api/license/<int:license_id>/toggle", methods=["POST"])
@login_required
def toggle_license_api(license_id):
"""Toggle license active status via API"""
try:
data = request.get_json()
is_active = data.get('is_active', False)
conn = get_connection()
cur = conn.cursor()
# Update license status
cur.execute("""
UPDATE licenses
SET is_active = %s
WHERE id = %s
""", (is_active, license_id))
conn.commit()
# Log the action
log_audit('UPDATE', 'license', license_id,
new_values={'is_active': is_active},
additional_info=f"Lizenz {'aktiviert' if is_active else 'deaktiviert'} via Toggle")
cur.close()
conn.close()
return jsonify({'success': True, 'message': 'Status erfolgreich geändert'})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@app.route("/api/licenses/bulk-activate", methods=["POST"])
@login_required
def bulk_activate_licenses():
"""Activate multiple licenses at once"""
try:
data = request.get_json()
license_ids = data.get('ids', [])
if not license_ids:
return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
conn = get_connection()
cur = conn.cursor()
# Update all selected licenses
cur.execute("""
UPDATE licenses
SET is_active = TRUE
WHERE id = ANY(%s)
""", (license_ids,))
affected_rows = cur.rowcount
conn.commit()
# Log the bulk action
log_audit('BULK_UPDATE', 'licenses', None,
new_values={'is_active': True, 'count': affected_rows},
additional_info=f"{affected_rows} Lizenzen aktiviert")
cur.close()
conn.close()
return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen aktiviert'})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@app.route("/api/licenses/bulk-deactivate", methods=["POST"])
@login_required
def bulk_deactivate_licenses():
"""Deactivate multiple licenses at once"""
try:
data = request.get_json()
license_ids = data.get('ids', [])
if not license_ids:
return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
conn = get_connection()
cur = conn.cursor()
# Update all selected licenses
cur.execute("""
UPDATE licenses
SET is_active = FALSE
WHERE id = ANY(%s)
""", (license_ids,))
affected_rows = cur.rowcount
conn.commit()
# Log the bulk action
log_audit('BULK_UPDATE', 'licenses', None,
new_values={'is_active': False, 'count': affected_rows},
additional_info=f"{affected_rows} Lizenzen deaktiviert")
cur.close()
conn.close()
return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen deaktiviert'})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@app.route("/api/licenses/bulk-delete", methods=["POST"])
@login_required
def bulk_delete_licenses():
"""Delete multiple licenses at once"""
try:
data = request.get_json()
license_ids = data.get('ids', [])
if not license_ids:
return jsonify({'success': False, 'message': 'Keine Lizenzen ausgewählt'}), 400
conn = get_connection()
cur = conn.cursor()
# Get license info for audit log
cur.execute("""
SELECT license_key
FROM licenses
WHERE id = ANY(%s)
""", (license_ids,))
license_keys = [row[0] for row in cur.fetchall()]
# Delete all selected licenses
cur.execute("""
DELETE FROM licenses
WHERE id = ANY(%s)
""", (license_ids,))
affected_rows = cur.rowcount
conn.commit()
# Log the bulk action
log_audit('BULK_DELETE', 'licenses', None,
old_values={'license_keys': license_keys, 'count': affected_rows},
additional_info=f"{affected_rows} Lizenzen gelöscht")
cur.close()
conn.close()
return jsonify({'success': True, 'message': f'{affected_rows} Lizenzen gelöscht'})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000) app.run(host="0.0.0.0", port=5000)

Datei anzeigen

@@ -0,0 +1,93 @@
-- Beispieldaten für v2-Docker Admin Panel
-- Führen Sie dieses Script aus, um Testdaten zu generieren
-- Kunden einfügen
INSERT INTO customers (name, email) VALUES
('TechStart GmbH', 'info@techstart.de'),
('Digital Solutions AG', 'kontakt@digital-solutions.ch'),
('WebMaster Pro', 'admin@webmaster-pro.com'),
('Social Media Experts', 'hello@social-experts.de'),
('Marketing Genius Ltd', 'contact@marketing-genius.co.uk'),
('StartUp Factory', 'team@startup-factory.de'),
('Innovation Hub GmbH', 'info@innovation-hub.de'),
('Creative Agency Berlin', 'office@creative-berlin.de'),
('Data Analytics Corp', 'support@data-analytics.com'),
('Cloud Services 24/7', 'info@cloud247.de'),
('Mobile First Solutions', 'contact@mobile-first.de'),
('AI Powered Marketing', 'hello@ai-marketing.de'),
('Performance Media Group', 'info@performance-media.de'),
('Growth Hacker Studio', 'team@growth-hacker.de'),
('Digital Transformation AG', 'contact@digital-transform.ch')
ON CONFLICT (email) DO NOTHING;
-- Lizenzen einfügen (verschiedene Status)
-- Aktive Vollversionen
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active) VALUES
('AF-202506F-A7K9-M3P2-X8R4', 1, 'full', CURRENT_DATE - INTERVAL '30 days', CURRENT_DATE + INTERVAL '335 days', true),
('AF-202506F-B2N5-K8L3-Q9W7', 2, 'full', CURRENT_DATE - INTERVAL '60 days', CURRENT_DATE + INTERVAL '305 days', true),
('AF-202506F-C4M8-P2R6-T5Y3', 3, 'full', CURRENT_DATE - INTERVAL '90 days', CURRENT_DATE + INTERVAL '275 days', true),
('AF-202506F-D9L2-S7K4-U8N6', 4, 'full', CURRENT_DATE - INTERVAL '15 days', CURRENT_DATE + INTERVAL '350 days', true),
('AF-202506F-E3P7-W5M9-V2X8', 5, 'full', CURRENT_DATE - INTERVAL '45 days', CURRENT_DATE + INTERVAL '320 days', true);
-- Bald ablaufende Lizenzen (innerhalb 30 Tage)
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active) VALUES
('AF-202506F-F6K2-Y8L4-Z3N9', 6, 'full', CURRENT_DATE - INTERVAL '350 days', CURRENT_DATE + INTERVAL '15 days', true),
('AF-202506F-G4M7-A9P3-B5R8', 7, 'full', CURRENT_DATE - INTERVAL '340 days', CURRENT_DATE + INTERVAL '25 days', true),
('AF-202506T-H8N3-C2K6-D7L9', 8, 'test', CURRENT_DATE - INTERVAL '25 days', CURRENT_DATE + INTERVAL '5 days', true),
('AF-202506T-J5P8-E4M2-F9N7', 9, 'test', CURRENT_DATE - INTERVAL '20 days', CURRENT_DATE + INTERVAL '10 days', true);
-- Abgelaufene Lizenzen
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active) VALUES
('AF-202406F-K3L7-G8P4-H2M9', 10, 'full', CURRENT_DATE - INTERVAL '400 days', CURRENT_DATE - INTERVAL '35 days', true),
('AF-202406F-L9N2-J5K8-M7P3', 11, 'full', CURRENT_DATE - INTERVAL '380 days', CURRENT_DATE - INTERVAL '15 days', true),
('AF-202406T-M4K6-L3N9-P8R2', 12, 'test', CURRENT_DATE - INTERVAL '45 days', CURRENT_DATE - INTERVAL '15 days', true);
-- Deaktivierte Lizenzen
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active) VALUES
('AF-202506F-N7P3-Q2L8-R5K4', 13, 'full', CURRENT_DATE - INTERVAL '120 days', CURRENT_DATE + INTERVAL '245 days', false),
('AF-202506F-P8M5-S4N7-T9L2', 14, 'full', CURRENT_DATE - INTERVAL '180 days', CURRENT_DATE + INTERVAL '185 days', false),
('AF-202506T-Q3K9-U7P5-V2M8', 15, 'test', CURRENT_DATE - INTERVAL '10 days', CURRENT_DATE + INTERVAL '20 days', false);
-- Testversionen
INSERT INTO licenses (license_key, customer_id, license_type, valid_from, valid_until, is_active) VALUES
('AF-202506T-R6N4-W8L2-X3P7', 1, 'test', CURRENT_DATE, CURRENT_DATE + INTERVAL '30 days', true),
('AF-202506T-S9K7-Y5M3-Z8N2', 3, 'test', CURRENT_DATE - INTERVAL '5 days', CURRENT_DATE + INTERVAL '25 days', true),
('AF-202506T-T4L8-A2P6-B7K3', 5, 'test', CURRENT_DATE - INTERVAL '10 days', CURRENT_DATE + INTERVAL '20 days', true);
-- Sessions einfügen (nur für Demonstration)
-- Aktive Sessions
INSERT INTO sessions (license_id, session_id, ip_address, user_agent, last_heartbeat, is_active) VALUES
(1, 'sess_' || gen_random_uuid(), '192.168.1.100', 'Mozilla/5.0 Windows NT 10.0', CURRENT_TIMESTAMP - INTERVAL '2 minutes', true),
(2, 'sess_' || gen_random_uuid(), '10.0.0.50', 'Mozilla/5.0 Macintosh', CURRENT_TIMESTAMP - INTERVAL '5 minutes', true),
(3, 'sess_' || gen_random_uuid(), '172.16.0.25', 'Chrome/91.0.4472.124', CURRENT_TIMESTAMP - INTERVAL '1 minute', true),
(4, 'sess_' || gen_random_uuid(), '192.168.2.75', 'Safari/14.1.1', CURRENT_TIMESTAMP - INTERVAL '8 minutes', true);
-- Inaktive Sessions (beendet)
INSERT INTO sessions (license_id, session_id, ip_address, user_agent, started_at, last_heartbeat, ended_at, is_active) VALUES
(5, 'sess_' || gen_random_uuid(), '10.10.10.10', 'Firefox/89.0', CURRENT_TIMESTAMP - INTERVAL '2 hours', CURRENT_TIMESTAMP - INTERVAL '1 hour', CURRENT_TIMESTAMP - INTERVAL '1 hour', false),
(6, 'sess_' || gen_random_uuid(), '172.20.0.100', 'Edge/91.0.864.59', CURRENT_TIMESTAMP - INTERVAL '5 hours', CURRENT_TIMESTAMP - INTERVAL '3 hours', CURRENT_TIMESTAMP - INTERVAL '3 hours', false);
-- Audit Log Einträge
INSERT INTO audit_log (username, action, entity_type, entity_id, new_values, ip_address, additional_info) VALUES
('rac00n', 'CREATE', 'customer', 1, '{"name": "TechStart GmbH", "email": "info@techstart.de"}', '192.168.1.1', 'Neuer Kunde angelegt'),
('w@rh@mm3r', 'CREATE', 'license', 1, '{"license_key": "AF-202506F-A7K9-M3P2-X8R4", "type": "full"}', '192.168.1.2', 'Vollversion erstellt'),
('rac00n', 'UPDATE', 'license', 13, '{"is_active": false}', '192.168.1.1', 'Lizenz deaktiviert'),
('w@rh@mm3r', 'DELETE', 'customer', 16, '{"name": "Test Kunde"}', '192.168.1.2', 'Kunde ohne Lizenzen gelöscht'),
('rac00n', 'EXPORT', 'licenses', null, '{"format": "excel", "count": 15}', '192.168.1.1', 'Lizenzexport durchgeführt'),
('system', 'CREATE_BATCH', 'licenses', null, '{"customer": "Digital Solutions AG", "count": 5}', '127.0.0.1', 'Batch-Lizenzen erstellt');
-- Login Attempts (für Security Dashboard)
INSERT INTO login_attempts (ip_address, attempt_count, last_username_tried, last_error_message) VALUES
('192.168.100.50', 2, 'admin', 'Falsches Passwort'),
('10.0.0.200', 3, 'test', 'Benutzer nicht gefunden'),
('172.16.50.100', 1, 'rac00n', 'Falsches Passwort');
-- Ein gesperrter Versuch
INSERT INTO login_attempts (ip_address, attempt_count, last_username_tried, last_error_message, blocked_until) VALUES
('192.168.200.100', 5, 'hacker', 'Zu viele Fehlversuche', CURRENT_TIMESTAMP + INTERVAL '20 hours');
-- Backup History
INSERT INTO backup_history (filename, filepath, filesize, backup_type, status, created_by, tables_count, records_count, duration_seconds, is_encrypted) VALUES
('backup_v2docker_20250608_120000_encrypted.sql.gz.enc', '/app/backups/backup_v2docker_20250608_120000_encrypted.sql.gz.enc', 1048576, 'manual', 'success', 'rac00n', 8, 150, 2.5, true),
('backup_v2docker_20250608_030000_encrypted.sql.gz.enc', '/app/backups/backup_v2docker_20250608_030000_encrypted.sql.gz.enc', 1024000, 'scheduled', 'success', 'system', 8, 145, 2.1, true),
('backup_v2docker_20250607_150000_encrypted.sql.gz.enc', '/app/backups/backup_v2docker_20250607_150000_encrypted.sql.gz.enc', 980000, 'manual', 'success', 'w@rh@mm3r', 8, 140, 1.9, true);

Datei anzeigen

@@ -80,6 +80,88 @@
z-index: 9999; z-index: 9999;
max-width: 400px; max-width: 400px;
} }
/* Table Improvements */
.table-container {
max-height: 600px;
overflow-y: auto;
position: relative;
}
.table-sticky thead {
position: sticky;
top: 0;
background-color: #fff;
z-index: 10;
box-shadow: 0 2px 2px -1px rgba(0, 0, 0, 0.1);
}
.table-sticky thead th {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
}
/* Inline Actions */
.btn-copy {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-copy:hover {
background-color: #e9ecef;
}
.btn-copy.copied {
background-color: var(--status-active);
color: white;
}
/* Toggle Switch */
.form-switch-custom {
display: inline-block;
}
.form-switch-custom .form-check-input {
cursor: pointer;
width: 3em;
height: 1.5em;
}
.form-switch-custom .form-check-input:checked {
background-color: var(--status-active);
border-color: var(--status-active);
}
/* Bulk Actions Bar */
.bulk-actions {
position: sticky;
bottom: 0;
background-color: #212529;
color: white;
padding: 1rem;
display: none;
z-index: 100;
box-shadow: 0 -4px 6px rgba(0, 0, 0, 0.1);
}
.bulk-actions.show {
display: flex;
align-items: center;
justify-content: space-between;
}
/* Checkbox Styling */
.checkbox-cell {
width: 40px;
}
.form-check-input-custom {
cursor: pointer;
width: 1.2em;
height: 1.2em;
}
</style> </style>
</head> </head>
<body class="bg-light"> <body class="bg-light">

Datei anzeigen

@@ -49,13 +49,6 @@
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
/* Chart styles */
.chart-container {
position: relative;
height: 100px;
margin: 1rem 0;
}
/* Progress bar styles */ /* Progress bar styles */
.progress-custom { .progress-custom {
height: 8px; height: 8px;
@@ -101,9 +94,6 @@
<div class="card-icon text-info">📋</div> <div class="card-icon text-info">📋</div>
<div class="card-value text-info">{{ stats.total_licenses }}</div> <div class="card-value text-info">{{ stats.total_licenses }}</div>
<div class="card-label text-muted">Lizenzen Gesamt</div> <div class="card-label text-muted">Lizenzen Gesamt</div>
<div class="chart-container">
<canvas id="licenseChart"></canvas>
</div>
</div> </div>
</div> </div>
</a> </a>
@@ -348,46 +338,4 @@
</div> </div>
</div> </div>
</div> </div>
{% endblock %}
{% block extra_js %}
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script>
// Donut Chart für Lizenzen
const ctx = document.getElementById('licenseChart');
if (ctx) {
const licenseChart = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['Aktiv', 'Abgelaufen'],
datasets: [{
data: [{{ stats.active_licenses }}, {{ stats.expired_licenses }}],
backgroundColor: ['#28a745', '#dc3545'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.parsed || 0;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(1);
return label + ': ' + value + ' (' + percentage + '%)';
}
}
}
},
cutout: '70%'
}
});
}
</script>
{% endblock %} {% endblock %}

Datei anzeigen

@@ -74,11 +74,14 @@
</div> </div>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body p-0">
<div class="table-responsive"> <div class="table-container">
<table class="table table-hover"> <table class="table table-hover table-sticky mb-0">
<thead> <thead>
<tr> <tr>
<th class="checkbox-cell">
<input type="checkbox" class="form-check-input form-check-input-custom" id="selectAll">
</th>
<th>ID</th> <th>ID</th>
<th>Lizenzschlüssel</th> <th>Lizenzschlüssel</th>
<th>Kunde</th> <th>Kunde</th>
@@ -94,8 +97,18 @@
<tbody> <tbody>
{% for license in licenses %} {% for license in licenses %}
<tr> <tr>
<td class="checkbox-cell">
<input type="checkbox" class="form-check-input form-check-input-custom license-checkbox" value="{{ license[0] }}">
</td>
<td>{{ license[0] }}</td> <td>{{ license[0] }}</td>
<td><code>{{ license[1] }}</code></td> <td>
<div class="d-flex align-items-center">
<code class="me-2">{{ license[1] }}</code>
<button class="btn btn-sm btn-outline-secondary btn-copy" onclick="copyToClipboard('{{ license[1] }}', this)" title="Kopieren">
📋
</button>
</div>
</td>
<td>{{ license[2] }}</td> <td>{{ license[2] }}</td>
<td>{{ license[3] or '-' }}</td> <td>{{ license[3] or '-' }}</td>
<td> <td>
@@ -117,11 +130,12 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if license[7] %} <div class="form-check form-switch form-switch-custom">
<span class="text-success"></span> <input class="form-check-input" type="checkbox"
{% else %} id="active_{{ license[0] }}"
<span class="text-danger"></span> {{ 'checked' if license[7] else '' }}
{% endif %} onchange="toggleLicenseStatus({{ license[0] }}, this.checked)">
</div>
</td> </td>
<td> <td>
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
@@ -190,4 +204,129 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Bulk Actions Bar -->
<div class="bulk-actions" id="bulkActionsBar">
<div>
<span id="selectedCount">0</span> Lizenzen ausgewählt
</div>
<div>
<button class="btn btn-success btn-sm me-2" onclick="bulkActivate()">✅ Aktivieren</button>
<button class="btn btn-warning btn-sm me-2" onclick="bulkDeactivate()">⏸️ Deaktivieren</button>
<button class="btn btn-danger btn-sm" onclick="bulkDelete()">🗑️ Löschen</button>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
// Copy to Clipboard
function copyToClipboard(text, button) {
navigator.clipboard.writeText(text).then(function() {
button.classList.add('copied');
button.innerHTML = '✅';
setTimeout(function() {
button.classList.remove('copied');
button.innerHTML = '📋';
}, 2000);
});
}
// Toggle License Status
function toggleLicenseStatus(licenseId, isActive) {
fetch(`/api/license/${licenseId}/toggle`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ is_active: isActive })
})
.then(response => response.json())
.then(data => {
if (data.success) {
// Optional: Show success message
} else {
// Revert toggle on error
document.getElementById(`active_${licenseId}`).checked = !isActive;
alert('Fehler beim Ändern des Status');
}
});
}
// Bulk Selection
const selectAll = document.getElementById('selectAll');
const checkboxes = document.querySelectorAll('.license-checkbox');
const bulkActionsBar = document.getElementById('bulkActionsBar');
const selectedCount = document.getElementById('selectedCount');
selectAll.addEventListener('change', function() {
checkboxes.forEach(cb => cb.checked = this.checked);
updateBulkActions();
});
checkboxes.forEach(cb => {
cb.addEventListener('change', updateBulkActions);
});
function updateBulkActions() {
const checkedBoxes = document.querySelectorAll('.license-checkbox:checked');
const count = checkedBoxes.length;
if (count > 0) {
bulkActionsBar.classList.add('show');
selectedCount.textContent = count;
} else {
bulkActionsBar.classList.remove('show');
}
// Update select all checkbox
selectAll.checked = count === checkboxes.length && count > 0;
selectAll.indeterminate = count > 0 && count < checkboxes.length;
}
// Bulk Actions
function getSelectedIds() {
return Array.from(document.querySelectorAll('.license-checkbox:checked'))
.map(cb => cb.value);
}
function bulkActivate() {
const ids = getSelectedIds();
if (confirm(`${ids.length} Lizenzen aktivieren?`)) {
performBulkAction('/api/licenses/bulk-activate', ids);
}
}
function bulkDeactivate() {
const ids = getSelectedIds();
if (confirm(`${ids.length} Lizenzen deaktivieren?`)) {
performBulkAction('/api/licenses/bulk-deactivate', ids);
}
}
function bulkDelete() {
const ids = getSelectedIds();
if (confirm(`${ids.length} Lizenzen wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden!`)) {
performBulkAction('/api/licenses/bulk-delete', ids);
}
}
function performBulkAction(url, ids) {
fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ ids: ids })
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Fehler bei der Bulk-Aktion: ' + data.message);
}
});
}
</script>
{% endblock %} {% endblock %}