Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions app/routes/account_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ def account_settings():
return redirect("/")

if request.method == "POST":
delete_user(user.username)
return redirect("/")
result = delete_user(user.username, session["username"])
if result:
return redirect("/")

return render_template(
"account_settings.html",
Expand Down
7 changes: 6 additions & 1 deletion app/routes/admin_panel_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ def admin_panel_users():
f"Admin: {session['username']} deleted user: {request.form['username']}"
)

delete_user(request.form["username"])
result = delete_user(request.form["username"], session["username"])
if not result:
Log.warning(
f"Admin: {session['username']} — delete_user rejected for "
f"{request.form['username']}"
)

if "user_role_change_button" in request.form:
Log.info(
Expand Down
4 changes: 3 additions & 1 deletion app/translations/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "Benutzer wurde gelöscht.",
"post": "Beitrag wurde gelöscht.",
"comment": "Kommentar wurde gelöscht."
"comment": "Kommentar wurde gelöscht.",
"not_authorized": "Sie sind nicht berechtigt, diesen Benutzer zu löschen",
"last_admin": "Diesen Benutzer kann nicht gelöscht werden: Er ist der letzte Administrator im System"
},
"set_language": {
"success": "Sprache wurde geändert."
Expand Down
4 changes: 3 additions & 1 deletion app/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "User has been deleted.",
"post": "Post has been deleted.",
"comment": "Comment has been deleted."
"comment": "Comment has been deleted.",
"not_authorized": "You are not authorized to delete this user.",
"last_admin": "Cannot delete the last administrator account."
},
"set_language": {
"success": "Language has been changed."
Expand Down
4 changes: 3 additions & 1 deletion app/translations/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "El usuario ha sido eliminado.",
"post": "La publicación ha sido eliminada.",
"comment": "El comentario ha sido eliminado."
"comment": "El comentario ha sido eliminado.",
"not_authorized": "No tiene autorización para eliminar este usuario",
"last_admin": "No se puede eliminar este usuario: es el último administrador en el sistema"
},
"set_language": {
"success": "El idioma ha sido cambiado."
Expand Down
4 changes: 3 additions & 1 deletion app/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "L'utilisateur a été supprimé.",
"post": "L'article a été supprimé.",
"comment": "Le commentaire a été supprimé."
"comment": "Le commentaire a été supprimé.",
"not_authorized": "Vous n'êtes pas autorisé à supprimer cet utilisateur",
"last_admin": "Impossible de supprimer cet utilisateur : c'est le dernier administrateur du système"
},
"set_language": {
"success": "La langue a été modifiée."
Expand Down
4 changes: 3 additions & 1 deletion app/translations/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "उपयोगकर्ता हटा दिया गया है।",
"post": "पोस्ट हटा दी गई है।",
"comment": "टिप्पणी हटा दी गई है।"
"comment": "टिप्पणी हटा दी गई है।",
"not_authorized": "आप इस उपयोगकर्ता को हटाने के लिए अनुमतित नहीं हैं",
"last_admin": "इस उपयोगकर्ता को हटाया नहीं जा सकता: यह तंत्र का अंतिम व्यवसायक है"
},
"set_language": {
"success": "भाषा सफलतापूर्वक बदल दी गई है।"
Expand Down
4 changes: 3 additions & 1 deletion app/translations/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "ユーザーが削除されました。",
"post": "投稿が削除されました。",
"comment": "コメントが削除されました。"
"comment": "コメントが削除されました。",
"not_authorized": "このユーザーを削除する権限がありません",
"last_admin": "このユーザーを削除できません:システム最後の管理者です"
},
"set_language": {
"success": "言語が変更されました。"
Expand Down
4 changes: 3 additions & 1 deletion app/translations/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "Użytkownik został usunięty.",
"post": "Post został usunięty.",
"comment": "Komentarz został usunięty."
"comment": "Komentarz został usunięty.",
"not_authorized": "Nie masz uprawnień do usunięcia tego użytkownika",
"last_admin": "Nie można usunąć tego użytkownika: jest ostatnim administratorem w systemie"
},
"set_language": {
"success": "Język został zmieniony."
Expand Down
4 changes: 3 additions & 1 deletion app/translations/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "Usuário foi excluído.",
"post": "Postagem foi excluída.",
"comment": "Comentário foi excluído."
"comment": "Comentário foi excluído.",
"not_authorized": "Você não está autorizado a excluir este usuário",
"last_admin": "Não é possível excluir este usuário: é o último administrador do sistema"
},
"set_language": {
"success": "O idioma foi alterado."
Expand Down
4 changes: 3 additions & 1 deletion app/translations/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "Пользователь был удален.",
"post": "Пост был удален.",
"comment": "Комментарий был удален."
"comment": "Комментарий был удален.",
"not_authorized": "У вас нет прав для удаления этого пользователя",
"last_admin": "Невозможно удалить этого пользователя: это последний администратор в системе"
},
"set_language": {
"success": "Язык был изменен."
Expand Down
4 changes: 3 additions & 1 deletion app/translations/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "Kullanıcı silindi.",
"post": "Gönderi silindi.",
"comment": "Yorum silindi."
"comment": "Yorum silindi.",
"not_authorized": "Bu kullanıcıyı silme yetkiniz yok",
"last_admin": "Bu kullanıcı silinemez: sistemdeki son yöneticidir"
},
"set_language": {
"success": "Dil değiştirildi."
Expand Down
4 changes: 3 additions & 1 deletion app/translations/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@
"delete": {
"user": "Користувача було видалено.",
"post": "Пост було видалено.",
"comment": "Коментар було видалено."
"comment": "Коментар було видалено.",
"not_authorized": "У вас немає прав для видалення цього користувача",
"last_admin": "Неможливо видалити цього користувача: це останній адміністратор у системі"
},
"set_language": {
"success": "Мову було змінено."
Expand Down
4 changes: 3 additions & 1 deletion app/translations/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@
"delete": {
"user": "用户已删除。",
"post": "帖子已删除。",
"comment": "评论已删除。"
"comment": "评论已删除。",
"not_authorized": "您沒有權限刪除此用戶",
"last_admin": "無法刪除此用戶:他是系統中最後的管理員"
},
"set_language": {
"success": "语言已更改。"
Expand Down
81 changes: 67 additions & 14 deletions app/utils/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,28 +61,81 @@ def delete_post(post_id, username=None):
return True


def delete_user(username):
def delete_user(target_username, perpetrator_username=None):
"""
This function deletes a user and all associated data from the database.

Authorization rules:
- Non-admin users may only delete their own account.
- Admins may delete any non-admin user.
- Admins may delete another admin only if >= 2 admins remain after deletion.
- No user (admin or not) may delete themselves via the admin panel
(admin self-deletion is blocked at the account-settings page).

Parameters:
username (str): The username of the user to be deleted.
target_username (str): The username of the user to be deleted.
perpetrator_username (str, optional): The username of the calling user,
taken from session["username"]. If None, falls back to session.

Returns:
None
bool: True if deleted, False if unauthorized, not found, or blocked.
"""
from sqlalchemy import func

user = User.query.filter(func.lower(User.username) == username.lower()).first()
if perpetrator_username is None:
perpetrator_username = session.get("username")

if not user:
Log.error(f'User: "{username}" not found')
return redirect("/")
target = User.query.filter(
func.lower(User.username) == target_username.lower()
).first()

if not target:
Log.error(f'User: "{target_username}" not found')
return False

perpetrator = User.query.filter_by(username=session["username"]).first()
perpetrator_role = perpetrator.role if perpetrator else None
perpetrator = User.query.filter_by(username=perpetrator_username).first()
if not perpetrator:
Log.error(f'Perpetrator: "{perpetrator_username}" not found')
return False

# Gate 1: Authorization
is_admin = perpetrator.role == "admin"
is_self = target.username.lower() == perpetrator.username.lower()

if not is_admin and not is_self:
Log.error(
f'User: "{perpetrator_username}" (role={perpetrator.role}) '
f'tried to delete user: "{target_username}" without authorization'
)
flash_message(
page="delete",
message="not_authorized",
category="error",
language=session.get("language", "en"),
)
return False

db.session.delete(user)
# Gate 2: Last-admin guard — never let admin count drop to zero
if target.role == "admin":
admin_count = (
db.session.query(func.count(User.user_id))
.filter(User.role == "admin")
.scalar()
)
if admin_count <= 1:
Log.error(
f'User: "{perpetrator_username}" tried to delete last admin: '
f'"{target_username}" — would leave no admins'
)
flash_message(
page="delete",
message="last_admin",
category="error",
language=session.get("language", "en"),
)
return False

db.session.delete(target)
db.session.commit()

flash_message(
Expand All @@ -91,13 +144,13 @@ def delete_user(username):
category="error",
language=session.get("language", "en"),
)
Log.success(f'User: "{username}" deleted')
Log.success(f'User: "{target.username}" deleted by "{perpetrator.username}"')

if perpetrator_role == "admin":
return redirect("/admin/users")
else:
if is_self:
session.clear()
return redirect("/")
else:
return redirect("/admin/users")


def delete_comment(comment_id, username=None):
Expand Down
Loading
Loading