From b5852870048761ecd16b02b7539b994f7838cfd3 Mon Sep 17 00:00:00 2001 From: Md Bayazid Bostame Date: Thu, 26 Mar 2026 10:07:49 +0100 Subject: [PATCH] snapshot: preserve role management and user lifecycle controls --- backend/config/settings.py | 1 + backend/locale/en/LC_MESSAGES/django.mo | Bin 27944 -> 29845 bytes backend/locale/en/LC_MESSAGES/django.po | 630 +++++++++++++----- backend/workflows/apps.py | 3 + backend/workflows/context_processors.py | 5 + backend/workflows/forms.py | 56 ++ .../commands/bootstrap_initial_users.py | 3 + backend/workflows/roles.py | 127 ++++ backend/workflows/signals.py | 12 + .../static/workflows/css/admin_tools.css | 1 + .../workflows/developer_handbook.html | 14 + .../workflows/templates/workflows/home.html | 27 +- .../templates/workflows/project_wiki.html | 5 + .../workflows/requests_dashboard.html | 26 +- .../templates/workflows/user_management.html | 151 +++++ backend/workflows/urls.py | 5 + backend/workflows/views.py | 344 +++++++--- 17 files changed, 1137 insertions(+), 273 deletions(-) create mode 100644 backend/workflows/context_processors.py create mode 100644 backend/workflows/roles.py create mode 100644 backend/workflows/signals.py create mode 100644 backend/workflows/templates/workflows/user_management.html diff --git a/backend/config/settings.py b/backend/config/settings.py index 4e4d7b2..5707e2c 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -58,6 +58,7 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', + 'workflows.context_processors.role_context', ], }, }, diff --git a/backend/locale/en/LC_MESSAGES/django.mo b/backend/locale/en/LC_MESSAGES/django.mo index bb415ac5989c47d2605abcb127af7d574e7bf020..f50c5ec5ece6db6f058e9581e46e90701a8a0cec 100644 GIT binary patch delta 9890 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 aXm{hW%a5f^4^;EH_QvnZiOncGocuqKRq+G> delta 7958 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 Kc{Y`u@c%zDCYuHT diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 6d0b407..04ef874 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: tubco-portal\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-26 00:10+0000\n" +"POT-Creation-Date: 2026-03-26 09:06+0000\n" "PO-Revision-Date: 2026-03-24 00:00+0000\n" "Language: en\n" "MIME-Version: 1.0\n" @@ -55,29 +55,74 @@ msgstr "" msgid "Remote Backup in Nextcloud konnte nicht gelöscht werden." msgstr "" -#: workflows/forms.py:338 +#: workflows/forms.py:102 +msgid "Vorname" +msgstr "" + +#: workflows/forms.py:103 +msgid "Nachname" +msgstr "" + +#: workflows/forms.py:104 workflows/templates/workflows/user_management.html:81 +msgid "Benutzername" +msgstr "" + +#: workflows/forms.py:105 +#, fuzzy +#| msgid "E-Mail" +msgid "E-Mail-Adresse" +msgstr "Email" + +#: workflows/forms.py:106 workflows/templates/workflows/user_management.html:83 +#: workflows/templates/workflows/user_management.html:102 +#, fuzzy +#| msgid "Rolle:" +msgid "Rolle" +msgstr "Role:" + +#: workflows/forms.py:107 +msgid "Passwort" +msgstr "Password" + +#: workflows/forms.py:108 +msgid "Passwort bestätigen" +msgstr "Confirm password" + +#: workflows/forms.py:121 +msgid "Dieser Benutzername ist bereits vergeben." +msgstr "This username is already taken." + +#: workflows/forms.py:130 workflows/views.py:433 +msgid "Ungültige Rolle." +msgstr "Invalid role." + +#: workflows/forms.py:138 +msgid "Die Passwörter stimmen nicht überein." +msgstr "The passwords do not match." + +#: workflows/forms.py:394 #, python-format msgid "" "Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen " "(frühestens %(date)s)." msgstr "" -#: workflows/models.py:55 workflows/views.py:181 +#: workflows/models.py:55 workflows/views.py:199 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:56 workflows/views.py:182 +#: workflows/models.py:56 workflows/views.py:200 msgid "In Bearbeitung" msgstr "Processing" -#: workflows/models.py:57 workflows/models.py:372 workflows/views.py:183 +#: workflows/models.py:57 workflows/models.py:372 workflows/views.py:201 msgid "Abgeschlossen" msgstr "Completed" #: workflows/models.py:58 workflows/models.py:312 #: workflows/templates/workflows/backup_recovery.html:70 #: workflows/templates/workflows/requests_dashboard.html:222 -#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:184 +#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:202 msgid "Fehlgeschlagen" msgstr "Failed" @@ -137,19 +182,19 @@ msgstr "" msgid "Automatisch" msgstr "" -#: workflows/models.py:171 workflows/views.py:87 +#: workflows/models.py:171 workflows/views.py:94 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:172 workflows/views.py:88 +#: workflows/models.py:172 workflows/views.py:95 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:173 workflows/views.py:89 +#: workflows/models.py:173 workflows/views.py:96 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:174 workflows/views.py:90 +#: workflows/models.py:174 workflows/views.py:97 msgid "Abschluss" msgstr "Finish" @@ -318,6 +363,22 @@ msgstr "" msgid "NFS" msgstr "" +#: workflows/roles.py:20 +msgid "Super Admin" +msgstr "Super Admin" + +#: workflows/roles.py:21 +msgid "Admin" +msgstr "Admin" + +#: workflows/roles.py:22 +msgid "IT Staff" +msgstr "IT Staff" + +#: workflows/roles.py:23 +msgid "Mitarbeiter" +msgstr "Staff" + #: workflows/tasks.py:591 #, python-format msgid "%(item)s übergeben und Grundfunktionen erklärt" @@ -428,7 +489,7 @@ msgstr "Sign in" #: workflows/templates/workflows/audit_log.html:4 #: workflows/templates/workflows/audit_log.html:15 -#: workflows/templates/workflows/home.html:120 +#: workflows/templates/workflows/home.html:132 msgid "Audit Log" msgstr "" @@ -524,7 +585,7 @@ msgstr "No requests available yet." #: workflows/templates/workflows/backup_recovery.html:4 #: workflows/templates/workflows/backup_recovery.html:12 -#: workflows/templates/workflows/home.html:125 +#: workflows/templates/workflows/home.html:139 msgid "Backup & Recovery" msgstr "Backup & Recovery" @@ -535,6 +596,7 @@ msgid "" msgstr "Create database and media backups and verify existing bundles safely." #: workflows/templates/workflows/backup_recovery.html:20 +#: workflows/templates/workflows/user_management.html:87 msgid "Aktionen" msgstr "Actions" @@ -542,12 +604,22 @@ msgstr "Actions" msgid "" "Erstellung und Verifikation laufen im App-Kontext. Restore bleibt bewusst " "CLI-only." -msgstr "Creation and verification run inside the app context. Restore intentionally remains CLI-only." +msgstr "" +"Creation and verification run inside the app context. Restore intentionally " +"remains CLI-only." #: workflows/templates/workflows/backup_recovery.html:23 msgid "Neues Backup jetzt erstellen?" msgstr "Create a new backup now?" +#: workflows/templates/workflows/backup_recovery.html:23 +msgid "Backup wird erstellt" +msgstr "Backup is being created" + +#: workflows/templates/workflows/backup_recovery.html:23 +msgid "Bitte warten. Datenbank- und Media-Bundle werden gerade vorbereitet." +msgstr "Please wait. The database and media bundle are being prepared." + #: workflows/templates/workflows/backup_recovery.html:25 msgid "Backup erstellen" msgstr "Create backup" @@ -570,7 +642,7 @@ msgid "Verifiziert" msgstr "Verified" #: workflows/templates/workflows/backup_recovery.html:40 -#: workflows/templates/workflows/home.html:98 +#: workflows/templates/workflows/home.html:99 #: workflows/templates/workflows/onboarding_intro_session.html:37 #: workflows/templates/workflows/request_timeline.html:70 #: workflows/templates/workflows/requests_dashboard.html:136 @@ -608,80 +680,53 @@ msgstr "Disabled" msgid "Lokal" msgstr "Local" +#: workflows/templates/workflows/backup_recovery.html:79 msgid "Lokal gespeichert" msgstr "Stored locally" +#: workflows/templates/workflows/backup_recovery.html:81 msgid "Lokal nicht vorhanden" msgstr "Not stored locally" -#: workflows/templates/workflows/backup_recovery.html:90 +#: workflows/templates/workflows/backup_recovery.html:95 msgid "Backup jetzt verifizieren?" msgstr "Verify backup now?" -#: workflows/templates/workflows/backup_recovery.html:92 +#: workflows/templates/workflows/backup_recovery.html:95 +msgid "Backup wird verifiziert" +msgstr "Backup is being verified" + +#: workflows/templates/workflows/backup_recovery.html:95 +msgid "Bitte warten. Bundle, Datenbank-Dump und Media-Archiv werden geprüft." +msgstr "" +"Please wait. The bundle, database dump, and media archive are being checked." + +#: workflows/templates/workflows/backup_recovery.html:97 msgid "Verifizieren" msgstr "Verify" -#: workflows/templates/workflows/backup_recovery.html:94 +#: workflows/templates/workflows/backup_recovery.html:99 msgid "Backup-Bundle wirklich löschen?" msgstr "Delete this backup bundle?" -#: workflows/templates/workflows/backup_recovery.html:96 +#: workflows/templates/workflows/backup_recovery.html:101 #: workflows/templates/workflows/form_builder.html:92 #: workflows/templates/workflows/form_builder.html:107 #: workflows/templates/workflows/integrations_setup.html:265 #: workflows/templates/workflows/intro_builder.html:66 #: workflows/templates/workflows/intro_builder.html:102 -#: workflows/templates/workflows/requests_dashboard.html:279 +#: workflows/templates/workflows/requests_dashboard.html:286 +#: workflows/templates/workflows/user_management.html:136 #: workflows/templates/workflows/welcome_emails.html:70 msgid "Löschen" msgstr "Delete" -#: workflows/templates/workflows/backup_recovery.html:106 +#: workflows/templates/workflows/backup_recovery.html:111 #, fuzzy #| msgid "Noch keine Vorgänge vorhanden." msgid "Noch keine Backup-Bundles vorhanden." msgstr "No backup bundles available yet." -msgid "Bitte warten" -msgstr "Please wait" - -msgid "Aktion läuft" -msgstr "Action in progress" - -msgid "Die Aktion wird im aktuellen Tab ausgeführt." -msgstr "The action is running in the current tab." - -msgid "Backup läuft" -msgstr "Backup in progress" - -msgid "Bitte warten. Die Aktion wird im aktuellen Tab ausgeführt." -msgstr "Please wait. The action is running in the current tab." - -msgid "Backup wird erstellt" -msgstr "Backup is being created" - -msgid "Bitte warten. Datenbank- und Media-Bundle werden gerade vorbereitet." -msgstr "Please wait. The database and media bundle are being prepared." - -msgid "Backup wird verifiziert" -msgstr "Backup is being verified" - -msgid "Bitte warten. Bundle, Datenbank-Dump und Media-Archiv werden geprüft." -msgstr "Please wait. The bundle, database dump, and media archive are being checked." - -msgid "Nextcloud-Test läuft" -msgstr "Nextcloud test in progress" - -msgid "Bitte warten. Verbindung und Upload in das konfigurierte Ziel werden geprüft." -msgstr "Please wait. The connection and upload to the configured target are being checked." - -msgid "SMTP-Test läuft" -msgstr "SMTP test in progress" - -msgid "Bitte warten. SMTP-Verbindung und Testversand werden geprüft." -msgstr "Please wait. The SMTP connection and test delivery are being checked." - #: workflows/templates/workflows/base_shell.html:24 msgid "Bitte bestätigen" msgstr "" @@ -695,9 +740,21 @@ msgstr "Cancel" msgid "Bestätigen" msgstr "" +#: workflows/templates/workflows/base_shell.html:36 +msgid "Bitte warten" +msgstr "Please wait" + +#: workflows/templates/workflows/base_shell.html:37 +msgid "Aktion läuft" +msgstr "Action in progress" + +#: workflows/templates/workflows/base_shell.html:38 +msgid "Die Aktion wird im aktuellen Tab ausgeführt." +msgstr "The action is running in the current tab." + #: workflows/templates/workflows/form_builder.html:4 #: workflows/templates/workflows/form_builder.html:14 -#: workflows/templates/workflows/home.html:135 +#: workflows/templates/workflows/home.html:153 msgid "Form Builder" msgstr "Form Builder" @@ -760,6 +817,7 @@ msgstr "Label (EN)" #: workflows/templates/workflows/form_builder.html:91 #: workflows/templates/workflows/integrations_setup.html:263 #: workflows/templates/workflows/intro_builder.html:65 +#: workflows/templates/workflows/user_management.html:84 msgid "Aktiv" msgstr "Active" @@ -821,7 +879,7 @@ msgstr "Save field text" #: workflows/templates/workflows/handbook.html:4 #: workflows/templates/workflows/handbook.html:15 -#: workflows/templates/workflows/home.html:145 +#: workflows/templates/workflows/home.html:165 msgid "Handbook" msgstr "Handbook" @@ -944,7 +1002,7 @@ msgstr "" #: workflows/templates/workflows/home.html:4 #: workflows/templates/workflows/home.html:35 -#: workflows/templates/workflows/requests_dashboard.html:295 +#: workflows/templates/workflows/requests_dashboard.html:303 msgid "TUBCO Onboarding & Offboarding Portal" msgstr "TUBCO Onboarding & Offboarding Portal" @@ -969,25 +1027,19 @@ msgstr "" msgid "Rolle:" msgstr "Role:" -#: workflows/templates/workflows/home.html:38 -msgid "Admin" -msgstr "Admin" - -#: workflows/templates/workflows/home.html:38 -msgid "Mitarbeiter" -msgstr "Staff" - #: workflows/templates/workflows/home.html:40 msgid "Nextcloud:" msgstr "Nextcloud:" #: workflows/templates/workflows/home.html:40 #: workflows/templates/workflows/integrations_setup.html:60 +#: workflows/templates/workflows/user_management.html:112 msgid "aktiv" msgstr "active" #: workflows/templates/workflows/home.html:40 #: workflows/templates/workflows/integrations_setup.html:60 +#: workflows/templates/workflows/user_management.html:112 msgid "inaktiv" msgstr "inactive" @@ -1063,13 +1115,13 @@ msgstr "IT return" msgid "Offboarding starten" msgstr "Start offboarding" -#: workflows/templates/workflows/home.html:94 +#: workflows/templates/workflows/home.html:95 #: workflows/templates/workflows/requests_dashboard.html:4 #: workflows/templates/workflows/requests_dashboard.html:33 msgid "Anfragen Dashboard" msgstr "Requests Dashboard" -#: workflows/templates/workflows/home.html:95 +#: workflows/templates/workflows/home.html:96 msgid "" "Status, Suchfunktion, PDF-Links und Verlauf aller Onboarding-/Offboarding-" "Anfragen." @@ -1077,89 +1129,100 @@ msgstr "" "Status, search, PDF links, and history of all onboarding/offboarding " "requests." -#: workflows/templates/workflows/home.html:97 +#: workflows/templates/workflows/home.html:98 msgid "Suche" msgstr "Search" -#: workflows/templates/workflows/home.html:99 +#: workflows/templates/workflows/home.html:100 msgid "PDF Zugriff" msgstr "PDF access" -#: workflows/templates/workflows/home.html:103 +#: workflows/templates/workflows/home.html:104 msgid "Dashboard öffnen" msgstr "Open dashboard" -#: workflows/templates/workflows/home.html:110 +#: workflows/templates/workflows/home.html:112 msgid "Admin Apps" msgstr "Admin Apps" -#: workflows/templates/workflows/home.html:111 +#: workflows/templates/workflows/home.html:113 msgid "Konfiguration, Tests und Steuerung." msgstr "Configuration, tests, and controls." -#: workflows/templates/workflows/home.html:115 +#: workflows/templates/workflows/home.html:118 msgid "Integrationen" msgstr "Integrations" -#: workflows/templates/workflows/home.html:116 +#: workflows/templates/workflows/home.html:119 msgid "Nextcloud- und E-Mail-Setup." msgstr "Nextcloud and email setup." -#: workflows/templates/workflows/home.html:117 -#: workflows/templates/workflows/home.html:122 +#: workflows/templates/workflows/home.html:120 #: workflows/templates/workflows/home.html:127 -#: workflows/templates/workflows/home.html:132 -#: workflows/templates/workflows/home.html:137 -#: workflows/templates/workflows/home.html:142 -#: workflows/templates/workflows/home.html:147 -#: workflows/templates/workflows/home.html:152 +#: workflows/templates/workflows/home.html:134 +#: workflows/templates/workflows/home.html:141 +#: workflows/templates/workflows/home.html:148 +#: workflows/templates/workflows/home.html:155 +#: workflows/templates/workflows/home.html:160 +#: workflows/templates/workflows/home.html:167 +#: workflows/templates/workflows/home.html:174 msgid "Öffnen" msgstr "Open" -#: workflows/templates/workflows/home.html:121 +#: workflows/templates/workflows/home.html:125 +#: workflows/templates/workflows/user_management.html:4 +#: workflows/templates/workflows/user_management.html:14 +msgid "Benutzer & Rollen" +msgstr "Users & roles" + +#: workflows/templates/workflows/home.html:126 +msgid "Benutzer anlegen, Rollen zuweisen und Zugriffe steuern." +msgstr "Create users, assign roles, and control access." + +#: workflows/templates/workflows/home.html:133 msgid "Wichtige Admin-Aktionen nachvollziehen und prüfen." msgstr "" -#: workflows/templates/workflows/home.html:126 +#: workflows/templates/workflows/home.html:140 msgid "Backups erstellen und sicher verifizieren." msgstr "" -#: workflows/templates/workflows/home.html:130 +#: workflows/templates/workflows/home.html:146 #: workflows/templates/workflows/welcome_emails.html:4 msgid "Welcome E-Mails" msgstr "Welcome Emails" -#: workflows/templates/workflows/home.html:131 +#: workflows/templates/workflows/home.html:147 msgid "Geplante Welcome Mails verwalten." msgstr "Manage scheduled welcome emails." -#: workflows/templates/workflows/home.html:136 +#: workflows/templates/workflows/home.html:154 msgid "Felder, Schritte und Optionen verwalten." msgstr "Manage fields, steps, and options." -#: workflows/templates/workflows/home.html:140 +#: workflows/templates/workflows/home.html:158 #: workflows/templates/workflows/intro_builder.html:4 #: workflows/templates/workflows/intro_builder.html:17 msgid "Einweisungs-Builder" msgstr "Introduction Builder" -#: workflows/templates/workflows/home.html:141 +#: workflows/templates/workflows/home.html:159 msgid "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." msgstr "Configure checklist items for the introduction protocol." -#: workflows/templates/workflows/home.html:146 +#: workflows/templates/workflows/home.html:166 msgid "Project wiki and developer documentation in one place." msgstr "Project wiki and developer documentation in one place." -#: workflows/templates/workflows/home.html:150 +#: workflows/templates/workflows/home.html:172 msgid "Django Admin" msgstr "Django Admin" -#: workflows/templates/workflows/home.html:151 +#: workflows/templates/workflows/home.html:173 msgid "Vollständige Datenverwaltung." msgstr "Full data management." -#: workflows/templates/workflows/home.html:158 +#: workflows/templates/workflows/home.html:181 msgid "Tipp: Die letzten Vorgänge sehen Sie jederzeit im Anfragen Dashboard." msgstr "Tip: You can always see the latest requests in the Requests Dashboard." @@ -1210,13 +1273,24 @@ msgstr "Backup target" msgid "Nextcloud speichern" msgstr "Save Nextcloud" +#: workflows/templates/workflows/integrations_setup.html:55 +msgid "Nextcloud-Test läuft" +msgstr "Nextcloud test in progress" + +#: workflows/templates/workflows/integrations_setup.html:55 +msgid "" +"Bitte warten. Verbindung und Upload in das konfigurierte Ziel werden geprüft." +msgstr "" +"Please wait. The connection and upload to the configured target are being " +"checked." + #: workflows/templates/workflows/integrations_setup.html:55 msgid "Nextcloud-Test starten" msgstr "Run Nextcloud test" #: workflows/templates/workflows/integrations_setup.html:59 #: workflows/templates/workflows/integrations_setup.html:121 -#: workflows/templates/workflows/requests_dashboard.html:262 +#: workflows/templates/workflows/requests_dashboard.html:266 msgid "Status:" msgstr "Status:" @@ -1253,6 +1327,14 @@ msgstr "SMTP TLS" msgid "Mail speichern" msgstr "Save mail settings" +#: workflows/templates/workflows/integrations_setup.html:117 +msgid "SMTP-Test läuft" +msgstr "SMTP test in progress" + +#: workflows/templates/workflows/integrations_setup.html:117 +msgid "Bitte warten. SMTP-Verbindung und Testversand werden geprüft." +msgstr "Please wait. The SMTP connection and test delivery are being checked." + #: workflows/templates/workflows/integrations_setup.html:117 msgid "SMTP-Test starten" msgstr "Run SMTP test" @@ -1408,27 +1490,27 @@ msgstr "Remote backup enabled" msgid "Remote Kopie nach lokalem Bundle erstellen" msgstr "Create remote copy after local bundle creation" -#: workflows/templates/workflows/integrations_setup.html:378 +#: workflows/templates/workflows/integrations_setup.html:379 msgid "Remote Backup Zieltyp" msgstr "Remote backup target type" -#: workflows/templates/workflows/integrations_setup.html:386 +#: workflows/templates/workflows/integrations_setup.html:387 msgid "Nextcloud Backup-Verzeichnis" msgstr "Nextcloud backup directory" -#: workflows/templates/workflows/integrations_setup.html:390 +#: workflows/templates/workflows/integrations_setup.html:391 msgid "S3 Bucket (optional)" msgstr "S3 bucket (optional)" -#: workflows/templates/workflows/integrations_setup.html:394 +#: workflows/templates/workflows/integrations_setup.html:395 msgid "NFS Pfad (optional)" msgstr "NFS path (optional)" -#: workflows/templates/workflows/integrations_setup.html:399 +#: workflows/templates/workflows/integrations_setup.html:401 msgid "Backup-Einstellungen speichern" msgstr "Save backup settings" -#: workflows/templates/workflows/integrations_setup.html:401 +#: workflows/templates/workflows/integrations_setup.html:403 msgid "" "Empfehlung: Nextcloud als erstes Remote-Ziel verwenden. S3 und NFS sind als " "Zieltypen vorbereitet, aber noch nicht aktiv implementiert." @@ -1436,7 +1518,7 @@ msgstr "" "Recommendation: use Nextcloud as the first remote target. S3 and NFS are " "prepared as target types but not yet actively implemented." -#: workflows/templates/workflows/integrations_setup.html:402 +#: workflows/templates/workflows/integrations_setup.html:404 msgid "" "Das Backup-Verzeichnis muss getrennt vom normalen Nextcloud Dokumentenordner " "sein, z. B. Group-on-off-boarding-backups." @@ -1692,6 +1774,7 @@ msgstr "Employee" #: workflows/templates/workflows/onboarding_intro_session.html:27 #: workflows/templates/workflows/request_timeline.html:66 +#: workflows/templates/workflows/user_management.html:80 msgid "Name" msgstr "Name" @@ -1704,7 +1787,7 @@ msgid "Dienstliche E-Mail" msgstr "Work email" #: workflows/templates/workflows/onboarding_intro_session.html:31 -#: workflows/views.py:487 +#: workflows/views.py:688 msgid "Vertragsbeginn" msgstr "Contract start" @@ -1776,7 +1859,7 @@ msgid "Live-Protokoll erzeugen" msgstr "Generate live protocol" #: workflows/templates/workflows/onboarding_intro_session.html:94 -#: workflows/templates/workflows/requests_dashboard.html:239 +#: workflows/templates/workflows/requests_dashboard.html:241 msgid "Live-Protokoll öffnen" msgstr "Open live protocol" @@ -1964,6 +2047,7 @@ msgstr "" #: workflows/templates/workflows/request_timeline.html:74 #: workflows/templates/workflows/requests_dashboard.html:190 +#: workflows/templates/workflows/user_management.html:82 msgid "E-Mail" msgstr "Email" @@ -2096,52 +2180,121 @@ msgstr "Introduction" msgid "Noch nicht verfügbar" msgstr "Not available yet" -#: workflows/templates/workflows/requests_dashboard.html:237 +#: workflows/templates/workflows/requests_dashboard.html:238 msgid "Einweisung öffnen" msgstr "Open introduction" -#: workflows/templates/workflows/requests_dashboard.html:244 +#: workflows/templates/workflows/requests_dashboard.html:247 msgid "Standard-Einweisungs-PDF" msgstr "Standard introduction PDF" -#: workflows/templates/workflows/requests_dashboard.html:249 +#: workflows/templates/workflows/requests_dashboard.html:252 msgid "Neu erzeugen" msgstr "Regenerate" -#: workflows/templates/workflows/requests_dashboard.html:251 +#: workflows/templates/workflows/requests_dashboard.html:254 msgid "Standard-PDF öffnen" msgstr "Open standard PDF" -#: workflows/templates/workflows/requests_dashboard.html:255 +#: workflows/templates/workflows/requests_dashboard.html:258 msgid "PDF erzeugen" msgstr "Generate PDF" -#: workflows/templates/workflows/requests_dashboard.html:266 +#: workflows/templates/workflows/requests_dashboard.html:270 msgid "Nicht relevant" msgstr "Not relevant" -#: workflows/templates/workflows/requests_dashboard.html:270 +#: workflows/templates/workflows/requests_dashboard.html:276 msgid "Timeline" msgstr "" -#: workflows/templates/workflows/requests_dashboard.html:272 +#: workflows/templates/workflows/requests_dashboard.html:278 msgid "Eintrag erneut verarbeiten?" msgstr "" -#: workflows/templates/workflows/requests_dashboard.html:274 +#: workflows/templates/workflows/requests_dashboard.html:280 msgid "Erneut versuchen" msgstr "" -#: workflows/templates/workflows/requests_dashboard.html:277 +#: workflows/templates/workflows/requests_dashboard.html:284 #, fuzzy #| msgid "Option wirklich löschen?" msgid "Eintrag wirklich löschen?" msgstr "Delete this option?" -#: workflows/templates/workflows/requests_dashboard.html:286 +#: workflows/templates/workflows/requests_dashboard.html:294 msgid "Noch keine Vorgänge vorhanden." msgstr "No requests available yet." +#: workflows/templates/workflows/user_management.html:15 +msgid "Super Admins verwalten Benutzerkonten, Rollen und den aktiven Zugriff." +msgstr "Super admins manage user accounts, roles, and active access." + +#: workflows/templates/workflows/user_management.html:22 +msgid "Benutzer anlegen" +msgstr "Create user" + +#: workflows/templates/workflows/user_management.html:64 +msgid "Benutzer erstellen" +msgstr "Create user" + +#: workflows/templates/workflows/user_management.html:72 +msgid "Benutzerübersicht" +msgstr "User overview" + +#: workflows/templates/workflows/user_management.html:73 +msgid "Rollen ändern, Zugriffe sperren oder ein neues Passwort setzen." +msgstr "Change roles, block access, or set a new password." + +#: workflows/templates/workflows/user_management.html:85 +msgid "Letzte Anmeldung" +msgstr "Last login" + +#: workflows/templates/workflows/user_management.html:86 +#: workflows/templates/workflows/user_management.html:117 +msgid "Neues Passwort" +msgstr "New password" + +#: workflows/templates/workflows/user_management.html:96 +msgid "Sie selbst" +msgstr "You" + +#: workflows/templates/workflows/user_management.html:118 +msgid "Optional" +msgstr "Optional" + +#: workflows/templates/workflows/user_management.html:124 +msgid "Speichern" +msgstr "Save" + +#: workflows/templates/workflows/user_management.html:128 +msgid "Reset-Link senden" +msgstr "" + +#: workflows/templates/workflows/user_management.html:131 +#, fuzzy +#| msgid "Welcome E-Mails" +msgid "Keine E-Mail" +msgstr "Welcome Emails" + +#: workflows/templates/workflows/user_management.html:136 +#, fuzzy +#| msgid "Option wirklich löschen?" +msgid "Benutzer wirklich löschen?" +msgstr "Delete this option?" + +#: workflows/templates/workflows/user_management.html:143 +msgid "Es sind noch keine Benutzer vorhanden." +msgstr "No users exist yet." + +#: workflows/templates/workflows/user_management.html:149 +msgid "" +"Hinweis: Der aktuell angemeldete Super Admin kann sich hier nicht selbst " +"deaktivieren oder auf eine niedrigere Rolle setzen." +msgstr "" +"Note: The currently signed-in super admin cannot deactivate themselves or " +"assign a lower role here." + #: workflows/templates/workflows/welcome_emails.html:14 msgid "Geplante Welcome E-Mails" msgstr "Scheduled welcome emails" @@ -2228,326 +2381,437 @@ msgstr "Resume" msgid "Keine geplanten Welcome E-Mails vorhanden." msgstr "No scheduled welcome emails available." -#: workflows/views.py:87 +#: workflows/views.py:94 msgid "Person, Rolle, Abteilung" msgstr "Person, role, department" -#: workflows/views.py:88 +#: workflows/views.py:95 msgid "Beschäftigung und Termine" msgstr "Employment and dates" -#: workflows/views.py:89 +#: workflows/views.py:96 msgid "Geräte, Software und Zugänge" msgstr "Devices, software, and access" -#: workflows/views.py:90 +#: workflows/views.py:97 msgid "Notizen und Freigabe" msgstr "Notes and approval" -#: workflows/views.py:191 +#: workflows/views.py:128 workflows/views.py:774 workflows/views.py:779 +msgid "Sie haben keine Berechtigung für diese Aktion." +msgstr "You do not have permission for this action." + +#: workflows/views.py:209 #, fuzzy #| msgid "Vorgänge" msgid "Vorgänge gelöscht" msgstr "Requests" -#: workflows/views.py:192 +#: workflows/views.py:210 msgid "Vorgang gelöscht" msgstr "" -#: workflows/views.py:193 +#: workflows/views.py:211 msgid "Vorgang erneut angestoßen" msgstr "" -#: workflows/views.py:194 +#: workflows/views.py:212 #, fuzzy #| msgid "Einweisung" msgid "Einweisungs-PDF erzeugt" msgstr "Introduction" -#: workflows/views.py:195 +#: workflows/views.py:213 #, fuzzy #| msgid "Live-Protokoll erzeugen" msgid "Live-Protokoll erzeugt" msgstr "Generate live protocol" -#: workflows/views.py:196 +#: workflows/views.py:214 #, fuzzy #| msgid "Einweisung wurde zurückgesetzt." msgid "Einweisung zurückgesetzt" msgstr "Introduction was reset." -#: workflows/views.py:197 +#: workflows/views.py:215 #, fuzzy #| msgid "Einweisung wurde als Entwurf gespeichert." msgid "Einweisung als Entwurf gespeichert" msgstr "Introduction was saved as draft." -#: workflows/views.py:198 +#: workflows/views.py:216 #, fuzzy #| msgid "Einweisung wurde als abgeschlossen gespeichert." msgid "Einweisung abgeschlossen" msgstr "Introduction was saved as completed." -#: workflows/views.py:199 +#: workflows/views.py:217 msgid "Formularoption gelöscht" msgstr "" -#: workflows/views.py:200 +#: workflows/views.py:218 #, fuzzy #| msgid "Optionen speichern" msgid "Formularoptionen gespeichert" msgstr "Save options" -#: workflows/views.py:201 +#: workflows/views.py:219 #, fuzzy #| msgid "Feldtexte speichern" msgid "Feldtexte gespeichert" msgstr "Save field text" -#: workflows/views.py:202 +#: workflows/views.py:220 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Formularlayout gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:203 +#: workflows/views.py:221 msgid "Einweisungs-Checkpunkt gelöscht" msgstr "" -#: workflows/views.py:204 +#: workflows/views.py:222 msgid "Einweisungs-Checkpunkt hinzugefügt" msgstr "" -#: workflows/views.py:205 +#: workflows/views.py:223 #, fuzzy #| msgid "Checkliste speichern" msgid "Einweisungs-Checkliste gespeichert" msgstr "Save checklist" -#: workflows/views.py:206 +#: workflows/views.py:224 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail sofort ausgelöst" msgstr "Welcome Emails" -#: workflows/views.py:207 +#: workflows/views.py:225 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Welcome E-Mail Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:208 +#: workflows/views.py:226 msgid "Welcome E-Mail Sammelaktion ausgeführt" msgstr "" -#: workflows/views.py:209 +#: workflows/views.py:227 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail pausiert" msgstr "Welcome Emails" -#: workflows/views.py:210 +#: workflows/views.py:228 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail fortgesetzt" msgstr "Welcome Emails" -#: workflows/views.py:211 +#: workflows/views.py:229 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail abgebrochen" msgstr "Welcome Emails" -#: workflows/views.py:212 +#: workflows/views.py:230 #, fuzzy #| msgid "SMTP-Test" msgid "SMTP-Test gesendet" msgstr "SMTP test" -#: workflows/views.py:213 +#: workflows/views.py:231 #, fuzzy #| msgid "Nextcloud-Test" msgid "Nextcloud-Testupload ausgeführt" msgstr "Nextcloud test" -#: workflows/views.py:214 +#: workflows/views.py:232 #, fuzzy #| msgid "Nextcloud schalten" msgid "Nextcloud-Modus umgeschaltet" msgstr "Toggle Nextcloud" -#: workflows/views.py:215 +#: workflows/views.py:233 msgid "E-Mail-Modus umgeschaltet" msgstr "" -#: workflows/views.py:216 +#: workflows/views.py:234 #, fuzzy #| msgid "Integrationen Setup" msgid "Integrationen gespeichert" msgstr "Integrations Setup" -#: workflows/views.py:217 +#: workflows/views.py:235 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Nextcloud-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:218 +#: workflows/views.py:236 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Mail-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:219 +#: workflows/views.py:237 #, fuzzy #| msgid "E-Mail Routing & Vorlagen speichern" msgid "E-Mail-Routing gespeichert" msgstr "Save email routing & templates" -#: workflows/views.py:220 +#: workflows/views.py:238 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Benachrichtigungsregeln gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:221 +#: workflows/views.py:239 +#, fuzzy +#| msgid "Anfrage gespeichert" +msgid "Benutzer erstellt" +msgstr "Request saved" + +#: workflows/views.py:240 +msgid "Benutzer aktualisiert" +msgstr "" + +#: workflows/views.py:241 +msgid "Passwort-Reset-Link versendet" +msgstr "" + +#: workflows/views.py:242 +#, fuzzy +#| msgid "Benutzerübersicht" +msgid "Benutzer gelöscht" +msgstr "User overview" + +#: workflows/views.py:243 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup erstellt" msgstr "Request saved" -#: workflows/views.py:222 +#: workflows/views.py:244 msgid "Backup verifiziert" msgstr "" -#: workflows/views.py:223 +#: workflows/views.py:245 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup gelöscht" msgstr "Request saved" -#: workflows/views.py:224 +#: workflows/views.py:246 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Backup-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:408 +#: workflows/views.py:407 +msgid "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." +msgstr "User could not be created. Please check the input." + +#: workflows/views.py:419 +#, python-format +msgid "Benutzer wurde erstellt: %(username)s" +msgstr "User created: %(username)s" + +#: workflows/views.py:437 +msgid "" +"Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren oder " +"herabstufen." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:440 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "" +"Der letzte aktive Super Admin kann nicht deaktiviert oder herabgestuft " +"werden." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:457 +#, python-format +msgid "Benutzer wurde aktualisiert: %(username)s" +msgstr "User updated: %(username)s" + +#: workflows/views.py:468 +msgid "Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt." +msgstr "" + +#: workflows/views.py:477 +#, python-format +msgid "Passwort zurücksetzen für %(username)s" +msgstr "" + +#: workflows/views.py:479 +#, python-format +msgid "" +"Hallo %(name)s,\n" +"\n" +"für Ihr Konto wurde ein Link zum Zurücksetzen des Passworts erstellt.\n" +"Bitte öffnen Sie den folgenden Link:\n" +"%(url)s\n" +"\n" +"Wenn Sie diese Anfrage nicht erwartet haben, können Sie diese E-Mail " +"ignorieren." +msgstr "" + +#: workflows/views.py:498 +#, fuzzy, python-format +#| msgid "Benutzer wurde erstellt: %(username)s" +msgid "Passwort-Reset-Link wurde versendet: %(username)s" +msgstr "User created: %(username)s" + +#: workflows/views.py:509 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "" +"Der aktuell angemeldete Super Admin kann sich hier nicht selbst löschen." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:512 +#, fuzzy +#| msgid "" +#| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " +#| "oder herabstufen." +msgid "Der letzte aktive Super Admin kann nicht gelöscht werden." +msgstr "" +"The currently signed-in super admin cannot lock or downgrade themselves here." + +#: workflows/views.py:525 +#, fuzzy, python-format +#| msgid "Benutzer wurde erstellt: %(username)s" +msgid "Benutzer wurde gelöscht: %(username)s" +msgstr "User created: %(username)s" + +#: workflows/views.py:612 #, python-format msgid "Backup wurde erstellt: %(name)s" msgstr "" -#: workflows/views.py:410 +#: workflows/views.py:614 #, python-format msgid "Backup konnte nicht erstellt werden: %(error)s" msgstr "" -#: workflows/views.py:427 +#: workflows/views.py:630 #, python-format msgid "Backup wurde verifiziert: %(name)s" msgstr "" -#: workflows/views.py:429 +#: workflows/views.py:632 #, python-format msgid "Backup-Verifikation fehlgeschlagen: %(error)s" msgstr "" -#: workflows/views.py:446 +#: workflows/views.py:648 #, python-format msgid "Backup wurde gelöscht: %(name)s" msgstr "" -#: workflows/views.py:448 +#: workflows/views.py:650 #, python-format msgid "Backup konnte nicht gelöscht werden: %(error)s" msgstr "" -#: workflows/views.py:475 +#: workflows/views.py:676 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Anfrage erstellt" msgstr "Request saved" -#: workflows/views.py:477 +#: workflows/views.py:678 #, fuzzy, python-format #| msgid "Sitzungsstatus" msgid "Status: %(status)s" msgstr "Session status" -#: workflows/views.py:489 +#: workflows/views.py:690 #, fuzzy #| msgid "Geplant für" msgid "Geplanter Start" msgstr "Scheduled for" -#: workflows/views.py:499 +#: workflows/views.py:700 msgid "Geräteübergabe / Hardware-Abholung" msgstr "" -#: workflows/views.py:501 +#: workflows/views.py:702 msgid "Geplanter Hardware-Termin" msgstr "" -#: workflows/views.py:510 +#: workflows/views.py:711 #, fuzzy #| msgid "Noch nicht verfügbar" msgid "PDF verfügbar" msgstr "Not available yet" -#: workflows/views.py:536 +#: workflows/views.py:737 #, fuzzy #| msgid "Einweisung" msgid "Einweisungssitzung" msgstr "Introduction" -#: workflows/views.py:548 +#: workflows/views.py:749 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/views.py:574 -msgid "Sie haben keine Berechtigung für diese Aktion." -msgstr "You do not have permission for this action." - -#: workflows/views.py:583 +#: workflows/views.py:788 msgid "Keine Einträge ausgewählt." msgstr "No entries selected." -#: workflows/views.py:626 +#: workflows/views.py:831 #, python-format msgid "%(count)s Eintrag/Einträge gelöscht." msgstr "%(count)s entry/entries deleted." -#: workflows/views.py:628 +#: workflows/views.py:833 #, python-format msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden." msgstr "%(count)s selection(s) could not be processed." -#: workflows/views.py:630 +#: workflows/views.py:835 msgid "Keine passenden Einträge gefunden." msgstr "No matching entries found." -#: workflows/views.py:850 +#: workflows/views.py:1062 msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt." msgstr "Introduction and handover protocol was generated." -#: workflows/views.py:868 +#: workflows/views.py:1079 msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt." msgstr "Introduction protocol from live status was generated." -#: workflows/views.py:898 +#: workflows/views.py:1108 msgid "Einweisung wurde zurückgesetzt." msgstr "Introduction was reset." -#: workflows/views.py:912 +#: workflows/views.py:1122 msgid "Einweisung wurde als abgeschlossen gespeichert." msgstr "Introduction was saved as completed." -#: workflows/views.py:925 +#: workflows/views.py:1135 msgid "Einweisung wurde als Entwurf gespeichert." msgstr "Introduction was saved as draft." +#~ msgid "Backup läuft" +#~ msgstr "Backup in progress" + +#~ msgid "Bitte warten. Die Aktion wird im aktuellen Tab ausgeführt." +#~ msgstr "Please wait. The action is running in the current tab." + #~ msgid "Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen." #~ msgstr "Login failed. Please check your credentials." diff --git a/backend/workflows/apps.py b/backend/workflows/apps.py index 756018c..bf4f6e7 100644 --- a/backend/workflows/apps.py +++ b/backend/workflows/apps.py @@ -4,3 +4,6 @@ from django.apps import AppConfig class WorkflowsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'workflows' + + def ready(self): + from . import signals # noqa: F401 diff --git a/backend/workflows/context_processors.py b/backend/workflows/context_processors.py new file mode 100644 index 0000000..64bad7f --- /dev/null +++ b/backend/workflows/context_processors.py @@ -0,0 +1,5 @@ +from .roles import template_role_context + + +def role_context(request): + return template_role_context(getattr(request, 'user', None)) diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index 2cea892..0c326ad 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -1,11 +1,13 @@ from django import forms from pathlib import Path from datetime import timedelta +from django.contrib.auth import get_user_model from django.utils import timezone from django.utils.translation import get_language, gettext as _ from .form_builder import apply_form_field_config from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, WorkflowConfig +from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')] @@ -96,6 +98,60 @@ HARDWARE_EXTRA_CHOICES = [('Smartphone', 'Smartphone'), ('Anderes', 'Anderes')] SOFTWARE_EXTRA_CHOICES = [('Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)', 'Adobe Acrobat Pro (Abonnement: Zusätzliche Kosten)'), ('Anderes', 'Anderes')] +class UserManagementCreateForm(forms.Form): + first_name = forms.CharField(label=_('Vorname'), max_length=150, required=False) + last_name = forms.CharField(label=_('Nachname'), max_length=150, required=False) + username = forms.CharField(label=_('Benutzername'), max_length=150) + email = forms.EmailField(label=_('E-Mail-Adresse')) + role_key = forms.ChoiceField(label=_('Rolle')) + password1 = forms.CharField(label=_('Passwort'), widget=forms.PasswordInput()) + password2 = forms.CharField(label=_('Passwort bestätigen'), widget=forms.PasswordInput()) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['role_key'].choices = [ + (role_key, str(ROLE_LABELS[role_key])) + for role_key in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF) + ] + + def clean_username(self): + username = (self.cleaned_data.get('username') or '').strip() + user_model = get_user_model() + if user_model.objects.filter(username=username).exists(): + raise forms.ValidationError(_('Dieser Benutzername ist bereits vergeben.')) + return username + + def clean_email(self): + return (self.cleaned_data.get('email') or '').strip().lower() + + def clean_role_key(self): + role_key = (self.cleaned_data.get('role_key') or '').strip() + if role_key not in ROLE_GROUP_NAMES: + raise forms.ValidationError(_('Ungültige Rolle.')) + return role_key + + def clean(self): + cleaned = super().clean() + password1 = cleaned.get('password1') + password2 = cleaned.get('password2') + if password1 and password2 and password1 != password2: + self.add_error('password2', _('Die Passwörter stimmen nicht überein.')) + return cleaned + + def save(self): + user_model = get_user_model() + user = user_model.objects.create_user( + username=self.cleaned_data['username'], + email=self.cleaned_data['email'], + password=self.cleaned_data['password1'], + first_name=self.cleaned_data.get('first_name', ''), + last_name=self.cleaned_data.get('last_name', ''), + is_active=True, + ) + assign_user_role(user, self.cleaned_data['role_key']) + return user + + class OnboardingRequestForm(forms.ModelForm): first_name = forms.CharField(label='Vorname', required=False) last_name = forms.CharField(label='Nachname', required=False) diff --git a/backend/workflows/management/commands/bootstrap_initial_users.py b/backend/workflows/management/commands/bootstrap_initial_users.py index bb6b872..5adfb7f 100644 --- a/backend/workflows/management/commands/bootstrap_initial_users.py +++ b/backend/workflows/management/commands/bootstrap_initial_users.py @@ -1,6 +1,7 @@ from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand +from workflows.roles import ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role, ensure_role_groups DEFAULT_USERS = [ { @@ -43,6 +44,8 @@ class Command(BaseCommand): is_staff=item['is_staff'], is_superuser=item['is_superuser'], ) + ensure_role_groups() + assign_user_role(user, ROLE_SUPER_ADMIN if item['username'] == 'admin_test' else ROLE_STAFF) self.stdout.write(f'created {user.username}') self.stdout.write(self.style.SUCCESS('initial users created')) diff --git a/backend/workflows/roles.py b/backend/workflows/roles.py new file mode 100644 index 0000000..f487490 --- /dev/null +++ b/backend/workflows/roles.py @@ -0,0 +1,127 @@ +from __future__ import annotations + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group +from django.utils.translation import gettext_lazy as _ + +ROLE_SUPER_ADMIN = 'super_admin' +ROLE_ADMIN = 'admin' +ROLE_IT_STAFF = 'it_staff' +ROLE_STAFF = 'staff' + +ROLE_GROUP_NAMES = { + ROLE_SUPER_ADMIN: 'Super Admin', + ROLE_ADMIN: 'Admin', + ROLE_IT_STAFF: 'IT Staff', + ROLE_STAFF: 'Staff', +} + +ROLE_LABELS = { + ROLE_SUPER_ADMIN: _('Super Admin'), + ROLE_ADMIN: _('Admin'), + ROLE_IT_STAFF: _('IT Staff'), + ROLE_STAFF: _('Mitarbeiter'), +} + +CAPABILITIES = { + 'manage_users': {ROLE_SUPER_ADMIN}, + 'access_requests_dashboard': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, + 'run_intro_session': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, + 'generate_intro_pdfs': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, + 'retry_requests': {ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF}, + 'delete_requests': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_integrations': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_welcome_emails': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_builders': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'view_audit_log': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'manage_backups': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'view_docs': {ROLE_SUPER_ADMIN, ROLE_ADMIN}, + 'access_django_admin_link': {ROLE_SUPER_ADMIN}, +} + + +def ensure_role_groups() -> None: + for name in ROLE_GROUP_NAMES.values(): + Group.objects.get_or_create(name=name) + + +def assign_user_role(user, role_key: str) -> None: + ensure_role_groups() + if role_key not in ROLE_GROUP_NAMES: + raise ValueError(f'Unknown role: {role_key}') + + role_groups = Group.objects.filter(name__in=ROLE_GROUP_NAMES.values()) + user.groups.remove(*role_groups) + user.groups.add(Group.objects.get(name=ROLE_GROUP_NAMES[role_key])) + + is_super_admin = role_key == ROLE_SUPER_ADMIN + user.is_staff = is_super_admin + user.is_superuser = is_super_admin + user.save(update_fields=['is_staff', 'is_superuser']) + + +def ensure_bootstrap_role_assignments() -> None: + user_model = get_user_model() + bootstrap_roles = { + 'admin_test': ROLE_SUPER_ADMIN, + 'user_test': ROLE_STAFF, + } + role_group_names = set(ROLE_GROUP_NAMES.values()) + for username, role_key in bootstrap_roles.items(): + try: + user = user_model.objects.get(username=username) + except user_model.DoesNotExist: + continue + if user.groups.filter(name__in=role_group_names).exists(): + continue + assign_user_role(user, role_key) + + +def get_user_role_key(user) -> str: + if not getattr(user, 'is_authenticated', False): + return ROLE_STAFF + if getattr(user, 'is_superuser', False): + return ROLE_SUPER_ADMIN + + group_names = set(user.groups.values_list('name', flat=True)) + for role_key in (ROLE_SUPER_ADMIN, ROLE_ADMIN, ROLE_IT_STAFF, ROLE_STAFF): + if ROLE_GROUP_NAMES[role_key] in group_names: + return role_key + + if getattr(user, 'is_staff', False): + return ROLE_ADMIN + return ROLE_STAFF + + +def get_user_role_label(user) -> str: + return str(ROLE_LABELS[get_user_role_key(user)]) + + +def user_has_capability(user, capability: str) -> bool: + if not getattr(user, 'is_authenticated', False): + return False + if getattr(user, 'is_superuser', False): + return True + allowed_roles = CAPABILITIES.get(capability, set()) + return get_user_role_key(user) in allowed_roles + + +def template_role_context(user) -> dict[str, object]: + role_key = get_user_role_key(user) + return { + 'role_key': role_key, + 'role_label': str(ROLE_LABELS[role_key]), + 'can_manage_users': user_has_capability(user, 'manage_users'), + 'can_access_requests_dashboard': user_has_capability(user, 'access_requests_dashboard'), + 'can_run_intro_session': user_has_capability(user, 'run_intro_session'), + 'can_generate_intro_pdfs': user_has_capability(user, 'generate_intro_pdfs'), + 'can_retry_requests': user_has_capability(user, 'retry_requests'), + 'can_delete_requests': user_has_capability(user, 'delete_requests'), + 'can_manage_integrations': user_has_capability(user, 'manage_integrations'), + 'can_manage_welcome_emails': user_has_capability(user, 'manage_welcome_emails'), + 'can_manage_builders': user_has_capability(user, 'manage_builders'), + 'can_view_audit_log': user_has_capability(user, 'view_audit_log'), + 'can_manage_backups': user_has_capability(user, 'manage_backups'), + 'can_view_docs': user_has_capability(user, 'view_docs'), + 'can_access_django_admin_link': user_has_capability(user, 'access_django_admin_link'), + } diff --git a/backend/workflows/signals.py b/backend/workflows/signals.py new file mode 100644 index 0000000..d761dbb --- /dev/null +++ b/backend/workflows/signals.py @@ -0,0 +1,12 @@ +from django.db.models.signals import post_migrate +from django.dispatch import receiver + +from .roles import ensure_bootstrap_role_assignments, ensure_role_groups + + +@receiver(post_migrate) +def workflows_post_migrate(sender, **kwargs): + if getattr(sender, 'name', '') != 'workflows': + return + ensure_role_groups() + ensure_bootstrap_role_assignments() diff --git a/backend/workflows/static/workflows/css/admin_tools.css b/backend/workflows/static/workflows/css/admin_tools.css index 99d5c76..014198d 100644 --- a/backend/workflows/static/workflows/css/admin_tools.css +++ b/backend/workflows/static/workflows/css/admin_tools.css @@ -1,5 +1,6 @@ body { margin: 0; font-family: Arial, sans-serif; background: #f4f8ff; color: #0f172a; padding: 20px; } [hidden] { display: none !important; } +.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } .shell { max-width: 1100px; margin: 0 auto; background: #fff; border: 1px solid #d8e3f0; border-radius: 14px; padding: 16px; } h1 { margin: 12px 0 6px; color: #000078; } .sub { margin: 0 0 12px; color: #54657c; } diff --git a/backend/workflows/templates/workflows/developer_handbook.html b/backend/workflows/templates/workflows/developer_handbook.html index 0f13616..0e94ede 100644 --- a/backend/workflows/templates/workflows/developer_handbook.html +++ b/backend/workflows/templates/workflows/developer_handbook.html @@ -52,6 +52,7 @@
  • /backend/config/: Django settings, WSGI, URL config
  • /backend/workflows/: application logic, views, models, tasks, templates, static assets
  • /backend/workflows/templates/workflows/base_shell.html: standard page shell for new staff-facing pages
  • +
  • /backend/workflows/roles.py: centralized role names, capability matrix, and template permission helpers
  • Rule: all interactive app pages should extend base_shell.html; do not rebuild topbar/frame logic in page-local templates.
  • /backend/media/templates/: PDF HTML templates and letterhead source files
  • /backend/media/pdfs/: generated PDF outputs on host volume
  • @@ -99,6 +100,19 @@ docker compose exec -T web python manage.py check
  • Fresh boot sequence runs migrations automatically in entrypoint-web.sh.
  • +

    Role and Permission Model

    +
      +
    • Stable Django group names: Super Admin, Admin, IT Staff, Staff.
    • +
    • Groups are created automatically through a post_migrate hook in workflows.signals.
    • +
    • Capability checks are centralized in workflows.roles.CAPABILITIES.
    • +
    • Use _require_capability(...) in views instead of flat is_staff checks.
    • +
    • Templates receive permission flags from workflows.context_processors.role_context.
    • +
    • Super-admin-only user management lives at /admin-tools/users/ and is the preferred path for normal role assignment, account activation, password-reset mail dispatch, and controlled user deletion.
    • +
    • Backward-compatibility rule: authenticated legacy users with is_staff=True but no explicit role group currently fall back to the Admin capability set.
    • +
    • superuser accounts resolve to Super Admin.
    • +
    • When adding a new operational page or action, define the capability in roles.py, gate the view, and hide the UI affordance when the capability is absent.
    • +
    +

    6) Translation Workflow

    Standard Django i18n path

    make i18n-update-en
    diff --git a/backend/workflows/templates/workflows/home.html b/backend/workflows/templates/workflows/home.html
    index dd98d39..568ed9a 100644
    --- a/backend/workflows/templates/workflows/home.html
    +++ b/backend/workflows/templates/workflows/home.html
    @@ -35,7 +35,7 @@
     

    {% trans "TUBCO Onboarding & Offboarding Portal" %}

    {% trans "Zentrale Arbeitsfläche für Anfragen, PDF-Generierung, E-Mail-Workflows und Ablage in Nextcloud." %}

    - {% trans "Rolle:" %} {% if request.user.is_staff %}{% trans "Admin" %}{% else %}{% trans "Mitarbeiter" %}{% endif %} + {% trans "Rolle:" %} {{ role_label }} {% trans "Nextcloud:" %} {% if nextcloud_enabled %}{% trans "aktiv" %}{% else %}{% trans "inaktiv" %}{% endif %} @@ -88,6 +88,7 @@
    + {% if can_access_requests_dashboard %}
    APP
    @@ -103,34 +104,51 @@ {% trans "Dashboard öffnen" %}
    + {% endif %} - {% if request.user.is_staff %} + {% if can_manage_users or can_manage_integrations or can_view_audit_log or can_manage_backups or can_manage_welcome_emails or can_manage_builders or can_view_docs or can_access_django_admin_link %}

    {% trans "Admin Apps" %}

    {% trans "Konfiguration, Tests und Steuerung." %}

    + {% if can_manage_integrations %}

    {% trans "Integrationen" %}

    {% trans "Nextcloud- und E-Mail-Setup." %}

    {% trans "Öffnen" %}
    + {% endif %} + {% if can_manage_users %} +
    +

    {% trans "Benutzer & Rollen" %}

    +

    {% trans "Benutzer anlegen, Rollen zuweisen und Zugriffe steuern." %}

    +{% trans "Öffnen" %} +
    + {% endif %} + {% if can_view_audit_log %}

    {% trans "Audit Log" %}

    {% trans "Wichtige Admin-Aktionen nachvollziehen und prüfen." %}

    {% trans "Öffnen" %}
    + {% endif %} + {% if can_manage_backups %}

    {% trans "Backup & Recovery" %}

    {% trans "Backups erstellen und sicher verifizieren." %}

    {% trans "Öffnen" %}
    + {% endif %} + {% if can_manage_welcome_emails %}

    {% trans "Welcome E-Mails" %}

    {% trans "Geplante Welcome Mails verwalten." %}

    {% trans "Öffnen" %}
    + {% endif %} + {% if can_manage_builders %}

    {% trans "Form Builder" %}

    {% trans "Felder, Schritte und Optionen verwalten." %}

    @@ -141,16 +159,21 @@

    {% trans "Checklistenpunkte für das Einweisungsprotokoll konfigurieren." %}

    {% trans "Öffnen" %}
    + {% endif %} + {% if can_view_docs %}

    {% trans "Handbook" %}

    {% trans "Project wiki and developer documentation in one place." %}

    {% trans "Öffnen" %}
    + {% endif %} + {% if can_access_django_admin_link %}

    {% trans "Django Admin" %}

    {% trans "Vollständige Datenverwaltung." %}

    {% trans "Öffnen" %}
    + {% endif %}
    {% endif %} diff --git a/backend/workflows/templates/workflows/project_wiki.html b/backend/workflows/templates/workflows/project_wiki.html index 6478a96..add71c6 100644 --- a/backend/workflows/templates/workflows/project_wiki.html +++ b/backend/workflows/templates/workflows/project_wiki.html @@ -172,9 +172,13 @@

    9) Admin Apps (Home)

      +
    • Rollenmodell: the app now uses four named roles: Super Admin, Admin, IT Staff, and Staff.
    • +
    • Zugriffslogik: page visibility and critical actions are controlled by capability checks, not only by is_staff.
    • +
    • Fallback-Verhalten: legacy staff users without an explicit role group currently fall back to Admin access so existing operations do not break during rollout.
    • Form Builder: manage field visibility/order/options.
    • Einweisungs-Builder: manage custom checklist items for the intro PDF and live introduction checklist, including section, visibility, and conditional display logic.
    • Integrations: Nextcloud, SMTP, default routing addresses, notification rules, workflow rules, and remote backup target settings.
    • +
    • Benutzer & Rollen: super-admin-only page for creating users, assigning roles, activating/deactivating access, sending password-reset links, and deleting accounts when appropriate.
    • Welcome Emails: scheduled jobs, pause/resume/cancel/trigger now.
    • Audit Log: staff-only trace of important admin changes such as builder edits, settings updates, PDF generation, welcome-email operations, and request deletions. Supports filtering by action, user, and date range.
    • Requests Dashboard: search records, open PDFs, delete records (single/bulk for staff).
    • @@ -223,6 +227,7 @@

      11) Security & Reliability Hardening (Current)

        +
      • Rollen & Berechtigungen: operational areas such as Integrationen, Builders, Audit Log, Backup & Recovery, request delete/retry, and intro-session actions are now protected by capability-based role checks.
      • Cookie + header hardening: HTTPOnly cookies, SameSite cookies, X-Content-Type-Options: nosniff, stricter referrer policy, and frame protection.
      • Optional secure-cookie mode: can be enabled via environment for HTTPS deployments.
      • Upload guards: server-side upload size limits plus signature image magic-byte validation for PNG/JPEG.
      • diff --git a/backend/workflows/templates/workflows/requests_dashboard.html b/backend/workflows/templates/workflows/requests_dashboard.html index 20e6536..ec06e1c 100644 --- a/backend/workflows/templates/workflows/requests_dashboard.html +++ b/backend/workflows/templates/workflows/requests_dashboard.html @@ -167,7 +167,7 @@ - {% if request.user.is_staff %} + {% if can_delete_requests %}
        {% csrf_token %} @@ -184,19 +184,19 @@ - {% if request.user.is_staff %}{% endif %} + {% if can_delete_requests %}{% endif %} -{% if request.user.is_staff %}{% endif %} -{% if request.user.is_staff %}{% endif %} +{% if can_run_intro_session or can_generate_intro_pdfs %}{% endif %} +{% if can_retry_requests or can_delete_requests or can_access_requests_dashboard %}{% endif %} {% for row in rows %} - {% if request.user.is_staff %} + {% if can_delete_requests %} @@ -225,7 +225,7 @@ {% endif %} {% endif %} - {% if request.user.is_staff %} + {% if can_run_intro_session or can_generate_intro_pdfs %} + {% endif %} + {% if can_retry_requests or can_delete_requests or can_access_requests_dashboard %} {% endif %} {% empty %} - + {% endfor %} diff --git a/backend/workflows/templates/workflows/user_management.html b/backend/workflows/templates/workflows/user_management.html new file mode 100644 index 0000000..98b1771 --- /dev/null +++ b/backend/workflows/templates/workflows/user_management.html @@ -0,0 +1,151 @@ +{% extends 'workflows/base_shell.html' %} +{% load static i18n %} + +{% block title %}{% trans "Benutzer & Rollen" %}{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block shell_body %} + {% include 'workflows/includes/app_header.html' with header_show_lang=1 header_show_home=1 header_inside_shell=1 %} +
        +
        +

        {% trans "Benutzer & Rollen" %}

        +

        {% trans "Super Admins verwalten Benutzerkonten, Rollen und den aktiven Zugriff." %}

        +
        +
        + + {% include 'workflows/includes/messages.html' %} + +
        +

        {% trans "Benutzer anlegen" %}

        +
        + {% csrf_token %} +
        +
        + + {{ create_form.first_name }} + {% for error in create_form.first_name.errors %}
        {{ error }}
        {% endfor %} +
        +
        + + {{ create_form.last_name }} + {% for error in create_form.last_name.errors %}
        {{ error }}
        {% endfor %} +
        +
        + + {{ create_form.username }} + {% for error in create_form.username.errors %}
        {{ error }}
        {% endfor %} +
        +
        + + {{ create_form.email }} + {% for error in create_form.email.errors %}
        {{ error }}
        {% endfor %} +
        +
        + + {{ create_form.role_key }} + {% for error in create_form.role_key.errors %}
        {{ error }}
        {% endfor %} +
        +
        + + {{ create_form.password1 }} + {% for error in create_form.password1.errors %}
        {{ error }}
        {% endfor %} +
        +
        + + {{ create_form.password2 }} + {% for error in create_form.password2.errors %}
        {{ error }}
        {% endfor %} +
        +
        + {% for error in create_form.non_field_errors %}
        {{ error }}
        {% endfor %} +
        + +
        + +
        + +
        +
        +
        +

        {% trans "Benutzerübersicht" %}

        +

        {% trans "Rollen ändern, Zugriffe sperren oder ein neues Passwort setzen." %}

        +
        +
        +
        +
        {% trans "Typ" %} {% trans "Person" %} {% trans "E-Mail" %} {% trans "Dokument" %}{% trans "Einweisung" %}{% trans "Aktion" %}{% trans "Einweisung" %}{% trans "Aktion" %}
        {% if row.kind_slug == 'onboarding' %}
        @@ -234,12 +234,15 @@
        {% trans "Live-Protokoll" %}
        + {% if can_run_intro_session %} {% trans "Einweisung öffnen" %} - {% if row.intro_session and row.intro_session.exported_pdf_url %} + {% endif %} + {% if can_run_intro_session and row.intro_session and row.intro_session.exported_pdf_url %} {% trans "Live-Protokoll öffnen" %} {% endif %}
        + {% if can_generate_intro_pdfs %}
        {% trans "Standard-Einweisungs-PDF" %}
        @@ -257,6 +260,7 @@ {% endif %}
        + {% endif %} {% if row.intro_session %}
        {% trans "Status:" %} {{ row.intro_session.get_status_display }}
        @@ -266,24 +270,28 @@ {% trans "Nicht relevant" %} {% endif %}
        {% trans "Timeline" %} - {% if row.status_key == 'failed' %} + {% if can_retry_requests and row.status_key == 'failed' %} {% csrf_token %} {% endif %} + {% if can_delete_requests %}
        {% csrf_token %}
        + {% endif %}
        {% trans "Noch keine Vorgänge vorhanden." %}{% trans "Noch keine Vorgänge vorhanden." %}
        + + + + + + + + + + + + + + {% for row in rows %} + + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
        {% trans "Name" %}{% trans "Benutzername" %}{% trans "E-Mail" %}{% trans "Rolle" %}{% trans "Aktiv" %}{% trans "Letzte Anmeldung" %}{% trans "Neues Passwort" %}{% trans "Aktionen" %}
        + {{ row.display_name|default:row.user.username }} + {% if row.user == request.user %} +
        {% trans "Sie selbst" %}
        + {% endif %} +
        {{ row.user.username }}{{ row.user.email|default:"-" }} + + + + + {{ row.user.last_login|date:"d.m.Y H:i"|default:"-" }} + + + +
        + {% csrf_token %} +
        + + {% if row.user.email %} +
        + {% csrf_token %} + +
        + {% else %} + {% trans "Keine E-Mail" %} + {% endif %} + {% if row.user != request.user %} +
        + {% csrf_token %} + +
        + {% endif %} +
        {% trans "Es sind noch keine Benutzer vorhanden." %}
        +
        +

        {% trans "Hinweis: Der aktuell angemeldete Super Admin kann sich hier nicht selbst deaktivieren oder auf eine niedrigere Rolle setzen." %}

        + +{% endblock %} diff --git a/backend/workflows/urls.py b/backend/workflows/urls.py index 665395b..88062a2 100644 --- a/backend/workflows/urls.py +++ b/backend/workflows/urls.py @@ -30,6 +30,11 @@ urlpatterns = [ path('admin-tools/welcome-emails//resume/', views.resume_welcome_email, name='resume_welcome_email'), path('admin-tools/welcome-emails//cancel/', views.cancel_welcome_email, name='cancel_welcome_email'), path('admin-tools/handbook/', views.handbook_page, name='handbook_page'), + path('admin-tools/users/', views.user_management_page, name='user_management_page'), + path('admin-tools/users/create/', views.create_user_from_admin, name='create_user_from_admin'), + path('admin-tools/users//update/', views.update_user_from_admin, name='update_user_from_admin'), + path('admin-tools/users//send-password-reset/', views.send_password_reset_from_admin, name='send_password_reset_from_admin'), + path('admin-tools/users//delete/', views.delete_user_from_admin, name='delete_user_from_admin'), path('admin-tools/wiki/', views.project_wiki_page, name='project_wiki_page'), path('admin-tools/developer-handbook/', views.developer_handbook_page, name='developer_handbook_page'), path('admin-tools/release-checklist/', views.release_checklist_page, name='release_checklist_page'), diff --git a/backend/workflows/views.py b/backend/workflows/views.py index 19906de..51d475b 100644 --- a/backend/workflows/views.py +++ b/backend/workflows/views.py @@ -2,6 +2,7 @@ from pathlib import Path from datetime import timedelta from tempfile import NamedTemporaryFile import json +from functools import wraps from celery import current_app from django.conf import settings @@ -10,16 +11,21 @@ from django.db import IntegrityError from django.db.models import Q from django.shortcuts import get_object_or_404, redirect, render from django.contrib import messages -from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required +from django.contrib.auth.tokens import default_token_generator from django.http import JsonResponse from django.views.decorators.http import require_POST from django.views.decorators.csrf import ensure_csrf_cookie from django.utils import timezone +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode from django.utils.translation import gettext as _, gettext_lazy from django.utils.translation import get_language, override +from django.urls import reverse from .backup_ops import create_backup_bundle, delete_backup_bundle, list_backup_bundles, verify_backup_bundle -from .forms import OffboardingRequestForm, OnboardingRequestForm +from .forms import OffboardingRequestForm, OnboardingRequestForm, UserManagementCreateForm from .form_builder import ( DEFAULT_FIELD_ORDER, LOCKED_FIELD_RULES, @@ -30,6 +36,7 @@ from .form_builder import ( ) from .models import AdminAuditLog, EmployeeProfile, FormFieldConfig, FormOption, IntroChecklistItem, NotificationRule, NotificationTemplate, OffboardingRequest, OnboardingIntroductionSession, OnboardingRequest, ScheduledWelcomeEmail, SystemEmailConfig, WorkflowConfig from .emailing import send_system_email +from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_SUPER_ADMIN, assign_user_role, get_user_role_key, get_user_role_label, user_has_capability from .services import get_email_test_redirect, is_email_test_mode, is_nextcloud_enabled, upload_to_nextcloud from .tasks import ( DEFAULT_NOTIFICATION_TEMPLATES, @@ -112,8 +119,19 @@ def healthz(request): ) -def _is_staff(user) -> bool: - return user.is_authenticated and user.is_staff +def _require_capability(capability: str): + def decorator(view_func): + @wraps(view_func) + @login_required + def wrapped(request, *args, **kwargs): + if not user_has_capability(request.user, capability): + messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.')) + return redirect('home') + return view_func(request, *args, **kwargs) + + return wrapped + + return decorator def _display_user_name(user) -> str: @@ -218,6 +236,10 @@ def _audit_action_label(action: str) -> str: 'mail_settings_saved': _('Mail-Einstellungen gespeichert'), 'email_routing_saved': _('E-Mail-Routing gespeichert'), 'notification_rules_saved': _('Benachrichtigungsregeln gespeichert'), + 'user_created': _('Benutzer erstellt'), + 'user_updated': _('Benutzer aktualisiert'), + 'user_password_reset_sent': _('Passwort-Reset-Link versendet'), + 'user_deleted': _('Benutzer gelöscht'), 'backup_created': _('Backup erstellt'), 'backup_verified': _('Backup verifiziert'), 'backup_deleted': _('Backup gelöscht'), @@ -311,36 +333,220 @@ def home(request): 'nextcloud_enabled': is_nextcloud_enabled(), 'email_test_mode': is_email_test_mode(), 'workflow_config': config, + 'role_label': get_user_role_label(request.user), }, ) -@login_required -@user_passes_test(_is_staff) +def _user_management_rows(): + user_model = get_user_model() + role_order = { + ROLE_SUPER_ADMIN: 0, + 'admin': 1, + 'it_staff': 2, + 'staff': 3, + } + rows = [] + for user in user_model.objects.all().order_by('-is_active', 'username'): + role_key = get_user_role_key(user) + rows.append( + { + 'user': user, + 'role_key': role_key, + 'role_label': str(ROLE_LABELS[role_key]), + 'role_sort': role_order.get(role_key, 99), + 'display_name': _display_user_name(user), + } + ) + rows.sort(key=lambda item: (not item['user'].is_active, item['role_sort'], item['user'].username.lower())) + return rows + + +def _render_user_management(request, create_form=None, status_code: int = 200): + return render( + request, + 'workflows/user_management.html', + { + 'create_form': create_form or UserManagementCreateForm(), + 'rows': _user_management_rows(), + 'role_choices': [(key, str(ROLE_LABELS[key])) for key in ROLE_GROUP_NAMES], + }, + status=status_code, + ) + + +def _super_admin_user_count() -> int: + user_model = get_user_model() + return sum(1 for user in user_model.objects.all() if get_user_role_key(user) == ROLE_SUPER_ADMIN and user.is_active) + + +def _would_remove_last_super_admin(user, new_role_key: str | None = None, new_is_active: bool | None = None, deleting: bool = False) -> bool: + if get_user_role_key(user) != ROLE_SUPER_ADMIN or not user.is_active: + return False + if _super_admin_user_count() > 1: + return False + if deleting: + return True + if new_role_key is not None and new_role_key != ROLE_SUPER_ADMIN: + return True + if new_is_active is not None and not new_is_active: + return True + return False + + +@_require_capability('manage_users') +def user_management_page(request): + return _render_user_management(request) + + +@_require_capability('manage_users') +@require_POST +def create_user_from_admin(request): + form = UserManagementCreateForm(request.POST) + if not form.is_valid(): + messages.error(request, _('Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben.')) + return _render_user_management(request, create_form=form, status_code=400) + + user = form.save() + _audit( + request, + 'user_created', + target_type='user', + target_id=user.id, + target_label=_display_user_name(user), + details={'username': user.username, 'role': get_user_role_key(user)}, + ) + messages.success(request, _('Benutzer wurde erstellt: %(username)s') % {'username': user.username}) + return redirect('user_management_page') + + +@_require_capability('manage_users') +@require_POST +def update_user_from_admin(request, user_id: int): + user_model = get_user_model() + target_user = get_object_or_404(user_model, id=user_id) + role_key = (request.POST.get('role_key') or '').strip() + is_active = request.POST.get('is_active') == 'on' + new_password = (request.POST.get('new_password') or '').strip() + + if role_key not in ROLE_GROUP_NAMES: + messages.error(request, _('Ungültige Rolle.')) + return redirect('user_management_page') + + if target_user == request.user and (role_key != ROLE_SUPER_ADMIN or not is_active): + messages.error(request, _('Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren oder herabstufen.')) + return redirect('user_management_page') + if _would_remove_last_super_admin(target_user, new_role_key=role_key, new_is_active=is_active): + messages.error(request, _('Der letzte aktive Super Admin kann nicht deaktiviert oder herabgestuft werden.')) + return redirect('user_management_page') + + assign_user_role(target_user, role_key) + target_user.is_active = is_active + if new_password: + target_user.set_password(new_password) + target_user.save() + + _audit( + request, + 'user_updated', + target_type='user', + target_id=target_user.id, + target_label=_display_user_name(target_user), + details={'username': target_user.username, 'role': role_key, 'is_active': is_active, 'password_changed': bool(new_password)}, + ) + messages.success(request, _('Benutzer wurde aktualisiert: %(username)s') % {'username': target_user.username}) + return redirect('user_management_page') + + +@_require_capability('manage_users') +@require_POST +def send_password_reset_from_admin(request, user_id: int): + user_model = get_user_model() + target_user = get_object_or_404(user_model, id=user_id) + email = (target_user.email or '').strip() + if not email: + messages.error(request, _('Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt.')) + return redirect('user_management_page') + + uid = urlsafe_base64_encode(force_bytes(target_user.pk)) + token = default_token_generator.make_token(target_user) + reset_path = reverse('password_reset_confirm', kwargs={'uidb64': uid, 'token': token}) + reset_url = request.build_absolute_uri(reset_path) + + send_system_email( + subject=_('Passwort zurücksetzen für %(username)s') % {'username': target_user.username}, + body=_( + 'Hallo %(name)s,\n\n' + 'für Ihr Konto wurde ein Link zum Zurücksetzen des Passworts erstellt.\n' + 'Bitte öffnen Sie den folgenden Link:\n' + '%(url)s\n\n' + 'Wenn Sie diese Anfrage nicht erwartet haben, können Sie diese E-Mail ignorieren.' + ) % { + 'name': _display_user_name(target_user), + 'url': reset_url, + }, + to=[email], + ) + _audit( + request, + 'user_password_reset_sent', + target_type='user', + target_id=target_user.id, + target_label=_display_user_name(target_user), + details={'username': target_user.username, 'email': email}, + ) + messages.success(request, _('Passwort-Reset-Link wurde versendet: %(username)s') % {'username': target_user.username}) + return redirect('user_management_page') + + +@_require_capability('manage_users') +@require_POST +def delete_user_from_admin(request, user_id: int): + user_model = get_user_model() + target_user = get_object_or_404(user_model, id=user_id) + + if target_user == request.user: + messages.error(request, _('Der aktuell angemeldete Super Admin kann sich hier nicht selbst löschen.')) + return redirect('user_management_page') + if _would_remove_last_super_admin(target_user, deleting=True): + messages.error(request, _('Der letzte aktive Super Admin kann nicht gelöscht werden.')) + return redirect('user_management_page') + + target_label = _display_user_name(target_user) + username = target_user.username + target_user.delete() + _audit( + request, + 'user_deleted', + target_type='user', + target_label=target_label, + details={'username': username}, + ) + messages.success(request, _('Benutzer wurde gelöscht: %(username)s') % {'username': username}) + return redirect('user_management_page') + + +@_require_capability('view_docs') def handbook_page(request): return render(request, 'workflows/handbook.html') -@login_required -@user_passes_test(_is_staff) +@_require_capability('view_docs') def project_wiki_page(request): return render(request, 'workflows/project_wiki.html') -@login_required -@user_passes_test(_is_staff) +@_require_capability('view_docs') def developer_handbook_page(request): return render(request, 'workflows/developer_handbook.html') -@login_required -@user_passes_test(_is_staff) +@_require_capability('view_docs') def release_checklist_page(request): return render(request, 'workflows/release_checklist.html') -@login_required -@user_passes_test(_is_staff) +@_require_capability('view_audit_log') def audit_log_page(request): action = (request.GET.get('action') or '').strip() user_query = (request.GET.get('user') or '').strip() @@ -380,8 +586,7 @@ def audit_log_page(request): ) -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_backups') def backup_recovery_page(request): return render( request, @@ -392,8 +597,7 @@ def backup_recovery_page(request): ) -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_backups') @require_POST def create_backup_from_admin(request): try: @@ -411,8 +615,7 @@ def create_backup_from_admin(request): return redirect('backup_recovery_page') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_backups') @require_POST def verify_backup_from_admin(request, backup_name: str): try: @@ -430,8 +633,7 @@ def verify_backup_from_admin(request, backup_name: str): return redirect('backup_recovery_page') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_backups') @require_POST def delete_backup_from_admin(request, backup_name: str): try: @@ -449,8 +651,7 @@ def delete_backup_from_admin(request, backup_name: str): return redirect('backup_recovery_page') -@login_required -@user_passes_test(_is_staff) +@_require_capability('access_requests_dashboard') def request_timeline_page(request, kind: str, request_id: int): if kind == 'onboarding': obj = get_object_or_404(OnboardingRequest, id=request_id) @@ -569,8 +770,12 @@ def request_timeline_page(request, kind: str, request_id: int): @login_required def requests_dashboard(request): + if not user_has_capability(request.user, 'access_requests_dashboard'): + messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.')) + return redirect('home') + if request.method == 'POST': - if not request.user.is_staff: + if not user_has_capability(request.user, 'delete_requests'): messages.error(request, _('Sie haben keine Berechtigung für diese Aktion.')) return redirect('requests_dashboard') @@ -765,6 +970,13 @@ def requests_dashboard(request): {'value': 'failed', 'label': _request_status_label('failed', language_code)}, ] has_filters = any([search_query, type_filter, status_filter, department_filter, date_from, date_to]) + column_count = 4 + if user_has_capability(request.user, 'delete_requests'): + column_count += 1 + if user_has_capability(request.user, 'run_intro_session') or user_has_capability(request.user, 'generate_intro_pdfs'): + column_count += 1 + if user_has_capability(request.user, 'access_requests_dashboard'): + column_count += 1 return render( request, 'workflows/requests_dashboard.html', @@ -779,6 +991,7 @@ def requests_dashboard(request): 'departments': departments, 'status_choices': status_choices, 'has_filters': has_filters, + 'column_count': column_count, 'onboarding_total': onboarding_total, 'offboarding_total': offboarding_total, 'combined_total': onboarding_total + offboarding_total, @@ -838,8 +1051,7 @@ def onboarding_success(request, request_id: int): return render(request, 'workflows/onboarding_success.html', {'obj': obj, 'pdf_url': pdf_url}) -@login_required -@user_passes_test(_is_staff) +@_require_capability('generate_intro_pdfs') @require_POST def generate_onboarding_intro_pdf(request, request_id: int): obj = get_object_or_404(OnboardingRequest, id=request_id) @@ -851,8 +1063,7 @@ def generate_onboarding_intro_pdf(request, request_id: int): return redirect('requests_dashboard') -@login_required -@user_passes_test(_is_staff) +@_require_capability('generate_intro_pdfs') @require_POST def generate_onboarding_intro_session_pdf(request, request_id: int): onboarding = get_object_or_404(OnboardingRequest, id=request_id) @@ -869,8 +1080,7 @@ def generate_onboarding_intro_session_pdf(request, request_id: int): return redirect('onboarding_intro_session_page', request_id=request_id) -@login_required -@user_passes_test(_is_staff) +@_require_capability('run_intro_session') def onboarding_intro_session_page(request, request_id: int): onboarding = get_object_or_404(OnboardingRequest, id=request_id) session, _created = OnboardingIntroductionSession.objects.get_or_create(onboarding_request=onboarding) @@ -1024,8 +1234,7 @@ def offboarding_success(request, request_id: int): return render(request, 'workflows/offboarding_success.html', {'obj': obj, 'pdf_url': pdf_url}) -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_builders') def form_builder_page(request): language_code = get_language() form_type = request.GET.get('form_type', 'onboarding') @@ -1200,8 +1409,7 @@ def form_builder_page(request): ) -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_builders') def intro_builder_page(request): if request.method == 'POST': delete_id = (request.POST.get('delete_item_id') or '').strip() @@ -1311,8 +1519,7 @@ def intro_builder_page(request): ) -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') def integrations_setup_page(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') kind = (request.GET.get('kind') or 'nextcloud').strip().lower() @@ -1342,8 +1549,7 @@ def integrations_setup_page(request): ) -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_welcome_emails') def welcome_emails_page(request): rows = ScheduledWelcomeEmail.objects.select_related('onboarding_request').order_by('-send_at', '-id')[:200] config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -1373,8 +1579,7 @@ def welcome_emails_page(request): ) -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_welcome_emails') @require_POST def trigger_welcome_email_now(request, schedule_id: int): scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first() @@ -1395,8 +1600,7 @@ def trigger_welcome_email_now(request, schedule_id: int): return redirect('welcome_emails_page') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_welcome_emails') @require_POST def save_welcome_email_settings(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -1498,8 +1702,7 @@ def _parse_selected_schedule_ids(raw: str) -> list[int]: return parsed -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_welcome_emails') @require_POST def bulk_welcome_email_action(request): action = (request.POST.get('bulk_action') or '').strip().lower() @@ -1569,8 +1772,7 @@ def bulk_welcome_email_action(request): return redirect('welcome_emails_page') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_welcome_emails') @require_POST def pause_welcome_email(request, schedule_id: int): scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first() @@ -1589,8 +1791,7 @@ def pause_welcome_email(request, schedule_id: int): return redirect('welcome_emails_page') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_welcome_emails') @require_POST def resume_welcome_email(request, schedule_id: int): scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first() @@ -1612,8 +1813,7 @@ def resume_welcome_email(request, schedule_id: int): return redirect('welcome_emails_page') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_welcome_emails') @require_POST def cancel_welcome_email(request, schedule_id: int): scheduled = ScheduledWelcomeEmail.objects.filter(id=schedule_id).first() @@ -1633,8 +1833,7 @@ def cancel_welcome_email(request, schedule_id: int): return redirect('welcome_emails_page') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_builders') @require_POST def form_builder_save_order(request): try: @@ -1698,8 +1897,7 @@ def form_builder_save_order(request): return JsonResponse({'ok': True, 'saved_count': len(configs)}) -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def send_test_email(request): mode = 'TEST_MODE_ON' if is_email_test_mode() else 'TEST_MODE_OFF' @@ -1718,8 +1916,7 @@ def send_test_email(request): return _redirect_back(request, 'home') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def nextcloud_test_upload(request): filename = f"nextcloud_test_{timezone.now().strftime('%Y%m%d_%H%M%S')}.txt" @@ -1751,8 +1948,7 @@ def nextcloud_test_upload(request): return _redirect_back(request, 'home') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def toggle_nextcloud_enabled(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -1766,8 +1962,7 @@ def toggle_nextcloud_enabled(request): return _redirect_back(request, 'home') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def toggle_email_mode(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -1781,8 +1976,7 @@ def toggle_email_mode(request): return _redirect_back(request, 'home') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def save_integrations_settings(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -1820,8 +2014,7 @@ def save_integrations_settings(request): return redirect('home') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def save_nextcloud_settings(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -1846,8 +2039,7 @@ def save_nextcloud_settings(request): return redirect('/admin-tools/integrations/?kind=nextcloud') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def save_workflow_rules(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -1877,8 +2069,7 @@ def save_workflow_rules(request): return redirect('/admin-tools/integrations/?kind=rules') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def save_backup_settings(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -1930,8 +2121,7 @@ def save_backup_settings(request): return redirect('/admin-tools/integrations/?kind=backup') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def save_mail_settings(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -1971,8 +2161,7 @@ def save_mail_settings(request): return redirect('/admin-tools/integrations/?kind=mail') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def save_email_routing_settings(request): config, _ = WorkflowConfig.objects.get_or_create(name='Default') @@ -2039,8 +2228,7 @@ def save_email_routing_settings(request): return redirect('/admin-tools/integrations/?kind=emails') -@login_required -@user_passes_test(_is_staff) +@_require_capability('manage_integrations') @require_POST def save_notification_rules(request): rule_ids = request.POST.getlist('rule_ids') @@ -2103,8 +2291,7 @@ def save_notification_rules(request): return redirect('/admin-tools/integrations/?kind=emails') -@login_required -@user_passes_test(_is_staff) +@_require_capability('delete_requests') @require_POST def delete_request_from_dashboard(request, kind: str, request_id: int): if kind == 'onboarding': @@ -2122,8 +2309,7 @@ def delete_request_from_dashboard(request, kind: str, request_id: int): return redirect('requests_dashboard') -@login_required -@user_passes_test(_is_staff) +@_require_capability('retry_requests') @require_POST def retry_request_from_dashboard(request, kind: str, request_id: int): if kind == 'onboarding':