From 7bfb2c90e4ae6d4de62d4af069ddc27dcc95ea91 Mon Sep 17 00:00:00 2001 From: raydeveloppeur-admin Date: Sun, 19 Jul 2026 19:49:43 +0200 Subject: [PATCH] =?UTF-8?q?Ban=20system=20:=20Add=20ban/unban=20support=20?= =?UTF-8?q?with=20a=20global=20modal=20and=20character=20counter.=20Accoun?= =?UTF-8?q?ts=20gain=20is=5Fbanned=20and=20ban=5Freason=20fields;=20servic?= =?UTF-8?q?es,=20ports=20and=20adapters=20handle=20ban/unban=20and=20block?= =?UTF-8?q?=20banned=20users.=20Frontend=20adds=20a=20site=E2=80=91wide=20?= =?UTF-8?q?ban=20dialog,=20reason=20field=20and=20counter,=20updates=20use?= =?UTF-8?q?r=20list=20and=20profile=20buttons,=20and=20cleans=20old=20ban?= =?UTF-8?q?=20code.=20Full=20test=20coverage=20validates=20domain,=20adapt?= =?UTF-8?q?ers,=20services=20and=20integration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- flask_setup/routes.py | 14 ++++ frontend/static/css/base.css | 56 ++++++++++++++ frontend/static/css/profile.css | 7 +- frontend/static/scripts/confirm-dialog.js | 32 ++++++++ frontend/templates/base.html | 25 ++++++ frontend/templates/profile.html | 12 ++- frontend/templates/user_list.html | 14 ++++ .../V13__add_ban_columns_to_accounts.sql | 5 ++ src/application/domain/account.py | 8 ++ .../input_ports/account_session_management.py | 29 +++++++ .../output_ports/account_repository.py | 12 +++ src/application/services/article_service.py | 3 + src/application/services/comment_service.py | 2 + src/application/services/login_service.py | 51 +++++++++++++ .../input_adapters/dto/account_response.py | 8 +- .../flask/flask_account_session_adapter.py | 65 ++++++++++++++++ .../flask/flask_login_adapter.py | 5 +- .../output_adapters/dto/account_record.py | 10 ++- .../in_memory/account_repository.py | 18 +++++ .../models/sqlalchemy_account_model.py | 8 +- .../sqlalchemy/sqlalchemy_account_adapter.py | 20 +++++ tests/test_domain_factories.py | 4 + .../dto/test_account_response.py | 34 +++++++++ .../test_flask_account_session_adapter.py | 76 +++++++++++++++++++ .../tests_flask/test_flask_login_adapter.py | 14 ++++ .../dto/test_account_record.py | 32 ++++++++ .../test_in_memory_adapters.py | 21 +++++ .../test_sqlalchemy_account_adapter.py | 19 +++++ .../test_security_integration.py | 28 +++++++ tests/tests_services/test_login_service.py | 52 +++++++++++++ 30 files changed, 672 insertions(+), 12 deletions(-) create mode 100644 migrations/V13__add_ban_columns_to_accounts.sql diff --git a/flask_setup/routes.py b/flask_setup/routes.py index 5295923c..46e00d16 100644 --- a/flask_setup/routes.py +++ b/flask_setup/routes.py @@ -135,6 +135,20 @@ def _register_auth_routes(app: Flask, adapters: dict) -> None: endpoint="auth.change_role", ) + app.add_url_rule( + "/admin/users//ban", + view_func=acc.ban_account, + methods=["POST"], + endpoint="auth.ban_account", + ) + + app.add_url_rule( + "/admin/users//unban", + view_func=acc.unban_account, + methods=["POST"], + endpoint="auth.unban_account", + ) + def _register_file_routes(app: Flask, adapters: dict) -> None: fad = adapters["file_adapter"] diff --git a/frontend/static/css/base.css b/frontend/static/css/base.css index d938702d..89ac95f4 100644 --- a/frontend/static/css/base.css +++ b/frontend/static/css/base.css @@ -562,6 +562,62 @@ button:hover, margin: 0; } +#ban-modal { + max-width: 32rem; +} + +#ban-modal .modal-input-group { + gap: 0; +} + +#ban-modal .modal-input-group label { + margin-bottom: var(--spacing-sm); + font-family: var(--font-primary); +} + +#ban-modal .char-count { + font-family: var(--font-primary); +} + +#ban-modal .ban-reason-textarea { + margin-top: var(--spacing-xs); +} + +.ban-reason-textarea { + width: 100%; + min-height: 4.5rem; + height: auto; + padding: var(--spacing-sm) var(--spacing-md); + border: 1px solid var(--outline-variant); + border-radius: var(--radius-default); + background: var(--surface-container); + color: var(--on-surface); + font-family: var(--font-primary); + font-size: var(--font-body-base); + line-height: 1.5; + resize: vertical; + outline: none; + box-sizing: border-box; +} + +.ban-reason-textarea:focus { + border-color: var(--primary); +} + +.hint-row { + display: flex; + justify-content: space-between; + align-items: center; + font-size: var(--font-body-sm); + color: var(--on-surface-variant); +} + +.char-count { + font-size: var(--font-body-sm); + color: var(--on-surface-variant); + font-family: var(--font-mono); +} + button:focus-visible, .btn:focus-visible, a:focus-visible { diff --git a/frontend/static/css/profile.css b/frontend/static/css/profile.css index de4d23c5..1049ca31 100644 --- a/frontend/static/css/profile.css +++ b/frontend/static/css/profile.css @@ -154,9 +154,12 @@ .profile-actions { flex-direction: column; + align-items: stretch; + width: fit-content; + margin-left: auto; + margin-right: auto; } - .profile-actions .btn-secondary, - .profile-actions .btn-danger { + .profile-actions .btn { width: 100%; } } diff --git a/frontend/static/scripts/confirm-dialog.js b/frontend/static/scripts/confirm-dialog.js index 0a6b2b4d..3d53051f 100644 --- a/frontend/static/scripts/confirm-dialog.js +++ b/frontend/static/scripts/confirm-dialog.js @@ -62,4 +62,36 @@ if (e.key === 'Escape') close(); }); }); + + const BAN_TRIGGER_SELECTOR = '.ban-trigger'; + + document.addEventListener('click', (e) => { + const trigger = e.target.closest(BAN_TRIGGER_SELECTOR); + if (!trigger) return; + + const userId = trigger.getAttribute('data-user-id'); + const username = trigger.getAttribute('data-username'); + const modal = document.getElementById('ban-modal'); + if (!modal) return; + + const form = document.getElementById('ban-form'); + form.action = '/admin/users/' + userId + '/ban'; + + document.getElementById('ban-username').textContent = username; + document.getElementById('ban-reason-input').value = ''; + document.getElementById('ban-reason-count').textContent = '0'; + modal.showModal(); + }); + + document.addEventListener('click', (e) => { + if (e.target.id === 'ban-modal-cancel') { + document.getElementById('ban-modal')?.close(); + } + }); + + document.addEventListener('input', (e) => { + if (e.target.id === 'ban-reason-input') { + document.getElementById('ban-reason-count').textContent = e.target.value.length; + } + }); })(); diff --git a/frontend/templates/base.html b/frontend/templates/base.html index 10d7b314..5ed5f2a9 100644 --- a/frontend/templates/base.html +++ b/frontend/templates/base.html @@ -110,6 +110,31 @@ + + +
+ + +
+
+