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
+104 -10
View File
@@ -6,13 +6,13 @@
<section class="page-header">
<div class="container">
<h1 class="page-header__title">Настройки профиля</h1>
<p class="page-header__subtitle">Измените email или пароль</p>
<p class="page-header__subtitle">Безопасность, passkey, сессии и GDPR</p>
</div>
</section>
<section class="auth-section">
<div class="container auth-container">
<div class="auth-card auth-card--wide">
<section class="auth-section profile-section">
<div class="container profile-grid">
<div class="auth-card auth-card--wide profile-card">
{% include "partials/alerts.html" %}
<div class="profile-info">
@@ -30,7 +30,9 @@
</div>
</div>
<h2 class="profile-card__title">Email и пароль</h2>
<form method="post" class="auth-form">
<input type="hidden" name="action" value="save">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required value="{{ current_user.email }}">
@@ -41,19 +43,111 @@
</div>
<div class="form-group">
<label for="new_password">Новый пароль (необязательно)</label>
<input type="password" id="new_password" name="new_password" minlength="6" placeholder="оставьте пустым, если не меняете">
<input type="password" id="new_password" name="new_password" minlength="6">
</div>
<div class="form-group">
<label for="new_password2">Подтверждение нового пароля</label>
<input type="password" id="new_password2" name="new_password2" minlength="6">
</div>
<button type="submit" class="btn btn--primary btn--full">Сохранить</button>
<button type="submit" class="btn btn--primary">Сохранить</button>
</form>
<p class="auth-card__footer">
<a href="{{ url_for('cabinet.index') }}">← Вернуться в кабинет</a>
</p>
</div>
<div class="auth-card auth-card--wide profile-card">
<h2 class="profile-card__title">Passkey</h2>
<p class="profile-card__hint">Вход без пароля через Face ID, Touch ID, Windows Hello или ключ безопасности.</p>
{% if passkeys %}
<ul class="session-list">
{% for passkey in passkeys %}
<li class="session-item">
<div>
<strong>{{ passkey.name }}</strong>
<span class="session-item__meta">Добавлен {{ passkey.created_at.strftime('%d.%m.%Y') }}</span>
</div>
<form method="post">
<input type="hidden" name="action" value="delete_passkey">
<input type="hidden" name="passkey_id" value="{{ passkey.id }}">
<button type="submit" class="btn btn--danger btn--sm">Удалить</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<p class="profile-card__empty">Passkey не настроен</p>
{% endif %}
<div class="form-group" style="margin-top:16px">
<label for="passkeyName">Название устройства</label>
<input type="text" id="passkeyName" value="Моё устройство" maxlength="120">
</div>
<button type="button" class="btn btn--ghost" id="addPasskeyBtn">Добавить passkey</button>
</div>
<div class="auth-card auth-card--wide profile-card">
<h2 class="profile-card__title">Активные сессии</h2>
<p class="profile-card__hint">Все устройства, где выполнен вход в ваш аккаунт.</p>
{% if sessions %}
<ul class="session-list">
{% for item in sessions %}
<li class="session-item {% if item.session_key == current_sid %}session-item--current{% endif %}">
<div>
<strong>{{ item.device_label }}</strong>
{% if item.session_key == current_sid %}<span class="badge badge--success">текущая</span>{% endif %}
<span class="session-item__meta">
{{ item.ip_address or 'IP неизвестен' }} ·
{{ item.last_seen_at.strftime('%d.%m.%Y %H:%M') }}
</span>
</div>
{% if item.session_key != current_sid %}
<form method="post">
<input type="hidden" name="action" value="revoke_session">
<input type="hidden" name="session_id" value="{{ item.id }}">
<button type="submit" class="btn btn--ghost btn--sm">Завершить</button>
</form>
{% endif %}
</li>
{% endfor %}
</ul>
<form method="post" style="margin-top:16px">
<input type="hidden" name="action" value="revoke_all_sessions">
<button type="submit" class="btn btn--danger btn--sm" onclick="return confirm('Завершить все сессии кроме текущей?');">
Выйти на всех устройствах
</button>
</form>
{% else %}
<p class="profile-card__empty">Нет активных сессий</p>
{% endif %}
</div>
<div class="auth-card auth-card--wide profile-card">
<h2 class="profile-card__title">GDPR и данные</h2>
<p class="profile-card__hint">
Вы можете скачать копию своих данных или удалить аккаунт.
<a href="{{ url_for('legal.gdpr') }}">Подробнее о правах</a>
</p>
<div class="profile-actions">
<a href="{{ url_for('cabinet.export_profile') }}" class="btn btn--ghost">Скачать мои данные (JSON)</a>
</div>
<form method="post" class="profile-delete-form" onsubmit="return confirm('Удалить аккаунт без возможности восстановления?');">
<input type="hidden" name="action" value="delete_account">
<div class="form-group">
<label for="delete_password">Пароль для удаления аккаунта</label>
<input type="password" id="delete_password" name="delete_password" required>
</div>
<button type="submit" class="btn btn--danger">Удалить аккаунт</button>
</form>
</div>
<p class="auth-card__footer profile-footer">
<a href="{{ url_for('cabinet.index') }}">← Вернуться в кабинет</a>
</p>
</div>
</section>
{% endblock %}
{% block scripts %}
<script src="{{ url_for('static', filename='js/passkey.js') }}"></script>
{% endblock %}