diff --git a/backend/locale/en/LC_MESSAGES/django.po b/backend/locale/en/LC_MESSAGES/django.po index 99412a3..2a65f22 100644 --- a/backend/locale/en/LC_MESSAGES/django.po +++ b/backend/locale/en/LC_MESSAGES/django.po @@ -2,14 +2,14 @@ msgid "" msgstr "" "Project-Id-Version: tubco-portal\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2026-03-27 01:45+0000\n" +"POT-Creation-Date: 2026-03-27 01:48+0000\n" "PO-Revision-Date: 2026-03-24 00:00+0000\n" "Language: en\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: workflows/app_registry.py:35 workflows/models.py:389 workflows/models.py:470 +#: workflows/app_registry.py:35 workflows/models.py:409 workflows/models.py:490 #: workflows/templates/workflows/onboarding_form.html:25 #: workflows/templates/workflows/requests_dashboard.html:68 #: workflows/templates/workflows/requests_dashboard.html:131 @@ -36,7 +36,7 @@ msgstr "Multi-step form" msgid "E-Mail Routing" msgstr "Email routing" -#: workflows/app_registry.py:46 workflows/models.py:390 workflows/models.py:471 +#: workflows/app_registry.py:46 workflows/models.py:410 workflows/models.py:491 #: workflows/templates/workflows/requests_dashboard.html:78 #: workflows/templates/workflows/requests_dashboard.html:132 msgid "Offboarding" @@ -263,7 +263,7 @@ msgstr "" msgid "Alle Firmenrollen" msgstr "" -#: workflows/app_registry.py:317 workflows/models.py:166 +#: workflows/app_registry.py:317 workflows/models.py:186 #: workflows/templates/workflows/app_registry.html:44 #: workflows/templates/workflows/app_registry.html:100 msgid "Apps" @@ -273,7 +273,7 @@ msgstr "Apps" msgid "Wählen Sie den gewünschten Prozess." msgstr "Choose the desired process." -#: workflows/app_registry.py:323 workflows/models.py:167 +#: workflows/app_registry.py:323 workflows/models.py:187 #: workflows/templates/workflows/app_registry.html:45 #: workflows/templates/workflows/app_registry.html:96 msgid "Platform Apps" @@ -285,7 +285,7 @@ msgstr "" msgid "Produktweite Konfiguration und Produktsteuerung." msgstr "Configuration, tests, and controls." -#: workflows/app_registry.py:329 workflows/models.py:168 +#: workflows/app_registry.py:329 workflows/models.py:188 #: workflows/templates/workflows/app_registry.html:46 #: workflows/templates/workflows/app_registry.html:98 msgid "Admin Apps" @@ -384,7 +384,7 @@ msgstr "" msgid "Remote Backup in Nextcloud konnte nicht gelöscht werden." msgstr "" -#: workflows/forms.py:106 workflows/forms.py:326 +#: workflows/forms.py:106 workflows/forms.py:399 #: workflows/templates/workflows/account_profile.html:66 #: workflows/templates/workflows/user_management.html:72 #: workflows/templates/workflows/user_management.html:170 @@ -395,77 +395,84 @@ msgstr "" msgid "Passwort" msgstr "Password" -#: workflows/forms.py:109 workflows/forms.py:265 workflows/forms.py:297 +#: workflows/forms.py:109 workflows/forms.py:279 workflows/forms.py:311 +#: workflows/forms.py:357 msgid "TOTP-Code" msgstr "" -#: workflows/forms.py:117 workflows/forms.py:286 workflows/forms.py:319 +#: workflows/forms.py:115 workflows/forms.py:317 workflows/forms.py:363 +msgid "Recovery-Code" +msgstr "" + +#: workflows/forms.py:123 workflows/forms.py:300 workflows/forms.py:343 +#: workflows/forms.py:389 msgid "Der TOTP-Code ist ungültig." msgstr "" -#: workflows/forms.py:118 +#: workflows/forms.py:124 msgid "Bitte geben Sie Ihren TOTP-Code ein." msgstr "" -#: workflows/forms.py:143 workflows/forms.py:207 workflows/forms.py:327 +#: workflows/forms.py:157 workflows/forms.py:221 workflows/forms.py:400 #, fuzzy #| msgid "E-Mail" msgid "E-Mail-Adresse" msgstr "Email" -#: workflows/forms.py:148 workflows/forms.py:167 +#: workflows/forms.py:162 workflows/forms.py:181 #: workflows/templates/workflows/user_management.html:77 #: workflows/templates/workflows/user_management.html:108 msgid "Neues Passwort" msgstr "New password" -#: workflows/forms.py:154 workflows/forms.py:173 +#: workflows/forms.py:168 workflows/forms.py:187 msgid "Neues Passwort bestätigen" msgstr "Confirm new password" -#: workflows/forms.py:162 workflows/forms.py:260 workflows/forms.py:292 +#: workflows/forms.py:176 workflows/forms.py:274 workflows/forms.py:306 +#: workflows/forms.py:352 #, fuzzy #| msgid "Neues Passwort" msgid "Aktuelles Passwort" msgstr "New password" -#: workflows/forms.py:184 workflows/templates/workflows/account_profile.html:36 +#: workflows/forms.py:198 workflows/templates/workflows/account_profile.html:36 #: workflows/templates/workflows/includes/app_header.html:27 msgid "Profilbild" msgstr "" -#: workflows/forms.py:200 +#: workflows/forms.py:214 msgid "Das Profilbild darf maximal 5 MB groß sein." msgstr "" -#: workflows/forms.py:205 workflows/forms.py:324 +#: workflows/forms.py:219 workflows/forms.py:397 #: workflows/templates/workflows/account_profile.html:112 msgid "Vorname" msgstr "" -#: workflows/forms.py:206 workflows/forms.py:325 +#: workflows/forms.py:220 workflows/forms.py:398 #: workflows/templates/workflows/account_profile.html:116 msgid "Nachname" msgstr "" -#: workflows/forms.py:208 +#: workflows/forms.py:222 #: workflows/templates/workflows/account_profile.html:120 msgid "Telefon" msgstr "" -#: workflows/forms.py:209 +#: workflows/forms.py:223 #: workflows/templates/workflows/account_profile.html:124 msgid "Mobil" msgstr "" -#: workflows/forms.py:210 workflows/templates/workflows/account_profile.html:70 +#: workflows/forms.py:224 workflows/templates/workflows/account_profile.html:70 #: workflows/templates/workflows/account_profile.html:128 #, fuzzy #| msgid "Produktion" msgid "Position" msgstr "Production" -#: workflows/forms.py:211 workflows/models.py:350 +#: workflows/forms.py:225 workflows/models.py:370 #: workflows/templates/workflows/account_profile.html:74 #: workflows/templates/workflows/account_profile.html:132 #: workflows/templates/workflows/onboarding_intro_session.html:28 @@ -473,27 +480,35 @@ msgstr "Production" msgid "Abteilung" msgstr "Department" -#: workflows/forms.py:212 +#: workflows/forms.py:226 #: workflows/templates/workflows/account_profile.html:136 msgid "Standort" msgstr "" -#: workflows/forms.py:214 +#: workflows/forms.py:228 #: workflows/templates/workflows/account_profile.html:140 #, fuzzy #| msgid "Einweisung" msgid "Hinweise" msgstr "Introduction" -#: workflows/forms.py:278 workflows/forms.py:310 +#: workflows/forms.py:292 workflows/forms.py:331 workflows/forms.py:377 msgid "Das aktuelle Passwort ist nicht korrekt." msgstr "" -#: workflows/forms.py:284 workflows/forms.py:316 +#: workflows/forms.py:298 msgid "Bitte geben Sie einen gültigen TOTP-Code ein." msgstr "" -#: workflows/forms.py:328 workflows/templates/workflows/account_profile.html:62 +#: workflows/forms.py:339 workflows/forms.py:385 +msgid "Bitte geben Sie einen gültigen TOTP-Code oder Recovery-Code ein." +msgstr "" + +#: workflows/forms.py:346 workflows/forms.py:392 +msgid "Der Recovery-Code ist ungültig." +msgstr "" + +#: workflows/forms.py:401 workflows/templates/workflows/account_profile.html:62 #: workflows/templates/workflows/user_management.html:74 #: workflows/templates/workflows/user_management.html:93 #: workflows/templates/workflows/user_management.html:171 @@ -502,207 +517,207 @@ msgstr "" msgid "Rolle" msgstr "Role:" -#: workflows/forms.py:342 +#: workflows/forms.py:415 msgid "Dieser Benutzername ist bereits vergeben." msgstr "This username is already taken." -#: workflows/forms.py:351 workflows/views.py:987 +#: workflows/forms.py:424 workflows/views.py:1020 msgid "Ungültige Rolle." msgstr "Invalid role." -#: workflows/forms.py:353 workflows/views.py:990 +#: workflows/forms.py:426 workflows/views.py:1023 msgid "Nur Platform Owner dürfen diese Rolle vergeben." msgstr "" -#: workflows/forms.py:392 +#: workflows/forms.py:465 msgid "Portal-Titel" msgstr "Portal title" -#: workflows/forms.py:393 +#: workflows/forms.py:466 msgid "Firmenname" msgstr "Company name" -#: workflows/forms.py:394 +#: workflows/forms.py:467 #, fuzzy #| msgid "Firmenname" msgid "Firmen-Domain" msgstr "Company name" -#: workflows/forms.py:395 +#: workflows/forms.py:468 msgid "Support-E-Mail" msgstr "Support email" -#: workflows/forms.py:396 +#: workflows/forms.py:469 msgid "Absender-Anzeigename" msgstr "" -#: workflows/forms.py:397 +#: workflows/forms.py:470 msgid "Login-Untertitel" msgstr "" -#: workflows/forms.py:398 +#: workflows/forms.py:471 msgid "Footer-Text DE" msgstr "" -#: workflows/forms.py:399 +#: workflows/forms.py:472 msgid "Footer-Text EN" msgstr "" -#: workflows/forms.py:400 +#: workflows/forms.py:473 msgid "Rechtlicher Hinweis DE" msgstr "" -#: workflows/forms.py:401 +#: workflows/forms.py:474 msgid "Rechtlicher Hinweis EN" msgstr "" -#: workflows/forms.py:402 +#: workflows/forms.py:475 msgid "Standardsprache" msgstr "Default language" -#: workflows/forms.py:403 +#: workflows/forms.py:476 msgid "Logo" msgstr "Logo" -#: workflows/forms.py:404 +#: workflows/forms.py:477 msgid "PDF-Briefkopf" msgstr "PDF letterhead" -#: workflows/forms.py:405 +#: workflows/forms.py:478 msgid "Favicon" msgstr "" -#: workflows/forms.py:406 +#: workflows/forms.py:479 #: workflows/templates/workflows/branding_settings.html:89 #: workflows/templates/workflows/branding_settings.html:162 msgid "Primärfarbe" msgstr "Primary color" -#: workflows/forms.py:407 +#: workflows/forms.py:480 #: workflows/templates/workflows/branding_settings.html:90 #: workflows/templates/workflows/branding_settings.html:163 msgid "Sekundärfarbe" msgstr "Secondary color" -#: workflows/forms.py:424 +#: workflows/forms.py:497 msgid "Das Logo darf maximal 5 MB groß sein." msgstr "" -#: workflows/forms.py:432 +#: workflows/forms.py:505 msgid "Der PDF-Briefkopf darf maximal 10 MB groß sein." msgstr "" -#: workflows/forms.py:440 +#: workflows/forms.py:513 msgid "Das Favicon darf maximal 2 MB groß sein." msgstr "" -#: workflows/forms.py:464 +#: workflows/forms.py:537 #, fuzzy #| msgid "Firmenname" msgid "Rechtlicher Firmenname" msgstr "Company name" -#: workflows/forms.py:465 +#: workflows/forms.py:538 msgid "Straße und Hausnummer" msgstr "" -#: workflows/forms.py:466 +#: workflows/forms.py:539 msgid "Postleitzahl" msgstr "" -#: workflows/forms.py:467 +#: workflows/forms.py:540 msgid "Stadt" msgstr "" -#: workflows/forms.py:468 +#: workflows/forms.py:541 msgid "Land" msgstr "" -#: workflows/forms.py:469 workflows/templates/workflows/base_shell.html:64 +#: workflows/forms.py:542 workflows/templates/workflows/base_shell.html:64 msgid "Website" msgstr "" -#: workflows/forms.py:470 +#: workflows/forms.py:543 msgid "Impressum-URL" msgstr "" -#: workflows/forms.py:471 +#: workflows/forms.py:544 msgid "Datenschutz-URL" msgstr "" -#: workflows/forms.py:472 +#: workflows/forms.py:545 msgid "HR-Kontakt" msgstr "" -#: workflows/forms.py:473 +#: workflows/forms.py:546 msgid "IT-Kontakt" msgstr "" -#: workflows/forms.py:474 +#: workflows/forms.py:547 #, fuzzy #| msgid "Operations" msgid "Operations-Kontakt" msgstr "Operations" -#: workflows/forms.py:475 +#: workflows/forms.py:548 msgid "Zentrale Telefonnummer" msgstr "" -#: workflows/forms.py:476 +#: workflows/forms.py:549 msgid "USt-IdNr." msgstr "" -#: workflows/forms.py:477 +#: workflows/forms.py:550 msgid "Register- oder Handelsnummer" msgstr "" -#: workflows/forms.py:494 +#: workflows/forms.py:567 msgid "Trial-Modus aktiv" msgstr "" -#: workflows/forms.py:495 +#: workflows/forms.py:568 msgid "Trial-Beginn" msgstr "" -#: workflows/forms.py:496 +#: workflows/forms.py:569 msgid "Trial-Ende" msgstr "" -#: workflows/forms.py:497 +#: workflows/forms.py:570 msgid "Produktive Integrationen begrenzen" msgstr "" -#: workflows/forms.py:498 +#: workflows/forms.py:571 msgid "Cleanup nach Ablauf zulassen" msgstr "" -#: workflows/forms.py:499 +#: workflows/forms.py:572 msgid "Banner-Text DE" msgstr "" -#: workflows/forms.py:500 +#: workflows/forms.py:573 msgid "Banner-Text EN" msgstr "" -#: workflows/forms.py:520 +#: workflows/forms.py:593 msgid "Bitte ein Trial-Ende festlegen." msgstr "" -#: workflows/forms.py:522 +#: workflows/forms.py:595 msgid "Das Trial-Ende muss nach dem Trial-Beginn liegen." msgstr "" -#: workflows/forms.py:661 workflows/forms.py:846 +#: workflows/forms.py:734 workflows/forms.py:919 #, python-format msgid "Bitte nutzen Sie das Format name@%(domain)s." msgstr "" -#: workflows/forms.py:683 workflows/forms.py:860 +#: workflows/forms.py:756 workflows/forms.py:933 #, python-format msgid "Bitte verwenden Sie eine @%(domain)s E-Mail-Adresse." msgstr "" -#: workflows/forms.py:768 +#: workflows/forms.py:841 #, python-format msgid "" "Das Übergabedatum muss mindestens %(days)s Tage in der Zukunft liegen " @@ -754,251 +769,251 @@ msgid "" "ausführen." msgstr "" -#: workflows/models.py:215 workflows/views.py:460 +#: workflows/models.py:235 workflows/views.py:493 #, fuzzy #| msgid "Gesamtbestand" msgid "Gestartet" msgstr "Total records" -#: workflows/models.py:216 workflows/views.py:460 +#: workflows/models.py:236 workflows/views.py:493 #, fuzzy #| msgid "Eingereicht" msgid "Erfolgreich" msgstr "Submitted" -#: workflows/models.py:217 workflows/models.py:270 workflows/models.py:524 +#: workflows/models.py:237 workflows/models.py:290 workflows/models.py:544 #: workflows/templates/workflows/backup_recovery.html:102 #: workflows/templates/workflows/requests_dashboard.html:222 -#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:286 -#: workflows/views.py:460 +#: workflows/templates/workflows/welcome_emails.html:108 workflows/views.py:319 +#: workflows/views.py:493 msgid "Fehlgeschlagen" msgstr "Failed" -#: workflows/models.py:267 workflows/views.py:283 +#: workflows/models.py:287 workflows/views.py:316 msgid "Eingereicht" msgstr "Submitted" -#: workflows/models.py:268 workflows/views.py:284 +#: workflows/models.py:288 workflows/views.py:317 msgid "In Bearbeitung" msgstr "Processing" -#: workflows/models.py:269 workflows/models.py:584 workflows/views.py:285 +#: workflows/models.py:289 workflows/models.py:604 workflows/views.py:318 msgid "Abgeschlossen" msgstr "Completed" -#: workflows/models.py:277 +#: workflows/models.py:297 msgid "Herr" msgstr "" -#: workflows/models.py:277 +#: workflows/models.py:297 msgid "Frau" msgstr "" -#: workflows/models.py:277 +#: workflows/models.py:297 msgid "Divers" msgstr "" -#: workflows/models.py:287 +#: workflows/models.py:307 msgid "befristet" msgstr "" -#: workflows/models.py:287 +#: workflows/models.py:307 msgid "unbefristet" msgstr "" -#: workflows/models.py:351 +#: workflows/models.py:371 msgid "Geräte" msgstr "" -#: workflows/models.py:352 +#: workflows/models.py:372 msgid "Software" msgstr "" -#: workflows/models.py:353 +#: workflows/models.py:373 #, fuzzy #| msgid "Vorgänge" msgid "Zugänge" msgstr "Requests" -#: workflows/models.py:354 +#: workflows/models.py:374 msgid "Workspace-Gruppen" msgstr "" -#: workflows/models.py:355 +#: workflows/models.py:375 msgid "Ressourcen" msgstr "" -#: workflows/models.py:356 +#: workflows/models.py:376 msgid "Telefonnummern" msgstr "" -#: workflows/models.py:382 +#: workflows/models.py:402 msgid "Automatisch" msgstr "" -#: workflows/models.py:383 workflows/views.py:102 +#: workflows/models.py:403 workflows/views.py:103 msgid "Stammdaten" msgstr "Master data" -#: workflows/models.py:384 workflows/views.py:103 +#: workflows/models.py:404 workflows/views.py:104 msgid "Vertrag" msgstr "Contract" -#: workflows/models.py:385 workflows/views.py:104 +#: workflows/models.py:405 workflows/views.py:105 msgid "IT-Setup" msgstr "IT setup" -#: workflows/models.py:386 workflows/views.py:105 +#: workflows/models.py:406 workflows/views.py:106 msgid "Abschluss" msgstr "Finish" -#: workflows/models.py:428 +#: workflows/models.py:448 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: IT" msgstr "Onboarding" -#: workflows/models.py:429 +#: workflows/models.py:449 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Onboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:430 +#: workflows/models.py:450 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Visitenkarte" msgstr "Start onboarding" -#: workflows/models.py:431 +#: workflows/models.py:451 #, fuzzy #| msgid "Onboarding" msgid "Onboarding: HR Works" msgstr "Onboarding" -#: workflows/models.py:432 +#: workflows/models.py:452 #, fuzzy #| msgid "Onboarding starten" msgid "Onboarding: Schlüssel" msgstr "Start onboarding" -#: workflows/models.py:433 +#: workflows/models.py:453 msgid "Onboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:434 +#: workflows/models.py:454 #, fuzzy #| msgid "Welcome E-Mails" msgid "Onboarding: Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/models.py:435 +#: workflows/models.py:455 #, fuzzy #| msgid "Offboarding" msgid "Offboarding: IT" msgstr "Offboarding" -#: workflows/models.py:436 +#: workflows/models.py:456 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Offboarding: Allgemeine Info" msgstr "Save offboarding request" -#: workflows/models.py:437 +#: workflows/models.py:457 #, fuzzy #| msgid "Offboarding starten" msgid "Offboarding: HR Works Deaktivierung" msgstr "Start offboarding" -#: workflows/models.py:438 +#: workflows/models.py:458 msgid "Offboarding: Referenz Anfordernde Person" msgstr "" -#: workflows/models.py:474 +#: workflows/models.py:494 msgid "Immer" msgstr "" -#: workflows/models.py:475 workflows/models.py:553 +#: workflows/models.py:495 workflows/models.py:573 msgid "Enthält" msgstr "" -#: workflows/models.py:476 workflows/models.py:554 +#: workflows/models.py:496 workflows/models.py:574 msgid "Ist gleich" msgstr "" -#: workflows/models.py:477 +#: workflows/models.py:497 msgid "Ist aktiv/Ja" msgstr "" -#: workflows/models.py:478 +#: workflows/models.py:498 #, fuzzy #| msgid "inaktiv" msgid "Ist inaktiv/Nein" msgstr "inactive" -#: workflows/models.py:520 +#: workflows/models.py:540 #: workflows/templates/workflows/welcome_emails.html:100 msgid "Geplant" msgstr "Scheduled" -#: workflows/models.py:521 +#: workflows/models.py:541 #: workflows/templates/workflows/welcome_emails.html:102 msgid "Pausiert" msgstr "Paused" -#: workflows/models.py:522 +#: workflows/models.py:542 #: workflows/templates/workflows/welcome_emails.html:104 msgid "Abgebrochen" msgstr "Cancelled" -#: workflows/models.py:523 +#: workflows/models.py:543 #: workflows/templates/workflows/welcome_emails.html:106 msgid "Gesendet" msgstr "Sent" -#: workflows/models.py:546 workflows/tasks.py:600 +#: workflows/models.py:566 workflows/tasks.py:600 msgid "Geräte und Arbeitsplatz" msgstr "Devices and workplace" -#: workflows/models.py:547 workflows/tasks.py:601 +#: workflows/models.py:567 workflows/tasks.py:601 msgid "Konten und Berechtigungen" msgstr "Accounts and permissions" -#: workflows/models.py:548 workflows/tasks.py:602 +#: workflows/models.py:568 workflows/tasks.py:602 msgid "Software und Tools" msgstr "Software and tools" -#: workflows/models.py:549 workflows/tasks.py:603 +#: workflows/models.py:569 workflows/tasks.py:603 msgid "Prozesse und Hinweise" msgstr "Processes and notes" -#: workflows/models.py:552 +#: workflows/models.py:572 msgid "Immer anzeigen" msgstr "Always show" -#: workflows/models.py:555 +#: workflows/models.py:575 msgid "Ist Ja / aktiv" msgstr "Is yes / active" -#: workflows/models.py:556 +#: workflows/models.py:576 msgid "Ist Nein / inaktiv" msgstr "Is no / inactive" -#: workflows/models.py:583 +#: workflows/models.py:603 msgid "Entwurf" msgstr "Draft" -#: workflows/models.py:603 +#: workflows/models.py:623 #, fuzzy #| msgid "Nextcloud:" msgid "Nextcloud" msgstr "Nextcloud:" -#: workflows/models.py:604 +#: workflows/models.py:624 msgid "S3" msgstr "" -#: workflows/models.py:605 +#: workflows/models.py:625 msgid "NFS" msgstr "" @@ -1133,7 +1148,7 @@ msgid "" msgstr "" #: workflows/templates/registration/login.html:37 -#: workflows/templates/workflows/auth/login.html:39 +#: workflows/templates/workflows/auth/login.html:43 msgid "Anmelden" msgstr "Sign in" @@ -1318,7 +1333,7 @@ msgid "Direkte Aktionen für Ihr Workdock-Konto." msgstr "Direct actions for your Workdock account." #: workflows/templates/workflows/account_profile.html:179 -#: workflows/templates/workflows/account_profile.html:278 +#: workflows/templates/workflows/account_profile.html:315 #: workflows/templates/workflows/auth/password_change_form.html:4 #: workflows/templates/workflows/auth/password_change_form.html:17 #: workflows/templates/workflows/includes/app_header.html:48 @@ -1392,31 +1407,45 @@ msgstr "" msgid "TOTP deaktivieren" msgstr "Enable" -#: workflows/templates/workflows/account_profile.html:247 +#: workflows/templates/workflows/account_profile.html:259 +msgid "Recovery-Codes neu erzeugen" +msgstr "" + +#: workflows/templates/workflows/account_profile.html:268 #, fuzzy #| msgid "Onboarding starten" msgid "Manueller Schlüssel" msgstr "Start onboarding" -#: workflows/templates/workflows/account_profile.html:251 +#: workflows/templates/workflows/account_profile.html:272 #, fuzzy #| msgid "Setup Mail" msgid "Setup-Link" msgstr "Setup Mail" -#: workflows/templates/workflows/account_profile.html:255 +#: workflows/templates/workflows/account_profile.html:276 msgid "" "Wenn Ihre App keinen QR-Code scannen kann, tragen Sie den Schlüssel oder den " "otpauth-Link manuell ein." msgstr "" -#: workflows/templates/workflows/account_profile.html:271 +#: workflows/templates/workflows/account_profile.html:292 #, fuzzy #| msgid "Aktivieren" msgid "TOTP aktivieren" msgstr "Enable" -#: workflows/templates/workflows/account_profile.html:281 +#: workflows/templates/workflows/account_profile.html:301 +msgid "Recovery-Codes" +msgstr "" + +#: workflows/templates/workflows/account_profile.html:302 +msgid "" +"Diese Codes werden nur jetzt im Klartext angezeigt. Jeden Code können Sie " +"genau einmal verwenden." +msgstr "" + +#: workflows/templates/workflows/account_profile.html:318 #: workflows/templates/workflows/includes/app_header.html:51 msgid "Abmelden" msgstr "Log out" @@ -1731,6 +1760,10 @@ msgstr "" msgid "Nur erforderlich, wenn TOTP für Ihr Konto aktiviert ist." msgstr "" +#: workflows/templates/workflows/auth/login.html:41 +msgid "Alternativ können Sie einen einmaligen Recovery-Code verwenden." +msgstr "" + #: workflows/templates/workflows/auth/password_change_done.html:4 #: workflows/templates/workflows/auth/password_change_done.html:17 #: workflows/templates/workflows/user_management.html:174 @@ -2877,7 +2910,7 @@ msgid "Dienstliche E-Mail" msgstr "Work email" #: workflows/templates/workflows/onboarding_intro_session.html:31 -#: workflows/views.py:1242 +#: workflows/views.py:1275 msgid "Vertragsbeginn" msgstr "Contract start" @@ -3681,302 +3714,312 @@ msgstr "Resume" msgid "Keine geplanten Welcome E-Mails vorhanden." msgstr "No scheduled welcome emails available." -#: workflows/views.py:102 +#: workflows/views.py:103 msgid "Person, Rolle, Abteilung" msgstr "Person, role, department" -#: workflows/views.py:103 +#: workflows/views.py:104 msgid "Beschäftigung und Termine" msgstr "Employment and dates" -#: workflows/views.py:104 +#: workflows/views.py:105 msgid "Geräte, Software und Zugänge" msgstr "Devices, software, and access" -#: workflows/views.py:105 +#: workflows/views.py:106 msgid "Notizen und Freigabe" msgstr "Notes and approval" -#: workflows/views.py:154 +#: workflows/views.py:158 #, fuzzy #| msgid "Lokal gespeichert" msgid "Profilbild gespeichert." msgstr "Stored locally" -#: workflows/views.py:156 +#: workflows/views.py:160 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Profilbild konnte nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:162 +#: workflows/views.py:166 #, fuzzy #| msgid "Lokal gespeichert" msgid "Profildaten gespeichert." msgstr "Stored locally" -#: workflows/views.py:164 +#: workflows/views.py:168 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "Profildaten konnten nicht gespeichert werden." msgstr "Password could not be saved" -#: workflows/views.py:171 +#: workflows/views.py:177 #, fuzzy #| msgid "Deaktivieren" msgid "TOTP wurde aktiviert." msgstr "Disabled" -#: workflows/views.py:173 +#: workflows/views.py:179 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "TOTP konnte nicht aktiviert werden." msgstr "Password could not be saved" -#: workflows/views.py:180 +#: workflows/views.py:186 msgid "TOTP wurde deaktiviert." msgstr "" -#: workflows/views.py:182 +#: workflows/views.py:188 #, fuzzy #| msgid "Passwort konnte nicht gespeichert werden" msgid "TOTP konnte nicht deaktiviert werden." msgstr "Password could not be saved" -#: workflows/views.py:212 workflows/views.py:1328 workflows/views.py:1333 +#: workflows/views.py:197 +msgid "Recovery-Codes wurden neu erzeugt." +msgstr "" + +#: workflows/views.py:199 +#, fuzzy +#| msgid "Passwort konnte nicht gespeichert werden" +msgid "Recovery-Codes konnten nicht neu erzeugt werden." +msgstr "Password could not be saved" + +#: workflows/views.py:245 workflows/views.py:1361 workflows/views.py:1366 msgid "Sie haben keine Berechtigung für diese Aktion." msgstr "You do not have permission for this action." -#: workflows/views.py:293 +#: workflows/views.py:326 #, fuzzy #| msgid "Vorgänge" msgid "Vorgänge gelöscht" msgstr "Requests" -#: workflows/views.py:294 +#: workflows/views.py:327 msgid "Vorgang gelöscht" msgstr "" -#: workflows/views.py:295 +#: workflows/views.py:328 msgid "Vorgang erneut angestoßen" msgstr "" -#: workflows/views.py:296 +#: workflows/views.py:329 #, fuzzy #| msgid "Einweisung" msgid "Einweisungs-PDF erzeugt" msgstr "Introduction" -#: workflows/views.py:297 +#: workflows/views.py:330 #, fuzzy #| msgid "Live-Protokoll erzeugen" msgid "Live-Protokoll erzeugt" msgstr "Generate live protocol" -#: workflows/views.py:298 +#: workflows/views.py:331 #, fuzzy #| msgid "Einweisung wurde zurückgesetzt." msgid "Einweisung zurückgesetzt" msgstr "Introduction was reset." -#: workflows/views.py:299 +#: workflows/views.py:332 #, fuzzy #| msgid "Einweisung wurde als Entwurf gespeichert." msgid "Einweisung als Entwurf gespeichert" msgstr "Introduction was saved as draft." -#: workflows/views.py:300 +#: workflows/views.py:333 #, fuzzy #| msgid "Einweisung wurde als abgeschlossen gespeichert." msgid "Einweisung abgeschlossen" msgstr "Introduction was saved as completed." -#: workflows/views.py:301 +#: workflows/views.py:334 msgid "Formularoption gelöscht" msgstr "" -#: workflows/views.py:302 +#: workflows/views.py:335 #, fuzzy #| msgid "Optionen speichern" msgid "Formularoptionen gespeichert" msgstr "Save options" -#: workflows/views.py:303 +#: workflows/views.py:336 #, fuzzy #| msgid "Feldtexte speichern" msgid "Feldtexte gespeichert" msgstr "Save field text" -#: workflows/views.py:304 +#: workflows/views.py:337 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Formularlayout gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:305 +#: workflows/views.py:338 msgid "Einweisungs-Checkpunkt gelöscht" msgstr "" -#: workflows/views.py:306 +#: workflows/views.py:339 msgid "Einweisungs-Checkpunkt hinzugefügt" msgstr "" -#: workflows/views.py:307 +#: workflows/views.py:340 #, fuzzy #| msgid "Checkliste speichern" msgid "Einweisungs-Checkliste gespeichert" msgstr "Save checklist" -#: workflows/views.py:308 +#: workflows/views.py:341 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail sofort ausgelöst" msgstr "Welcome Emails" -#: workflows/views.py:309 +#: workflows/views.py:342 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Welcome E-Mail Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:310 +#: workflows/views.py:343 msgid "Welcome E-Mail Sammelaktion ausgeführt" msgstr "" -#: workflows/views.py:311 +#: workflows/views.py:344 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail pausiert" msgstr "Welcome Emails" -#: workflows/views.py:312 +#: workflows/views.py:345 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail fortgesetzt" msgstr "Welcome Emails" -#: workflows/views.py:313 +#: workflows/views.py:346 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail abgebrochen" msgstr "Welcome Emails" -#: workflows/views.py:314 +#: workflows/views.py:347 #, fuzzy #| msgid "SMTP-Test" msgid "SMTP-Test gesendet" msgstr "SMTP test" -#: workflows/views.py:315 +#: workflows/views.py:348 #, fuzzy #| msgid "Nextcloud-Test" msgid "Nextcloud-Testupload ausgeführt" msgstr "Nextcloud test" -#: workflows/views.py:316 +#: workflows/views.py:349 #, fuzzy #| msgid "Nextcloud schalten" msgid "Nextcloud-Modus umgeschaltet" msgstr "Toggle Nextcloud" -#: workflows/views.py:317 +#: workflows/views.py:350 msgid "E-Mail-Modus umgeschaltet" msgstr "" -#: workflows/views.py:318 +#: workflows/views.py:351 #, fuzzy #| msgid "Integrationen Setup" msgid "Integrationen gespeichert" msgstr "Integrations Setup" -#: workflows/views.py:319 +#: workflows/views.py:352 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Nextcloud-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:320 +#: workflows/views.py:353 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Mail-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:321 +#: workflows/views.py:354 #, fuzzy #| msgid "E-Mail Routing & Vorlagen speichern" msgid "E-Mail-Routing gespeichert" msgstr "Save email routing & templates" -#: workflows/views.py:322 +#: workflows/views.py:355 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Benachrichtigungsregeln gespeichert" msgstr "Save offboarding request" -#: workflows/views.py:323 +#: workflows/views.py:356 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Benutzer erstellt" msgstr "Request saved" -#: workflows/views.py:324 +#: workflows/views.py:357 msgid "Benutzer aktualisiert" msgstr "" -#: workflows/views.py:325 +#: workflows/views.py:358 msgid "Passwort-Reset-Link versendet" msgstr "" -#: workflows/views.py:326 +#: workflows/views.py:359 #, fuzzy #| msgid "Benutzerübersicht" msgid "Benutzer gelöscht" msgstr "User overview" -#: workflows/views.py:327 +#: workflows/views.py:360 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup erstellt" msgstr "Request saved" -#: workflows/views.py:328 +#: workflows/views.py:361 msgid "Backup verifiziert" msgstr "" -#: workflows/views.py:329 +#: workflows/views.py:362 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Backup gelöscht" msgstr "Request saved" -#: workflows/views.py:330 +#: workflows/views.py:363 #, fuzzy #| msgid "Welcome-Einstellungen speichern" msgid "Backup-Einstellungen gespeichert" msgstr "Save welcome settings" -#: workflows/views.py:331 +#: workflows/views.py:364 #, fuzzy #| msgid "Anfrage gespeichert" msgid "App-Registry gespeichert" msgstr "Request saved" -#: workflows/views.py:503 +#: workflows/views.py:536 #, fuzzy #| msgid "Anfrage gespeichert" msgid "App-Registry gespeichert." msgstr "Request saved" -#: workflows/views.py:602 +#: workflows/views.py:635 msgid "Für diesen Benutzer ist keine E-Mail-Adresse hinterlegt." msgstr "" -#: workflows/views.py:611 +#: workflows/views.py:644 #, python-format msgid "Zugangseinladung für %(username)s" msgstr "" -#: workflows/views.py:613 +#: workflows/views.py:646 #, python-format msgid "" "Hallo %(name)s,\n" @@ -3989,12 +4032,12 @@ msgid "" "Ihrem Administrator." msgstr "" -#: workflows/views.py:624 +#: workflows/views.py:657 #, python-format msgid "Passwort zurücksetzen für %(username)s" msgstr "" -#: workflows/views.py:626 +#: workflows/views.py:659 #, python-format msgid "" "Hallo %(name)s,\n" @@ -4007,7 +4050,7 @@ msgid "" "ignorieren." msgstr "" -#: workflows/views.py:677 +#: workflows/views.py:710 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4015,69 +4058,69 @@ msgid "" "Branding konnte nicht gespeichert werden. Bitte prüfen Sie die Eingaben." msgstr "User could not be created. Please check the input." -#: workflows/views.py:705 +#: workflows/views.py:738 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Portal-Branding wurde gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:722 +#: workflows/views.py:755 msgid "Identität" msgstr "" -#: workflows/views.py:723 +#: workflows/views.py:756 msgid "Titel, Firmenname und zentrale Spracheinstellungen." msgstr "" -#: workflows/views.py:727 +#: workflows/views.py:760 msgid "" "Wird für E-Mail-Vorschläge und Domain-bezogene Standardtexte verwendet, z. " "B. tub.co." msgstr "" -#: workflows/views.py:732 +#: workflows/views.py:765 msgid "Farben & Erscheinungsbild" msgstr "" -#: workflows/views.py:733 +#: workflows/views.py:766 msgid "Zentrale visuelle Markenwerte und Browser-Icon." msgstr "" -#: workflows/views.py:737 +#: workflows/views.py:770 msgid "Erlaubte Formate: SVG, PNG, JPG, JPEG, WEBP. Maximal 5 MB." msgstr "" -#: workflows/views.py:738 +#: workflows/views.py:771 msgid "Erlaubte Formate: ICO, PNG, SVG, WEBP. Maximal 2 MB." msgstr "" -#: workflows/views.py:743 +#: workflows/views.py:776 #, fuzzy #| msgid "Produktion" msgid "Kommunikation" msgstr "Production" -#: workflows/views.py:744 +#: workflows/views.py:777 msgid "Absender, Support und PDF-Branding für ausgehende Kommunikation." msgstr "" -#: workflows/views.py:748 +#: workflows/views.py:781 msgid "Wird für ausgehende System-E-Mails als Anzeigename verwendet." msgstr "" -#: workflows/views.py:749 +#: workflows/views.py:782 msgid "Erlaubtes Format: PDF. Maximal 10 MB." msgstr "" -#: workflows/views.py:754 +#: workflows/views.py:787 msgid "Footer & Rechtliches" msgstr "" -#: workflows/views.py:755 +#: workflows/views.py:788 msgid "Gemeinsame Footer-Texte und rechtliche Hinweise für die Shell." msgstr "" -#: workflows/views.py:809 +#: workflows/views.py:842 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4086,53 +4129,53 @@ msgid "" "Eingaben." msgstr "User could not be created. Please check the input." -#: workflows/views.py:838 +#: workflows/views.py:871 #, fuzzy #| msgid "Offboarding-Anfrage speichern" msgid "Firmenkonfiguration wurde gespeichert." msgstr "Save offboarding request" -#: workflows/views.py:855 +#: workflows/views.py:888 #, fuzzy #| msgid "Firmenname" msgid "Firmenprofil" msgstr "Company name" -#: workflows/views.py:856 +#: workflows/views.py:889 msgid "Rechtlicher Name und zentrale Stammdaten der Firma." msgstr "" -#: workflows/views.py:861 +#: workflows/views.py:894 msgid "Adresse & Register" msgstr "" -#: workflows/views.py:862 +#: workflows/views.py:895 msgid "Anschrift sowie optionale Register- und Steuerangaben." msgstr "" -#: workflows/views.py:867 +#: workflows/views.py:900 msgid "Kontaktpunkte" msgstr "" -#: workflows/views.py:868 +#: workflows/views.py:901 msgid "Zentrale Ansprechpartner für HR, IT und Operations." msgstr "" -#: workflows/views.py:873 +#: workflows/views.py:906 msgid "Recht & Öffentlichkeit" msgstr "" -#: workflows/views.py:874 +#: workflows/views.py:907 msgid "Öffentliche Links für Website, Impressum und Datenschutz." msgstr "" -#: workflows/views.py:876 +#: workflows/views.py:909 msgid "" "Diese Links können später im Portal-Footer oder in öffentlichen Seiten " "verwendet werden." msgstr "" -#: workflows/views.py:916 +#: workflows/views.py:949 #, fuzzy #| msgid "" #| "Benutzer konnte nicht erstellt werden. Bitte prüfen Sie die Eingaben." @@ -4141,21 +4184,21 @@ msgid "" "Eingaben." msgstr "Trial configuration could not be saved. Please check the input." -#: workflows/views.py:943 +#: workflows/views.py:976 msgid "Trial-Konfiguration wurde gespeichert." msgstr "Trial configuration was saved." -#: workflows/views.py:960 +#: workflows/views.py:993 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:973 +#: workflows/views.py:1006 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Benutzer wurde erstellt und eingeladen: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:995 +#: workflows/views.py:1028 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4166,14 +4209,14 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:998 +#: workflows/views.py:1031 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:1001 +#: workflows/views.py:1034 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4184,7 +4227,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1004 +#: workflows/views.py:1037 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4195,18 +4238,18 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1021 +#: workflows/views.py:1054 #, python-format msgid "Benutzer wurde aktualisiert: %(username)s" msgstr "User updated: %(username)s" -#: workflows/views.py:1043 +#: workflows/views.py:1076 #, 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:1055 +#: workflows/views.py:1088 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4216,7 +4259,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1058 +#: workflows/views.py:1091 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4226,7 +4269,7 @@ msgid "" msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1061 +#: workflows/views.py:1094 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4235,7 +4278,7 @@ msgid "Der letzte aktive Platform Owner kann nicht gelöscht werden." msgstr "" "The currently signed-in super admin cannot lock or downgrade themselves here." -#: workflows/views.py:1064 +#: workflows/views.py:1097 #, fuzzy #| msgid "" #| "Der aktuell angemeldete Super Admin kann sich hier nicht selbst sperren " @@ -4244,121 +4287,121 @@ 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:1077 +#: workflows/views.py:1110 #, fuzzy, python-format #| msgid "Benutzer wurde erstellt: %(username)s" msgid "Benutzer wurde gelöscht: %(username)s" msgstr "User created: %(username)s" -#: workflows/views.py:1166 +#: workflows/views.py:1199 #, python-format msgid "Backup wurde erstellt: %(name)s" msgstr "" -#: workflows/views.py:1168 +#: workflows/views.py:1201 #, python-format msgid "Backup konnte nicht erstellt werden: %(error)s" msgstr "" -#: workflows/views.py:1184 +#: workflows/views.py:1217 #, python-format msgid "Backup wurde verifiziert: %(name)s" msgstr "" -#: workflows/views.py:1186 +#: workflows/views.py:1219 #, python-format msgid "Backup-Verifikation fehlgeschlagen: %(error)s" msgstr "" -#: workflows/views.py:1202 +#: workflows/views.py:1235 #, python-format msgid "Backup wurde gelöscht: %(name)s" msgstr "" -#: workflows/views.py:1204 +#: workflows/views.py:1237 #, python-format msgid "Backup konnte nicht gelöscht werden: %(error)s" msgstr "" -#: workflows/views.py:1230 +#: workflows/views.py:1263 #, fuzzy #| msgid "Anfrage gespeichert" msgid "Anfrage erstellt" msgstr "Request saved" -#: workflows/views.py:1232 +#: workflows/views.py:1265 #, fuzzy, python-format #| msgid "Sitzungsstatus" msgid "Status: %(status)s" msgstr "Session status" -#: workflows/views.py:1244 +#: workflows/views.py:1277 #, fuzzy #| msgid "Geplant für" msgid "Geplanter Start" msgstr "Scheduled for" -#: workflows/views.py:1254 +#: workflows/views.py:1287 msgid "Geräteübergabe / Hardware-Abholung" msgstr "" -#: workflows/views.py:1256 +#: workflows/views.py:1289 msgid "Geplanter Hardware-Termin" msgstr "" -#: workflows/views.py:1265 +#: workflows/views.py:1298 #, fuzzy #| msgid "Noch nicht verfügbar" msgid "PDF verfügbar" msgstr "Not available yet" -#: workflows/views.py:1291 +#: workflows/views.py:1324 #, fuzzy #| msgid "Einweisung" msgid "Einweisungssitzung" msgstr "Introduction" -#: workflows/views.py:1303 +#: workflows/views.py:1336 #, fuzzy #| msgid "Welcome E-Mails" msgid "Welcome E-Mail" msgstr "Welcome Emails" -#: workflows/views.py:1342 +#: workflows/views.py:1375 msgid "Keine Einträge ausgewählt." msgstr "No entries selected." -#: workflows/views.py:1385 +#: workflows/views.py:1418 #, python-format msgid "%(count)s Eintrag/Einträge gelöscht." msgstr "%(count)s entry/entries deleted." -#: workflows/views.py:1387 +#: workflows/views.py:1420 #, python-format msgid "%(count)s Auswahl(en) konnten nicht verarbeitet werden." msgstr "%(count)s selection(s) could not be processed." -#: workflows/views.py:1389 +#: workflows/views.py:1422 msgid "Keine passenden Einträge gefunden." msgstr "No matching entries found." -#: workflows/views.py:1617 +#: workflows/views.py:1650 msgid "Einweisungs- und Übergabeprotokoll wurde erzeugt." msgstr "Introduction and handover protocol was generated." -#: workflows/views.py:1634 +#: workflows/views.py:1667 msgid "Einweisungsprotokoll aus Live-Status wurde erzeugt." msgstr "Introduction protocol from live status was generated." -#: workflows/views.py:1663 +#: workflows/views.py:1696 msgid "Einweisung wurde zurückgesetzt." msgstr "Introduction was reset." -#: workflows/views.py:1677 +#: workflows/views.py:1710 msgid "Einweisung wurde als abgeschlossen gespeichert." msgstr "Introduction was saved as completed." -#: workflows/views.py:1690 +#: workflows/views.py:1723 msgid "Einweisung wurde als Entwurf gespeichert." msgstr "Introduction was saved as draft." diff --git a/backend/requirements.txt b/backend/requirements.txt index 918b900..172d22d 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -10,3 +10,4 @@ pypdf==5.1.0 jinja2==3.1.4 xhtml2pdf==0.2.16 gunicorn==23.0.0 +qrcode==8.2 diff --git a/backend/workflows/forms.py b/backend/workflows/forms.py index ccb0a3d..a9f71a7 100644 --- a/backend/workflows/forms.py +++ b/backend/workflows/forms.py @@ -11,7 +11,7 @@ from .branding import get_company_email_domain from .form_builder import apply_form_field_config from .models import EmployeeProfile, FormOption, OffboardingRequest, OnboardingRequest, PortalBranding, PortalCompanyConfig, PortalTrialConfig, UserProfile, WorkflowConfig from .roles import ROLE_ADMIN, ROLE_GROUP_NAMES, ROLE_IT_STAFF, ROLE_LABELS, ROLE_PLATFORM_OWNER, ROLE_STAFF, ROLE_SUPER_ADMIN, assign_user_role -from .totp import normalize_totp_token, verify_totp_token +from .totp import normalize_recovery_code, normalize_totp_token, verify_totp_token YES_NO_CHOICES = [('', '--'), ('ja', 'Ja'), ('nein', 'Nein')] @@ -111,6 +111,12 @@ class AppAuthenticationForm(AuthenticationForm): max_length=12, widget=forms.TextInput(attrs={'autocomplete': 'one-time-code', 'inputmode': 'numeric'}), ) + recovery_code = forms.CharField( + label=gettext_lazy('Recovery-Code'), + required=False, + max_length=32, + widget=forms.TextInput(attrs={'autocomplete': 'one-time-code'}), + ) error_messages = { **AuthenticationForm.error_messages, @@ -126,6 +132,14 @@ class AppAuthenticationForm(AuthenticationForm): profile, _ = UserProfile.objects.get_or_create(user=user) if profile.totp_enabled: otp_code = normalize_totp_token(cleaned_data.get('otp_code')) + recovery_code = normalize_recovery_code(cleaned_data.get('recovery_code')) + if recovery_code: + if not profile.consume_recovery_code(recovery_code): + raise ValidationError( + self.error_messages['invalid_otp'], + code='invalid_otp', + ) + return cleaned_data if not otp_code: raise ValidationError( self.error_messages['missing_otp'], @@ -296,8 +310,15 @@ class AccountTOTPDisableForm(forms.Form): verification_code = forms.CharField( label=gettext_lazy('TOTP-Code'), max_length=12, + required=False, widget=forms.TextInput(attrs={'autocomplete': 'one-time-code', 'inputmode': 'numeric'}), ) + recovery_code = forms.CharField( + label=gettext_lazy('Recovery-Code'), + max_length=32, + required=False, + widget=forms.TextInput(attrs={'autocomplete': 'one-time-code'}), + ) def __init__(self, *args, user=None, profile=None, **kwargs): super().__init__(*args, **kwargs) @@ -310,14 +331,66 @@ class AccountTOTPDisableForm(forms.Form): raise ValidationError(_('Das aktuelle Passwort ist nicht korrekt.')) return password - def clean_verification_code(self): - code = normalize_totp_token(self.cleaned_data.get('verification_code')) - if not code: - raise ValidationError(_('Bitte geben Sie einen gültigen TOTP-Code ein.')) + def clean(self): + cleaned_data = super().clean() + code = normalize_totp_token(cleaned_data.get('verification_code')) + recovery_code = normalize_recovery_code(cleaned_data.get('recovery_code')) + if not code and not recovery_code: + raise ValidationError(_('Bitte geben Sie einen gültigen TOTP-Code oder Recovery-Code ein.')) secret = getattr(self.profile, 'totp_secret', '') or '' - if not secret or not verify_totp_token(secret, code, for_time=int(timezone.now().timestamp())): - raise ValidationError(_('Der TOTP-Code ist ungültig.')) - return code + if code: + if not secret or not verify_totp_token(secret, code, for_time=int(timezone.now().timestamp())): + raise ValidationError(_('Der TOTP-Code ist ungültig.')) + return cleaned_data + if not self.profile.consume_recovery_code(recovery_code): + raise ValidationError(_('Der Recovery-Code ist ungültig.')) + return cleaned_data + + +class AccountTOTPRegenerateRecoveryCodesForm(forms.Form): + current_password = forms.CharField( + label=gettext_lazy('Aktuelles Passwort'), + strip=False, + widget=forms.PasswordInput(attrs={'autocomplete': 'current-password'}), + ) + verification_code = forms.CharField( + label=gettext_lazy('TOTP-Code'), + max_length=12, + required=False, + widget=forms.TextInput(attrs={'autocomplete': 'one-time-code', 'inputmode': 'numeric'}), + ) + recovery_code = forms.CharField( + label=gettext_lazy('Recovery-Code'), + max_length=32, + required=False, + widget=forms.TextInput(attrs={'autocomplete': 'one-time-code'}), + ) + + def __init__(self, *args, user=None, profile=None, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + self.profile = profile + + def clean_current_password(self): + password = self.cleaned_data.get('current_password') or '' + if not self.user or not self.user.check_password(password): + raise ValidationError(_('Das aktuelle Passwort ist nicht korrekt.')) + return password + + def clean(self): + cleaned_data = super().clean() + code = normalize_totp_token(cleaned_data.get('verification_code')) + recovery_code = normalize_recovery_code(cleaned_data.get('recovery_code')) + if not code and not recovery_code: + raise ValidationError(_('Bitte geben Sie einen gültigen TOTP-Code oder Recovery-Code ein.')) + secret = getattr(self.profile, 'totp_secret', '') or '' + if code: + if not secret or not verify_totp_token(secret, code, for_time=int(timezone.now().timestamp())): + raise ValidationError(_('Der TOTP-Code ist ungültig.')) + return cleaned_data + if not self.profile.consume_recovery_code(recovery_code): + raise ValidationError(_('Der Recovery-Code ist ungültig.')) + return cleaned_data class UserManagementCreateForm(forms.Form): diff --git a/backend/workflows/migrations/0050_userprofile_totp_recovery_codes.py b/backend/workflows/migrations/0050_userprofile_totp_recovery_codes.py new file mode 100644 index 0000000..f1fd757 --- /dev/null +++ b/backend/workflows/migrations/0050_userprofile_totp_recovery_codes.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('workflows', '0049_userprofile_totp_fields'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='totp_recovery_codes', + field=models.JSONField(blank=True, default=list), + ), + ] diff --git a/backend/workflows/models.py b/backend/workflows/models.py index b637743..0b5f3eb 100644 --- a/backend/workflows/models.py +++ b/backend/workflows/models.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.contrib.auth.hashers import check_password, make_password from django.core.validators import FileExtensionValidator from django.db import models from django.utils.translation import get_language @@ -43,6 +44,7 @@ class UserProfile(models.Model): totp_secret = models.CharField(max_length=64, blank=True, default='') totp_enabled = models.BooleanField(default=False) totp_confirmed_at = models.DateTimeField(null=True, blank=True) + totp_recovery_codes = models.JSONField(default=list, blank=True) updated_at = models.DateTimeField(auto_now=True) class Meta: @@ -56,13 +58,31 @@ class UserProfile(models.Model): self.totp_secret = '' self.totp_enabled = False self.totp_confirmed_at = None - self.save(update_fields=['totp_secret', 'totp_enabled', 'totp_confirmed_at', 'updated_at']) + self.totp_recovery_codes = [] + self.save(update_fields=['totp_secret', 'totp_enabled', 'totp_confirmed_at', 'totp_recovery_codes', 'updated_at']) - def enable_totp(self, secret: str) -> None: + def enable_totp(self, secret: str, recovery_codes: list[str]) -> None: self.totp_secret = secret self.totp_enabled = True self.totp_confirmed_at = timezone.now() - self.save(update_fields=['totp_secret', 'totp_enabled', 'totp_confirmed_at', 'updated_at']) + self.set_recovery_codes(recovery_codes) + self.save(update_fields=['totp_secret', 'totp_enabled', 'totp_confirmed_at', 'totp_recovery_codes', 'updated_at']) + + def set_recovery_codes(self, recovery_codes: list[str]) -> None: + self.totp_recovery_codes = [make_password(code) for code in recovery_codes] + + def consume_recovery_code(self, raw_code: str) -> bool: + remaining_hashes = [] + matched = False + for hashed_code in self.totp_recovery_codes or []: + if not matched and check_password(raw_code, hashed_code): + matched = True + continue + remaining_hashes.append(hashed_code) + if matched: + self.totp_recovery_codes = remaining_hashes + self.save(update_fields=['totp_recovery_codes', 'updated_at']) + return matched class PortalBranding(models.Model): diff --git a/backend/workflows/static/workflows/css/account.css b/backend/workflows/static/workflows/css/account.css index 839bd3f..bd235a8 100644 --- a/backend/workflows/static/workflows/css/account.css +++ b/backend/workflows/static/workflows/css/account.css @@ -291,13 +291,46 @@ body { word-break: break-word; } -.account-action-grid { +.account-security-overview { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; margin-bottom: 18px; } +.account-security-item { + padding: 16px 18px; + border-radius: 18px; + border: 1px solid #dbe5f2; + background: + radial-gradient(circle at top right, rgba(30, 64, 175, 0.06), transparent 26%), + linear-gradient(180deg, rgba(255,255,255,0.96), rgba(246,250,255,0.9)); +} + +.account-security-item span { + display: block; + margin-bottom: 6px; + color: #6b7a90; + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.account-security-item strong { + display: block; + color: #132238; + font-size: 20px; + line-height: 1.2; +} + +.account-security-item p { + margin: 8px 0 0; + color: #617389; + font-size: 13px; + line-height: 1.5; +} + .account-totp-card { margin-bottom: 18px; padding: 18px; @@ -326,56 +359,77 @@ body { margin-top: 14px; } -.account-action-card { - display: grid; - gap: 6px; - padding: 16px 18px; +.account-qr-card, +.account-recovery-card { + margin-top: 14px; + padding: 16px; border-radius: 18px; border: 1px solid #dbe5f2; - background: - radial-gradient(circle at top right, rgba(30, 64, 175, 0.08), transparent 28%), - #f9fbff; - color: inherit; - text-decoration: none; - transition: transform 160ms cubic-bezier(0.2, 0.8, 0.2, 1), box-shadow 160ms cubic-bezier(0.2, 0.8, 0.2, 1); + background: rgba(255, 255, 255, 0.82); } -.account-action-card:hover { - transform: translateY(-1px); - box-shadow: 0 10px 24px rgba(28, 45, 79, 0.08); +.account-qr-card svg { + display: block; + width: min(220px, 100%); + height: auto; + margin: 0 auto; } -.account-action-card strong { - color: #132238; - font-size: 15px; +.account-secret-panel { + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid #dbe5f2; } -.account-action-card span { - color: #617389; - font-size: 13px; - line-height: 1.5; -} - -.account-action-card-muted { - cursor: default; -} - -.account-action-card-muted:hover { - transform: none; - box-shadow: none; -} - -.account-actions { +.account-secret-head { display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.account-secret-head span { + display: block; + margin-bottom: 4px; + color: #6b7a90; + font-size: 12px; +} + +.account-secret-head strong { + color: #132238; + font-size: 14px; +} + +.account-secret-toggle { + min-width: 48px; + padding-left: 0; + padding-right: 0; +} + +.account-secret-body { + margin-top: 12px; +} + +.account-secret-body.is-hidden { + display: none; +} + +.account-recovery-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; } -.account-actions .btn { - width: auto; -} - -.account-actions form { - margin: 0; +.account-recovery-code { + padding: 12px 14px; + border-radius: 14px; + border: 1px dashed #c8d7ea; + background: #f7faff; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 13px; + font-weight: 700; + letter-spacing: 0.04em; + color: #17345e; } .account-inline-view.is-hidden, @@ -491,23 +545,16 @@ body { } .account-detail-grid, - .account-action-grid, - .account-form-grid { + .account-security-overview, + .account-form-grid, + .account-recovery-grid { grid-template-columns: 1fr; } - .account-actions { - flex-direction: column; - } - .account-inline-actions { flex-direction: column; } - .account-actions .btn { - width: 100%; - } - .account-panel-head { flex-direction: column; } diff --git a/backend/workflows/static/workflows/css/app_chrome.css b/backend/workflows/static/workflows/css/app_chrome.css index addc72f..c67924e 100644 --- a/backend/workflows/static/workflows/css/app_chrome.css +++ b/backend/workflows/static/workflows/css/app_chrome.css @@ -275,6 +275,7 @@ display: grid; gap: 4px; z-index: 40; + overflow: hidden; } .app-user-panel-head { @@ -299,7 +300,10 @@ .app-user-panel a, .app-user-panel button { + display: flex; + align-items: center; width: 100%; + min-height: 42px; border: 0; border-radius: 12px; background: transparent; @@ -322,6 +326,13 @@ color: var(--app-brand-blue); } +.app-user-panel a:focus-visible, +.app-user-panel button:focus-visible, +.app-user-trigger:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(0, 0, 120, 0.10); +} + .app-user-panel form { margin: 0; } diff --git a/backend/workflows/templates/workflows/account_profile.html b/backend/workflows/templates/workflows/account_profile.html index 6124e0c..349aa89 100644 --- a/backend/workflows/templates/workflows/account_profile.html +++ b/backend/workflows/templates/workflows/account_profile.html @@ -20,10 +20,6 @@

{% trans "Profil" %}

{% trans "Ihre aktuelle Workdock-Kontoübersicht und wichtige Sicherheitsaktionen." %}

-
- {{ role_label }} - {{ account_user.username }} -
@@ -58,14 +54,6 @@

{{ account_user.email|default:account_user.username }}

-
- {% trans "Rolle" %} - {{ role_label }} -
-
- {% trans "Benutzername" %} - {{ account_user.username }} -
{% trans "Position" %} {{ account_user_profile.job_title|default:"-" }} @@ -171,29 +159,36 @@
@@ -295,6 +332,9 @@ var cancel = document.querySelector('[data-account-edit-cancel="details"]'); var view = document.querySelector('[data-account-edit-view="details"]'); var form = document.querySelector('[data-account-edit-form="details"]'); + var secretToggle = document.querySelector('[data-secret-toggle]'); + var secretBody = document.querySelector('[data-secret-body]'); + var secretIcon = document.querySelector('[data-secret-toggle-icon]'); if (!toggle || !cancel || !view || !form) return; function setMode(editing) { @@ -310,6 +350,17 @@ cancel.addEventListener('click', function () { setMode(false); }); + + if (secretToggle && secretBody) { + secretToggle.addEventListener('click', function () { + var isOpen = secretToggle.getAttribute('aria-expanded') === 'true'; + secretToggle.setAttribute('aria-expanded', isOpen ? 'false' : 'true'); + secretBody.classList.toggle('is-hidden', isOpen); + if (secretIcon) { + secretIcon.textContent = isOpen ? '◐' : '◑'; + } + }); + } }()); {% endblock %} diff --git a/backend/workflows/templates/workflows/auth/login.html b/backend/workflows/templates/workflows/auth/login.html index 25e80ee..d3f2208 100644 --- a/backend/workflows/templates/workflows/auth/login.html +++ b/backend/workflows/templates/workflows/auth/login.html @@ -36,6 +36,10 @@ {{ form.otp_code.label_tag }}{{ form.otp_code }}
{% trans "Nur erforderlich, wenn TOTP für Ihr Konto aktiviert ist." %}
+
+ {{ form.recovery_code.label_tag }}{{ form.recovery_code }} +
{% trans "Alternativ können Sie einen einmaligen Recovery-Code verwenden." %}
+
diff --git a/backend/workflows/tests/test_account_ui.py b/backend/workflows/tests/test_account_ui.py index 86b19fa..c76c4c2 100644 --- a/backend/workflows/tests/test_account_ui.py +++ b/backend/workflows/tests/test_account_ui.py @@ -90,12 +90,15 @@ class AccountUISmokeTests(TestCase): profile.refresh_from_db() self.assertTrue(profile.totp_enabled) self.assertTrue(profile.totp_secret) + self.assertEqual(len(profile.totp_recovery_codes), 8) + self.assertContains(response, 'Recovery-Codes') def test_login_requires_totp_when_enabled(self): profile = self.user.profile profile.totp_secret = 'JBSWY3DPEHPK3PXP' profile.totp_enabled = True - profile.save(update_fields=['totp_secret', 'totp_enabled', 'updated_at']) + profile.set_recovery_codes(['ABCDE-12345']) + profile.save(update_fields=['totp_secret', 'totp_enabled', 'totp_recovery_codes', 'updated_at']) client = Client() response = client.post( @@ -113,3 +116,13 @@ class AccountUISmokeTests(TestCase): HTTP_HOST='localhost', ) self.assertEqual(response.status_code, 302) + + client = Client() + response = client.post( + '/accounts/login/', + {'username': 'profile-user', 'password': 'secret-12345', 'recovery_code': 'ABCDE-12345'}, + HTTP_HOST='localhost', + ) + self.assertEqual(response.status_code, 302) + profile.refresh_from_db() + self.assertEqual(profile.totp_recovery_codes, []) diff --git a/backend/workflows/totp.py b/backend/workflows/totp.py index a15063b..7283a31 100644 --- a/backend/workflows/totp.py +++ b/backend/workflows/totp.py @@ -5,6 +5,7 @@ import hashlib import hmac import secrets import struct +import string from urllib.parse import quote @@ -47,3 +48,19 @@ def build_otpauth_uri(secret: str, *, account_name: str, issuer: str) -> str: label = quote(f'{issuer}:{account_name}') issuer_q = quote(issuer) return f'otpauth://totp/{label}?secret={secret}&issuer={issuer_q}&algorithm=SHA1&digits=6&period=30' + + +def normalize_recovery_code(value: str | None) -> str: + raw = (value or '').strip().upper().replace(' ', '') + return raw + + +def generate_recovery_codes(count: int = 8) -> list[str]: + alphabet = string.ascii_uppercase + string.digits + codes = [] + for _ in range(count): + parts = [] + for _part in range(2): + parts.append(''.join(secrets.choice(alphabet) for _ in range(5))) + codes.append('-'.join(parts)) + return codes diff --git a/backend/workflows/views.py b/backend/workflows/views.py index 695f488..f363e71 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 io import BytesIO from functools import wraps from celery import current_app @@ -33,7 +34,7 @@ from .backup_ops import ( verify_backup_bundle, ) from .branding import get_branding_email_copy, get_company_email_domain, get_default_notification_templates, get_portal_trial_config, is_trial_expired -from .forms import AccountAvatarForm, AccountDetailsForm, AccountTOTPDisableForm, AccountTOTPEnableForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm +from .forms import AccountAvatarForm, AccountDetailsForm, AccountTOTPDisableForm, AccountTOTPEnableForm, AccountTOTPRegenerateRecoveryCodesForm, OffboardingRequestForm, OnboardingRequestForm, PortalBrandingForm, PortalCompanyConfigForm, PortalTrialConfigForm, UserManagementCreateForm from .form_builder import ( DEFAULT_FIELD_ORDER, LOCKED_FIELD_RULES, @@ -46,7 +47,7 @@ from .models import AdminAuditLog, AsyncTaskLog, EmployeeProfile, FormFieldConfi from .emailing import send_system_email from .roles import ROLE_GROUP_NAMES, ROLE_LABELS, ROLE_PLATFORM_OWNER, 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 .totp import build_otpauth_uri, generate_totp_secret +from .totp import build_otpauth_uri, generate_recovery_codes, generate_totp_secret from .tasks import ( _generate_onboarding_intro_pdf, _generate_onboarding_intro_session_pdf, @@ -130,7 +131,9 @@ def healthz(request): @login_required def account_profile_page(request): session_secret_key = 'account_totp_pending_secret' + session_codes_key = 'account_totp_recovery_codes' profile, created = UserProfile.objects.get_or_create(user=request.user) + recovery_codes = request.session.pop(session_codes_key, []) pending_totp_secret = request.session.get(session_secret_key) or '' if profile.totp_enabled: pending_totp_secret = '' @@ -143,6 +146,7 @@ def account_profile_page(request): details_form = AccountDetailsForm(user=request.user, profile=profile) totp_enable_form = AccountTOTPEnableForm(user=request.user, secret=pending_totp_secret) totp_disable_form = AccountTOTPDisableForm(user=request.user, profile=profile) + totp_regenerate_form = AccountTOTPRegenerateRecoveryCodesForm(user=request.user, profile=profile) account_edit_open = False totp_edit_open = False if request.method == 'POST': @@ -166,7 +170,9 @@ def account_profile_page(request): totp_edit_open = True totp_enable_form = AccountTOTPEnableForm(request.POST, user=request.user, secret=pending_totp_secret) if totp_enable_form.is_valid(): - profile.enable_totp(pending_totp_secret) + recovery_codes = generate_recovery_codes() + profile.enable_totp(pending_totp_secret, recovery_codes) + request.session[session_codes_key] = recovery_codes request.session.pop(session_secret_key, None) messages.success(request, _('TOTP wurde aktiviert.')) return redirect('account_profile_page') @@ -180,10 +186,34 @@ def account_profile_page(request): messages.success(request, _('TOTP wurde deaktiviert.')) return redirect('account_profile_page') messages.error(request, _('TOTP konnte nicht deaktiviert werden.')) + elif form_kind == 'totp_regenerate_codes': + totp_edit_open = True + totp_regenerate_form = AccountTOTPRegenerateRecoveryCodesForm(request.POST, user=request.user, profile=profile) + if totp_regenerate_form.is_valid(): + recovery_codes = generate_recovery_codes() + profile.set_recovery_codes(recovery_codes) + profile.save(update_fields=['totp_recovery_codes', 'updated_at']) + request.session[session_codes_key] = recovery_codes + messages.success(request, _('Recovery-Codes wurden neu erzeugt.')) + return redirect('account_profile_page') + messages.error(request, _('Recovery-Codes konnten nicht neu erzeugt werden.')) branding_context = get_branding_email_copy() totp_account_name = (request.user.email or request.user.username or '').strip() totp_issuer = (branding_context.get('portal_title') or branding_context.get('company_name') or 'Workdock').strip() + totp_otpauth_uri = '' if profile.totp_enabled else build_otpauth_uri(pending_totp_secret, account_name=totp_account_name, issuer=totp_issuer) + totp_qr_svg = '' + if totp_otpauth_uri: + try: + import qrcode + import qrcode.image.svg + + qr_image = qrcode.make(totp_otpauth_uri, image_factory=qrcode.image.svg.SvgPathImage) + stream = BytesIO() + qr_image.save(stream) + totp_qr_svg = stream.getvalue().decode('utf-8') + except Exception: + totp_qr_svg = '' return render( request, 'workflows/account_profile.html', @@ -194,11 +224,14 @@ def account_profile_page(request): 'details_form': details_form, 'totp_enable_form': totp_enable_form, 'totp_disable_form': totp_disable_form, + 'totp_regenerate_form': totp_regenerate_form, 'account_edit_open': account_edit_open, 'totp_edit_open': totp_edit_open, 'role_label': get_user_role_label(request.user), 'totp_pending_secret': pending_totp_secret, - 'totp_otpauth_uri': '' if profile.totp_enabled else build_otpauth_uri(pending_totp_secret, account_name=totp_account_name, issuer=totp_issuer), + 'totp_otpauth_uri': totp_otpauth_uri, + 'totp_qr_svg': totp_qr_svg, + 'totp_recovery_codes': recovery_codes, }, )