- Add /admin/status route with real-time system metrics (CPU, RAM, disk) - Add /api/admin/status API endpoint for auto-refresh - Add technology stack section showing all platform technologies - Add auto-refresh (5 min) to GeoIP stats in security dashboard - Add "Status systemu" link to admin navigation menu - Fix /health/full endpoint list (remove non-existent endpoints) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
791 lines
27 KiB
HTML
791 lines
27 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Panel bezpieczeństwa - Admin{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.security-header {
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.security-header h1 {
|
|
font-size: var(--font-size-2xl);
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.security-header p {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-card.alert {
|
|
border-color: #fcd34d;
|
|
background: #fffbeb;
|
|
}
|
|
|
|
.stat-card.danger {
|
|
border-color: #fca5a5;
|
|
background: #fef2f2;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: var(--font-size-3xl);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.section {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-xl);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.section-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-lg);
|
|
padding-bottom: var(--spacing-md);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.section-header h2 {
|
|
font-size: var(--font-size-lg);
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
}
|
|
|
|
.data-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.data-table th {
|
|
text-align: left;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
background: var(--background);
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.data-table td {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-bottom: 1px solid var(--border);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.data-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.badge-new {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.badge-acknowledged {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.badge-resolved {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.badge-low {
|
|
background: #e0e7ff;
|
|
color: #3730a3;
|
|
}
|
|
|
|
.badge-medium {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.badge-high {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.badge-critical {
|
|
background: #7f1d1d;
|
|
color: white;
|
|
}
|
|
|
|
.btn-sm {
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
font-size: var(--font-size-xs);
|
|
border: none;
|
|
border-radius: var(--radius);
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: var(--background);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.btn-danger {
|
|
background: var(--error);
|
|
color: white;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-xl);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.ip-address {
|
|
font-family: monospace;
|
|
font-size: var(--font-size-xs);
|
|
background: var(--background);
|
|
padding: 2px 6px;
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.timestamp {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.alert-type {
|
|
text-transform: uppercase;
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.locked-row {
|
|
background: #fef2f2;
|
|
}
|
|
|
|
.tabs {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-lg);
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.tab {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: none;
|
|
background: none;
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
border-radius: var(--radius);
|
|
}
|
|
|
|
.tab.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
/* Security mechanisms styles */
|
|
.mechanisms-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.mechanism-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-lg);
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
border-left: 4px solid transparent;
|
|
}
|
|
|
|
.mechanism-item.stars-5 { border-left-color: #22c55e; }
|
|
.mechanism-item.stars-4 { border-left-color: #3b82f6; }
|
|
.mechanism-item.stars-3 { border-left-color: #f59e0b; }
|
|
|
|
.mechanism-stars {
|
|
font-size: 1rem;
|
|
color: #fbbf24;
|
|
min-width: 100px;
|
|
letter-spacing: 2px;
|
|
}
|
|
|
|
.mechanism-name {
|
|
flex: 1;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.mechanism-desc {
|
|
flex: 2;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.mechanism-status {
|
|
font-size: var(--font-size-xs);
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.mechanism-status.active {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.mechanism-status.inactive {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
/* GeoIP stats */
|
|
.geoip-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.geoip-stat {
|
|
background: var(--background);
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius);
|
|
text-align: center;
|
|
}
|
|
|
|
.geoip-stat-value {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.geoip-stat-label {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.country-breakdown {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-sm);
|
|
margin-top: var(--spacing-lg);
|
|
}
|
|
|
|
.country-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
background: var(--background);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.country-flag {
|
|
font-size: 1.2em;
|
|
}
|
|
|
|
.country-count {
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="security-header">
|
|
<h1>🛡️ Panel bezpieczeństwa</h1>
|
|
<p>Monitoring alertów, audit log i zarządzanie bezpieczeństwem</p>
|
|
</div>
|
|
|
|
<!-- Stats -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card {% if stats.new_alerts > 0 %}alert{% endif %}">
|
|
<div class="stat-value">{{ stats.new_alerts }}</div>
|
|
<div class="stat-label">Nowe alerty</div>
|
|
</div>
|
|
<div class="stat-card {% if stats.locked_accounts > 0 %}danger{% endif %}">
|
|
<div class="stat-value">{{ stats.locked_accounts }}</div>
|
|
<div class="stat-label">Zablokowane konta</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ stats.users_with_2fa }}/{{ stats.total_admins }}</div>
|
|
<div class="stat-label">Admini z 2FA</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value">{{ stats.alert_breakdown.values()|sum if stats.alert_breakdown else 0 }}</div>
|
|
<div class="stat-label">Wszystkie alerty</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="tabs">
|
|
<button class="tab active" onclick="showTab('mechanisms')">Mechanizmy</button>
|
|
<button class="tab" onclick="showTab('geoip')">GeoIP</button>
|
|
<button class="tab" onclick="showTab('alerts')">Alerty</button>
|
|
<button class="tab" onclick="showTab('audit')">Audit log</button>
|
|
<button class="tab" onclick="showTab('locked')">Zablokowane konta</button>
|
|
</div>
|
|
|
|
<!-- Mechanisms Tab -->
|
|
<div id="tab-mechanisms" class="tab-content active">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<h2>🔐 Mechanizmy bezpieczeństwa</h2>
|
|
</div>
|
|
<p style="color: var(--text-secondary); margin-bottom: var(--spacing-lg);">
|
|
Lista wszystkich aktywnych mechanizmów ochrony systemu, posortowana według ważności (5★ = krytyczne).
|
|
</p>
|
|
|
|
<div class="mechanisms-list">
|
|
<!-- 5 stars - CRITICAL -->
|
|
<div class="mechanism-item stars-5">
|
|
<span class="mechanism-stars">★★★★★</span>
|
|
<span class="mechanism-name">Uwierzytelnianie dwuskładnikowe (2FA)</span>
|
|
<span class="mechanism-desc">TOTP przez aplikacje mobilne (Google/Microsoft Authenticator)</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-5">
|
|
<span class="mechanism-stars">★★★★★</span>
|
|
<span class="mechanism-name">Ochrona przed CSRF</span>
|
|
<span class="mechanism-desc">Tokeny CSRF w formularzach (Flask-WTF)</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-5">
|
|
<span class="mechanism-stars">★★★★★</span>
|
|
<span class="mechanism-name">Szyfrowanie połączeń (HTTPS/TLS)</span>
|
|
<span class="mechanism-desc">Let's Encrypt SSL z automatycznym odnowieniem</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-5">
|
|
<span class="mechanism-stars">★★★★★</span>
|
|
<span class="mechanism-name">Hashowanie haseł</span>
|
|
<span class="mechanism-desc">Bezpieczne przechowywanie z Werkzeug (bcrypt)</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-5">
|
|
<span class="mechanism-stars">★★★★★</span>
|
|
<span class="mechanism-name">Ochrona przed SQL Injection</span>
|
|
<span class="mechanism-desc">Parametryzowane zapytania przez SQLAlchemy ORM</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-5">
|
|
<span class="mechanism-stars">★★★★★</span>
|
|
<span class="mechanism-name">Ochrona przed XSS</span>
|
|
<span class="mechanism-desc">Automatyczne escapowanie w szablonach Jinja2</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
|
|
<!-- 4 stars - IMPORTANT -->
|
|
<div class="mechanism-item stars-4">
|
|
<span class="mechanism-stars">★★★★☆</span>
|
|
<span class="mechanism-name">Blokowanie geograficzne (GeoIP)</span>
|
|
<span class="mechanism-desc">Blokada krajów wysokiego ryzyka: RU, CN, KP, IR, BY, SY, VE, CU</span>
|
|
<span class="mechanism-status {{ 'active' if geoip_enabled else 'inactive' }}">{{ 'Aktywne' if geoip_enabled else 'Nieaktywne' }}</span>
|
|
</div>
|
|
<div class="mechanism-item stars-4">
|
|
<span class="mechanism-stars">★★★★☆</span>
|
|
<span class="mechanism-name">Rate Limiting</span>
|
|
<span class="mechanism-desc">Ograniczenie żądań przez Flask-Limiter z Redis</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-4">
|
|
<span class="mechanism-stars">★★★★☆</span>
|
|
<span class="mechanism-name">Blokada konta</span>
|
|
<span class="mechanism-desc">Automatyczna blokada po 5 nieudanych logowaniach</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-4">
|
|
<span class="mechanism-stars">★★★★☆</span>
|
|
<span class="mechanism-name">Audit Log</span>
|
|
<span class="mechanism-desc">Śledzenie wszystkich działań administracyjnych</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-4">
|
|
<span class="mechanism-stars">★★★★☆</span>
|
|
<span class="mechanism-name">Bezpieczne sesje</span>
|
|
<span class="mechanism-desc">Zarządzanie sesjami przez Flask-Login</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
|
|
<!-- 3 stars - ADDITIONAL -->
|
|
<div class="mechanism-item stars-3">
|
|
<span class="mechanism-stars">★★★☆☆</span>
|
|
<span class="mechanism-name">Honeypot endpoints</span>
|
|
<span class="mechanism-desc">Wykrywanie skanerów i botów (/.env, /wp-admin, /phpmyadmin)</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-3">
|
|
<span class="mechanism-stars">★★★☆☆</span>
|
|
<span class="mechanism-name">Security Alerting</span>
|
|
<span class="mechanism-desc">Powiadomienia email o krytycznych zdarzeniach</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
<div class="mechanism-item stars-3">
|
|
<span class="mechanism-stars">★★★☆☆</span>
|
|
<span class="mechanism-name">Security Logging</span>
|
|
<span class="mechanism-desc">Centralne logowanie zdarzeń bezpieczeństwa</span>
|
|
<span class="mechanism-status active">Aktywne</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- GeoIP Tab -->
|
|
<div id="tab-geoip" class="tab-content">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<h2>🌍 Statystyki GeoIP Blocking</h2>
|
|
<div style="text-align: right; font-size: var(--font-size-xs); color: var(--text-secondary);">
|
|
<div>Ostatnia aktualizacja: <span id="geoip-timestamp" style="font-family: monospace; font-weight: 500;">{{ generated_at.strftime('%H:%M:%S') }}</span></div>
|
|
<div style="margin-top: 4px;">
|
|
<span style="display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: #22c55e; margin-right: 4px; animation: pulse 2s infinite;"></span>
|
|
Auto-refresh: <span id="geoip-countdown">5:00</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if geoip_enabled %}
|
|
<div class="geoip-stats">
|
|
<div class="geoip-stat">
|
|
<div class="geoip-stat-value" id="geoip-today">{{ geoip_stats.today }}</div>
|
|
<div class="geoip-stat-label">Zablokowanych dziś</div>
|
|
</div>
|
|
<div class="geoip-stat">
|
|
<div class="geoip-stat-value" id="geoip-month">{{ geoip_stats.this_month }}</div>
|
|
<div class="geoip-stat-label">W tym miesiącu</div>
|
|
</div>
|
|
<div class="geoip-stat">
|
|
<div class="geoip-stat-value" id="geoip-year">{{ geoip_stats.this_year }}</div>
|
|
<div class="geoip-stat-label">W tym roku</div>
|
|
</div>
|
|
<div class="geoip-stat">
|
|
<div class="geoip-stat-value" id="geoip-total">{{ geoip_stats.total }}</div>
|
|
<div class="geoip-stat-label">Od początku</div>
|
|
</div>
|
|
</div>
|
|
|
|
<h3 style="margin-bottom: var(--spacing-md);">Zablokowane kraje</h3>
|
|
<p style="color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--spacing-md);">
|
|
Blokowane: 🇷🇺 Rosja, 🇨🇳 Chiny, 🇰🇵 Korea Północna, 🇮🇷 Iran, 🇧🇾 Białoruś, 🇸🇾 Syria, 🇻🇪 Wenezuela, 🇨🇺 Kuba
|
|
</p>
|
|
|
|
<div class="country-breakdown" id="country-breakdown">
|
|
{% if geoip_stats.by_country %}
|
|
{% for country in geoip_stats.by_country %}
|
|
<div class="country-badge">
|
|
<span class="country-flag">{{ country.flag }}</span>
|
|
<span>{{ country.name }}</span>
|
|
<span class="country-count">{{ country.count }}</span>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak zablokowanych połączeń</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state" style="background: #fef3c7; border-radius: var(--radius-lg); padding: var(--spacing-xl);">
|
|
<p style="color: #92400e;">⚠️ GeoIP Blocking jest wyłączone</p>
|
|
<p style="color: #78716c; font-size: var(--font-size-sm);">Ustaw GEOIP_ENABLED=true w .env aby włączyć</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alerts Tab -->
|
|
<div id="tab-alerts" class="tab-content">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<h2>🚨 Alerty bezpieczeństwa</h2>
|
|
</div>
|
|
|
|
{% if alerts %}
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Typ</th>
|
|
<th>Ważność</th>
|
|
<th>IP</th>
|
|
<th>Użytkownik</th>
|
|
<th>Status</th>
|
|
<th>Data</th>
|
|
<th>Akcje</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for alert in alerts %}
|
|
<tr>
|
|
<td><span class="alert-type">{{ alert.alert_type }}</span></td>
|
|
<td><span class="badge badge-{{ alert.severity }}">{{ alert.severity }}</span></td>
|
|
<td><span class="ip-address">{{ alert.ip_address or '-' }}</span></td>
|
|
<td>{{ alert.user_email or '-' }}</td>
|
|
<td><span class="badge badge-{{ alert.status }}">{{ alert.status }}</span></td>
|
|
<td><span class="timestamp">{{ alert.created_at.strftime('%Y-%m-%d %H:%M') }}</span></td>
|
|
<td>
|
|
{% if alert.status == 'new' %}
|
|
<div class="action-buttons">
|
|
<form method="POST" action="{{ url_for('acknowledge_security_alert', alert_id=alert.id) }}" style="display:inline;">
|
|
{{ csrf_token() }}
|
|
<button type="submit" class="btn-sm btn-secondary">Potwierdź</button>
|
|
</form>
|
|
<form method="POST" action="{{ url_for('resolve_security_alert', alert_id=alert.id) }}" style="display:inline;">
|
|
{{ csrf_token() }}
|
|
<button type="submit" class="btn-sm btn-primary">Rozwiąż</button>
|
|
</form>
|
|
</div>
|
|
{% elif alert.status == 'acknowledged' %}
|
|
<form method="POST" action="{{ url_for('resolve_security_alert', alert_id=alert.id) }}" style="display:inline;">
|
|
{{ csrf_token() }}
|
|
<button type="submit" class="btn-sm btn-primary">Rozwiąż</button>
|
|
</form>
|
|
{% else %}
|
|
<span class="timestamp">Rozwiązany</span>
|
|
{% endif %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>✅ Brak alertów bezpieczeństwa</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Log Tab -->
|
|
<div id="tab-audit" class="tab-content">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<h2>📋 Audit log</h2>
|
|
</div>
|
|
|
|
{% if audit_logs %}
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Użytkownik</th>
|
|
<th>Akcja</th>
|
|
<th>Encja</th>
|
|
<th>IP</th>
|
|
<th>Data</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for log in audit_logs %}
|
|
<tr>
|
|
<td>{{ log.user_email }}</td>
|
|
<td><code>{{ log.action }}</code></td>
|
|
<td>
|
|
{{ log.entity_type }}
|
|
{% if log.entity_id %}:{{ log.entity_id }}{% endif %}
|
|
{% if log.entity_name %}<br><small>{{ log.entity_name }}</small>{% endif %}
|
|
</td>
|
|
<td><span class="ip-address">{{ log.ip_address or '-' }}</span></td>
|
|
<td><span class="timestamp">{{ log.created_at.strftime('%Y-%m-%d %H:%M') }}</span></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak wpisów w audit log</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Locked Accounts Tab -->
|
|
<div id="tab-locked" class="tab-content">
|
|
<div class="section">
|
|
<div class="section-header">
|
|
<h2>🔒 Zablokowane konta</h2>
|
|
</div>
|
|
|
|
{% if locked_accounts %}
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Email</th>
|
|
<th>Nieudane próby</th>
|
|
<th>Zablokowane do</th>
|
|
<th>Akcje</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in locked_accounts %}
|
|
<tr class="locked-row">
|
|
<td>{{ user.email }}</td>
|
|
<td>{{ user.failed_login_attempts }}</td>
|
|
<td><span class="timestamp">{{ user.locked_until.strftime('%Y-%m-%d %H:%M') }}</span></td>
|
|
<td>
|
|
<form method="POST" action="{{ url_for('unlock_account', user_id=user.id) }}" style="display:inline;">
|
|
{{ csrf_token() }}
|
|
<button type="submit" class="btn-sm btn-danger" onclick="return confirm('Odblokować konto {{ user.email }}?')">
|
|
Odblokuj
|
|
</button>
|
|
</form>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>✅ Brak zablokowanych kont</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
function showTab(tabName) {
|
|
// Hide all tabs
|
|
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
|
|
document.querySelectorAll('.tab').forEach(el => el.classList.remove('active'));
|
|
|
|
// Show selected tab
|
|
document.getElementById('tab-' + tabName).classList.add('active');
|
|
event.target.classList.add('active');
|
|
}
|
|
|
|
// GeoIP auto-refresh every 5 minutes
|
|
let geoipCountdown = 300;
|
|
|
|
function updateGeoipCountdown() {
|
|
const minutes = Math.floor(geoipCountdown / 60);
|
|
const seconds = geoipCountdown % 60;
|
|
const countdownEl = document.getElementById('geoip-countdown');
|
|
if (countdownEl) {
|
|
countdownEl.textContent = `${minutes}:${seconds.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
if (geoipCountdown <= 0) {
|
|
refreshGeoipStats();
|
|
geoipCountdown = 300;
|
|
} else {
|
|
geoipCountdown--;
|
|
}
|
|
}
|
|
|
|
async function refreshGeoipStats() {
|
|
try {
|
|
const response = await fetch('/api/admin/security/geoip-stats');
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
// Update timestamp
|
|
const timestamp = new Date(data.timestamp);
|
|
const timestampEl = document.getElementById('geoip-timestamp');
|
|
if (timestampEl) {
|
|
timestampEl.textContent = timestamp.toLocaleTimeString('pl-PL');
|
|
}
|
|
|
|
if (data.enabled) {
|
|
// Update stats
|
|
const todayEl = document.getElementById('geoip-today');
|
|
const monthEl = document.getElementById('geoip-month');
|
|
const yearEl = document.getElementById('geoip-year');
|
|
const totalEl = document.getElementById('geoip-total');
|
|
|
|
if (todayEl) todayEl.textContent = data.today;
|
|
if (monthEl) monthEl.textContent = data.this_month;
|
|
if (yearEl) yearEl.textContent = data.this_year;
|
|
if (totalEl) totalEl.textContent = data.total;
|
|
|
|
// Update country breakdown
|
|
const breakdownEl = document.getElementById('country-breakdown');
|
|
if (breakdownEl && data.by_country) {
|
|
if (data.by_country.length > 0) {
|
|
breakdownEl.innerHTML = data.by_country.map(c =>
|
|
`<div class="country-badge">
|
|
<span class="country-flag">${c.flag}</span>
|
|
<span>${c.name}</span>
|
|
<span class="country-count">${c.count}</span>
|
|
</div>`
|
|
).join('');
|
|
} else {
|
|
breakdownEl.innerHTML = '<div class="empty-state"><p>Brak zablokowanych połączeń</p></div>';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to refresh GeoIP stats:', error);
|
|
}
|
|
}
|
|
|
|
// Start countdown
|
|
setInterval(updateGeoipCountdown, 1000);
|
|
|
|
// Refresh when page becomes visible
|
|
document.addEventListener('visibilitychange', function() {
|
|
if (!document.hidden) {
|
|
refreshGeoipStats();
|
|
geoipCountdown = 300;
|
|
}
|
|
});
|
|
{% endblock %}
|