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
+87 -2
View File
@@ -7,6 +7,7 @@ from flask import (
current_app,
flash,
jsonify,
make_response,
redirect,
render_template,
request,
@@ -209,9 +210,44 @@ def index():
@cabinet_bp.route("/profile", methods=["GET", "POST"])
@login_required
def profile():
from app.models import User
from app.models import User, UserPasskey
from app.passkey_service import delete_passkey
from app.session_service import (
get_current_session_key,
list_user_sessions,
revoke_all_sessions,
revoke_session,
)
if request.method == "POST":
action = request.form.get("action", "save")
if action == "revoke_session":
session_id = request.form.get("session_id", type=int)
if session_id and revoke_session(session_id, current_user.id):
flash("Сессия завершена", "success")
return redirect(url_for("cabinet.profile"))
if action == "revoke_all_sessions":
count = revoke_all_sessions(current_user.id, except_current=True)
flash(f"Завершено сессий: {count}", "success")
return redirect(url_for("cabinet.profile"))
if action == "delete_passkey":
passkey_id = request.form.get("passkey_id", type=int)
if passkey_id and delete_passkey(current_user, passkey_id):
flash("Passkey удалён", "success")
return redirect(url_for("cabinet.profile"))
if action == "delete_account":
password = request.form.get("delete_password", "")
if not current_user.check_password(password):
flash("Неверный пароль", "error")
else:
_delete_user_account(current_user)
flash("Аккаунт удалён", "success")
return redirect(url_for("main.index"))
email = request.form.get("email", "").strip().lower()
current_password = request.form.get("current_password", "")
new_password = request.form.get("new_password", "")
@@ -234,4 +270,53 @@ def profile():
flash("Профиль обновлён", "success")
return redirect(url_for("cabinet.profile"))
return render_template("cabinet/profile.html")
sessions = list_user_sessions(current_user.id)
passkeys = current_user.passkeys.order_by(UserPasskey.created_at.desc()).all()
current_sid = get_current_session_key()
return render_template(
"cabinet/profile.html",
sessions=sessions,
passkeys=passkeys,
current_sid=current_sid,
)
@cabinet_bp.route("/profile/export")
@login_required
def export_profile():
from app.legal import export_user_data
import json
data = export_user_data(current_user)
response = make_response(json.dumps(data, ensure_ascii=False, indent=2))
response.headers["Content-Type"] = "application/json; charset=utf-8"
response.headers["Content-Disposition"] = (
f'attachment; filename="photohost-{current_user.username}.json"'
)
return response
def _delete_user_account(user):
from app.models import UserPasskey, UserSession
from app.session_service import revoke_all_sessions, revoke_current_session
from app.storage_service import delete_photo_file
from flask_login import logout_user
revoke_all_sessions(user.id, except_current=False)
UserSession.query.filter_by(user_id=user.id).delete()
UserPasskey.query.filter_by(user_id=user.id).delete()
for photo in user.photos.all():
delete_photo_file(photo.filename, photo.storage_backend)
db.session.delete(photo)
for folder in user.folders.all():
for photo in folder.photos.all():
delete_photo_file(photo.filename, photo.storage_backend)
db.session.delete(photo)
db.session.delete(folder)
db.session.delete(user)
db.session.commit()
revoke_current_session()
logout_user()