Первый коммит: загрузка проекта SecretText

This commit is contained in:
2026-09-05 15:35:25 +03:00
commit faa6bbc574
36 changed files with 5333 additions and 0 deletions
+365
View File
@@ -0,0 +1,365 @@
{% extends "base.html" %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">Панель администратора</h2>
<div>
<span class="text-muted me-2">{{ session.username }}</span>
<a href="{{ url_for('admin_logout') }}" class="btn btn-sm btn-outline-secondary">Выйти</a>
</div>
</div>
<div class="row mb-4">
<div class="col-xl-3 col-md-6 mb-3">
<div class="card">
<div class="card-body">
<div class="text-uppercase text-muted small">Всего секретов</div>
<div class="h5 mb-0">{{ stats.total }}</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card">
<div class="card-body">
<div class="text-uppercase text-muted small">Просмотрено</div>
<div class="h5 mb-0">{{ stats.viewed }} ({{ stats.viewed_percent }}%)</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card">
<div class="card-body">
<div class="text-uppercase text-muted small">Ожидают просмотра</div>
<div class="h5 mb-0">{{ stats.not_viewed }}</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6 mb-3">
<div class="card">
<div class="card-body">
<div class="text-uppercase text-muted small">Заблокированные IP</div>
<div class="h5 mb-0">{{ stats.blocked_ips }}</div>
</div>
</div>
</div>
</div>
<ul class="nav nav-tabs mb-4" id="adminTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="secrets-tab" data-bs-toggle="tab" data-bs-target="#secrets"
type="button" role="tab" aria-controls="secrets" aria-selected="true">
Секреты <span class="badge bg-secondary">{{ logs|length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="users-tab" data-bs-toggle="tab" data-bs-target="#users" type="button"
role="tab" aria-controls="users" aria-selected="false">
Пользователи <span class="badge bg-secondary">{{ users|length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="security-tab" data-bs-toggle="tab" data-bs-target="#security" type="button"
role="tab" aria-controls="security" aria-selected="false">
Безопасность
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="blocked-tab" data-bs-toggle="tab" data-bs-target="#blocked" type="button"
role="tab" aria-controls="blocked" aria-selected="false">
Заблокированные <span class="badge bg-secondary">{{ blocked_ips|length }}</span>
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="secrets" role="tabpanel" aria-labelledby="secrets-tab">
<div class="card">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<span>Фильтр:</span>
<div>
<a href="{{ url_for('admin_panel') }}?filter=all"
class="btn btn-sm btn-outline-secondary {% if filter == 'all' or not filter %}active{% endif %}">Все</a>
<a href="{{ url_for('admin_panel') }}?filter=pending"
class="btn btn-sm btn-outline-secondary {% if filter == 'pending' %}active{% endif %}">Ожидают</a>
<a href="{{ url_for('admin_panel') }}?filter=viewed"
class="btn btn-sm btn-outline-secondary {% if filter == 'viewed' %}active{% endif %}">Просмотрены</a>
</div>
</div>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Создан</th>
<th>IP создания</th>
<th>Пользователь</th>
<th>Просмотрен</th>
<th>IP просмотра</th>
<th>Статус</th>
</tr>
</thead>
<tbody>
{% for log in logs %}
<tr>
<td><code class="bg-light p-1 rounded">{{ log.secret_id[:12] }}...</code></td>
<td>{{ log.created_at|format_datetime if log.created_at else '-' }}</td>
<td><code>{{ log.created_ip or '-' }}</code></td>
<td>
{% if log.user %}
<span class="badge bg-secondary">{{ log.user.username }}</span>
{% else %}
<span class="badge bg-secondary">Аноним</span>
{% endif %}
</td>
<td>{{ log.viewed_at|format_datetime if log.viewed_at else '-' }}</td>
<td><code>{{ log.viewed_ip or '-' }}</code></td>
<td>
{% if log.is_viewed %}
<span class="badge bg-secondary">Просмотрен</span>
{% else %}
<span class="badge bg-secondary">Ожидает</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center py-4 text-muted">Нет записей</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% if logs|length >= per_page %}
<div class="p-3">
<nav>
<ul class="pagination justify-content-center mb-0">
<li class="page-item {% if page <= 1 %}disabled{% endif %}">
<a class="page-link"
href="{{ url_for('admin_panel', page=page-1, filter=filter) if page > 1 else '#' }}">Назад</a>
</li>
<li class="page-item active">
<span class="page-link">{{ page }}</span>
</li>
<li class="page-item">
<a class="page-link"
href="{{ url_for('admin_panel', page=page+1, filter=filter) }}">Вперед</a>
</li>
</ul>
</nav>
</div>
{% endif %}
</div>
</div>
</div>
<div class="tab-pane fade" id="users" role="tabpanel" aria-labelledby="users-tab">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Управление пользователями</span>
<a href="{{ url_for('admin_create_user') }}" class="btn btn-sm btn-secondary">Создать
пользователя</a>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>ID</th>
<th>Имя пользователя</th>
<th>Роль</th>
<th>Создан</th>
<th>Последний вход</th>
<th>Статус</th>
<th>Действия</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>{{ user.id }}</td>
<td><strong>{{ user.username }}</strong></td>
<td>
<form action="{{ url_for('admin_change_role', user_id=user.id) }}" method="POST"
class="d-flex">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<select name="role" class="form-select form-select-sm me-1"
style="width: auto;">
<option value="user" {% if user.role==
'user' %}selected{% endif %}>user</option>
<option value="admin" {% if user.role==
'admin' %}selected{% endif %}>admin</option>
</select>
<button type="submit" class="btn btn-sm btn-outline-secondary">Сохранить
</button>
</form>
</td>
<td>{{ user.created_at|format_datetime if user.created_at else '-' }}</td>
<td>{{ user.last_login|format_datetime if user.last_login else 'Никогда' }}</td>
<td>
{% if user.is_active %}
<span class="badge bg-secondary">Активен</span>
{% else %}
<span class="badge bg-secondary">Деактивирован</span>
{% endif %}
</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('admin_toggle_user', user_id=user.id) }}"
class="btn btn-outline-secondary">
{% if user.is_active %}Деактивировать{% else %}Активировать{% endif %}
</a>
<a href="{{ url_for('admin_delete_user', user_id=user.id) }}"
class="btn btn-outline-secondary"
onclick="return confirm('Удалить пользователя?')">Удалить</a>
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="7" class="text-center py-4 text-muted">Нет пользователей</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<div class="card mt-4">
<div class="card-header">Сменить пароль</div>
<div class="card-body">
<form action="{{ url_for('admin_change_password') }}" method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<div class="row">
<div class="col-md-4">
<div class="mb-3">
<label for="old_password" class="form-label small">Текущий пароль</label>
<input type="password" name="old_password" id="old_password"
class="form-control form-control-sm" required>
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="new_password" class="form-label small">Новый пароль</label>
<input type="password" name="new_password" id="new_password"
class="form-control form-control-sm" required minlength="6">
</div>
</div>
<div class="col-md-4">
<div class="mb-3">
<label for="confirm_password" class="form-label small">Подтвердите пароль</label>
<input type="password" name="confirm_password" id="confirm_password"
class="form-control form-control-sm" required>
</div>
</div>
</div>
<button type="submit" class="btn btn-sm btn-secondary">Сменить пароль</button>
</form>
</div>
</div>
</div>
<div class="tab-pane fade" id="security" role="tabpanel" aria-labelledby="security-tab">
<div class="card">
<div class="card-header">Журнал событий безопасности</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Время</th>
<th>IP</th>
<th>Тип события</th>
<th>Детали</th>
<th>Пользователь</th>
</tr>
</thead>
<tbody>
{% for log in security_logs %}
<tr>
<td>{{ log.timestamp|format_datetime_seconds if log.timestamp else '-' }}</td>
<td><code>{{ log.ip }}</code></td>
<td><span class="badge bg-secondary">{{ log.event_type }}</span></td>
<td>{{ log.details or '-' }}</td>
<td>
{% if log.user %}
<span class="badge bg-secondary">{{ log.user.username }}</span>
{% else %}
<span class="badge bg-secondary">Аноним</span>
{% endif %}
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-4 text-muted">Нет событий безопасности</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="blocked" role="tabpanel" aria-labelledby="blocked-tab">
<div class="card">
<div class="card-header">Заблокированные IP-адреса</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>IP адрес</th>
<th>Тип блокировки</th>
<th>Осталось времени</th>
<th>Количество блокировок</th>
<th>Действие</th>
</tr>
</thead>
<tbody>
{% for ip in blocked_ips %}
<tr>
<td><code>{{ ip.ip }}</code></td>
<td>
{% if ip.permanent %}
<span class="badge bg-secondary">Постоянная</span>
{% else %}
<span class="badge bg-secondary">Временная</span>
{% endif %}
</td>
<td>
{% if ip.permanent %}
{% else %}
{{ ip.remaining_minutes }} мин.
{% endif %}
</td>
<td>{{ ip.block_count }}</td>
<td>
<div class="btn-group btn-group-sm">
<a href="{{ url_for('admin_unblock', ip=ip.ip) }}"
class="btn btn-outline-secondary">Разблокировать</a>
{% if not ip.permanent %}
<a href="{{ url_for('admin_block', ip=ip.ip) }}"
class="btn btn-outline-secondary"
onclick="return confirm('Заблокировать IP постоянно?')">Заблокировать</a>
{% endif %}
</div>
</td>
</tr>
{% else %}
<tr>
<td colspan="5" class="text-center py-4 text-muted">Нет заблокированных IP-адресов</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock %}