Release v2.1: GDPR, passkeys, session management, admin redesign

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-07 02:43:57 +03:00
parent d4f0eaa7d9
commit 0a51001791
32 changed files with 1529 additions and 193 deletions
+21 -7
View File
@@ -1,9 +1,23 @@
<nav class="admin-nav">
<a href="{{ url_for('admin.dashboard') }}" class="admin-nav__link {% if request.endpoint == 'admin.dashboard' %}admin-nav__link--active{% endif %}">Обзор</a>
<a href="{{ url_for('admin.users') }}" class="admin-nav__link {% if request.endpoint == 'admin.users' %}admin-nav__link--active{% endif %}">Пользователи</a>
<a href="{{ url_for('admin.groups') }}" class="admin-nav__link {% if request.endpoint in ['admin.groups', 'admin.edit_group', 'admin.delete_group'] %}admin-nav__link--active{% endif %}">Группы</a>
<a href="{{ url_for('admin.banners') }}" class="admin-nav__link {% if request.endpoint in ['admin.banners', 'admin.edit_banner', 'admin.delete_banner', 'admin.toggle_banner'] %}admin-nav__link--active{% endif %}">Баннеры</a>
<a href="{{ url_for('admin.photos') }}" class="admin-nav__link {% if request.endpoint == 'admin.photos' %}admin-nav__link--active{% endif %}">Фото</a>
<a href="{{ url_for('admin.deploy') }}" class="admin-nav__link {% if request.endpoint == 'admin.deploy' %}admin-nav__link--active{% endif %}">Версии Git</a>
<a href="{{ url_for('admin.settings') }}" class="admin-nav__link {% if request.endpoint == 'admin.settings' %}admin-nav__link--active{% endif %}">Настройки</a>
<a href="{{ url_for('admin.dashboard') }}" class="admin-nav__link {% if request.endpoint == 'admin.dashboard' %}admin-nav__link--active{% endif %}">
<span class="admin-nav__icon">📊</span> Обзор
</a>
<a href="{{ url_for('admin.users') }}" class="admin-nav__link {% if request.endpoint == 'admin.users' %}admin-nav__link--active{% endif %}">
<span class="admin-nav__icon">👥</span> Пользователи
</a>
<a href="{{ url_for('admin.groups') }}" class="admin-nav__link {% if request.endpoint in ['admin.groups', 'admin.edit_group', 'admin.delete_group'] %}admin-nav__link--active{% endif %}">
<span class="admin-nav__icon">🏷️</span> Группы
</a>
<a href="{{ url_for('admin.banners') }}" class="admin-nav__link {% if request.endpoint in ['admin.banners', 'admin.edit_banner', 'admin.delete_banner', 'admin.toggle_banner'] %}admin-nav__link--active{% endif %}">
<span class="admin-nav__icon">📢</span> Баннеры
</a>
<a href="{{ url_for('admin.photos') }}" class="admin-nav__link {% if request.endpoint == 'admin.photos' %}admin-nav__link--active{% endif %}">
<span class="admin-nav__icon">🖼️</span> Фото
</a>
<a href="{{ url_for('admin.deploy') }}" class="admin-nav__link {% if request.endpoint == 'admin.deploy' %}admin-nav__link--active{% endif %}">
<span class="admin-nav__icon">🚀</span> Версии Git
</a>
<a href="{{ url_for('admin.settings') }}" class="admin-nav__link {% if request.endpoint == 'admin.settings' %}admin-nav__link--active{% endif %}">
<span class="admin-nav__icon">🔧</span> Настройки
</a>
</nav>
+4 -16
View File
@@ -1,20 +1,10 @@
{% extends "base.html" %}
{% extends "admin/layout.html" %}
{% block title %}Рекламные баннеры — Админка{% endblock %}
{% block admin_title %}Рекламные баннеры{% endblock %}
{% block admin_subtitle %}<p class="admin-main__subtitle">Баннеры на главной, в кабинете и в подвале</p>{% endblock %}
{% block content %}
<section class="page-header page-header--admin">
<div class="container">
<h1 class="page-header__title">Рекламные баннеры</h1>
<p class="page-header__subtitle">Баннеры на главной, в кабинете и в подвале сайта</p>
</div>
</section>
<section class="admin-section">
<div class="container">
{% include "admin/_nav.html" %}
{% include "partials/alerts.html" %}
{% block admin_content %}
<div class="admin-panel folder-create">
<h2 class="admin-panel__title">Добавить баннер</h2>
<form method="post" class="auth-form folder-create__form">
@@ -121,6 +111,4 @@
</tbody>
</table>
</div>
</div>
</section>
{% endblock %}
+68 -80
View File
@@ -1,92 +1,80 @@
{% extends "base.html" %}
{% extends "admin/layout.html" %}
{% from "macros.html" import format_size %}
{% block title %}Админка — PhotoHost{% endblock %}
{% block admin_title %}Обзор{% endblock %}
{% block admin_subtitle %}<p class="admin-main__subtitle">Статистика и последние действия</p>{% endblock %}
{% block content %}
<section class="page-header page-header--admin">
<div class="container">
<h1 class="page-header__title">Панель администратора</h1>
<p class="page-header__subtitle">Управление пользователями и контентом</p>
{% block admin_content %}
<div class="admin-stats admin-stats--cards">
<div class="admin-stat-card">
<span class="admin-stat-card__value">{{ stats.users }}</span>
<span class="admin-stat-card__label">Пользователей</span>
</div>
</section>
<div class="admin-stat-card">
<span class="admin-stat-card__value">{{ stats.photos }}</span>
<span class="admin-stat-card__label">Фотографий</span>
</div>
<div class="admin-stat-card">
<span class="admin-stat-card__value">{{ stats.admins }}</span>
<span class="admin-stat-card__label">Администраторов</span>
</div>
<div class="admin-stat-card">
<span class="admin-stat-card__value">{{ stats.groups }}</span>
<span class="admin-stat-card__label">Групп</span>
</div>
<div class="admin-stat-card admin-stat-card--accent">
<span class="admin-stat-card__value">{{ format_size(stats.storage) }}</span>
<span class="admin-stat-card__label">Хранилище</span>
</div>
</div>
<section class="admin-section">
<div class="container">
{% include "admin/_nav.html" %}
{% include "partials/alerts.html" %}
{% if current_version %}
<p class="admin-version-bar">
Версия Git: <strong>{{ current_version }}</strong>
· <a href="{{ url_for('admin.deploy') }}">Управление версиями</a>
{% if not deploy_enabled %}<span class="badge badge--muted">deploy off</span>{% endif %}
</p>
{% endif %}
<div class="admin-stats">
<div class="stat-card stat-card--admin">
<span class="stat-card__value">{{ stats.users }}</span>
<span class="stat-card__label">пользователей</span>
</div>
<div class="stat-card stat-card--admin">
<span class="stat-card__value">{{ stats.photos }}</span>
<span class="stat-card__label">фотографий</span>
</div>
<div class="stat-card stat-card--admin">
<span class="stat-card__value">{{ stats.admins }}</span>
<span class="stat-card__label">администраторов</span>
</div>
<div class="stat-card stat-card--admin">
<span class="stat-card__value">{{ stats.groups }}</span>
<span class="stat-card__label">групп</span>
</div>
<div class="stat-card stat-card--admin">
<span class="stat-card__value">{{ format_size(stats.storage) }}</span>
<span class="stat-card__label">хранилище</span>
</div>
</div>
{% if current_version %}
<p class="folder-hint" style="margin-bottom: 24px;">
Версия Git: <strong>{{ current_version }}</strong>
· <a href="{{ url_for('admin.deploy') }}">Управление версиями</a>
{% if not deploy_enabled %}(deploy выключен){% endif %}
</p>
{% endif %}
<div class="admin-grid">
<div class="admin-panel">
<h2 class="admin-panel__title">Новые пользователи</h2>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>Логин</th>
<th>Email</th>
<th>Дата</th>
</tr>
</thead>
<tbody>
{% for user in recent_users %}
<tr>
<td>{{ user.username }}{% if user.is_admin %} <span class="badge badge--admin">admin</span>{% endif %}</td>
<td>{{ user.email }}</td>
<td>{{ user.created_at.strftime('%d.%m.%Y') }}</td>
</tr>
{% else %}
<tr><td colspan="3">Нет пользователей</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="admin-panel">
<h2 class="admin-panel__title">Последние фото</h2>
<div class="admin-mini-gallery">
{% for photo in recent_photos %}
<a href="{{ photo.url }}" target="_blank" class="admin-mini-gallery__item">
<img src="{{ photo.url }}" alt="{{ photo.original_name }}">
</a>
<div class="admin-grid">
<div class="admin-panel admin-panel--elevated">
<h2 class="admin-panel__title">Новые пользователи</h2>
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
<tr>
<th>Логин</th>
<th>Email</th>
<th>Дата</th>
</tr>
</thead>
<tbody>
{% for user in recent_users %}
<tr>
<td>{{ user.username }}{% if user.is_admin %} <span class="badge badge--admin">admin</span>{% endif %}</td>
<td>{{ user.email }}</td>
<td>{{ user.created_at.strftime('%d.%m.%Y') }}</td>
</tr>
{% else %}
<p class="admin-empty">Нет фотографий</p>
<tr><td colspan="3">Нет пользователей</td></tr>
{% endfor %}
</div>
</div>
</tbody>
</table>
</div>
</div>
</section>
<div class="admin-panel admin-panel--elevated">
<h2 class="admin-panel__title">Последние фото</h2>
<div class="admin-mini-gallery">
{% for photo in recent_photos %}
<a href="{{ photo.url }}" target="_blank" class="admin-mini-gallery__item">
<img src="{{ photo.url }}" alt="{{ photo.original_name }}">
</a>
{% else %}
<p class="admin-empty">Нет фотографий</p>
{% endfor %}
</div>
</div>
</div>
{% endblock %}
+4 -16
View File
@@ -1,20 +1,10 @@
{% extends "base.html" %}
{% extends "admin/layout.html" %}
{% block title %}Версии Git — Админка{% endblock %}
{% block admin_title %}Обновление Git{% endblock %}
{% block admin_subtitle %}<p class="admin-main__subtitle">Переключение релизов и пересборка Docker</p>{% endblock %}
{% block content %}
<section class="page-header page-header--admin">
<div class="container">
<h1 class="page-header__title">Обновление и версии Git</h1>
<p class="page-header__subtitle">Переключение между релизами и пересборка Docker</p>
</div>
</section>
<section class="admin-section">
<div class="container">
{% include "admin/_nav.html" %}
{% include "partials/alerts.html" %}
{% block admin_content %}
<div class="admin-stats">
<div class="stat-card stat-card--admin">
<span class="stat-card__value">{{ status.current or '—' }}</span>
@@ -87,6 +77,4 @@
</form>
</div>
</div>
</div>
</section>
{% endblock %}
+4 -16
View File
@@ -1,21 +1,11 @@
{% extends "base.html" %}
{% extends "admin/layout.html" %}
{% from "macros.html" import format_size %}
{% block title %}Группы — Админка{% endblock %}
{% block admin_title %}Группы пользователей{% endblock %}
{% block admin_subtitle %}<p class="admin-main__subtitle">Квоты диска, лимиты папок и фото</p>{% endblock %}
{% block content %}
<section class="page-header page-header--admin">
<div class="container">
<h1 class="page-header__title">Группы пользователей</h1>
<p class="page-header__subtitle">Квоты диска, лимиты папок и фото для каждой группы</p>
</div>
</section>
<section class="admin-section">
<div class="container">
{% include "admin/_nav.html" %}
{% include "partials/alerts.html" %}
{% block admin_content %}
<div class="admin-panel folder-create">
<h2 class="admin-panel__title">Создать группу</h2>
<form method="post" class="auth-form folder-create__form">
@@ -86,6 +76,4 @@
</tbody>
</table>
</div>
</div>
</section>
{% endblock %}
+27
View File
@@ -0,0 +1,27 @@
{% extends "base.html" %}
{% block content %}
<div class="admin-shell">
<aside class="admin-sidebar">
<div class="admin-sidebar__head">
<span class="admin-sidebar__logo">⚙️</span>
<div>
<strong>PhotoHost</strong>
<span>Админ-панель</span>
</div>
</div>
{% include "admin/_nav.html" %}
<a href="{{ url_for('main.index') }}" class="admin-sidebar__back">← На сайт</a>
</aside>
<div class="admin-main">
<div class="admin-main__header">
<div>
<h1 class="admin-main__title">{% block admin_title %}Админка{% endblock %}</h1>
{% block admin_subtitle %}{% endblock %}
</div>
</div>
{% include "partials/alerts.html" %}
{% block admin_content %}{% endblock %}
</div>
</div>
{% endblock %}
+3 -15
View File
@@ -1,22 +1,10 @@
{% extends "base.html" %}
{% extends "admin/layout.html" %}
{% block title %}Фото — Админка{% endblock %}
{% block admin_title %}Все фотографии{% endblock %}
{% block content %}
<section class="page-header page-header--admin">
<div class="container">
<h1 class="page-header__title">Все фотографии</h1>
</div>
</section>
<section class="admin-section">
<div class="container">
{% include "admin/_nav.html" %}
{% include "partials/alerts.html" %}
{% block admin_content %}
{% with photos=photos, show_owner=true, delete_mode='admin', empty_title='Нет фотографий', empty_text='Пользователи ещё не загружали фото' %}
{% include "partials/photo_gallery.html" %}
{% endwith %}
</div>
</section>
{% endblock %}
+4 -16
View File
@@ -1,20 +1,10 @@
{% extends "base.html" %}
{% extends "admin/layout.html" %}
{% block title %}Настройки — Админка{% endblock %}
{% block admin_title %}Настройки системы{% endblock %}
{% block admin_subtitle %}<p class="admin-main__subtitle">S3, SFTP, FTP, SMTP и лимиты загрузки</p>{% endblock %}
{% block content %}
<section class="page-header page-header--admin">
<div class="container">
<h1 class="page-header__title">Настройки системы</h1>
<p class="page-header__subtitle">S3, SFTP, FTP, SMTP и лимиты загрузки</p>
</div>
</section>
<section class="admin-section">
<div class="container">
{% include "admin/_nav.html" %}
{% include "partials/alerts.html" %}
{% block admin_content %}
<form method="post" class="settings-form">
<input type="hidden" name="action" value="save">
@@ -88,6 +78,4 @@
<input type="hidden" name="action" value="test_smtp">
<button type="submit" class="btn btn--ghost">Отправить тестовое письмо на {{ current_user.email }}</button>
</form>
</div>
</section>
{% endblock %}
+3 -15
View File
@@ -1,19 +1,9 @@
{% extends "base.html" %}
{% extends "admin/layout.html" %}
{% block title %}Пользователи — Админка{% endblock %}
{% block admin_title %}Пользователи{% endblock %}
{% block content %}
<section class="page-header page-header--admin">
<div class="container">
<h1 class="page-header__title">Пользователи</h1>
</div>
</section>
<section class="admin-section">
<div class="container">
{% include "admin/_nav.html" %}
{% include "partials/alerts.html" %}
{% block admin_content %}
<div class="admin-table-wrap">
<table class="admin-table">
<thead>
@@ -84,6 +74,4 @@
</tbody>
</table>
</div>
</div>
</section>
{% endblock %}