I18n
EBO <eric.bouhana@softalys.com> committé le 2026-03-09 09:43
955fb60464d45f79303f830a2b64dacc8fccd797
1 parent(s)
19 fichiers modifiés
+2454
-883
M
locales/de.yml
+227
-1
@@ -34,6 +34,7 @@ login:
| 34 | 34 | resend_prompt: "Bitte geben Sie Ihre E-Mail ein, bevor Sie erneut senden." |
| 35 | 35 | register_success: "Konto erfolgreich erstellt! Bitte überprüfen Sie Ihre E-Mail." |
| 36 | 36 | register_error: "Registrierungsfehler" |
| 37 | + email_label: "E-Mail" | |
| 37 | 38 | footer_text: "Nextcloud Fork in Rust" |
| 38 | 39 | |
| 39 | 40 | # Fehlermeldungen |
@@ -63,14 +64,86 @@ error:
| 63 | 64 | password_require_digits: "Das Passwort muss mindestens eine Ziffer enthalten" |
| 64 | 65 | password_require_special: "Das Passwort muss mindestens ein Sonderzeichen (!@#$%^&*()_+-=[]{}|;:,.<>?) enthalten" |
| 65 | 66 | |
| 67 | +# Bestätigungs-E-Mail erneut senden | |
| 68 | +resend_verification: | |
| 69 | + page_title: "Bestätigungs-E-Mail erneut senden - RustWarden" | |
| 70 | + title: "Bestätigungs-E-Mail erneut senden" | |
| 71 | + subtitle: "Geben Sie Ihre E-Mail-Adresse ein, um einen neuen Bestätigungslink zu erhalten." | |
| 72 | + email_label: "E-Mail-Adresse" | |
| 73 | + email_placeholder: "sie@beispiel.de" | |
| 74 | + submit: "Bestätigungslink senden" | |
| 75 | + back_to_login: "Zurück zur Anmeldung" | |
| 76 | + error_default: "Die Anfrage konnte nicht gesendet werden." | |
| 77 | + success_default: "Wenn das Konto existiert und nicht verifiziert ist, wurde eine E-Mail gesendet." | |
| 78 | + error_missing_email: "Bitte geben Sie Ihre E-Mail ein." | |
| 79 | + error_cannot_send: "Die Anfrage kann derzeit nicht gesendet werden." | |
| 80 | + | |
| 81 | +# Passwort zurücksetzen (über E-Mail-Link) | |
| 82 | +password_reset: | |
| 83 | + page_title: "Passwort zurücksetzen" | |
| 84 | + title: "Passwort zurücksetzen" | |
| 85 | + subtitle: "Geben Sie unten Ihr neues Passwort ein." | |
| 86 | + new_password_label: "Neues Passwort" | |
| 87 | + new_password_placeholder: "Neues sicheres Passwort" | |
| 88 | + requirements: "⚠️ Muss enthalten: 8+ Zeichen, Großbuchstabe, Kleinbuchstabe, Ziffer, Sonderzeichen" | |
| 89 | + confirm_password_label: "Passwort bestätigen" | |
| 90 | + confirm_password_placeholder: "Bestätigen Sie Ihr neues Passwort" | |
| 91 | + missing_token_url: "Zurücksetzungs-Token fehlt in der URL." | |
| 92 | + missing_token: "Zurücksetzungs-Token fehlt." | |
| 93 | + missing_new_password: "Bitte geben Sie ein neues Passwort ein." | |
| 94 | + passwords_not_match: "Die Passwörter stimmen nicht überein." | |
| 95 | + success_message: "Ihr Passwort wurde erfolgreich zurückgesetzt. Sie können sich jetzt anmelden." | |
| 96 | + error_link_invalid: "Das Passwort konnte nicht zurückgesetzt werden. Der Link ist möglicherweise abgelaufen oder ungültig." | |
| 97 | + error_generic: "Das Passwort kann derzeit nicht zurückgesetzt werden." | |
| 98 | + | |
| 66 | 99 | # Einstellungen |
| 67 | 100 | settings: |
| 68 | 101 | title: "Einstellungen" |
| 102 | + page_title: "Einstellungen - RustWarden" | |
| 103 | + subtitle: "Verwalten Sie Ihre Präferenzen und Kontoeinstellungen" | |
| 104 | + profile_title: "Mein Profil" | |
| 105 | + profile_desc: "Bearbeiten Sie Ihre persönlichen Informationen (Benutzername, E-Mail)" | |
| 106 | + password_title: "Passwort" | |
| 107 | + password_desc: "Ändern Sie Ihr Passwort, um Ihr Konto zu sichern" | |
| 108 | + sessions_title: "Aktive Sitzungen" | |
| 109 | + sessions_desc: "Verwalten Sie Ihre aktiven Sitzungen und widerrufen Sie den Zugriff von anderen Geräten" | |
| 110 | + preferences_title: "Präferenzen" | |
| 111 | + preferences_desc: "Passen Sie Ihre Erfahrung an (Design, Sprache)" | |
| 112 | + access: "Zugriff" | |
| 113 | + info_title: "Informationen" | |
| 114 | + info_desc: "Verwenden Sie die obigen Abschnitte, um verschiedene Aspekte Ihres Kontos zu verwalten. Alle Änderungen sind sicher und erfordern Ihre Authentifizierung." | |
| 69 | 115 | language: "Sprache" |
| 70 | 116 | language_description: "Wählen Sie Ihre bevorzugte Sprache" |
| 71 | 117 | save: "Speichern" |
| 72 | 118 | saved: "Einstellungen gespeichert" |
| 73 | 119 | |
| 120 | +# Startseite | |
| 121 | +home: | |
| 122 | + welcome: "Willkommen, {username}!" | |
| 123 | + subtitle: "Willkommen in Ihrem persönlichen Bereich" | |
| 124 | + my_account: "Mein Konto" | |
| 125 | + upcoming_features: "Kommende Funktionen" | |
| 126 | + stat_username: "Benutzername" | |
| 127 | + stat_email: "E-Mail" | |
| 128 | + stat_member_since: "Mitglied seit" | |
| 129 | + stat_roles: "Rollen" | |
| 130 | + feature_secrets_name: "Geheimnisse-Manager" | |
| 131 | + feature_secrets_desc: "Sichere Speicherung von Passwörtern und Geheimnissen" | |
| 132 | + feature_2fa_name: "2FA-Authentifizierung" | |
| 133 | + feature_2fa_desc: "Zwei-Faktor-Authentifizierung" | |
| 134 | + feature_files_name: "Dateimanager" | |
| 135 | + feature_files_desc: "Dateien und Ordner durchsuchen, erstellen, umbenennen und löschen" | |
| 136 | + feature_upload_name: "Upload/Download" | |
| 137 | + feature_upload_desc: "Dateien mit Fortschrittsbalken hoch- und herunterladen" | |
| 138 | + feature_webdav_name: "WebDAV & Synchronisierung" | |
| 139 | + feature_webdav_desc: "Dateien mit Desktop-Clients über WebDAV synchronisieren" | |
| 140 | + feature_mobile_name: "Mobile API" | |
| 141 | + feature_mobile_desc: "Synchronisierung mit mobilen Apps (iOS/Android)" | |
| 142 | + badge_soon: "Bald" | |
| 143 | + badge_planned: "Geplant" | |
| 144 | + did_you_know: "Wussten Sie schon?" | |
| 145 | + did_you_know_text: "RustWarden ist ein sicherer Passwort-Manager, der in Rust geschrieben wurde, um bessere Leistung und erhöhte Sicherheit zu bieten. Bleiben Sie dran für neue Funktionen!" | |
| 146 | + | |
| 74 | 147 | # Allgemeine Nachrichten |
| 75 | 148 | common: |
| 76 | 149 | welcome: "Willkommen {username}" |
@@ -154,8 +227,120 @@ breadcrumb:
| 154 | 227 | password: "Passwort" |
| 155 | 228 | sessions: "Sitzungen" |
| 156 | 229 | |
| 157 | -# Admin-Einstellungen | |
| 230 | +# Admin | |
| 158 | 231 | admin: |
| 232 | + dashboard: | |
| 233 | + title: "Verwaltung" | |
| 234 | + heading: "Administrator-Dashboard" | |
| 235 | + users_card_title: "Benutzer" | |
| 236 | + users_card_subtitle: "Benutzer gesamt" | |
| 237 | + access_button: "Zugreifen" | |
| 238 | + sessions_card_title: "Aktive Sitzungen" | |
| 239 | + sessions_card_subtitle: "Laufende Sitzungen" | |
| 240 | + admins_card_title: "Administratoren" | |
| 241 | + admins_card_subtitle: "Admin-Konten" | |
| 242 | + last_activity_card_title: "Letzte Aktivität" | |
| 243 | + last_activity_card_subtitle: "Letzte Anmeldung" | |
| 244 | + audit_section_title: "Audit-Log-Statistiken" | |
| 245 | + audit_total_logs: "Logs gesamt" | |
| 246 | + audit_last_24h: "Letzte 24h" | |
| 247 | + audit_by_level: "Nach Stufe" | |
| 248 | + chart_label: "Logs nach Stufe" | |
| 249 | + settings_card_title: "Anwendungseinstellungen" | |
| 250 | + settings_card_desc: "Globale Anwendungseinstellungen konfigurieren" | |
| 251 | + apps_card_title: "Anwendungsverwaltung" | |
| 252 | + apps_card_desc: "Modulare Anwendungen aktivieren oder deaktivieren" | |
| 253 | + orgs_card_title: "Organisationsverwaltung" | |
| 254 | + orgs_card_desc: "Organisationen und deren Mitglieder erstellen und verwalten" | |
| 255 | + orgs_stat_title: "Organisationen" | |
| 256 | + orgs_members_stat_title: "Mitglieder gesamt" | |
| 257 | + errors: | |
| 258 | + title: "Fehlercodes" | |
| 259 | + description: "Dokumentation der Anwendungsfehlercodes. Jeder Fehlercode ist mit einer Middleware verknüpft und liefert Kontextinformationen für die Fehlersuche." | |
| 260 | + breadcrumb_errors: "Fehlercodes" | |
| 261 | + table_code: "Code" | |
| 262 | + table_description: "Beschreibung" | |
| 263 | + table_category: "Kategorie" | |
| 264 | + table_middleware: "Middleware" | |
| 265 | + table_context: "Kontext" | |
| 266 | + table_example: "Beispiel" | |
| 267 | + legend_title: "Legende" | |
| 268 | + legend_code: "Eindeutige Fehler-ID (Format: CATEGORY_NUMBER)" | |
| 269 | + legend_description: "Beschreibung des Fehlers" | |
| 270 | + legend_category: "Fehlertyp (Authentifizierung, CSRF, Rate Limiting, etc.)" | |
| 271 | + legend_middleware: "Middleware, die diesen Fehler erzeugt" | |
| 272 | + legend_context: "Detaillierte Erklärung, wann dieser Fehler auftritt" | |
| 273 | + legend_example: "Konkretes Beispiel einer Situation, die diesen Fehler erzeugt" | |
| 274 | + error_auth001_desc: "Ungültige Anmeldedaten" | |
| 275 | + error_auth001_category: "Authentifizierung" | |
| 276 | + error_auth001_context: "Der Benutzer hat bei der Anmeldung falsche Anmeldedaten eingegeben" | |
| 277 | + error_auth001_example: "Anmeldeversuch mit falschem Passwort" | |
| 278 | + error_auth002_desc: "Ungültiges Token" | |
| 279 | + error_auth002_category: "Authentifizierung" | |
| 280 | + error_auth002_context: "Das bereitgestellte JWT-Token ist ungültig oder fehlerhaft" | |
| 281 | + error_auth002_example: "Beschädigtes JWT-Token oder mit anderem Schlüssel signiert" | |
| 282 | + error_auth003_desc: "Token abgelaufen" | |
| 283 | + error_auth003_category: "Authentifizierung" | |
| 284 | + error_auth003_context: "Das JWT-Token ist abgelaufen und muss erneuert werden" | |
| 285 | + error_auth003_example: "Zugriffsversuch mit abgelaufenem Token nach der Gültigkeitsdauer" | |
| 286 | + error_csrf001_desc: "CSRF-Token fehlt" | |
| 287 | + error_csrf001_category: "CSRF-Schutz" | |
| 288 | + error_csrf001_context: "Die POST/PUT/DELETE-Anfrage enthält kein CSRF-Token" | |
| 289 | + error_csrf001_example: "Formular ohne CSRF-Token im Header oder Body abgesendet" | |
| 290 | + error_csrf002_desc: "Ungültiges CSRF-Token" | |
| 291 | + error_csrf002_category: "CSRF-Schutz" | |
| 292 | + error_csrf002_context: "Das bereitgestellte CSRF-Token stimmt nicht mit dem Sitzungs-Token überein" | |
| 293 | + error_csrf002_example: "Geändertes oder gestohlenes CSRF-Token" | |
| 294 | + error_rate001_desc: "Ratenlimit überschritten" | |
| 295 | + error_rate001_category: "Rate Limiting" | |
| 296 | + error_rate001_context: "Die Anzahl der Anfragen pro Minute wurde überschritten" | |
| 297 | + error_rate001_example: "Mehr als 100 Anfragen/Min auf Admin-Endpunkten" | |
| 298 | + error_valid001_desc: "Ungültige Eingabedaten" | |
| 299 | + error_valid001_category: "Validierung" | |
| 300 | + error_valid001_context: "Die bereitgestellten Daten entsprechen nicht den Validierungsregeln" | |
| 301 | + error_valid001_example: "Ungültiges E-Mail-Format oder fehlendes Pflichtfeld" | |
| 302 | + error_valid002_desc: "Passwortkomplexität nicht erfüllt" | |
| 303 | + error_valid002_category: "Validierung" | |
| 304 | + error_valid002_context: "Das Passwort erfüllt nicht die konfigurierten Komplexitätsregeln" | |
| 305 | + error_valid002_example: "Passwort zu kurz oder fehlende erforderliche Zeichen" | |
| 306 | + error_rbac001_desc: "Unzureichende Berechtigungen" | |
| 307 | + error_rbac001_category: "Autorisierung" | |
| 308 | + error_rbac001_context: "Der Benutzer hat nicht die erforderlichen Berechtigungen für diese Aktion" | |
| 309 | + error_rbac001_example: "Nicht-Admin-Benutzer versucht auf /admin/users zuzugreifen" | |
| 310 | + error_rbac002_desc: "Administratorrolle erforderlich" | |
| 311 | + error_rbac002_category: "Autorisierung" | |
| 312 | + error_rbac002_context: "Die Aktion erfordert eine Administratorrolle" | |
| 313 | + error_rbac002_example: "Versuch einen Benutzer ohne Admin-Rolle zu löschen" | |
| 314 | + error_db001_desc: "Datenbankoperationsfehler" | |
| 315 | + error_db001_category: "Datenbank" | |
| 316 | + error_db001_context: "Eine Datenbankoperation ist fehlgeschlagen" | |
| 317 | + error_db001_example: "Fremdschlüssel-Constraint verletzt oder DB-Verbindung verloren" | |
| 318 | + error_db002_desc: "Ressource nicht gefunden" | |
| 319 | + error_db002_category: "Datenbank" | |
| 320 | + error_db002_context: "Die angeforderte Ressource existiert nicht in der Datenbank" | |
| 321 | + error_db002_example: "Benutzer mit nicht existierender ID" | |
| 322 | + user_form: | |
| 323 | + breadcrumb_create: "Erstellen" | |
| 324 | + breadcrumb_edit: "Bearbeiten" | |
| 325 | + title_create: "Benutzer erstellen" | |
| 326 | + title_edit: "Benutzer bearbeiten" | |
| 327 | + back_to_list: "Zurück zur Liste" | |
| 328 | + username_label: "Benutzername" | |
| 329 | + username_placeholder: "benutzername" | |
| 330 | + username_readonly_hint: "Der Benutzername kann nach der Erstellung nicht geändert werden" | |
| 331 | + email_label: "E-Mail" | |
| 332 | + email_placeholder: "benutzer@example.com" | |
| 333 | + password_label: "Passwort" | |
| 334 | + password_optional: "Optional" | |
| 335 | + password_placeholder: "Leer lassen, um ein zufälliges Passwort zu generieren" | |
| 336 | + password_hint: "Wenn leer gelassen, wird automatisch ein sicheres Passwort generiert" | |
| 337 | + roles_label: "Rollen" | |
| 338 | + roles_hint: "Wählen Sie mindestens eine Rolle" | |
| 339 | + must_change_password_label: "Passwortänderung erzwingen" | |
| 340 | + must_change_password_desc: "Der Benutzer muss sein Passwort bei der nächsten Anmeldung ändern" | |
| 341 | + cancel: "Abbrechen" | |
| 342 | + submit_create: "Benutzer erstellen" | |
| 343 | + submit_edit: "Änderungen speichern" | |
| 159 | 344 | settings: |
| 160 | 345 | title: "Anwendungseinstellungen" |
| 161 | 346 | page_title: "Anwendungseinstellungen - Admin - RustCloud" |
@@ -174,6 +359,34 @@ admin:
| 174 | 359 | saved: "Einstellungen erfolgreich gespeichert" |
| 175 | 360 | error: "Fehler beim Speichern der Einstellungen" |
| 176 | 361 | network_error: "Netzwerkfehler. Bitte versuchen Sie es erneut." |
| 362 | + password_security: "Passwortsicherheit" | |
| 363 | + password_min_length: "Mindestlänge" | |
| 364 | + password_min_length_hint: "Zwischen 6 und 128 Zeichen" | |
| 365 | + password_complexity: "Komplexitätsanforderungen" | |
| 366 | + require_uppercase: "Großbuchstabe erforderlich" | |
| 367 | + require_lowercase: "Kleinbuchstabe erforderlich" | |
| 368 | + require_digits: "Ziffer erforderlich" | |
| 369 | + require_special: "Sonderzeichen erforderlich" | |
| 370 | + password_expiration: "Passwortablauf" | |
| 371 | + password_expiration_desc: "Periodische Passwortänderung erzwingen" | |
| 372 | + expiration_days: "Ablauf nach (Tagen)" | |
| 373 | + expiration_alert: "E-Mail-Warnung vor Ablauf" | |
| 374 | + expiration_alert_days: "Warnung (Tage vorher)" | |
| 375 | + audit_logging: "Audit-Protokollierung" | |
| 376 | + log_level: "Protokollebene" | |
| 377 | + log_level_debug: "DEBUG - Alle Details" | |
| 378 | + log_level_info: "INFO - Normale Informationen" | |
| 379 | + log_level_warn: "WARN - Nur Warnungen" | |
| 380 | + log_level_error: "ERROR - Nur Fehler" | |
| 381 | + log_level_hint: "Steuert die Ausführlichkeit der Audit-Logs" | |
| 382 | + log_retention: "Log-Aufbewahrung (Tage)" | |
| 383 | + log_retention_hint: "Aufbewahrungsdauer der Logs (1-365 Tage)" | |
| 384 | + audit_info_message: "Audit-Logs zeichnen alle administrativen Aktionen auf (Erstellung, Änderung, Löschung von Benutzern)." | |
| 385 | + save_changes: "Änderungen speichern" | |
| 386 | + save_changes_desc: "Einstellungen werden sofort angewendet" | |
| 387 | + saving: "Speichern..." | |
| 388 | + save_success: "Einstellungen erfolgreich gespeichert!" | |
| 389 | + save_error_prefix: "Fehler" | |
| 177 | 390 | users_list: |
| 178 | 391 | page_title: "Benutzerverwaltung - Admin - RustCloud" |
| 179 | 392 | title: "Benutzerverwaltung" |
@@ -211,8 +424,21 @@ admin:
| 211 | 424 | delete_confirm_email: "E-Mail:" |
| 212 | 425 | delete_irreversible: "Diese Aktion ist unwiderruflich!" |
| 213 | 426 | delete_button: "Endgültig löschen" |
| 427 | + delete: "Löschen" | |
| 214 | 428 | delete_error: "Fehler beim Löschen des Benutzers" |
| 215 | 429 | network_error: "Netzwerkfehler" |
| 430 | + edit_user_title: "Benutzer bearbeiten" | |
| 431 | + resetting: "Zurücksetzen..." | |
| 432 | + reset_by_email_title: "Zurücksetzen per E-Mail" | |
| 433 | + reset_email: "Per E-Mail zurücksetzen" | |
| 434 | + close: "Schließen" | |
| 435 | + password_reset_success_title: "Passwort zurückgesetzt" | |
| 436 | + password_warning_once: "Dieses Passwort wird nur einmal angezeigt. Kopieren Sie es jetzt!" | |
| 437 | + new_password_label: "Neues Passwort" | |
| 438 | + copy: "Kopieren" | |
| 439 | + password_copied: "Passwort in die Zwischenablage kopiert" | |
| 440 | + password_set_success: "Passwort erfolgreich zurückgesetzt" | |
| 441 | + reset_error: "Fehler beim Zurücksetzen des Passworts" | |
| 216 | 442 | |
| 217 | 443 | # Passwort ändern |
| 218 | 444 | password: |
M
locales/en.yml
+226
-1
@@ -34,8 +34,41 @@ login:
| 34 | 34 | resend_prompt: "Please enter your email before resending." |
| 35 | 35 | register_success: "Account created successfully! Please check your email." |
| 36 | 36 | register_error: "Registration error" |
| 37 | + email_label: "Email" | |
| 37 | 38 | footer_text: "Nextcloud fork in Rust" |
| 38 | 39 | |
| 40 | +# Resend verification email | |
| 41 | +resend_verification: | |
| 42 | + page_title: "Resend verification email - RustWarden" | |
| 43 | + title: "Resend verification email" | |
| 44 | + subtitle: "Enter your email address to receive a new verification link." | |
| 45 | + email_label: "Email address" | |
| 46 | + email_placeholder: "you@example.com" | |
| 47 | + submit: "Send verification link" | |
| 48 | + back_to_login: "Back to login" | |
| 49 | + error_default: "Unable to send the request." | |
| 50 | + success_default: "If the account exists and is not verified, an email has been sent." | |
| 51 | + error_missing_email: "Please enter your email." | |
| 52 | + error_cannot_send: "Unable to send the request at this time." | |
| 53 | + | |
| 54 | +# Password reset (from email link) | |
| 55 | +password_reset: | |
| 56 | + page_title: "Reset password" | |
| 57 | + title: "Reset password" | |
| 58 | + subtitle: "Enter your new password below." | |
| 59 | + new_password_label: "New password" | |
| 60 | + new_password_placeholder: "New secure password" | |
| 61 | + requirements: "⚠️ Must contain: 8+ characters, uppercase, lowercase, digit, special character" | |
| 62 | + confirm_password_label: "Confirm password" | |
| 63 | + confirm_password_placeholder: "Confirm your new password" | |
| 64 | + missing_token_url: "Reset token missing in URL." | |
| 65 | + missing_token: "Reset token is missing." | |
| 66 | + missing_new_password: "Please enter a new password." | |
| 67 | + passwords_not_match: "Passwords do not match." | |
| 68 | + success_message: "Your password has been reset successfully. You can now log in." | |
| 69 | + error_link_invalid: "Unable to reset password. The link may be expired or invalid." | |
| 70 | + error_generic: "Unable to reset password at this time." | |
| 71 | + | |
| 39 | 72 | # Error messages |
| 40 | 73 | error: |
| 41 | 74 | invalid_credentials: "Invalid credentials" |
@@ -66,11 +99,51 @@ error:
| 66 | 99 | # Settings |
| 67 | 100 | settings: |
| 68 | 101 | title: "Settings" |
| 102 | + page_title: "Settings - RustWarden" | |
| 103 | + subtitle: "Manage your preferences and account settings" | |
| 104 | + profile_title: "My Profile" | |
| 105 | + profile_desc: "Edit your personal information (username, email)" | |
| 106 | + password_title: "Password" | |
| 107 | + password_desc: "Change your password to secure your account" | |
| 108 | + sessions_title: "Active Sessions" | |
| 109 | + sessions_desc: "Manage your active sessions and revoke access from other devices" | |
| 110 | + preferences_title: "Preferences" | |
| 111 | + preferences_desc: "Customize your experience (theme, language)" | |
| 112 | + access: "Access" | |
| 113 | + info_title: "Information" | |
| 114 | + info_desc: "Use the sections above to manage different aspects of your account. All changes are secure and require your authentication." | |
| 69 | 115 | language: "Language" |
| 70 | 116 | language_description: "Choose your preferred language" |
| 71 | 117 | save: "Save" |
| 72 | 118 | saved: "Settings saved" |
| 73 | 119 | |
| 120 | +# Home page | |
| 121 | +home: | |
| 122 | + welcome: "Welcome, {username}!" | |
| 123 | + subtitle: "Welcome to your personal space" | |
| 124 | + my_account: "My Account" | |
| 125 | + upcoming_features: "Upcoming Features" | |
| 126 | + stat_username: "Username" | |
| 127 | + stat_email: "Email" | |
| 128 | + stat_member_since: "Member since" | |
| 129 | + stat_roles: "Roles" | |
| 130 | + feature_secrets_name: "Secrets Manager" | |
| 131 | + feature_secrets_desc: "Secure storage for passwords and secrets" | |
| 132 | + feature_2fa_name: "2FA Authentication" | |
| 133 | + feature_2fa_desc: "Two-factor authentication" | |
| 134 | + feature_files_name: "File Manager" | |
| 135 | + feature_files_desc: "Browse, create, rename and delete your files and folders" | |
| 136 | + feature_upload_name: "Upload/Download" | |
| 137 | + feature_upload_desc: "Download and upload files with progress bar" | |
| 138 | + feature_webdav_name: "WebDAV & Sync" | |
| 139 | + feature_webdav_desc: "Sync your files with desktop clients via WebDAV" | |
| 140 | + feature_mobile_name: "Mobile API" | |
| 141 | + feature_mobile_desc: "Sync with mobile apps (iOS/Android)" | |
| 142 | + badge_soon: "Soon" | |
| 143 | + badge_planned: "Planned" | |
| 144 | + did_you_know: "Did You Know?" | |
| 145 | + did_you_know_text: "RustWarden is a secure password manager written in Rust for better performance and enhanced security. Stay tuned for new features!" | |
| 146 | + | |
| 74 | 147 | # Common messages |
| 75 | 148 | common: |
| 76 | 149 | welcome: "Welcome {username}" |
@@ -159,8 +232,120 @@ breadcrumb:
| 159 | 232 | password: "Password" |
| 160 | 233 | sessions: "Sessions" |
| 161 | 234 | |
| 162 | -# Admin Settings | |
| 235 | +# Admin | |
| 163 | 236 | admin: |
| 237 | + dashboard: | |
| 238 | + title: "Administration" | |
| 239 | + heading: "Administrator Dashboard" | |
| 240 | + users_card_title: "Users" | |
| 241 | + users_card_subtitle: "Total users" | |
| 242 | + access_button: "Access" | |
| 243 | + sessions_card_title: "Active Sessions" | |
| 244 | + sessions_card_subtitle: "Ongoing sessions" | |
| 245 | + admins_card_title: "Administrators" | |
| 246 | + admins_card_subtitle: "Admin accounts" | |
| 247 | + last_activity_card_title: "Last Activity" | |
| 248 | + last_activity_card_subtitle: "Last login" | |
| 249 | + audit_section_title: "Audit Log Statistics" | |
| 250 | + audit_total_logs: "Total logs" | |
| 251 | + audit_last_24h: "Last 24h" | |
| 252 | + audit_by_level: "By level" | |
| 253 | + chart_label: "Logs by level" | |
| 254 | + settings_card_title: "Application Settings" | |
| 255 | + settings_card_desc: "Configure global application settings" | |
| 256 | + apps_card_title: "Application Management" | |
| 257 | + apps_card_desc: "Enable or disable modular applications" | |
| 258 | + orgs_card_title: "Organisation Management" | |
| 259 | + orgs_card_desc: "Create and manage organisations and their members" | |
| 260 | + orgs_stat_title: "Organisations" | |
| 261 | + orgs_members_stat_title: "Total members" | |
| 262 | + errors: | |
| 263 | + title: "Error Codes" | |
| 264 | + description: "Application error code documentation. Each error code is associated with a middleware and provides context information for debugging." | |
| 265 | + breadcrumb_errors: "Error Codes" | |
| 266 | + table_code: "Code" | |
| 267 | + table_description: "Description" | |
| 268 | + table_category: "Category" | |
| 269 | + table_middleware: "Middleware" | |
| 270 | + table_context: "Context" | |
| 271 | + table_example: "Example" | |
| 272 | + legend_title: "Legend" | |
| 273 | + legend_code: "Unique error identifier (format: CATEGORY_NUMBER)" | |
| 274 | + legend_description: "Description of the error" | |
| 275 | + legend_category: "Error type (Authentication, CSRF, Rate Limiting, etc.)" | |
| 276 | + legend_middleware: "Middleware responsible for generating this error" | |
| 277 | + legend_context: "Detailed explanation of when this error occurs" | |
| 278 | + legend_example: "Concrete example of a situation generating this error" | |
| 279 | + error_auth001_desc: "Invalid credentials" | |
| 280 | + error_auth001_category: "Authentication" | |
| 281 | + error_auth001_context: "The user provided incorrect credentials during login" | |
| 282 | + error_auth001_example: "Login attempt with an incorrect password" | |
| 283 | + error_auth002_desc: "Invalid token" | |
| 284 | + error_auth002_category: "Authentication" | |
| 285 | + error_auth002_context: "The provided JWT token is invalid or malformed" | |
| 286 | + error_auth002_example: "Corrupted JWT token or signed with a different key" | |
| 287 | + error_auth003_desc: "Token expired" | |
| 288 | + error_auth003_category: "Authentication" | |
| 289 | + error_auth003_context: "The JWT token has expired and needs to be renewed" | |
| 290 | + error_auth003_example: "Access attempt with an expired token after the validity period" | |
| 291 | + error_csrf001_desc: "Missing CSRF token" | |
| 292 | + error_csrf001_category: "CSRF Protection" | |
| 293 | + error_csrf001_context: "The POST/PUT/DELETE request does not contain a CSRF token" | |
| 294 | + error_csrf001_example: "Form submitted without a CSRF token in the header or body" | |
| 295 | + error_csrf002_desc: "Invalid CSRF token" | |
| 296 | + error_csrf002_category: "CSRF Protection" | |
| 297 | + error_csrf002_context: "The provided CSRF token does not match the session token" | |
| 298 | + error_csrf002_example: "Modified or stolen CSRF token" | |
| 299 | + error_rate001_desc: "Rate limit exceeded" | |
| 300 | + error_rate001_category: "Rate Limiting" | |
| 301 | + error_rate001_context: "The number of requests per minute has been exceeded" | |
| 302 | + error_rate001_example: "More than 100 requests/min on admin endpoints" | |
| 303 | + error_valid001_desc: "Invalid input data" | |
| 304 | + error_valid001_category: "Validation" | |
| 305 | + error_valid001_context: "The provided data does not comply with validation rules" | |
| 306 | + error_valid001_example: "Invalid email format or missing required field" | |
| 307 | + error_valid002_desc: "Password complexity not met" | |
| 308 | + error_valid002_category: "Validation" | |
| 309 | + error_valid002_context: "The password does not meet the configured complexity rules" | |
| 310 | + error_valid002_example: "Password too short or missing required characters" | |
| 311 | + error_rbac001_desc: "Insufficient permissions" | |
| 312 | + error_rbac001_category: "Authorization" | |
| 313 | + error_rbac001_context: "The user does not have the necessary permissions for this action" | |
| 314 | + error_rbac001_example: "Non-admin user attempting to access /admin/users" | |
| 315 | + error_rbac002_desc: "Admin role required" | |
| 316 | + error_rbac002_category: "Authorization" | |
| 317 | + error_rbac002_context: "The action requires an administrator role" | |
| 318 | + error_rbac002_example: "Attempting to delete a user without admin role" | |
| 319 | + error_db001_desc: "Database operation error" | |
| 320 | + error_db001_category: "Database" | |
| 321 | + error_db001_context: "A database operation failed" | |
| 322 | + error_db001_example: "Foreign key constraint violated or DB connection lost" | |
| 323 | + error_db002_desc: "Resource not found" | |
| 324 | + error_db002_category: "Database" | |
| 325 | + error_db002_context: "The requested resource does not exist in the database" | |
| 326 | + error_db002_example: "User with non-existent ID" | |
| 327 | + user_form: | |
| 328 | + breadcrumb_create: "Create" | |
| 329 | + breadcrumb_edit: "Edit" | |
| 330 | + title_create: "Create a user" | |
| 331 | + title_edit: "Edit a user" | |
| 332 | + back_to_list: "Back to list" | |
| 333 | + username_label: "Username" | |
| 334 | + username_placeholder: "username" | |
| 335 | + username_readonly_hint: "Username cannot be changed after creation" | |
| 336 | + email_label: "Email" | |
| 337 | + email_placeholder: "user@example.com" | |
| 338 | + password_label: "Password" | |
| 339 | + password_optional: "Optional" | |
| 340 | + password_placeholder: "Leave empty to generate a random password" | |
| 341 | + password_hint: "If left empty, a secure password will be automatically generated" | |
| 342 | + roles_label: "Roles" | |
| 343 | + roles_hint: "Select at least one role" | |
| 344 | + must_change_password_label: "Force password change" | |
| 345 | + must_change_password_desc: "The user will have to change their password at next login" | |
| 346 | + cancel: "Cancel" | |
| 347 | + submit_create: "Create user" | |
| 348 | + submit_edit: "Save changes" | |
| 164 | 349 | settings: |
| 165 | 350 | title: "Application Settings" |
| 166 | 351 | page_title: "Application Settings - Admin - RustCloud" |
@@ -179,6 +364,34 @@ admin:
| 179 | 364 | saved: "Settings saved successfully" |
| 180 | 365 | error: "Error saving settings" |
| 181 | 366 | network_error: "Network error. Please try again." |
| 367 | + password_security: "Password Security" | |
| 368 | + password_min_length: "Minimum length" | |
| 369 | + password_min_length_hint: "Between 6 and 128 characters" | |
| 370 | + password_complexity: "Complexity requirements" | |
| 371 | + require_uppercase: "Uppercase required" | |
| 372 | + require_lowercase: "Lowercase required" | |
| 373 | + require_digits: "Digit required" | |
| 374 | + require_special: "Special character required" | |
| 375 | + password_expiration: "Password expiration" | |
| 376 | + password_expiration_desc: "Force periodic password change" | |
| 377 | + expiration_days: "Expiration after (days)" | |
| 378 | + expiration_alert: "Email alert before expiration" | |
| 379 | + expiration_alert_days: "Alert (days before)" | |
| 380 | + audit_logging: "Audit Logging" | |
| 381 | + log_level: "Log level" | |
| 382 | + log_level_debug: "DEBUG - All details" | |
| 383 | + log_level_info: "INFO - Normal information" | |
| 384 | + log_level_warn: "WARN - Warnings only" | |
| 385 | + log_level_error: "ERROR - Errors only" | |
| 386 | + log_level_hint: "Controls the verbosity of audit logs" | |
| 387 | + log_retention: "Log retention (days)" | |
| 388 | + log_retention_hint: "Log retention period (1-365 days)" | |
| 389 | + audit_info_message: "Audit logs record all administrative actions (creation, modification, deletion of users)." | |
| 390 | + save_changes: "Save changes" | |
| 391 | + save_changes_desc: "Settings will be applied immediately" | |
| 392 | + saving: "Saving..." | |
| 393 | + save_success: "Settings saved successfully!" | |
| 394 | + save_error_prefix: "Error" | |
| 182 | 395 | users_list: |
| 183 | 396 | page_title: "User Management - Admin - RustCloud" |
| 184 | 397 | title: "User Management" |
@@ -217,8 +430,20 @@ admin:
| 217 | 430 | delete_confirm_email: "Email:" |
| 218 | 431 | delete_irreversible: "This action is irreversible!" |
| 219 | 432 | delete_button: "Delete permanently" |
| 433 | + delete: "Delete" | |
| 220 | 434 | delete_error: "Error deleting user" |
| 221 | 435 | network_error: "Network error" |
| 436 | + edit_user_title: "Edit user" | |
| 437 | + resetting: "Resetting..." | |
| 438 | + reset_by_email_title: "Reset by email" | |
| 439 | + close: "Close" | |
| 440 | + password_reset_success_title: "Password reset" | |
| 441 | + password_warning_once: "This password will only be displayed once. Copy it now!" | |
| 442 | + new_password_label: "New password" | |
| 443 | + copy: "Copy" | |
| 444 | + password_copied: "Password copied to clipboard" | |
| 445 | + password_set_success: "Password reset successfully" | |
| 446 | + reset_error: "Error resetting password" | |
| 222 | 447 | |
| 223 | 448 | # Password Change |
| 224 | 449 | password: |
M
locales/es.yml
+227
-1
@@ -34,6 +34,7 @@ login:
| 34 | 34 | resend_prompt: "Por favor ingrese su email antes de reenviar." |
| 35 | 35 | register_success: "¡Cuenta creada con éxito! Por favor verifique su email." |
| 36 | 36 | register_error: "Error en el registro" |
| 37 | + email_label: "Correo electrónico" | |
| 37 | 38 | footer_text: "Fork de Nextcloud en Rust" |
| 38 | 39 | |
| 39 | 40 | # Mensajes de error |
@@ -63,14 +64,86 @@ error:
| 63 | 64 | password_require_digits: "La contraseña debe contener al menos un dígito" |
| 64 | 65 | password_require_special: "La contraseña debe contener al menos un carácter especial (!@#$%^&*()_+-=[]{}|;:,.<>?)" |
| 65 | 66 | |
| 67 | +# Reenviar email de verificación | |
| 68 | +resend_verification: | |
| 69 | + page_title: "Reenviar email de verificación - RustWarden" | |
| 70 | + title: "Reenviar email de verificación" | |
| 71 | + subtitle: "Ingrese su dirección de correo electrónico para recibir un nuevo enlace de verificación." | |
| 72 | + email_label: "Dirección de correo electrónico" | |
| 73 | + email_placeholder: "usted@ejemplo.com" | |
| 74 | + submit: "Enviar enlace de verificación" | |
| 75 | + back_to_login: "Volver al inicio de sesión" | |
| 76 | + error_default: "No se pudo enviar la solicitud." | |
| 77 | + success_default: "Si la cuenta existe y no está verificada, se ha enviado un email." | |
| 78 | + error_missing_email: "Por favor ingrese su correo electrónico." | |
| 79 | + error_cannot_send: "No se puede enviar la solicitud en este momento." | |
| 80 | + | |
| 81 | +# Restablecimiento de contraseña (desde enlace de email) | |
| 82 | +password_reset: | |
| 83 | + page_title: "Restablecer contraseña" | |
| 84 | + title: "Restablecer contraseña" | |
| 85 | + subtitle: "Ingrese su nueva contraseña a continuación." | |
| 86 | + new_password_label: "Nueva contraseña" | |
| 87 | + new_password_placeholder: "Nueva contraseña segura" | |
| 88 | + requirements: "⚠️ Debe contener: 8+ caracteres, mayúscula, minúscula, dígito, carácter especial" | |
| 89 | + confirm_password_label: "Confirmar contraseña" | |
| 90 | + confirm_password_placeholder: "Confirme su nueva contraseña" | |
| 91 | + missing_token_url: "Falta el token de restablecimiento en la URL." | |
| 92 | + missing_token: "Falta el token de restablecimiento." | |
| 93 | + missing_new_password: "Por favor ingrese una nueva contraseña." | |
| 94 | + passwords_not_match: "Las contraseñas no coinciden." | |
| 95 | + success_message: "Su contraseña ha sido restablecida con éxito. Ahora puede iniciar sesión." | |
| 96 | + error_link_invalid: "No se pudo restablecer la contraseña. El enlace puede haber expirado o ser inválido." | |
| 97 | + error_generic: "No se puede restablecer la contraseña en este momento." | |
| 98 | + | |
| 66 | 99 | # Configuración |
| 67 | 100 | settings: |
| 68 | 101 | title: "Configuración" |
| 102 | + page_title: "Configuración - RustWarden" | |
| 103 | + subtitle: "Gestione sus preferencias y configuración de cuenta" | |
| 104 | + profile_title: "Mi Perfil" | |
| 105 | + profile_desc: "Edite su información personal (nombre de usuario, email)" | |
| 106 | + password_title: "Contraseña" | |
| 107 | + password_desc: "Cambie su contraseña para asegurar su cuenta" | |
| 108 | + sessions_title: "Sesiones activas" | |
| 109 | + sessions_desc: "Gestione sus sesiones activas y revoque el acceso desde otros dispositivos" | |
| 110 | + preferences_title: "Preferencias" | |
| 111 | + preferences_desc: "Personalice su experiencia (tema, idioma)" | |
| 112 | + access: "Acceder" | |
| 113 | + info_title: "Información" | |
| 114 | + info_desc: "Utilice las secciones anteriores para gestionar diferentes aspectos de su cuenta. Todos los cambios son seguros y requieren su autenticación." | |
| 69 | 115 | language: "Idioma" |
| 70 | 116 | language_description: "Elija su idioma preferido" |
| 71 | 117 | save: "Guardar" |
| 72 | 118 | saved: "Configuración guardada" |
| 73 | 119 | |
| 120 | +# Página de inicio | |
| 121 | +home: | |
| 122 | + welcome: "¡Bienvenido, {username}!" | |
| 123 | + subtitle: "Bienvenido a tu espacio personal" | |
| 124 | + my_account: "Mi Cuenta" | |
| 125 | + upcoming_features: "Próximas Funcionalidades" | |
| 126 | + stat_username: "Nombre de usuario" | |
| 127 | + stat_email: "Correo electrónico" | |
| 128 | + stat_member_since: "Miembro desde" | |
| 129 | + stat_roles: "Roles" | |
| 130 | + feature_secrets_name: "Gestor de secretos" | |
| 131 | + feature_secrets_desc: "Almacenamiento seguro de contraseñas y secretos" | |
| 132 | + feature_2fa_name: "Autenticación 2FA" | |
| 133 | + feature_2fa_desc: "Autenticación de dos factores" | |
| 134 | + feature_files_name: "Gestor de Archivos" | |
| 135 | + feature_files_desc: "Explorar, crear, renombrar y eliminar archivos y carpetas" | |
| 136 | + feature_upload_name: "Subir/Descargar" | |
| 137 | + feature_upload_desc: "Descargar y subir archivos con barra de progreso" | |
| 138 | + feature_webdav_name: "WebDAV y Sincronización" | |
| 139 | + feature_webdav_desc: "Sincronizar archivos con clientes de escritorio vía WebDAV" | |
| 140 | + feature_mobile_name: "API Móvil" | |
| 141 | + feature_mobile_desc: "Sincronización con aplicaciones móviles (iOS/Android)" | |
| 142 | + badge_soon: "Pronto" | |
| 143 | + badge_planned: "Planificado" | |
| 144 | + did_you_know: "¿Sabías que...?" | |
| 145 | + did_you_know_text: "RustWarden es un gestor de contraseñas seguro escrito en Rust para ofrecer mejor rendimiento y seguridad mejorada. ¡Mantente atento a las nuevas funcionalidades!" | |
| 146 | + | |
| 74 | 147 | # Mensajes comunes |
| 75 | 148 | common: |
| 76 | 149 | welcome: "Bienvenido {username}" |
@@ -154,8 +227,120 @@ breadcrumb:
| 154 | 227 | password: "Contraseña" |
| 155 | 228 | sessions: "Sesiones" |
| 156 | 229 | |
| 157 | -# Configuración de administración | |
| 230 | +# Administración | |
| 158 | 231 | admin: |
| 232 | + dashboard: | |
| 233 | + title: "Administración" | |
| 234 | + heading: "Panel de Administración" | |
| 235 | + users_card_title: "Usuarios" | |
| 236 | + users_card_subtitle: "Usuarios totales" | |
| 237 | + access_button: "Acceder" | |
| 238 | + sessions_card_title: "Sesiones activas" | |
| 239 | + sessions_card_subtitle: "Sesiones en curso" | |
| 240 | + admins_card_title: "Administradores" | |
| 241 | + admins_card_subtitle: "Cuentas admin" | |
| 242 | + last_activity_card_title: "Última actividad" | |
| 243 | + last_activity_card_subtitle: "Último inicio de sesión" | |
| 244 | + audit_section_title: "Estadísticas de logs de auditoría" | |
| 245 | + audit_total_logs: "Logs totales" | |
| 246 | + audit_last_24h: "Últimas 24h" | |
| 247 | + audit_by_level: "Por nivel" | |
| 248 | + chart_label: "Logs por nivel" | |
| 249 | + settings_card_title: "Configuración de la Aplicación" | |
| 250 | + settings_card_desc: "Configurar los parámetros globales de la aplicación" | |
| 251 | + apps_card_title: "Gestión de Aplicaciones" | |
| 252 | + apps_card_desc: "Activar o desactivar aplicaciones modulares" | |
| 253 | + orgs_card_title: "Gestión de Organizaciones" | |
| 254 | + orgs_card_desc: "Crear y gestionar organizaciones y sus miembros" | |
| 255 | + orgs_stat_title: "Organizaciones" | |
| 256 | + orgs_members_stat_title: "Miembros totales" | |
| 257 | + errors: | |
| 258 | + title: "Códigos de Error" | |
| 259 | + description: "Documentación de los códigos de error de la aplicación. Cada código de error está asociado a un middleware y proporciona información de contexto para la depuración." | |
| 260 | + breadcrumb_errors: "Códigos de Error" | |
| 261 | + table_code: "Código" | |
| 262 | + table_description: "Descripción" | |
| 263 | + table_category: "Categoría" | |
| 264 | + table_middleware: "Middleware" | |
| 265 | + table_context: "Contexto" | |
| 266 | + table_example: "Ejemplo" | |
| 267 | + legend_title: "Leyenda" | |
| 268 | + legend_code: "Identificador único del error (formato: CATEGORY_NUMBER)" | |
| 269 | + legend_description: "Descripción del error" | |
| 270 | + legend_category: "Tipo de error (Autenticación, CSRF, Rate Limiting, etc.)" | |
| 271 | + legend_middleware: "Middleware responsable de generar este error" | |
| 272 | + legend_context: "Explicación detallada de cuándo ocurre este error" | |
| 273 | + legend_example: "Ejemplo concreto de una situación que genera este error" | |
| 274 | + error_auth001_desc: "Credenciales inválidas" | |
| 275 | + error_auth001_category: "Autenticación" | |
| 276 | + error_auth001_context: "El usuario proporcionó credenciales incorrectas al iniciar sesión" | |
| 277 | + error_auth001_example: "Intento de inicio de sesión con contraseña incorrecta" | |
| 278 | + error_auth002_desc: "Token inválido" | |
| 279 | + error_auth002_category: "Autenticación" | |
| 280 | + error_auth002_context: "El token JWT proporcionado es inválido o está malformado" | |
| 281 | + error_auth002_example: "Token JWT corrupto o firmado con una clave diferente" | |
| 282 | + error_auth003_desc: "Token expirado" | |
| 283 | + error_auth003_category: "Autenticación" | |
| 284 | + error_auth003_context: "El token JWT ha expirado y debe renovarse" | |
| 285 | + error_auth003_example: "Intento de acceso con un token expirado después del período de validez" | |
| 286 | + error_csrf001_desc: "Token CSRF faltante" | |
| 287 | + error_csrf001_category: "Protección CSRF" | |
| 288 | + error_csrf001_context: "La solicitud POST/PUT/DELETE no contiene un token CSRF" | |
| 289 | + error_csrf001_example: "Formulario enviado sin token CSRF en el header o body" | |
| 290 | + error_csrf002_desc: "Token CSRF inválido" | |
| 291 | + error_csrf002_category: "Protección CSRF" | |
| 292 | + error_csrf002_context: "El token CSRF proporcionado no coincide con el token de sesión" | |
| 293 | + error_csrf002_example: "Token CSRF modificado o robado" | |
| 294 | + error_rate001_desc: "Límite de tasa excedido" | |
| 295 | + error_rate001_category: "Rate Limiting" | |
| 296 | + error_rate001_context: "Se ha excedido el número de solicitudes por minuto" | |
| 297 | + error_rate001_example: "Más de 100 solicitudes/min en endpoints admin" | |
| 298 | + error_valid001_desc: "Datos de entrada inválidos" | |
| 299 | + error_valid001_category: "Validación" | |
| 300 | + error_valid001_context: "Los datos proporcionados no cumplen con las reglas de validación" | |
| 301 | + error_valid001_example: "Formato de email inválido o campo requerido faltante" | |
| 302 | + error_valid002_desc: "Complejidad de contraseña no cumplida" | |
| 303 | + error_valid002_category: "Validación" | |
| 304 | + error_valid002_context: "La contraseña no cumple con las reglas de complejidad configuradas" | |
| 305 | + error_valid002_example: "Contraseña demasiado corta o sin los caracteres requeridos" | |
| 306 | + error_rbac001_desc: "Permisos insuficientes" | |
| 307 | + error_rbac001_category: "Autorización" | |
| 308 | + error_rbac001_context: "El usuario no tiene los permisos necesarios para esta acción" | |
| 309 | + error_rbac001_example: "Usuario no administrador intentando acceder a /admin/users" | |
| 310 | + error_rbac002_desc: "Rol de administrador requerido" | |
| 311 | + error_rbac002_category: "Autorización" | |
| 312 | + error_rbac002_context: "La acción requiere un rol de administrador" | |
| 313 | + error_rbac002_example: "Intento de eliminar un usuario sin rol admin" | |
| 314 | + error_db001_desc: "Error de operación de base de datos" | |
| 315 | + error_db001_category: "Base de datos" | |
| 316 | + error_db001_context: "Una operación de base de datos falló" | |
| 317 | + error_db001_example: "Restricción de clave foránea violada o conexión DB perdida" | |
| 318 | + error_db002_desc: "Recurso no encontrado" | |
| 319 | + error_db002_category: "Base de datos" | |
| 320 | + error_db002_context: "El recurso solicitado no existe en la base de datos" | |
| 321 | + error_db002_example: "Usuario con ID inexistente" | |
| 322 | + user_form: | |
| 323 | + breadcrumb_create: "Crear" | |
| 324 | + breadcrumb_edit: "Modificar" | |
| 325 | + title_create: "Crear un usuario" | |
| 326 | + title_edit: "Modificar un usuario" | |
| 327 | + back_to_list: "Volver a la lista" | |
| 328 | + username_label: "Nombre de usuario" | |
| 329 | + username_placeholder: "nombre_usuario" | |
| 330 | + username_readonly_hint: "El nombre de usuario no se puede cambiar después de la creación" | |
| 331 | + email_label: "Email" | |
| 332 | + email_placeholder: "usuario@example.com" | |
| 333 | + password_label: "Contraseña" | |
| 334 | + password_optional: "Opcional" | |
| 335 | + password_placeholder: "Dejar vacío para generar una contraseña aleatoria" | |
| 336 | + password_hint: "Si se deja vacío, se generará automáticamente una contraseña segura" | |
| 337 | + roles_label: "Roles" | |
| 338 | + roles_hint: "Seleccione al menos un rol" | |
| 339 | + must_change_password_label: "Forzar cambio de contraseña" | |
| 340 | + must_change_password_desc: "El usuario deberá cambiar su contraseña en el próximo inicio de sesión" | |
| 341 | + cancel: "Cancelar" | |
| 342 | + submit_create: "Crear usuario" | |
| 343 | + submit_edit: "Guardar cambios" | |
| 159 | 344 | settings: |
| 160 | 345 | title: "Configuración de la aplicación" |
| 161 | 346 | page_title: "Configuración de la aplicación - Admin - RustCloud" |
@@ -174,6 +359,34 @@ admin:
| 174 | 359 | saved: "Configuración guardada con éxito" |
| 175 | 360 | error: "Error al guardar la configuración" |
| 176 | 361 | network_error: "Error de red. Por favor intente nuevamente." |
| 362 | + password_security: "Seguridad de Contraseñas" | |
| 363 | + password_min_length: "Longitud mínima" | |
| 364 | + password_min_length_hint: "Entre 6 y 128 caracteres" | |
| 365 | + password_complexity: "Requisitos de complejidad" | |
| 366 | + require_uppercase: "Mayúscula requerida" | |
| 367 | + require_lowercase: "Minúscula requerida" | |
| 368 | + require_digits: "Dígito requerido" | |
| 369 | + require_special: "Carácter especial requerido" | |
| 370 | + password_expiration: "Expiración de contraseñas" | |
| 371 | + password_expiration_desc: "Forzar cambio periódico de contraseña" | |
| 372 | + expiration_days: "Expiración después de (días)" | |
| 373 | + expiration_alert: "Alerta por email antes de la expiración" | |
| 374 | + expiration_alert_days: "Alerta (días antes)" | |
| 375 | + audit_logging: "Registro de Auditoría" | |
| 376 | + log_level: "Nivel de log" | |
| 377 | + log_level_debug: "DEBUG - Todos los detalles" | |
| 378 | + log_level_info: "INFO - Información normal" | |
| 379 | + log_level_warn: "WARN - Solo advertencias" | |
| 380 | + log_level_error: "ERROR - Solo errores" | |
| 381 | + log_level_hint: "Controla la verbosidad de los logs de auditoría" | |
| 382 | + log_retention: "Retención de logs (días)" | |
| 383 | + log_retention_hint: "Período de retención de logs (1-365 días)" | |
| 384 | + audit_info_message: "Los logs de auditoría registran todas las acciones administrativas (creación, modificación, eliminación de usuarios)." | |
| 385 | + save_changes: "Guardar cambios" | |
| 386 | + save_changes_desc: "La configuración se aplicará inmediatamente" | |
| 387 | + saving: "Guardando..." | |
| 388 | + save_success: "¡Configuración guardada con éxito!" | |
| 389 | + save_error_prefix: "Error" | |
| 177 | 390 | users_list: |
| 178 | 391 | page_title: "Gestión de Usuarios - Admin - RustCloud" |
| 179 | 392 | title: "Gestión de Usuarios" |
@@ -211,8 +424,21 @@ admin:
| 211 | 424 | delete_confirm_email: "Email:" |
| 212 | 425 | delete_irreversible: "¡Esta acción es irreversible!" |
| 213 | 426 | delete_button: "Eliminar permanentemente" |
| 427 | + delete: "Eliminar" | |
| 214 | 428 | delete_error: "Error al eliminar el usuario" |
| 215 | 429 | network_error: "Error de red" |
| 430 | + edit_user_title: "Modificar usuario" | |
| 431 | + resetting: "Restableciendo..." | |
| 432 | + reset_by_email_title: "Restablecer por email" | |
| 433 | + reset_email: "Restablecer por email" | |
| 434 | + close: "Cerrar" | |
| 435 | + password_reset_success_title: "Contraseña restablecida" | |
| 436 | + password_warning_once: "¡Esta contraseña solo se mostrará una vez. Cópiela ahora!" | |
| 437 | + new_password_label: "Nueva contraseña" | |
| 438 | + copy: "Copiar" | |
| 439 | + password_copied: "Contraseña copiada al portapapeles" | |
| 440 | + password_set_success: "Contraseña restablecida con éxito" | |
| 441 | + reset_error: "Error al restablecer la contraseña" | |
| 216 | 442 | |
| 217 | 443 | # Cambio de contraseña |
| 218 | 444 | password: |
M
locales/fr.yml
+225
-0
@@ -34,8 +34,41 @@ login:
| 34 | 34 | resend_prompt: "Veuillez renseigner votre email avant de renvoyer." |
| 35 | 35 | register_success: "Compte créé avec succès ! Veuillez vérifier votre email." |
| 36 | 36 | register_error: "Erreur lors de l'inscription" |
| 37 | + email_label: "Email" | |
| 37 | 38 | footer_text: "Fork de Nextcloud en Rust" |
| 38 | 39 | |
| 40 | +# Renvoyer l'email de validation | |
| 41 | +resend_verification: | |
| 42 | + page_title: "Renvoyer l'email de validation - RustWarden" | |
| 43 | + title: "Renvoyer l'email de validation" | |
| 44 | + subtitle: "Entrez votre adresse email pour recevoir un nouveau lien de validation." | |
| 45 | + email_label: "Adresse email" | |
| 46 | + email_placeholder: "vous@example.com" | |
| 47 | + submit: "Envoyer le lien de validation" | |
| 48 | + back_to_login: "Revenir à la connexion" | |
| 49 | + error_default: "Impossible d'envoyer la demande." | |
| 50 | + success_default: "Si le compte existe et n'est pas vérifié, un email a été envoyé." | |
| 51 | + error_missing_email: "Veuillez renseigner votre email." | |
| 52 | + error_cannot_send: "Impossible d'envoyer la demande pour l'instant." | |
| 53 | + | |
| 54 | +# Réinitialisation de mot de passe (via lien email) | |
| 55 | +password_reset: | |
| 56 | + page_title: "Réinitialiser le mot de passe" | |
| 57 | + title: "Réinitialiser le mot de passe" | |
| 58 | + subtitle: "Entrez votre nouveau mot de passe ci-dessous." | |
| 59 | + new_password_label: "Nouveau mot de passe" | |
| 60 | + new_password_placeholder: "Nouveau mot de passe sécurisé" | |
| 61 | + requirements: "⚠️ Doit contenir : 8+ caractères, majuscule, minuscule, chiffre, caractère spécial" | |
| 62 | + confirm_password_label: "Confirmer le mot de passe" | |
| 63 | + confirm_password_placeholder: "Confirmez votre nouveau mot de passe" | |
| 64 | + missing_token_url: "Token de réinitialisation manquant dans l'URL." | |
| 65 | + missing_token: "Token de réinitialisation manquant." | |
| 66 | + missing_new_password: "Veuillez entrer un nouveau mot de passe." | |
| 67 | + passwords_not_match: "Les mots de passe ne correspondent pas." | |
| 68 | + success_message: "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter." | |
| 69 | + error_link_invalid: "Impossible de réinitialiser le mot de passe. Le lien peut être expiré ou invalide." | |
| 70 | + error_generic: "Impossible de réinitialiser le mot de passe pour l'instant." | |
| 71 | + | |
| 39 | 72 | # Messages d'erreur |
| 40 | 73 | error: |
| 41 | 74 | invalid_credentials: "Identifiants incorrects" |
@@ -66,11 +99,51 @@ error:
| 66 | 99 | # Paramètres |
| 67 | 100 | settings: |
| 68 | 101 | title: "Paramètres" |
| 102 | + page_title: "Paramètres - RustWarden" | |
| 103 | + subtitle: "Gérez vos préférences et paramètres de compte" | |
| 104 | + profile_title: "Mon Profil" | |
| 105 | + profile_desc: "Modifiez vos informations personnelles (nom d'utilisateur, email)" | |
| 106 | + password_title: "Mot de passe" | |
| 107 | + password_desc: "Changez votre mot de passe pour sécuriser votre compte" | |
| 108 | + sessions_title: "Sessions actives" | |
| 109 | + sessions_desc: "Gérez vos sessions actives et révoquez l'accès depuis d'autres appareils" | |
| 110 | + preferences_title: "Préférences" | |
| 111 | + preferences_desc: "Personnalisez votre expérience (thème, langue)" | |
| 112 | + access: "Accéder" | |
| 113 | + info_title: "Informations" | |
| 114 | + info_desc: "Utilisez les sections ci-dessus pour gérer différents aspects de votre compte. Toutes les modifications sont sécurisées et nécessitent votre authentification." | |
| 69 | 115 | language: "Langue" |
| 70 | 116 | language_description: "Choisissez votre langue préférée" |
| 71 | 117 | save: "Enregistrer" |
| 72 | 118 | saved: "Paramètres enregistrés" |
| 73 | 119 | |
| 120 | +# Page d'accueil | |
| 121 | +home: | |
| 122 | + welcome: "Bienvenue, {username} !" | |
| 123 | + subtitle: "Bienvenue sur votre espace personnel" | |
| 124 | + my_account: "Mon Compte" | |
| 125 | + upcoming_features: "Fonctionnalités à Venir" | |
| 126 | + stat_username: "Nom d'utilisateur" | |
| 127 | + stat_email: "Email" | |
| 128 | + stat_member_since: "Membre depuis" | |
| 129 | + stat_roles: "Rôles" | |
| 130 | + feature_secrets_name: "Gestion des secrets" | |
| 131 | + feature_secrets_desc: "Stockage sécurisé de mots de passe et secrets" | |
| 132 | + feature_2fa_name: "Authentification 2FA" | |
| 133 | + feature_2fa_desc: "Authentification à deux facteurs" | |
| 134 | + feature_files_name: "Gestionnaire de Fichiers" | |
| 135 | + feature_files_desc: "Parcourir, créer, renommer et supprimer vos fichiers et dossiers" | |
| 136 | + feature_upload_name: "Upload/Download" | |
| 137 | + feature_upload_desc: "Télécharger et téléverser des fichiers avec barre de progression" | |
| 138 | + feature_webdav_name: "WebDAV et Synchronisation" | |
| 139 | + feature_webdav_desc: "Synchroniser vos fichiers avec clients desktop via WebDAV" | |
| 140 | + feature_mobile_name: "API Mobile" | |
| 141 | + feature_mobile_desc: "Synchronisation avec applications mobiles (iOS/Android)" | |
| 142 | + badge_soon: "Bientôt" | |
| 143 | + badge_planned: "Prévu" | |
| 144 | + did_you_know: "Le Saviez-Vous ?" | |
| 145 | + did_you_know_text: "RustWarden est un gestionnaire de mots de passe sécurisé écrit en Rust pour offrir de meilleures performances et une sécurité renforcée. Restez à l'écoute pour les nouvelles fonctionnalités !" | |
| 146 | + | |
| 74 | 147 | # Messages généraux |
| 75 | 148 | common: |
| 76 | 149 | welcome: "Bienvenue {username}" |
@@ -158,10 +231,123 @@ breadcrumb:
| 158 | 231 | preferences: "Préférences" |
| 159 | 232 | profile: "Profil" |
| 160 | 233 | password: "Mot de passe" |
| 234 | + users: "Utilisateurs" | |
| 161 | 235 | sessions: "Sessions" |
| 162 | 236 | |
| 163 | 237 | # Admin Settings |
| 164 | 238 | admin: |
| 239 | + dashboard: | |
| 240 | + title: "Administration" | |
| 241 | + heading: "Tableau de bord Administrateur" | |
| 242 | + users_card_title: "Utilisateurs" | |
| 243 | + users_card_subtitle: "Total utilisateurs" | |
| 244 | + access_button: "Accéder" | |
| 245 | + sessions_card_title: "Sessions actives" | |
| 246 | + sessions_card_subtitle: "Sessions en cours" | |
| 247 | + admins_card_title: "Administrateurs" | |
| 248 | + admins_card_subtitle: "Comptes admin" | |
| 249 | + last_activity_card_title: "Dernière activité" | |
| 250 | + last_activity_card_subtitle: "Dernière connexion" | |
| 251 | + audit_section_title: "Statistiques des logs d'audit" | |
| 252 | + audit_total_logs: "Total des logs" | |
| 253 | + audit_last_24h: "Dernières 24h" | |
| 254 | + audit_by_level: "Par niveau" | |
| 255 | + chart_label: "Logs par niveau" | |
| 256 | + settings_card_title: "Paramètres Application" | |
| 257 | + settings_card_desc: "Configurer les paramètres globaux de l'application" | |
| 258 | + apps_card_title: "Gestion des Applications" | |
| 259 | + apps_card_desc: "Activer ou désactiver les applications modulaires" | |
| 260 | + orgs_card_title: "Gestion des Organisations" | |
| 261 | + orgs_card_desc: "Créer et gérer les organisations et leurs membres" | |
| 262 | + orgs_stat_title: "Organisations" | |
| 263 | + orgs_members_stat_title: "Membres totaux" | |
| 264 | + errors: | |
| 265 | + title: "Codes d'Erreur" | |
| 266 | + description: "Documentation des codes d'erreur de l'application. Chaque code d'erreur est associé à un middleware et fournit des informations de contexte pour le débogage." | |
| 267 | + breadcrumb_errors: "Codes d'Erreur" | |
| 268 | + table_code: "Code" | |
| 269 | + table_description: "Description" | |
| 270 | + table_category: "Catégorie" | |
| 271 | + table_middleware: "Middleware" | |
| 272 | + table_context: "Contexte" | |
| 273 | + table_example: "Exemple" | |
| 274 | + legend_title: "Légende" | |
| 275 | + legend_code: "Identifiant unique de l'erreur (format: CATEGORY_NUMBER)" | |
| 276 | + legend_description: "Description en français de l'erreur" | |
| 277 | + legend_category: "Type d'erreur (Authentification, CSRF, Rate Limiting, etc.)" | |
| 278 | + legend_middleware: "Middleware responsable de générer cette erreur" | |
| 279 | + legend_context: "Explication détaillée de quand cette erreur se produit" | |
| 280 | + legend_example: "Exemple concret de situation générant cette erreur" | |
| 281 | + error_auth001_desc: "Identifiants invalides" | |
| 282 | + error_auth001_category: "Authentification" | |
| 283 | + error_auth001_context: "L'utilisateur a fourni des identifiants incorrects lors de la connexion" | |
| 284 | + error_auth001_example: "Tentative de connexion avec un mot de passe incorrect" | |
| 285 | + error_auth002_desc: "Token invalide" | |
| 286 | + error_auth002_category: "Authentification" | |
| 287 | + error_auth002_context: "Le token JWT fourni est invalide ou malformé" | |
| 288 | + error_auth002_example: "Token JWT corrompu ou signé avec une clé différente" | |
| 289 | + error_auth003_desc: "Token expiré" | |
| 290 | + error_auth003_category: "Authentification" | |
| 291 | + error_auth003_context: "Le token JWT a expiré et doit être renouvelé" | |
| 292 | + error_auth003_example: "Tentative d'accès avec un token expiré après la durée de validité" | |
| 293 | + error_csrf001_desc: "Token CSRF manquant" | |
| 294 | + error_csrf001_category: "CSRF Protection" | |
| 295 | + error_csrf001_context: "La requête POST/PUT/DELETE ne contient pas de token CSRF" | |
| 296 | + error_csrf001_example: "Formulaire soumis sans token CSRF dans le header ou le body" | |
| 297 | + error_csrf002_desc: "Token CSRF invalide" | |
| 298 | + error_csrf002_category: "CSRF Protection" | |
| 299 | + error_csrf002_context: "Le token CSRF fourni ne correspond pas au token de la session" | |
| 300 | + error_csrf002_example: "Token CSRF modifié ou volé" | |
| 301 | + error_rate001_desc: "Limite de taux dépassée" | |
| 302 | + error_rate001_category: "Rate Limiting" | |
| 303 | + error_rate001_context: "Le nombre de requêtes par minute a été dépassé" | |
| 304 | + error_rate001_example: "Plus de 100 requêtes/min sur les endpoints admin" | |
| 305 | + error_valid001_desc: "Données d'entrée invalides" | |
| 306 | + error_valid001_category: "Validation" | |
| 307 | + error_valid001_context: "Les données fournies ne respectent pas les règles de validation" | |
| 308 | + error_valid001_example: "Email au format invalide ou champ requis manquant" | |
| 309 | + error_valid002_desc: "Complexité du mot de passe non respectée" | |
| 310 | + error_valid002_category: "Validation" | |
| 311 | + error_valid002_context: "Le mot de passe ne respecte pas les règles de complexité configurées" | |
| 312 | + error_valid002_example: "Mot de passe trop court ou ne contenant pas les caractères requis" | |
| 313 | + error_rbac001_desc: "Permissions insuffisantes" | |
| 314 | + error_rbac001_category: "Autorisation" | |
| 315 | + error_rbac001_context: "L'utilisateur n'a pas les permissions nécessaires pour cette action" | |
| 316 | + error_rbac001_example: "Utilisateur non-admin tentant d'accéder à /admin/users" | |
| 317 | + error_rbac002_desc: "Rôle administrateur requis" | |
| 318 | + error_rbac002_category: "Autorisation" | |
| 319 | + error_rbac002_context: "L'action nécessite le rôle administrateur" | |
| 320 | + error_rbac002_example: "Tentative de suppression d'utilisateur sans rôle admin" | |
| 321 | + error_db001_desc: "Erreur d'opération de base de données" | |
| 322 | + error_db001_category: "Base de données" | |
| 323 | + error_db001_context: "Une opération de base de données a échoué" | |
| 324 | + error_db001_example: "Contrainte de clé étrangère violée ou connexion DB perdue" | |
| 325 | + error_db002_desc: "Ressource non trouvée" | |
| 326 | + error_db002_category: "Base de données" | |
| 327 | + error_db002_context: "La ressource demandée n'existe pas dans la base de données" | |
| 328 | + error_db002_example: "Utilisateur avec ID inexistant" | |
| 329 | + user_form: | |
| 330 | + breadcrumb_create: "Créer" | |
| 331 | + breadcrumb_edit: "Modifier" | |
| 332 | + title_create: "Créer un utilisateur" | |
| 333 | + title_edit: "Modifier un utilisateur" | |
| 334 | + back_to_list: "Retour à la liste" | |
| 335 | + username_label: "Nom d'utilisateur" | |
| 336 | + username_placeholder: "nom_utilisateur" | |
| 337 | + username_readonly_hint: "Le nom d'utilisateur ne peut pas être modifié après création" | |
| 338 | + email_label: "Email" | |
| 339 | + email_placeholder: "utilisateur@example.com" | |
| 340 | + password_label: "Mot de passe" | |
| 341 | + password_optional: "Optionnel" | |
| 342 | + password_placeholder: "Laisser vide pour générer un mot de passe aléatoire" | |
| 343 | + password_hint: "Si laissé vide, un mot de passe sécurisé sera généré automatiquement" | |
| 344 | + roles_label: "Rôles" | |
| 345 | + roles_hint: "Sélectionnez au moins un rôle" | |
| 346 | + must_change_password_label: "Forcer le changement de mot de passe" | |
| 347 | + must_change_password_desc: "L'utilisateur devra changer son mot de passe à la prochaine connexion" | |
| 348 | + cancel: "Annuler" | |
| 349 | + submit_create: "Créer l'utilisateur" | |
| 350 | + submit_edit: "Enregistrer les modifications" | |
| 165 | 351 | settings: |
| 166 | 352 | title: "Paramètres de l'Application" |
| 167 | 353 | page_title: "Paramètres de l'Application - Admin - RustCloud" |
@@ -180,6 +366,34 @@ admin:
| 180 | 366 | saved: "Paramètres enregistrés avec succès" |
| 181 | 367 | error: "Erreur lors de l'enregistrement" |
| 182 | 368 | network_error: "Erreur réseau. Veuillez réessayer." |
| 369 | + password_security: "Sécurité des Mots de Passe" | |
| 370 | + password_min_length: "Longueur minimale" | |
| 371 | + password_min_length_hint: "Entre 6 et 128 caractères" | |
| 372 | + password_complexity: "Exigences de complexité" | |
| 373 | + require_uppercase: "Majuscule requise" | |
| 374 | + require_lowercase: "Minuscule requise" | |
| 375 | + require_digits: "Chiffre requis" | |
| 376 | + require_special: "Caractère spécial requis" | |
| 377 | + password_expiration: "Expiration des mots de passe" | |
| 378 | + password_expiration_desc: "Forcer le changement périodique" | |
| 379 | + expiration_days: "Expiration après (jours)" | |
| 380 | + expiration_alert: "Alerte email avant expiration" | |
| 381 | + expiration_alert_days: "Alerte (jours avant)" | |
| 382 | + audit_logging: "Journalisation des Audits" | |
| 383 | + log_level: "Niveau de log" | |
| 384 | + log_level_debug: "DEBUG - Tous les détails" | |
| 385 | + log_level_info: "INFO - Informations normales" | |
| 386 | + log_level_warn: "WARN - Avertissements uniquement" | |
| 387 | + log_level_error: "ERROR - Erreurs uniquement" | |
| 388 | + log_level_hint: "Contrôle la verbosité des logs d'audit" | |
| 389 | + log_retention: "Rétention des logs (jours)" | |
| 390 | + log_retention_hint: "Durée de conservation des logs (1-365 jours)" | |
| 391 | + audit_info_message: "Les logs d'audit enregistrent toutes les actions administratives (création, modification, suppression d'utilisateurs)." | |
| 392 | + save_changes: "Sauvegarder les modifications" | |
| 393 | + save_changes_desc: "Les paramètres seront appliqués immédiatement" | |
| 394 | + saving: "Enregistrement..." | |
| 395 | + save_success: "Paramètres enregistrés avec succès !" | |
| 396 | + save_error_prefix: "Erreur" | |
| 183 | 397 | users_list: |
| 184 | 398 | page_title: "Gestion des Utilisateurs - Admin - RustCloud" |
| 185 | 399 | title: "Gestion des Utilisateurs" |
@@ -220,8 +434,19 @@ admin:
| 220 | 434 | delete_confirm_email: "Email :" |
| 221 | 435 | delete_irreversible: "Cette action est irréversible !" |
| 222 | 436 | delete_button: "Supprimer définitivement" |
| 437 | + edit_user_title: "Modifier l'utilisateur" | |
| 223 | 438 | delete_error: "Erreur lors de la suppression de l'utilisateur" |
| 224 | 439 | network_error: "Erreur réseau" |
| 440 | + resetting: "Réinitialisation..." | |
| 441 | + reset_by_email_title: "Réinitialisation par email" | |
| 442 | + close: "Fermer" | |
| 443 | + password_reset_success_title: "Mot de passe réinitialisé" | |
| 444 | + password_warning_once: "Ce mot de passe ne sera affiché qu'une seule fois. Copiez-le maintenant !" | |
| 445 | + new_password_label: "Nouveau mot de passe" | |
| 446 | + copy: "Copier" | |
| 447 | + password_copied: "Mot de passe copié dans le presse-papiers" | |
| 448 | + password_set_success: "Mot de passe réinitialisé avec succès" | |
| 449 | + reset_error: "Erreur lors de la réinitialisation du mot de passe" | |
| 225 | 450 | |
| 226 | 451 | # Password Change |
| 227 | 452 | password: |
M
src/api/handlers/admin_handler.rs
+459
-213
@@ -9,17 +9,36 @@ use axum::{
| 9 | 9 | }; |
| 10 | 10 | use sea_orm::DatabaseConnection; |
| 11 | 11 | use serde::Deserialize; |
| 12 | +use std::sync::Arc; | |
| 12 | 13 | use tower_sessions::Session; |
| 13 | 14 | |
| 14 | 15 | use crate::api::middleware::csrf; |
| 15 | 16 | use crate::auth::jwt::Claims; |
| 16 | 17 | use crate::error::AppError; |
| 18 | +use crate::i18n::extractor::Locale; | |
| 19 | +use crate::i18n::service::I18nService; | |
| 17 | 20 | use crate::impl_template_response; |
| 18 | 21 | use crate::models::{role, user_role}; |
| 22 | +use crate::services::app_settings_service::AppSettingsService; | |
| 19 | 23 | use crate::templates::helpers::build_base_context_with_claims; |
| 20 | 24 | use axum::extract::Extension; |
| 21 | 25 | use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; |
| 22 | 26 | |
| 27 | +/// Load app_domain and app_description from the database with sane defaults. | |
| 28 | +async fn load_app_domain_and_description( | |
| 29 | + db: &DatabaseConnection, | |
| 30 | +) -> Result<(String, String), AppError> { | |
| 31 | + let app_domain = AppSettingsService::get_setting(db, "app_domain") | |
| 32 | + .await? | |
| 33 | + .map(|s| s.value) | |
| 34 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 35 | + let app_description = AppSettingsService::get_setting(db, "app_description") | |
| 36 | + .await? | |
| 37 | + .map(|s| s.value) | |
| 38 | + .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 39 | + Ok((app_domain, app_description)) | |
| 40 | +} | |
| 41 | + | |
| 23 | 42 | /// Inline DTO: Create user request from admin form |
| 24 | 43 | #[derive(Debug, Clone, serde::Deserialize)] |
| 25 | 44 | pub struct CreateUserRequest { |
@@ -122,6 +141,32 @@ struct AdminDashboardTemplate {
| 122 | 141 | user_menu_profile: String, |
| 123 | 142 | user_menu_logout: String, |
| 124 | 143 | user_menu_settings: String, |
| 144 | + // Translated strings | |
| 145 | + breadcrumb_home: String, | |
| 146 | + breadcrumb_admin: String, | |
| 147 | + heading: String, | |
| 148 | + users_card_title: String, | |
| 149 | + users_card_subtitle: String, | |
| 150 | + access_button: String, | |
| 151 | + sessions_card_title: String, | |
| 152 | + sessions_card_subtitle: String, | |
| 153 | + admins_card_title: String, | |
| 154 | + admins_card_subtitle: String, | |
| 155 | + last_activity_card_title: String, | |
| 156 | + last_activity_card_subtitle: String, | |
| 157 | + audit_section_title: String, | |
| 158 | + audit_total_logs_label: String, | |
| 159 | + audit_last_24h: String, | |
| 160 | + audit_by_level: String, | |
| 161 | + chart_label: String, | |
| 162 | + settings_card_title: String, | |
| 163 | + settings_card_desc: String, | |
| 164 | + apps_card_title: String, | |
| 165 | + apps_card_desc: String, | |
| 166 | + orgs_card_title: String, | |
| 167 | + orgs_card_desc: String, | |
| 168 | + orgs_stat_title: String, | |
| 169 | + orgs_members_stat_title: String, | |
| 125 | 170 | } |
| 126 | 171 | |
| 127 | 172 | crate::impl_template_response!(AdminDashboardTemplate); |
@@ -129,11 +174,15 @@ crate::impl_template_response!(AdminDashboardTemplate);
| 129 | 174 | /// Handler for GET /admin - Admin Dashboard (PROTECTED - Admin only) |
| 130 | 175 | pub async fn admin_dashboard( |
| 131 | 176 | Extension(claims): Extension<Claims>, |
| 177 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 178 | + Extension(locale): Extension<Locale>, | |
| 132 | 179 | State(db): State<DatabaseConnection>, |
| 133 | 180 | session: Session, |
| 134 | 181 | ) -> Result<impl IntoResponse, AppError> { |
| 182 | + let locale_str = locale.as_str(); | |
| 135 | 183 | let base_context = build_base_context_with_claims(&session, &db, &claims).await?; |
| 136 | 184 | let csrf_token = csrf::get_or_create_csrf_token(&session).await?; |
| 185 | + let (app_domain, app_description) = load_app_domain_and_description(&db).await?; | |
| 137 | 186 | |
| 138 | 187 | // Compute basic stats from DB |
| 139 | 188 | use crate::models::user; |
@@ -160,12 +209,12 @@ pub async fn admin_dashboard(
| 160 | 209 | }; |
| 161 | 210 | |
| 162 | 211 | let template = AdminDashboardTemplate { |
| 163 | - page_title: "Administration".to_string(), | |
| 212 | + page_title: i18n_service.translate("admin.dashboard.title", locale_str).await, | |
| 164 | 213 | csrf_token, |
| 165 | 214 | app_name: base_context.app_name, |
| 166 | 215 | app_version: base_context.app_version, |
| 167 | - app_domain: "localhost".to_string(), | |
| 168 | - app_description: "Secure password manager".to_string(), | |
| 216 | + app_domain, | |
| 217 | + app_description, | |
| 169 | 218 | username: base_context.username, |
| 170 | 219 | email: base_context.email, |
| 171 | 220 | initials: base_context.initials, |
@@ -184,9 +233,34 @@ pub async fn admin_dashboard(
| 184 | 233 | audit_info_count: 0, |
| 185 | 234 | audit_warn_count: 0, |
| 186 | 235 | audit_error_count: 0, |
| 187 | - user_menu_profile: "Profil".to_string(), | |
| 188 | - user_menu_logout: "Déconnexion".to_string(), | |
| 189 | - user_menu_settings: "Paramètres".to_string(), | |
| 236 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 237 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 238 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 239 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 240 | + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await, | |
| 241 | + heading: i18n_service.translate("admin.dashboard.heading", locale_str).await, | |
| 242 | + users_card_title: i18n_service.translate("admin.dashboard.users_card_title", locale_str).await, | |
| 243 | + users_card_subtitle: i18n_service.translate("admin.dashboard.users_card_subtitle", locale_str).await, | |
| 244 | + access_button: i18n_service.translate("admin.dashboard.access_button", locale_str).await, | |
| 245 | + sessions_card_title: i18n_service.translate("admin.dashboard.sessions_card_title", locale_str).await, | |
| 246 | + sessions_card_subtitle: i18n_service.translate("admin.dashboard.sessions_card_subtitle", locale_str).await, | |
| 247 | + admins_card_title: i18n_service.translate("admin.dashboard.admins_card_title", locale_str).await, | |
| 248 | + admins_card_subtitle: i18n_service.translate("admin.dashboard.admins_card_subtitle", locale_str).await, | |
| 249 | + last_activity_card_title: i18n_service.translate("admin.dashboard.last_activity_card_title", locale_str).await, | |
| 250 | + last_activity_card_subtitle: i18n_service.translate("admin.dashboard.last_activity_card_subtitle", locale_str).await, | |
| 251 | + audit_section_title: i18n_service.translate("admin.dashboard.audit_section_title", locale_str).await, | |
| 252 | + audit_total_logs_label: i18n_service.translate("admin.dashboard.audit_total_logs", locale_str).await, | |
| 253 | + audit_last_24h: i18n_service.translate("admin.dashboard.audit_last_24h", locale_str).await, | |
| 254 | + audit_by_level: i18n_service.translate("admin.dashboard.audit_by_level", locale_str).await, | |
| 255 | + chart_label: i18n_service.translate("admin.dashboard.chart_label", locale_str).await, | |
| 256 | + settings_card_title: i18n_service.translate("admin.dashboard.settings_card_title", locale_str).await, | |
| 257 | + settings_card_desc: i18n_service.translate("admin.dashboard.settings_card_desc", locale_str).await, | |
| 258 | + apps_card_title: i18n_service.translate("admin.dashboard.apps_card_title", locale_str).await, | |
| 259 | + apps_card_desc: i18n_service.translate("admin.dashboard.apps_card_desc", locale_str).await, | |
| 260 | + orgs_card_title: i18n_service.translate("admin.dashboard.orgs_card_title", locale_str).await, | |
| 261 | + orgs_card_desc: i18n_service.translate("admin.dashboard.orgs_card_desc", locale_str).await, | |
| 262 | + orgs_stat_title: i18n_service.translate("admin.dashboard.orgs_stat_title", locale_str).await, | |
| 263 | + orgs_members_stat_title: i18n_service.translate("admin.dashboard.orgs_members_stat_title", locale_str).await, | |
| 190 | 264 | }; |
| 191 | 265 | |
| 192 | 266 | Ok(template) |
@@ -262,7 +336,7 @@ struct UserListTemplate {
| 262 | 336 | user_menu_profile: String, |
| 263 | 337 | user_menu_logout: String, |
| 264 | 338 | user_menu_settings: String, |
| 265 | - // Translated strings (hardcoded FR) | |
| 339 | + // Translated strings | |
| 266 | 340 | title: String, |
| 267 | 341 | breadcrumb_home: String, |
| 268 | 342 | breadcrumb_admin: String, |
@@ -306,6 +380,17 @@ struct UserListTemplate {
| 306 | 380 | delete_error: String, |
| 307 | 381 | network_error: String, |
| 308 | 382 | cancel: String, |
| 383 | + // JS modal strings | |
| 384 | + resetting: String, | |
| 385 | + reset_by_email_title: String, | |
| 386 | + close: String, | |
| 387 | + password_reset_success_title: String, | |
| 388 | + password_warning_once: String, | |
| 389 | + new_password_label: String, | |
| 390 | + copy: String, | |
| 391 | + password_copied: String, | |
| 392 | + password_set_success: String, | |
| 393 | + reset_error: String, | |
| 309 | 394 | } |
| 310 | 395 | |
| 311 | 396 | crate::impl_template_response!(UserListTemplate); |
@@ -336,6 +421,28 @@ struct UserFormTemplate {
| 336 | 421 | user_menu_profile: String, |
| 337 | 422 | user_menu_logout: String, |
| 338 | 423 | user_menu_settings: String, |
| 424 | + // Translated strings | |
| 425 | + breadcrumb_home: String, | |
| 426 | + breadcrumb_admin: String, | |
| 427 | + breadcrumb_users: String, | |
| 428 | + breadcrumb_mode: String, | |
| 429 | + form_title: String, | |
| 430 | + back_to_list: String, | |
| 431 | + username_label: String, | |
| 432 | + username_placeholder: String, | |
| 433 | + username_readonly_hint: String, | |
| 434 | + email_label: String, | |
| 435 | + email_placeholder: String, | |
| 436 | + password_label: String, | |
| 437 | + password_optional: String, | |
| 438 | + password_placeholder: String, | |
| 439 | + password_hint: String, | |
| 440 | + roles_label: String, | |
| 441 | + roles_hint: String, | |
| 442 | + must_change_password_label: String, | |
| 443 | + must_change_password_desc: String, | |
| 444 | + cancel: String, | |
| 445 | + submit_label: String, | |
| 339 | 446 | } |
| 340 | 447 | |
| 341 | 448 | crate::impl_template_response!(UserFormTemplate); |
@@ -345,10 +452,13 @@ crate::impl_template_response!(UserFormTemplate);
| 345 | 452 | /// Displays a paginated list of users with search and role filtering. |
| 346 | 453 | pub async fn list_users( |
| 347 | 454 | Extension(claims): Extension<Claims>, |
| 455 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 456 | + Extension(locale): Extension<Locale>, | |
| 348 | 457 | State(db): State<DatabaseConnection>, |
| 349 | 458 | session: Session, |
| 350 | 459 | Query(params): Query<UserListQuery>, |
| 351 | 460 | ) -> Result<impl IntoResponse, AppError> { |
| 461 | + let locale_str = locale.as_str(); | |
| 352 | 462 | let page = params.page.unwrap_or(1); |
| 353 | 463 | let per_page = params.per_page.unwrap_or(50); |
| 354 | 464 | let search = params.search; |
@@ -394,6 +504,7 @@ pub async fn list_users(
| 394 | 504 | |
| 395 | 505 | let csrf_token = csrf::get_or_create_csrf_token(&session).await?; |
| 396 | 506 | let base_context = build_base_context_with_claims(&session, &db, &claims).await?; |
| 507 | + let (app_domain, app_description) = load_app_domain_and_description(&db).await?; | |
| 397 | 508 | |
| 398 | 509 | let mut users_display = Vec::new(); |
| 399 | 510 | for u in page_users { |
@@ -435,21 +546,32 @@ pub async fn list_users(
| 435 | 546 | }); |
| 436 | 547 | } |
| 437 | 548 | |
| 438 | - let results_summary = format!("{} utilisateur(s) sur {}", users_display.len(), total); | |
| 549 | + let results_summary_tpl = i18n_service | |
| 550 | + .translate("admin.users_list.results_summary", locale_str) | |
| 551 | + .await; | |
| 552 | + let results_summary = results_summary_tpl | |
| 553 | + .replace("{count}", &users_display.len().to_string()) | |
| 554 | + .replace("{total}", &total.to_string()); | |
| 555 | + | |
| 439 | 556 | let page_info = if total_pages > 1 { |
| 440 | - format!("Page {} sur {}", page, total_pages) | |
| 557 | + let page_info_tpl = i18n_service | |
| 558 | + .translate("admin.users_list.page_info", locale_str) | |
| 559 | + .await; | |
| 560 | + page_info_tpl | |
| 561 | + .replace("{page}", &page.to_string()) | |
| 562 | + .replace("{total_pages}", &total_pages.to_string()) | |
| 441 | 563 | } else { |
| 442 | 564 | String::new() |
| 443 | 565 | }; |
| 444 | 566 | |
| 445 | 567 | let template = UserListTemplate { |
| 446 | - page_title: "Gestion des utilisateurs".to_string(), | |
| 568 | + page_title: i18n_service.translate("admin.users_list.page_title", locale_str).await, | |
| 447 | 569 | csrf_token, |
| 448 | 570 | app_name: base_context.app_name, |
| 449 | - locale: "fr".to_string(), | |
| 571 | + locale: locale_str.to_string(), | |
| 450 | 572 | app_version: base_context.app_version, |
| 451 | - app_domain: "localhost".to_string(), | |
| 452 | - app_description: "Secure password manager".to_string(), | |
| 573 | + app_domain, | |
| 574 | + app_description, | |
| 453 | 575 | username: base_context.username, |
| 454 | 576 | email: base_context.email, |
| 455 | 577 | initials: base_context.initials, |
@@ -466,52 +588,62 @@ pub async fn list_users(
| 466 | 588 | has_role_filter: role_filter.is_some() && !role_filter.as_ref().unwrap().is_empty(), |
| 467 | 589 | is_admin_filter: role_filter.as_ref().map(|r| r == "admin").unwrap_or(false), |
| 468 | 590 | is_user_filter: role_filter.as_ref().map(|r| r == "user").unwrap_or(false), |
| 469 | - title: "Utilisateurs".to_string(), | |
| 470 | - breadcrumb_home: "Accueil".to_string(), | |
| 471 | - breadcrumb_admin: "Administration".to_string(), | |
| 472 | - breadcrumb_users: "Utilisateurs".to_string(), | |
| 473 | - create_user: "Créer un utilisateur".to_string(), | |
| 474 | - search_label: "Rechercher".to_string(), | |
| 475 | - search_placeholder: "Nom d'utilisateur ou email...".to_string(), | |
| 476 | - role_filter_label: "Filtrer par rôle".to_string(), | |
| 477 | - all_roles: "Tous les rôles".to_string(), | |
| 478 | - per_page_label: "Par page".to_string(), | |
| 479 | - search_button: "Rechercher".to_string(), | |
| 591 | + title: i18n_service.translate("admin.users_list.title", locale_str).await, | |
| 592 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 593 | + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await, | |
| 594 | + breadcrumb_users: i18n_service.translate("breadcrumb.users", locale_str).await, | |
| 595 | + create_user: i18n_service.translate("admin.users_list.create_user", locale_str).await, | |
| 596 | + search_label: i18n_service.translate("admin.users_list.search_label", locale_str).await, | |
| 597 | + search_placeholder: i18n_service.translate("admin.users_list.search_placeholder", locale_str).await, | |
| 598 | + role_filter_label: i18n_service.translate("admin.users_list.role_filter_label", locale_str).await, | |
| 599 | + all_roles: i18n_service.translate("admin.users_list.all_roles", locale_str).await, | |
| 600 | + per_page_label: i18n_service.translate("admin.users_list.per_page_label", locale_str).await, | |
| 601 | + search_button: i18n_service.translate("admin.users_list.search_button", locale_str).await, | |
| 480 | 602 | results_summary, |
| 481 | 603 | page_info, |
| 482 | - table_username: "Nom d'utilisateur".to_string(), | |
| 483 | - table_email: "Email".to_string(), | |
| 484 | - table_roles: "Rôles".to_string(), | |
| 485 | - table_created_at: "Créé le".to_string(), | |
| 486 | - table_last_login: "Dernière connexion".to_string(), | |
| 487 | - table_actions: "Actions".to_string(), | |
| 488 | - never: "Jamais".to_string(), | |
| 489 | - edit: "Modifier".to_string(), | |
| 490 | - delete: "Supprimer".to_string(), | |
| 491 | - reset_password: "Réinitialiser le mot de passe".to_string(), | |
| 492 | - no_users: "Aucun utilisateur trouvé".to_string(), | |
| 493 | - reset_password_title: "Réinitialiser le mot de passe".to_string(), | |
| 494 | - reset_generate: "Générer un mot de passe".to_string(), | |
| 495 | - reset_custom: "Mot de passe personnalisé".to_string(), | |
| 496 | - reset_email: "Envoyer par email".to_string(), | |
| 497 | - reset_new_password_label: "Nouveau mot de passe".to_string(), | |
| 498 | - reset_new_password_placeholder: "Entrez le nouveau mot de passe".to_string(), | |
| 499 | - reset_password_requirements: "Le mot de passe doit contenir au moins 8 caractères".to_string(), | |
| 500 | - reset_invalidate_sessions: "Invalider les sessions".to_string(), | |
| 501 | - reset_invalidate_sessions_desc: "Déconnecte l'utilisateur de toutes ses sessions actives".to_string(), | |
| 502 | - reset_button: "Réinitialiser".to_string(), | |
| 503 | - delete_confirm_title: "Confirmer la suppression".to_string(), | |
| 504 | - delete_confirm_message: "Êtes-vous sûr de vouloir supprimer cet utilisateur ?".to_string(), | |
| 505 | - delete_confirm_username: "Nom d'utilisateur".to_string(), | |
| 506 | - delete_confirm_email: "Email".to_string(), | |
| 507 | - delete_irreversible: "Cette action est irréversible.".to_string(), | |
| 508 | - delete_button: "Supprimer".to_string(), | |
| 509 | - delete_error: "Erreur lors de la suppression".to_string(), | |
| 510 | - network_error: "Erreur réseau".to_string(), | |
| 511 | - cancel: "Annuler".to_string(), | |
| 512 | - user_menu_profile: "Profil".to_string(), | |
| 513 | - user_menu_logout: "Déconnexion".to_string(), | |
| 514 | - user_menu_settings: "Paramètres".to_string(), | |
| 604 | + table_username: i18n_service.translate("admin.users_list.table_username", locale_str).await, | |
| 605 | + table_email: i18n_service.translate("admin.users_list.table_email", locale_str).await, | |
| 606 | + table_roles: i18n_service.translate("admin.users_list.table_roles", locale_str).await, | |
| 607 | + table_created_at: i18n_service.translate("admin.users_list.table_created_at", locale_str).await, | |
| 608 | + table_last_login: i18n_service.translate("admin.users_list.table_last_login", locale_str).await, | |
| 609 | + table_actions: i18n_service.translate("admin.users_list.table_actions", locale_str).await, | |
| 610 | + never: i18n_service.translate("admin.users_list.never", locale_str).await, | |
| 611 | + edit: i18n_service.translate("admin.users_list.edit", locale_str).await, | |
| 612 | + delete: i18n_service.translate("admin.users_list.delete", locale_str).await, | |
| 613 | + reset_password: i18n_service.translate("admin.users_list.reset_password", locale_str).await, | |
| 614 | + no_users: i18n_service.translate("admin.users_list.no_users", locale_str).await, | |
| 615 | + reset_password_title: i18n_service.translate("admin.users_list.reset_password_title", locale_str).await, | |
| 616 | + reset_generate: i18n_service.translate("admin.users_list.reset_generate", locale_str).await, | |
| 617 | + reset_custom: i18n_service.translate("admin.users_list.reset_custom", locale_str).await, | |
| 618 | + reset_email: i18n_service.translate("admin.users_list.reset_email", locale_str).await, | |
| 619 | + reset_new_password_label: i18n_service.translate("admin.users_list.reset_new_password_label", locale_str).await, | |
| 620 | + reset_new_password_placeholder: i18n_service.translate("admin.users_list.reset_new_password_placeholder", locale_str).await, | |
| 621 | + reset_password_requirements: i18n_service.translate("admin.users_list.reset_password_requirements", locale_str).await, | |
| 622 | + reset_invalidate_sessions: i18n_service.translate("admin.users_list.reset_invalidate_sessions", locale_str).await, | |
| 623 | + reset_invalidate_sessions_desc: i18n_service.translate("admin.users_list.reset_invalidate_sessions_desc", locale_str).await, | |
| 624 | + reset_button: i18n_service.translate("admin.users_list.reset_button", locale_str).await, | |
| 625 | + delete_confirm_title: i18n_service.translate("admin.users_list.delete_confirm_title", locale_str).await, | |
| 626 | + delete_confirm_message: i18n_service.translate("admin.users_list.delete_confirm_message", locale_str).await, | |
| 627 | + delete_confirm_username: i18n_service.translate("admin.users_list.delete_confirm_username", locale_str).await, | |
| 628 | + delete_confirm_email: i18n_service.translate("admin.users_list.delete_confirm_email", locale_str).await, | |
| 629 | + delete_irreversible: i18n_service.translate("admin.users_list.delete_irreversible", locale_str).await, | |
| 630 | + delete_button: i18n_service.translate("admin.users_list.delete_button", locale_str).await, | |
| 631 | + delete_error: i18n_service.translate("admin.users_list.delete_error", locale_str).await, | |
| 632 | + network_error: i18n_service.translate("admin.users_list.network_error", locale_str).await, | |
| 633 | + cancel: i18n_service.translate("common.cancel", locale_str).await, | |
| 634 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 635 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 636 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 637 | + resetting: i18n_service.translate("admin.users_list.resetting", locale_str).await, | |
| 638 | + reset_by_email_title: i18n_service.translate("admin.users_list.reset_by_email_title", locale_str).await, | |
| 639 | + close: i18n_service.translate("admin.users_list.close", locale_str).await, | |
| 640 | + password_reset_success_title: i18n_service.translate("admin.users_list.password_reset_success_title", locale_str).await, | |
| 641 | + password_warning_once: i18n_service.translate("admin.users_list.password_warning_once", locale_str).await, | |
| 642 | + new_password_label: i18n_service.translate("admin.users_list.new_password_label", locale_str).await, | |
| 643 | + copy: i18n_service.translate("admin.users_list.copy", locale_str).await, | |
| 644 | + password_copied: i18n_service.translate("admin.users_list.password_copied", locale_str).await, | |
| 645 | + password_set_success: i18n_service.translate("admin.users_list.password_set_success", locale_str).await, | |
| 646 | + reset_error: i18n_service.translate("admin.users_list.reset_error", locale_str).await, | |
| 515 | 647 | }; |
| 516 | 648 | |
| 517 | 649 | Ok(template) |
@@ -520,11 +652,15 @@ pub async fn list_users(
| 520 | 652 | /// Handler for GET /admin/users/create - User creation form (PROTECTED - Admin only) |
| 521 | 653 | pub async fn create_user_form( |
| 522 | 654 | Extension(claims): Extension<Claims>, |
| 655 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 656 | + Extension(locale): Extension<Locale>, | |
| 523 | 657 | State(db): State<DatabaseConnection>, |
| 524 | 658 | session: Session, |
| 525 | 659 | ) -> Result<impl IntoResponse, AppError> { |
| 660 | + let locale_str = locale.as_str(); | |
| 526 | 661 | let csrf_token = csrf::get_or_create_csrf_token(&session).await?; |
| 527 | 662 | let base_context = build_base_context_with_claims(&session, &db, &claims).await?; |
| 663 | + let (app_domain, app_description) = load_app_domain_and_description(&db).await?; | |
| 528 | 664 | |
| 529 | 665 | use crate::services::role_service::RoleService; |
| 530 | 666 | let all_roles = RoleService::list_all_roles(&db).await?; |
@@ -539,12 +675,12 @@ pub async fn create_user_form(
| 539 | 675 | .collect(); |
| 540 | 676 | |
| 541 | 677 | let template = UserFormTemplate { |
| 542 | - page_title: "Créer un utilisateur".to_string(), | |
| 678 | + page_title: i18n_service.translate("admin.users_list.create_user", locale_str).await, | |
| 543 | 679 | csrf_token, |
| 544 | 680 | app_name: base_context.app_name, |
| 545 | 681 | app_version: base_context.app_version, |
| 546 | - app_domain: "localhost".to_string(), | |
| 547 | - app_description: "Secure password manager".to_string(), | |
| 682 | + app_domain, | |
| 683 | + app_description, | |
| 548 | 684 | username: base_context.username, |
| 549 | 685 | email: base_context.email, |
| 550 | 686 | initials: base_context.initials, |
@@ -557,9 +693,30 @@ pub async fn create_user_form(
| 557 | 693 | form_roles: Vec::new(), |
| 558 | 694 | available_roles: available_roles_display, |
| 559 | 695 | must_change_password: false, |
| 560 | - user_menu_profile: "Profil".to_string(), | |
| 561 | - user_menu_logout: "Déconnexion".to_string(), | |
| 562 | - user_menu_settings: "Paramètres".to_string(), | |
| 696 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 697 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 698 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 699 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 700 | + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await, | |
| 701 | + breadcrumb_users: i18n_service.translate("breadcrumb.users", locale_str).await, | |
| 702 | + breadcrumb_mode: i18n_service.translate("admin.user_form.breadcrumb_create", locale_str).await, | |
| 703 | + form_title: i18n_service.translate("admin.user_form.title_create", locale_str).await, | |
| 704 | + back_to_list: i18n_service.translate("admin.user_form.back_to_list", locale_str).await, | |
| 705 | + username_label: i18n_service.translate("admin.user_form.username_label", locale_str).await, | |
| 706 | + username_placeholder: i18n_service.translate("admin.user_form.username_placeholder", locale_str).await, | |
| 707 | + username_readonly_hint: i18n_service.translate("admin.user_form.username_readonly_hint", locale_str).await, | |
| 708 | + email_label: i18n_service.translate("admin.user_form.email_label", locale_str).await, | |
| 709 | + email_placeholder: i18n_service.translate("admin.user_form.email_placeholder", locale_str).await, | |
| 710 | + password_label: i18n_service.translate("admin.user_form.password_label", locale_str).await, | |
| 711 | + password_optional: i18n_service.translate("admin.user_form.password_optional", locale_str).await, | |
| 712 | + password_placeholder: i18n_service.translate("admin.user_form.password_placeholder", locale_str).await, | |
| 713 | + password_hint: i18n_service.translate("admin.user_form.password_hint", locale_str).await, | |
| 714 | + roles_label: i18n_service.translate("admin.user_form.roles_label", locale_str).await, | |
| 715 | + roles_hint: i18n_service.translate("admin.user_form.roles_hint", locale_str).await, | |
| 716 | + must_change_password_label: i18n_service.translate("admin.user_form.must_change_password_label", locale_str).await, | |
| 717 | + must_change_password_desc: i18n_service.translate("admin.user_form.must_change_password_desc", locale_str).await, | |
| 718 | + cancel: i18n_service.translate("admin.user_form.cancel", locale_str).await, | |
| 719 | + submit_label: i18n_service.translate("admin.user_form.submit_create", locale_str).await, | |
| 563 | 720 | }; |
| 564 | 721 | |
| 565 | 722 | Ok(template) |
@@ -628,12 +785,16 @@ pub async fn create_user(
| 628 | 785 | /// Handler for GET /admin/users/:id - Edit user form (PROTECTED - Admin only) |
| 629 | 786 | pub async fn edit_user_form( |
| 630 | 787 | Extension(claims): Extension<Claims>, |
| 788 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 789 | + Extension(locale): Extension<Locale>, | |
| 631 | 790 | State(db): State<DatabaseConnection>, |
| 632 | 791 | session: Session, |
| 633 | 792 | axum::extract::Path(user_id): axum::extract::Path<uuid::Uuid>, |
| 634 | 793 | ) -> Result<impl IntoResponse, AppError> { |
| 794 | + let locale_str = locale.as_str(); | |
| 635 | 795 | let csrf_token = csrf::get_or_create_csrf_token(&session).await?; |
| 636 | 796 | let base_context = build_base_context_with_claims(&session, &db, &claims).await?; |
| 797 | + let (app_domain, app_description) = load_app_domain_and_description(&db).await?; | |
| 637 | 798 | |
| 638 | 799 | use crate::services::user_service::UserService; |
| 639 | 800 | let user = UserService::get_user_by_id(&db, user_id) |
@@ -664,13 +825,17 @@ pub async fn edit_user_form(
| 664 | 825 | }) |
| 665 | 826 | .collect(); |
| 666 | 827 | |
| 828 | + let edit_title = i18n_service | |
| 829 | + .translate("admin.users_list.edit_user_title", locale_str) | |
| 830 | + .await; | |
| 831 | + | |
| 667 | 832 | let template = UserFormTemplate { |
| 668 | - page_title: format!("Modifier l'utilisateur {}", user.username), | |
| 833 | + page_title: format!("{} {}", edit_title, user.username), | |
| 669 | 834 | csrf_token, |
| 670 | 835 | app_name: base_context.app_name, |
| 671 | 836 | app_version: base_context.app_version, |
| 672 | - app_domain: "localhost".to_string(), | |
| 673 | - app_description: "Secure password manager".to_string(), | |
| 837 | + app_domain, | |
| 838 | + app_description, | |
| 674 | 839 | username: base_context.username, |
| 675 | 840 | email: base_context.email, |
| 676 | 841 | initials: base_context.initials, |
@@ -683,9 +848,30 @@ pub async fn edit_user_form(
| 683 | 848 | form_roles: current_role_names, |
| 684 | 849 | available_roles: available_roles_display, |
| 685 | 850 | must_change_password: user.must_change_password, |
| 686 | - user_menu_profile: "Profil".to_string(), | |
| 687 | - user_menu_logout: "Déconnexion".to_string(), | |
| 688 | - user_menu_settings: "Paramètres".to_string(), | |
| 851 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 852 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 853 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 854 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 855 | + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await, | |
| 856 | + breadcrumb_users: i18n_service.translate("breadcrumb.users", locale_str).await, | |
| 857 | + breadcrumb_mode: i18n_service.translate("admin.user_form.breadcrumb_edit", locale_str).await, | |
| 858 | + form_title: i18n_service.translate("admin.user_form.title_edit", locale_str).await, | |
| 859 | + back_to_list: i18n_service.translate("admin.user_form.back_to_list", locale_str).await, | |
| 860 | + username_label: i18n_service.translate("admin.user_form.username_label", locale_str).await, | |
| 861 | + username_placeholder: i18n_service.translate("admin.user_form.username_placeholder", locale_str).await, | |
| 862 | + username_readonly_hint: i18n_service.translate("admin.user_form.username_readonly_hint", locale_str).await, | |
| 863 | + email_label: i18n_service.translate("admin.user_form.email_label", locale_str).await, | |
| 864 | + email_placeholder: i18n_service.translate("admin.user_form.email_placeholder", locale_str).await, | |
| 865 | + password_label: i18n_service.translate("admin.user_form.password_label", locale_str).await, | |
| 866 | + password_optional: i18n_service.translate("admin.user_form.password_optional", locale_str).await, | |
| 867 | + password_placeholder: i18n_service.translate("admin.user_form.password_placeholder", locale_str).await, | |
| 868 | + password_hint: i18n_service.translate("admin.user_form.password_hint", locale_str).await, | |
| 869 | + roles_label: i18n_service.translate("admin.user_form.roles_label", locale_str).await, | |
| 870 | + roles_hint: i18n_service.translate("admin.user_form.roles_hint", locale_str).await, | |
| 871 | + must_change_password_label: i18n_service.translate("admin.user_form.must_change_password_label", locale_str).await, | |
| 872 | + must_change_password_desc: i18n_service.translate("admin.user_form.must_change_password_desc", locale_str).await, | |
| 873 | + cancel: i18n_service.translate("admin.user_form.cancel", locale_str).await, | |
| 874 | + submit_label: i18n_service.translate("admin.user_form.submit_edit", locale_str).await, | |
| 689 | 875 | }; |
| 690 | 876 | |
| 691 | 877 | Ok(template) |
@@ -883,7 +1069,7 @@ pub async fn reset_user_password(
| 883 | 1069 | } |
| 884 | 1070 | |
| 885 | 1071 | // ============================================================================ |
| 886 | -// Admin Settings - Simplified for Rustwarden (no AppSettingsService) | |
| 1072 | +// Admin Settings | |
| 887 | 1073 | // ============================================================================ |
| 888 | 1074 | |
| 889 | 1075 | /// Admin application settings HTML template |
@@ -906,7 +1092,7 @@ struct AdminSettingsTemplate {
| 906 | 1092 | user_menu_profile: String, |
| 907 | 1093 | user_menu_logout: String, |
| 908 | 1094 | user_menu_settings: String, |
| 909 | - // Translated strings (hardcoded FR) | |
| 1095 | + // Translated strings | |
| 910 | 1096 | title: String, |
| 911 | 1097 | subtitle: String, |
| 912 | 1098 | general: String, |
@@ -926,6 +1112,37 @@ struct AdminSettingsTemplate {
| 926 | 1112 | breadcrumb_home: String, |
| 927 | 1113 | breadcrumb_admin: String, |
| 928 | 1114 | breadcrumb_settings: String, |
| 1115 | + // Password security section | |
| 1116 | + password_security: String, | |
| 1117 | + password_min_length: String, | |
| 1118 | + password_min_length_hint: String, | |
| 1119 | + password_complexity: String, | |
| 1120 | + require_uppercase: String, | |
| 1121 | + require_lowercase: String, | |
| 1122 | + require_digits: String, | |
| 1123 | + require_special: String, | |
| 1124 | + password_expiration: String, | |
| 1125 | + password_expiration_desc: String, | |
| 1126 | + expiration_days: String, | |
| 1127 | + expiration_alert: String, | |
| 1128 | + expiration_alert_days: String, | |
| 1129 | + // Audit section | |
| 1130 | + audit_logging: String, | |
| 1131 | + log_level: String, | |
| 1132 | + log_level_debug: String, | |
| 1133 | + log_level_info: String, | |
| 1134 | + log_level_warn: String, | |
| 1135 | + log_level_error: String, | |
| 1136 | + log_level_hint: String, | |
| 1137 | + log_retention: String, | |
| 1138 | + log_retention_hint: String, | |
| 1139 | + audit_info_message: String, | |
| 1140 | + // Save section | |
| 1141 | + save_changes: String, | |
| 1142 | + save_changes_desc: String, | |
| 1143 | + saving: String, | |
| 1144 | + save_success: String, | |
| 1145 | + save_error_prefix: String, | |
| 929 | 1146 | } |
| 930 | 1147 | |
| 931 | 1148 | impl_template_response!(AdminSettingsTemplate); |
@@ -954,67 +1171,110 @@ struct AppSettingsTemplateContext {
| 954 | 1171 | /// GET /admin/settings - Application settings page |
| 955 | 1172 | pub async fn get_app_settings( |
| 956 | 1173 | Extension(claims): Extension<crate::auth::jwt::Claims>, |
| 1174 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 1175 | + Extension(locale): Extension<Locale>, | |
| 957 | 1176 | State(db): State<DatabaseConnection>, |
| 958 | 1177 | session: Session, |
| 959 | 1178 | ) -> Result<impl IntoResponse, AppError> { |
| 1179 | + let locale_str = locale.as_str(); | |
| 960 | 1180 | let base_context = build_base_context_with_claims(&session, &db, &claims).await?; |
| 961 | 1181 | let csrf_token = csrf::get_or_create_csrf_token(&session).await?; |
| 962 | 1182 | |
| 963 | - // Default settings (no AppSettingsService in Rustwarden) | |
| 1183 | + // Load all settings from DB in one query for efficiency | |
| 1184 | + let all_settings = AppSettingsService::get_all_settings(&db).await?; | |
| 1185 | + let get_val = |key: &str, default: &str| -> String { | |
| 1186 | + all_settings | |
| 1187 | + .iter() | |
| 1188 | + .find(|s| s.key == key) | |
| 1189 | + .map(|s| s.value.clone()) | |
| 1190 | + .unwrap_or_else(|| default.to_string()) | |
| 1191 | + }; | |
| 1192 | + | |
| 1193 | + let app_domain = get_val("app_domain", "localhost"); | |
| 1194 | + let app_description = get_val("app_description", "Secure password manager"); | |
| 1195 | + | |
| 964 | 1196 | let settings_ctx = AppSettingsTemplateContext { |
| 965 | - app_name: "RustWarden".to_string(), | |
| 966 | - app_domain: "localhost".to_string(), | |
| 967 | - app_description: "Secure password manager".to_string(), | |
| 968 | - allow_registration: "true".to_string(), | |
| 969 | - validation_email_requise: "false".to_string(), | |
| 970 | - password_min_length: "8".to_string(), | |
| 971 | - password_require_uppercase: "true".to_string(), | |
| 972 | - password_require_lowercase: "true".to_string(), | |
| 973 | - password_require_digits: "true".to_string(), | |
| 974 | - password_require_special: "false".to_string(), | |
| 975 | - password_expiration_enabled: "false".to_string(), | |
| 976 | - password_expiration_days: "90".to_string(), | |
| 977 | - password_expiration_alert_enabled: "false".to_string(), | |
| 978 | - password_expiration_alert_days_before: "7".to_string(), | |
| 979 | - audit_log_level: "INFO".to_string(), | |
| 980 | - audit_log_retention_days: "365".to_string(), | |
| 1197 | + app_name: base_context.app_name.clone(), | |
| 1198 | + app_domain: app_domain.clone(), | |
| 1199 | + app_description: app_description.clone(), | |
| 1200 | + allow_registration: get_val("allow_registration", "true"), | |
| 1201 | + validation_email_requise: get_val("validation_email_required", "false"), | |
| 1202 | + password_min_length: get_val("password_min_length", "8"), | |
| 1203 | + password_require_uppercase: get_val("password_require_uppercase", "true"), | |
| 1204 | + password_require_lowercase: get_val("password_require_lowercase", "true"), | |
| 1205 | + password_require_digits: get_val("password_require_digits", "true"), | |
| 1206 | + password_require_special: get_val("password_require_special", "false"), | |
| 1207 | + password_expiration_enabled: get_val("password_expiration_enabled", "false"), | |
| 1208 | + password_expiration_days: get_val("password_expiration_days", "90"), | |
| 1209 | + password_expiration_alert_enabled: get_val("password_expiration_alert_enabled", "false"), | |
| 1210 | + password_expiration_alert_days_before: get_val("password_expiration_alert_days_before", "7"), | |
| 1211 | + audit_log_level: get_val("audit_log_level", "INFO"), | |
| 1212 | + audit_log_retention_days: get_val("audit_log_retention_days", "365"), | |
| 981 | 1213 | }; |
| 982 | 1214 | |
| 983 | 1215 | let template = AdminSettingsTemplate { |
| 984 | - page_title: "Paramètres de l'application".to_string(), | |
| 1216 | + page_title: i18n_service.translate("admin.settings.title", locale_str).await, | |
| 985 | 1217 | csrf_token, |
| 986 | 1218 | app_name: base_context.app_name, |
| 987 | 1219 | app_version: base_context.app_version, |
| 988 | - app_domain: "localhost".to_string(), | |
| 989 | - app_description: "Secure password manager".to_string(), | |
| 1220 | + app_domain, | |
| 1221 | + app_description, | |
| 990 | 1222 | username: base_context.username, |
| 991 | 1223 | email: base_context.email, |
| 992 | 1224 | initials: base_context.initials, |
| 993 | 1225 | avatar_color: base_context.avatar_color, |
| 994 | 1226 | roles_text: base_context.roles.join(", "), |
| 995 | 1227 | settings: settings_ctx, |
| 996 | - title: "Paramètres de l'application".to_string(), | |
| 997 | - subtitle: "Configurez les paramètres généraux de l'application".to_string(), | |
| 998 | - general: "Général".to_string(), | |
| 999 | - app_name_label: "Nom de l'application".to_string(), | |
| 1000 | - app_domain_label: "Domaine".to_string(), | |
| 1001 | - app_domain_placeholder: "example.com".to_string(), | |
| 1002 | - app_description_label: "Description".to_string(), | |
| 1003 | - allow_registration_label: "Autoriser l'inscription".to_string(), | |
| 1004 | - allow_registration_desc: "Permettre aux nouveaux utilisateurs de s'inscrire".to_string(), | |
| 1005 | - email_validation_required_label: "Validation email requise".to_string(), | |
| 1006 | - email_validation_required_desc: "Exiger la validation de l'email lors de l'inscription".to_string(), | |
| 1007 | - save_button: "Enregistrer".to_string(), | |
| 1008 | - cancel: "Annuler".to_string(), | |
| 1009 | - saved: "Paramètres enregistrés avec succès".to_string(), | |
| 1010 | - error: "Erreur".to_string(), | |
| 1011 | - network_error: "Erreur réseau".to_string(), | |
| 1012 | - breadcrumb_home: "Accueil".to_string(), | |
| 1013 | - breadcrumb_admin: "Administration".to_string(), | |
| 1014 | - breadcrumb_settings: "Paramètres".to_string(), | |
| 1015 | - user_menu_profile: "Profil".to_string(), | |
| 1016 | - user_menu_logout: "Déconnexion".to_string(), | |
| 1017 | - user_menu_settings: "Paramètres".to_string(), | |
| 1228 | + title: i18n_service.translate("admin.settings.title", locale_str).await, | |
| 1229 | + subtitle: i18n_service.translate("admin.settings.subtitle", locale_str).await, | |
| 1230 | + general: i18n_service.translate("admin.settings.general", locale_str).await, | |
| 1231 | + app_name_label: i18n_service.translate("admin.settings.app_name", locale_str).await, | |
| 1232 | + app_domain_label: i18n_service.translate("admin.settings.app_domain", locale_str).await, | |
| 1233 | + app_domain_placeholder: i18n_service.translate("admin.settings.app_domain_placeholder", locale_str).await, | |
| 1234 | + app_description_label: i18n_service.translate("admin.settings.app_description", locale_str).await, | |
| 1235 | + allow_registration_label: i18n_service.translate("admin.settings.allow_registration", locale_str).await, | |
| 1236 | + allow_registration_desc: i18n_service.translate("admin.settings.allow_registration_desc", locale_str).await, | |
| 1237 | + email_validation_required_label: i18n_service.translate("admin.settings.email_validation_required", locale_str).await, | |
| 1238 | + email_validation_required_desc: i18n_service.translate("admin.settings.email_validation_required_desc", locale_str).await, | |
| 1239 | + save_button: i18n_service.translate("admin.settings.save", locale_str).await, | |
| 1240 | + cancel: i18n_service.translate("admin.settings.cancel", locale_str).await, | |
| 1241 | + saved: i18n_service.translate("admin.settings.saved", locale_str).await, | |
| 1242 | + error: i18n_service.translate("admin.settings.error", locale_str).await, | |
| 1243 | + network_error: i18n_service.translate("admin.settings.network_error", locale_str).await, | |
| 1244 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 1245 | + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await, | |
| 1246 | + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await, | |
| 1247 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 1248 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 1249 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 1250 | + password_security: i18n_service.translate("admin.settings.password_security", locale_str).await, | |
| 1251 | + password_min_length: i18n_service.translate("admin.settings.password_min_length", locale_str).await, | |
| 1252 | + password_min_length_hint: i18n_service.translate("admin.settings.password_min_length_hint", locale_str).await, | |
| 1253 | + password_complexity: i18n_service.translate("admin.settings.password_complexity", locale_str).await, | |
| 1254 | + require_uppercase: i18n_service.translate("admin.settings.require_uppercase", locale_str).await, | |
| 1255 | + require_lowercase: i18n_service.translate("admin.settings.require_lowercase", locale_str).await, | |
| 1256 | + require_digits: i18n_service.translate("admin.settings.require_digits", locale_str).await, | |
| 1257 | + require_special: i18n_service.translate("admin.settings.require_special", locale_str).await, | |
| 1258 | + password_expiration: i18n_service.translate("admin.settings.password_expiration", locale_str).await, | |
| 1259 | + password_expiration_desc: i18n_service.translate("admin.settings.password_expiration_desc", locale_str).await, | |
| 1260 | + expiration_days: i18n_service.translate("admin.settings.expiration_days", locale_str).await, | |
| 1261 | + expiration_alert: i18n_service.translate("admin.settings.expiration_alert", locale_str).await, | |
| 1262 | + expiration_alert_days: i18n_service.translate("admin.settings.expiration_alert_days", locale_str).await, | |
| 1263 | + audit_logging: i18n_service.translate("admin.settings.audit_logging", locale_str).await, | |
| 1264 | + log_level: i18n_service.translate("admin.settings.log_level", locale_str).await, | |
| 1265 | + log_level_debug: i18n_service.translate("admin.settings.log_level_debug", locale_str).await, | |
| 1266 | + log_level_info: i18n_service.translate("admin.settings.log_level_info", locale_str).await, | |
| 1267 | + log_level_warn: i18n_service.translate("admin.settings.log_level_warn", locale_str).await, | |
| 1268 | + log_level_error: i18n_service.translate("admin.settings.log_level_error", locale_str).await, | |
| 1269 | + log_level_hint: i18n_service.translate("admin.settings.log_level_hint", locale_str).await, | |
| 1270 | + log_retention: i18n_service.translate("admin.settings.log_retention", locale_str).await, | |
| 1271 | + log_retention_hint: i18n_service.translate("admin.settings.log_retention_hint", locale_str).await, | |
| 1272 | + audit_info_message: i18n_service.translate("admin.settings.audit_info_message", locale_str).await, | |
| 1273 | + save_changes: i18n_service.translate("admin.settings.save_changes", locale_str).await, | |
| 1274 | + save_changes_desc: i18n_service.translate("admin.settings.save_changes_desc", locale_str).await, | |
| 1275 | + saving: i18n_service.translate("admin.settings.saving", locale_str).await, | |
| 1276 | + save_success: i18n_service.translate("admin.settings.save_success", locale_str).await, | |
| 1277 | + save_error_prefix: i18n_service.translate("admin.settings.save_error_prefix", locale_str).await, | |
| 1018 | 1278 | }; |
| 1019 | 1279 | |
| 1020 | 1280 | Ok(template) |
@@ -1065,6 +1325,29 @@ struct AdminErrorsTemplate {
| 1065 | 1325 | avatar_color: String, |
| 1066 | 1326 | roles_text: String, |
| 1067 | 1327 | error_codes: Vec<ErrorCodeInfo>, |
| 1328 | + // User menu | |
| 1329 | + user_menu_profile: String, | |
| 1330 | + user_menu_logout: String, | |
| 1331 | + user_menu_settings: String, | |
| 1332 | + // Translated strings | |
| 1333 | + breadcrumb_home: String, | |
| 1334 | + breadcrumb_admin: String, | |
| 1335 | + breadcrumb_errors: String, | |
| 1336 | + title: String, | |
| 1337 | + description: String, | |
| 1338 | + table_code: String, | |
| 1339 | + table_description: String, | |
| 1340 | + table_category: String, | |
| 1341 | + table_middleware: String, | |
| 1342 | + table_context: String, | |
| 1343 | + table_example: String, | |
| 1344 | + legend_title: String, | |
| 1345 | + legend_code: String, | |
| 1346 | + legend_description: String, | |
| 1347 | + legend_category: String, | |
| 1348 | + legend_middleware: String, | |
| 1349 | + legend_context: String, | |
| 1350 | + legend_example: String, | |
| 1068 | 1351 | } |
| 1069 | 1352 | |
| 1070 | 1353 | impl_template_response!(AdminErrorsTemplate); |
@@ -1072,125 +1355,88 @@ impl_template_response!(AdminErrorsTemplate);
| 1072 | 1355 | /// Handler for GET /admin/errors - Error codes documentation page |
| 1073 | 1356 | pub async fn admin_errors( |
| 1074 | 1357 | Extension(claims): Extension<Claims>, |
| 1358 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 1359 | + Extension(locale): Extension<Locale>, | |
| 1075 | 1360 | session: Session, |
| 1076 | 1361 | State(db): State<DatabaseConnection>, |
| 1077 | 1362 | ) -> Result<impl IntoResponse, AppError> { |
| 1363 | + let locale_str = locale.as_str(); | |
| 1078 | 1364 | let base_context = build_base_context_with_claims(&session, &db, &claims).await?; |
| 1079 | 1365 | let csrf_token = csrf::get_or_create_csrf_token(&session).await?; |
| 1366 | + let (app_domain, app_description) = load_app_domain_and_description(&db).await?; | |
| 1080 | 1367 | |
| 1081 | 1368 | use crate::api::middleware::error_handler::ErrorCode; |
| 1082 | - let error_codes = vec![ | |
| 1083 | - ErrorCodeInfo { | |
| 1084 | - code: ErrorCode::Auth001.as_str().to_string(), | |
| 1085 | - description: "Identifiants invalides".to_string(), | |
| 1086 | - category: "Authentification".to_string(), | |
| 1087 | - middleware: "auth_required".to_string(), | |
| 1088 | - context: "L'utilisateur a fourni des identifiants incorrects lors de la connexion".to_string(), | |
| 1089 | - example: "Tentative de connexion avec un mot de passe incorrect".to_string(), | |
| 1090 | - }, | |
| 1091 | - ErrorCodeInfo { | |
| 1092 | - code: ErrorCode::Auth002.as_str().to_string(), | |
| 1093 | - description: "Token invalide".to_string(), | |
| 1094 | - category: "Authentification".to_string(), | |
| 1095 | - middleware: "auth_required".to_string(), | |
| 1096 | - context: "Le token JWT fourni est invalide ou malformé".to_string(), | |
| 1097 | - example: "Token JWT corrompu ou signé avec une clé différente".to_string(), | |
| 1098 | - }, | |
| 1099 | - ErrorCodeInfo { | |
| 1100 | - code: ErrorCode::Auth003.as_str().to_string(), | |
| 1101 | - description: "Token expiré".to_string(), | |
| 1102 | - category: "Authentification".to_string(), | |
| 1103 | - middleware: "auth_required".to_string(), | |
| 1104 | - context: "Le token JWT a expiré et doit être renouvelé".to_string(), | |
| 1105 | - example: "Tentative d'accès avec un token expiré après la durée de validité".to_string(), | |
| 1106 | - }, | |
| 1107 | - ErrorCodeInfo { | |
| 1108 | - code: ErrorCode::Csrf001.as_str().to_string(), | |
| 1109 | - description: "Token CSRF manquant".to_string(), | |
| 1110 | - category: "CSRF Protection".to_string(), | |
| 1111 | - middleware: "csrf".to_string(), | |
| 1112 | - context: "La requête POST/PUT/DELETE ne contient pas de token CSRF".to_string(), | |
| 1113 | - example: "Formulaire soumis sans token CSRF dans le header ou le body".to_string(), | |
| 1114 | - }, | |
| 1115 | - ErrorCodeInfo { | |
| 1116 | - code: ErrorCode::Csrf002.as_str().to_string(), | |
| 1117 | - description: "Token CSRF invalide".to_string(), | |
| 1118 | - category: "CSRF Protection".to_string(), | |
| 1119 | - middleware: "csrf".to_string(), | |
| 1120 | - context: "Le token CSRF fourni ne correspond pas au token de la session".to_string(), | |
| 1121 | - example: "Token CSRF modifié ou volé".to_string(), | |
| 1122 | - }, | |
| 1123 | - ErrorCodeInfo { | |
| 1124 | - code: ErrorCode::Rate001.as_str().to_string(), | |
| 1125 | - description: "Limite de taux dépassée".to_string(), | |
| 1126 | - category: "Rate Limiting".to_string(), | |
| 1127 | - middleware: "rate_limit".to_string(), | |
| 1128 | - context: "Le nombre de requêtes par minute a été dépassé".to_string(), | |
| 1129 | - example: "Plus de 100 requêtes/min sur les endpoints admin".to_string(), | |
| 1130 | - }, | |
| 1131 | - ErrorCodeInfo { | |
| 1132 | - code: ErrorCode::Valid001.as_str().to_string(), | |
| 1133 | - description: "Données d'entrée invalides".to_string(), | |
| 1134 | - category: "Validation".to_string(), | |
| 1135 | - middleware: "validation".to_string(), | |
| 1136 | - context: "Les données fournies ne respectent pas les règles de validation".to_string(), | |
| 1137 | - example: "Email au format invalide ou champ requis manquant".to_string(), | |
| 1138 | - }, | |
| 1139 | - ErrorCodeInfo { | |
| 1140 | - code: ErrorCode::Valid002.as_str().to_string(), | |
| 1141 | - description: "Complexité du mot de passe non respectée".to_string(), | |
| 1142 | - category: "Validation".to_string(), | |
| 1143 | - middleware: "password_complexity".to_string(), | |
| 1144 | - context: "Le mot de passe ne respecte pas les règles de complexité configurées".to_string(), | |
| 1145 | - example: "Mot de passe trop court ou ne contenant pas les caractères requis".to_string(), | |
| 1146 | - }, | |
| 1147 | - ErrorCodeInfo { | |
| 1148 | - code: ErrorCode::Rbac001.as_str().to_string(), | |
| 1149 | - description: "Permissions insuffisantes".to_string(), | |
| 1150 | - category: "Autorisation".to_string(), | |
| 1151 | - middleware: "admin_required".to_string(), | |
| 1152 | - context: "L'utilisateur n'a pas les permissions nécessaires pour cette action".to_string(), | |
| 1153 | - example: "Utilisateur non-admin tentant d'accéder à /admin/users".to_string(), | |
| 1154 | - }, | |
| 1155 | - ErrorCodeInfo { | |
| 1156 | - code: ErrorCode::Rbac002.as_str().to_string(), | |
| 1157 | - description: "Rôle administrateur requis".to_string(), | |
| 1158 | - category: "Autorisation".to_string(), | |
| 1159 | - middleware: "admin_required".to_string(), | |
| 1160 | - context: "L'action nécessite le rôle administrateur".to_string(), | |
| 1161 | - example: "Tentative de suppression d'utilisateur sans rôle admin".to_string(), | |
| 1162 | - }, | |
| 1163 | - ErrorCodeInfo { | |
| 1164 | - code: ErrorCode::Db001.as_str().to_string(), | |
| 1165 | - description: "Erreur d'opération de base de données".to_string(), | |
| 1166 | - category: "Base de données".to_string(), | |
| 1167 | - middleware: "database".to_string(), | |
| 1168 | - context: "Une opération de base de données a échoué".to_string(), | |
| 1169 | - example: "Contrainte de clé étrangère violée ou connexion DB perdue".to_string(), | |
| 1170 | - }, | |
| 1171 | - ErrorCodeInfo { | |
| 1172 | - code: ErrorCode::Db002.as_str().to_string(), | |
| 1173 | - description: "Ressource non trouvée".to_string(), | |
| 1174 | - category: "Base de données".to_string(), | |
| 1175 | - middleware: "database".to_string(), | |
| 1176 | - context: "La ressource demandée n'existe pas dans la base de données".to_string(), | |
| 1177 | - example: "Utilisateur avec ID inexistant".to_string(), | |
| 1178 | - }, | |
| 1369 | + | |
| 1370 | + struct ErrorDef { | |
| 1371 | + code_enum: ErrorCode, | |
| 1372 | + desc_key: &'static str, | |
| 1373 | + cat_key: &'static str, | |
| 1374 | + middleware: &'static str, | |
| 1375 | + ctx_key: &'static str, | |
| 1376 | + ex_key: &'static str, | |
| 1377 | + } | |
| 1378 | + | |
| 1379 | + let error_defs = vec![ | |
| 1380 | + ErrorDef { code_enum: ErrorCode::Auth001, desc_key: "admin.errors.error_auth001_desc", cat_key: "admin.errors.error_auth001_category", middleware: "auth_required", ctx_key: "admin.errors.error_auth001_context", ex_key: "admin.errors.error_auth001_example" }, | |
| 1381 | + ErrorDef { code_enum: ErrorCode::Auth002, desc_key: "admin.errors.error_auth002_desc", cat_key: "admin.errors.error_auth002_category", middleware: "auth_required", ctx_key: "admin.errors.error_auth002_context", ex_key: "admin.errors.error_auth002_example" }, | |
| 1382 | + ErrorDef { code_enum: ErrorCode::Auth003, desc_key: "admin.errors.error_auth003_desc", cat_key: "admin.errors.error_auth003_category", middleware: "auth_required", ctx_key: "admin.errors.error_auth003_context", ex_key: "admin.errors.error_auth003_example" }, | |
| 1383 | + ErrorDef { code_enum: ErrorCode::Csrf001, desc_key: "admin.errors.error_csrf001_desc", cat_key: "admin.errors.error_csrf001_category", middleware: "csrf", ctx_key: "admin.errors.error_csrf001_context", ex_key: "admin.errors.error_csrf001_example" }, | |
| 1384 | + ErrorDef { code_enum: ErrorCode::Csrf002, desc_key: "admin.errors.error_csrf002_desc", cat_key: "admin.errors.error_csrf002_category", middleware: "csrf", ctx_key: "admin.errors.error_csrf002_context", ex_key: "admin.errors.error_csrf002_example" }, | |
| 1385 | + ErrorDef { code_enum: ErrorCode::Rate001, desc_key: "admin.errors.error_rate001_desc", cat_key: "admin.errors.error_rate001_category", middleware: "rate_limit", ctx_key: "admin.errors.error_rate001_context", ex_key: "admin.errors.error_rate001_example" }, | |
| 1386 | + ErrorDef { code_enum: ErrorCode::Valid001, desc_key: "admin.errors.error_valid001_desc", cat_key: "admin.errors.error_valid001_category", middleware: "validation", ctx_key: "admin.errors.error_valid001_context", ex_key: "admin.errors.error_valid001_example" }, | |
| 1387 | + ErrorDef { code_enum: ErrorCode::Valid002, desc_key: "admin.errors.error_valid002_desc", cat_key: "admin.errors.error_valid002_category", middleware: "password_complexity", ctx_key: "admin.errors.error_valid002_context", ex_key: "admin.errors.error_valid002_example" }, | |
| 1388 | + ErrorDef { code_enum: ErrorCode::Rbac001, desc_key: "admin.errors.error_rbac001_desc", cat_key: "admin.errors.error_rbac001_category", middleware: "admin_required", ctx_key: "admin.errors.error_rbac001_context", ex_key: "admin.errors.error_rbac001_example" }, | |
| 1389 | + ErrorDef { code_enum: ErrorCode::Rbac002, desc_key: "admin.errors.error_rbac002_desc", cat_key: "admin.errors.error_rbac002_category", middleware: "admin_required", ctx_key: "admin.errors.error_rbac002_context", ex_key: "admin.errors.error_rbac002_example" }, | |
| 1390 | + ErrorDef { code_enum: ErrorCode::Db001, desc_key: "admin.errors.error_db001_desc", cat_key: "admin.errors.error_db001_category", middleware: "database", ctx_key: "admin.errors.error_db001_context", ex_key: "admin.errors.error_db001_example" }, | |
| 1391 | + ErrorDef { code_enum: ErrorCode::Db002, desc_key: "admin.errors.error_db002_desc", cat_key: "admin.errors.error_db002_category", middleware: "database", ctx_key: "admin.errors.error_db002_context", ex_key: "admin.errors.error_db002_example" }, | |
| 1179 | 1392 | ]; |
| 1180 | 1393 | |
| 1394 | + let mut error_codes = Vec::new(); | |
| 1395 | + for def in &error_defs { | |
| 1396 | + error_codes.push(ErrorCodeInfo { | |
| 1397 | + code: def.code_enum.as_str().to_string(), | |
| 1398 | + description: i18n_service.translate(def.desc_key, locale_str).await, | |
| 1399 | + category: i18n_service.translate(def.cat_key, locale_str).await, | |
| 1400 | + middleware: def.middleware.to_string(), | |
| 1401 | + context: i18n_service.translate(def.ctx_key, locale_str).await, | |
| 1402 | + example: i18n_service.translate(def.ex_key, locale_str).await, | |
| 1403 | + }); | |
| 1404 | + } | |
| 1405 | + | |
| 1181 | 1406 | let template = AdminErrorsTemplate { |
| 1182 | - page_title: "Codes d'Erreur".to_string(), | |
| 1407 | + page_title: i18n_service.translate("admin.errors.title", locale_str).await, | |
| 1183 | 1408 | csrf_token, |
| 1184 | 1409 | app_name: base_context.app_name, |
| 1185 | 1410 | app_version: base_context.app_version, |
| 1186 | - app_domain: "localhost".to_string(), | |
| 1187 | - app_description: "Secure password manager".to_string(), | |
| 1411 | + app_domain, | |
| 1412 | + app_description, | |
| 1188 | 1413 | username: base_context.username, |
| 1189 | 1414 | email: base_context.email, |
| 1190 | 1415 | initials: base_context.initials, |
| 1191 | 1416 | avatar_color: base_context.avatar_color, |
| 1192 | 1417 | roles_text: base_context.roles.join(", "), |
| 1193 | 1418 | error_codes, |
| 1419 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 1420 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 1421 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 1422 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 1423 | + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await, | |
| 1424 | + breadcrumb_errors: i18n_service.translate("admin.errors.breadcrumb_errors", locale_str).await, | |
| 1425 | + title: i18n_service.translate("admin.errors.title", locale_str).await, | |
| 1426 | + description: i18n_service.translate("admin.errors.description", locale_str).await, | |
| 1427 | + table_code: i18n_service.translate("admin.errors.table_code", locale_str).await, | |
| 1428 | + table_description: i18n_service.translate("admin.errors.table_description", locale_str).await, | |
| 1429 | + table_category: i18n_service.translate("admin.errors.table_category", locale_str).await, | |
| 1430 | + table_middleware: i18n_service.translate("admin.errors.table_middleware", locale_str).await, | |
| 1431 | + table_context: i18n_service.translate("admin.errors.table_context", locale_str).await, | |
| 1432 | + table_example: i18n_service.translate("admin.errors.table_example", locale_str).await, | |
| 1433 | + legend_title: i18n_service.translate("admin.errors.legend_title", locale_str).await, | |
| 1434 | + legend_code: i18n_service.translate("admin.errors.legend_code", locale_str).await, | |
| 1435 | + legend_description: i18n_service.translate("admin.errors.legend_description", locale_str).await, | |
| 1436 | + legend_category: i18n_service.translate("admin.errors.legend_category", locale_str).await, | |
| 1437 | + legend_middleware: i18n_service.translate("admin.errors.legend_middleware", locale_str).await, | |
| 1438 | + legend_context: i18n_service.translate("admin.errors.legend_context", locale_str).await, | |
| 1439 | + legend_example: i18n_service.translate("admin.errors.legend_example", locale_str).await, | |
| 1194 | 1440 | }; |
| 1195 | 1441 | |
| 1196 | 1442 | Ok(template) |
M
src/api/handlers/home_handler.rs
+177
-139
@@ -8,91 +8,18 @@ use axum::{
| 8 | 8 | response::IntoResponse, |
| 9 | 9 | }; |
| 10 | 10 | use sea_orm::DatabaseConnection; |
| 11 | +use std::collections::HashMap; | |
| 12 | +use std::sync::Arc; | |
| 11 | 13 | |
| 14 | +use crate::api::dto::home_dto::{AccountStatDTO, UpcomingFeatureDTO}; | |
| 15 | +use crate::api::dto::navigation_dto::NavigationDTO; | |
| 16 | +use crate::api::dto::user_profile_dto::UserProfileDTO; | |
| 12 | 17 | use crate::auth::jwt::Claims; |
| 13 | 18 | use crate::error::AppError; |
| 19 | +use crate::i18n::extractor::Locale; | |
| 20 | +use crate::i18n::service::I18nService; | |
| 14 | 21 | use crate::services::user_service::UserService; |
| 15 | 22 | |
| 16 | -/// Account stat for home page | |
| 17 | -#[derive(Debug, Clone)] | |
| 18 | -struct AccountStatDTO { | |
| 19 | - label: String, | |
| 20 | - value: String, | |
| 21 | - icon: String, | |
| 22 | -} | |
| 23 | - | |
| 24 | -/// Upcoming feature for home page | |
| 25 | -#[derive(Debug, Clone)] | |
| 26 | -struct UpcomingFeatureDTO { | |
| 27 | - id: String, | |
| 28 | - name: String, | |
| 29 | - description: String, | |
| 30 | - icon: String, | |
| 31 | - badge: String, | |
| 32 | -} | |
| 33 | - | |
| 34 | -/// Simplified navigation item for templates | |
| 35 | -#[derive(Clone)] | |
| 36 | -#[allow(dead_code)] | |
| 37 | -struct NavItem { | |
| 38 | - id: String, | |
| 39 | - label: String, | |
| 40 | - icon: String, | |
| 41 | - url: String, | |
| 42 | - active: bool, | |
| 43 | - enabled: bool, | |
| 44 | - badge: String, | |
| 45 | - has_badge: bool, | |
| 46 | - disabled_message: String, | |
| 47 | - has_disabled_message: bool, | |
| 48 | -} | |
| 49 | - | |
| 50 | -fn build_nav_items(roles: &[String], active_id: &str) -> Vec<NavItem> { | |
| 51 | - let mut items = vec![ | |
| 52 | - NavItem { | |
| 53 | - id: "home".to_string(), | |
| 54 | - label: "Accueil".to_string(), | |
| 55 | - icon: "home".to_string(), | |
| 56 | - url: "/home".to_string(), | |
| 57 | - active: active_id == "home", | |
| 58 | - enabled: true, | |
| 59 | - badge: String::new(), | |
| 60 | - has_badge: false, | |
| 61 | - disabled_message: String::new(), | |
| 62 | - has_disabled_message: false, | |
| 63 | - }, | |
| 64 | - NavItem { | |
| 65 | - id: "settings".to_string(), | |
| 66 | - label: "Paramètres".to_string(), | |
| 67 | - icon: "settings".to_string(), | |
| 68 | - url: "/settings".to_string(), | |
| 69 | - active: active_id == "settings", | |
| 70 | - enabled: true, | |
| 71 | - badge: String::new(), | |
| 72 | - has_badge: false, | |
| 73 | - disabled_message: String::new(), | |
| 74 | - has_disabled_message: false, | |
| 75 | - }, | |
| 76 | - ]; | |
| 77 | - | |
| 78 | - if roles.contains(&"admin".to_string()) { | |
| 79 | - items.push(NavItem { | |
| 80 | - id: "admin".to_string(), | |
| 81 | - label: "Administration".to_string(), | |
| 82 | - icon: "shield".to_string(), | |
| 83 | - url: "/admin".to_string(), | |
| 84 | - active: active_id == "admin", | |
| 85 | - enabled: true, | |
| 86 | - badge: String::new(), | |
| 87 | - has_badge: false, | |
| 88 | - disabled_message: String::new(), | |
| 89 | - has_disabled_message: false, | |
| 90 | - }); | |
| 91 | - } | |
| 92 | - | |
| 93 | - items | |
| 94 | -} | |
| 95 | - | |
| 96 | 23 | /// Home page template |
| 97 | 24 | #[derive(Template)] |
| 98 | 25 | #[template(path = "home.html")] |
@@ -112,6 +39,11 @@ struct HomeTemplate {
| 112 | 39 | nav_items: Vec<NavItem>, |
| 113 | 40 | has_pending_password_change: bool, |
| 114 | 41 | welcome_message: String, |
| 42 | + home_subtitle: String, | |
| 43 | + home_my_account: String, | |
| 44 | + home_upcoming_features: String, | |
| 45 | + home_did_you_know: String, | |
| 46 | + home_did_you_know_text: String, | |
| 115 | 47 | account_stats: Vec<AccountStatDTO>, |
| 116 | 48 | upcoming_features: Vec<UpcomingFeatureDTO>, |
| 117 | 49 | user_menu_profile: String, |
@@ -119,11 +51,30 @@ struct HomeTemplate {
| 119 | 51 | user_menu_settings: String, |
| 120 | 52 | } |
| 121 | 53 | |
| 54 | +/// Simplified navigation item for templates | |
| 55 | +#[derive(Clone)] | |
| 56 | +#[allow(dead_code)] | |
| 57 | +struct NavItem { | |
| 58 | + #[allow(dead_code)] | |
| 59 | + id: String, | |
| 60 | + label: String, | |
| 61 | + icon: String, | |
| 62 | + url: String, | |
| 63 | + active: bool, | |
| 64 | + enabled: bool, | |
| 65 | + badge: String, | |
| 66 | + has_badge: bool, | |
| 67 | + disabled_message: String, | |
| 68 | + has_disabled_message: bool, | |
| 69 | +} | |
| 70 | + | |
| 122 | 71 | crate::impl_template_response!(HomeTemplate); |
| 123 | 72 | |
| 124 | 73 | /// Handler for GET /home - Home page (PROTECTED) |
| 125 | 74 | pub async fn home( |
| 126 | 75 | Extension(claims): Extension<Claims>, |
| 76 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 77 | + Extension(locale): Extension<Locale>, | |
| 127 | 78 | State(db): State<DatabaseConnection>, |
| 128 | 79 | ) -> Result<impl IntoResponse, AppError> { |
| 129 | 80 | let username = claims.username.clone(); |
@@ -137,86 +88,173 @@ pub async fn home(
| 137 | 88 | })?; |
| 138 | 89 | |
| 139 | 90 | let roles = UserService::get_user_roles(&db, user_model.id).await?; |
| 91 | + let user = UserProfileDTO::new(user_model, roles.clone()); | |
| 140 | 92 | |
| 141 | - let initials = { | |
| 142 | - let parts: Vec<&str> = user_model.username.split_whitespace().collect(); | |
| 143 | - match parts.len() { | |
| 144 | - 0 => "??".to_string(), | |
| 145 | - 1 => user_model.username.chars().take(2).collect::<String>().to_uppercase(), | |
| 146 | - _ => format!( | |
| 147 | - "{}{}", | |
| 148 | - parts[0].chars().next().unwrap_or('?'), | |
| 149 | - parts[1].chars().next().unwrap_or('?') | |
| 150 | - ).to_uppercase(), | |
| 151 | - } | |
| 152 | - }; | |
| 93 | + let locale_str = locale.as_str(); | |
| 153 | 94 | |
| 154 | - let avatar_color = { | |
| 155 | - let hash: u32 = user_model.username | |
| 156 | - .bytes() | |
| 157 | - .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32)); | |
| 158 | - let hue = hash % 180; | |
| 159 | - let sat = 60 + (hash % 20) as u8; | |
| 160 | - let light = 45 + (hash % 15) as u8; | |
| 161 | - format!("hsl({}, {}%, {}%)", hue, sat, light) | |
| 162 | - }; | |
| 163 | - | |
| 164 | - let layout = crate::templates::LayoutContext::new("Accueil", ""); | |
| 95 | + // Translate welcome message with variable interpolation | |
| 96 | + let mut vars = HashMap::new(); | |
| 97 | + vars.insert("username".to_string(), user.username.clone()); | |
| 98 | + let welcome_message = i18n_service | |
| 99 | + .translate_with_vars("home.welcome", locale_str, &vars) | |
| 100 | + .await; | |
| 165 | 101 | |
| 166 | - let nav_items = build_nav_items(&roles, "home"); | |
| 102 | + // Translate section titles | |
| 103 | + let home_subtitle = i18n_service.translate("home.subtitle", locale_str).await; | |
| 104 | + let home_my_account = i18n_service.translate("home.my_account", locale_str).await; | |
| 105 | + let home_upcoming_features = i18n_service | |
| 106 | + .translate("home.upcoming_features", locale_str) | |
| 107 | + .await; | |
| 108 | + let home_did_you_know = i18n_service | |
| 109 | + .translate("home.did_you_know", locale_str) | |
| 110 | + .await; | |
| 111 | + let home_did_you_know_text = i18n_service | |
| 112 | + .translate("home.did_you_know_text", locale_str) | |
| 113 | + .await; | |
| 167 | 114 | |
| 168 | - let welcome_message = format!("Bienvenue, {} !", user_model.username); | |
| 115 | + // Build translated account stats | |
| 116 | + let stat_username_label = i18n_service | |
| 117 | + .translate("home.stat_username", locale_str) | |
| 118 | + .await; | |
| 119 | + let stat_email_label = i18n_service.translate("home.stat_email", locale_str).await; | |
| 120 | + let stat_member_since_label = i18n_service | |
| 121 | + .translate("home.stat_member_since", locale_str) | |
| 122 | + .await; | |
| 123 | + let stat_roles_label = i18n_service.translate("home.stat_roles", locale_str).await; | |
| 169 | 124 | |
| 170 | 125 | let account_stats = vec![ |
| 171 | - AccountStatDTO { | |
| 172 | - label: "Rôles".to_string(), | |
| 173 | - value: roles.len().to_string(), | |
| 174 | - icon: "shield".to_string(), | |
| 175 | - }, | |
| 176 | - AccountStatDTO { | |
| 177 | - label: "Email vérifié".to_string(), | |
| 178 | - value: if user_model.email_verified { "Oui" } else { "Non" }.to_string(), | |
| 179 | - icon: "mail".to_string(), | |
| 180 | - }, | |
| 126 | + AccountStatDTO::new(&stat_username_label, &user.username, "user"), | |
| 127 | + AccountStatDTO::new(&stat_email_label, &user.email, "mail"), | |
| 128 | + AccountStatDTO::new( | |
| 129 | + &stat_member_since_label, | |
| 130 | + &user.created_at.format("%d/%m/%Y").to_string(), | |
| 131 | + "calendar", | |
| 132 | + ), | |
| 133 | + AccountStatDTO::new(&stat_roles_label, &user.roles.join(", "), "shield"), | |
| 181 | 134 | ]; |
| 182 | 135 | |
| 136 | + // Build translated upcoming features | |
| 137 | + let badge_soon = i18n_service.translate("home.badge_soon", locale_str).await; | |
| 138 | + let badge_planned = i18n_service | |
| 139 | + .translate("home.badge_planned", locale_str) | |
| 140 | + .await; | |
| 141 | + | |
| 183 | 142 | let upcoming_features = vec![ |
| 184 | - UpcomingFeatureDTO { | |
| 185 | - id: "secrets".to_string(), | |
| 186 | - name: "Gestion des secrets".to_string(), | |
| 187 | - description: "Stockage sécurisé de mots de passe et secrets".to_string(), | |
| 188 | - icon: "folder".to_string(), | |
| 189 | - badge: "Bientôt".to_string(), | |
| 190 | - }, | |
| 191 | - UpcomingFeatureDTO { | |
| 192 | - id: "2fa".to_string(), | |
| 193 | - name: "Authentification 2FA".to_string(), | |
| 194 | - description: "Authentification à deux facteurs".to_string(), | |
| 195 | - icon: "shield".to_string(), | |
| 196 | - badge: "Planifié".to_string(), | |
| 197 | - }, | |
| 143 | + UpcomingFeatureDTO::new( | |
| 144 | + "005", | |
| 145 | + &i18n_service | |
| 146 | + .translate("home.feature_files_name", locale_str) | |
| 147 | + .await, | |
| 148 | + &i18n_service | |
| 149 | + .translate("home.feature_files_desc", locale_str) | |
| 150 | + .await, | |
| 151 | + "folder", | |
| 152 | + &badge_soon, | |
| 153 | + ), | |
| 154 | + UpcomingFeatureDTO::new( | |
| 155 | + "006", | |
| 156 | + &i18n_service | |
| 157 | + .translate("home.feature_upload_name", locale_str) | |
| 158 | + .await, | |
| 159 | + &i18n_service | |
| 160 | + .translate("home.feature_upload_desc", locale_str) | |
| 161 | + .await, | |
| 162 | + "upload", | |
| 163 | + &badge_soon, | |
| 164 | + ), | |
| 165 | + UpcomingFeatureDTO::new( | |
| 166 | + "007", | |
| 167 | + &i18n_service | |
| 168 | + .translate("home.feature_webdav_name", locale_str) | |
| 169 | + .await, | |
| 170 | + &i18n_service | |
| 171 | + .translate("home.feature_webdav_desc", locale_str) | |
| 172 | + .await, | |
| 173 | + "cloud", | |
| 174 | + &badge_planned, | |
| 175 | + ), | |
| 176 | + UpcomingFeatureDTO::new( | |
| 177 | + "008", | |
| 178 | + &i18n_service | |
| 179 | + .translate("home.feature_mobile_name", locale_str) | |
| 180 | + .await, | |
| 181 | + &i18n_service | |
| 182 | + .translate("home.feature_mobile_desc", locale_str) | |
| 183 | + .await, | |
| 184 | + "smartphone", | |
| 185 | + &badge_planned, | |
| 186 | + ), | |
| 198 | 187 | ]; |
| 199 | 188 | |
| 189 | + // Translate user menu | |
| 190 | + let user_menu_profile = i18n_service | |
| 191 | + .translate("user_menu.profile", locale_str) | |
| 192 | + .await; | |
| 193 | + let user_menu_logout = i18n_service | |
| 194 | + .translate("user_menu.logout", locale_str) | |
| 195 | + .await; | |
| 196 | + let user_menu_settings = i18n_service | |
| 197 | + .translate("user_menu.settings", locale_str) | |
| 198 | + .await; | |
| 199 | + | |
| 200 | + // Build navigation | |
| 201 | + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await; | |
| 202 | + let nav_items: Vec<NavItem> = navigation | |
| 203 | + .items | |
| 204 | + .iter() | |
| 205 | + .enumerate() | |
| 206 | + .map(|(i, item)| NavItem { | |
| 207 | + id: item.id.clone(), | |
| 208 | + label: item.label.clone(), | |
| 209 | + icon: item.icon.clone(), | |
| 210 | + url: item.url.clone(), | |
| 211 | + active: i == 0, | |
| 212 | + enabled: item.enabled, | |
| 213 | + badge: item.badge.clone(), | |
| 214 | + has_badge: item.has_badge, | |
| 215 | + disabled_message: item.disabled_message.clone(), | |
| 216 | + has_disabled_message: item.has_disabled_message, | |
| 217 | + }) | |
| 218 | + .collect(); | |
| 219 | + | |
| 220 | + // Load app settings from database | |
| 221 | + use crate::services::app_settings_service::AppSettingsService; | |
| 222 | + let app_domain = AppSettingsService::get_setting(&db, "app_domain") | |
| 223 | + .await? | |
| 224 | + .map(|s| s.value) | |
| 225 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 226 | + let app_description = AppSettingsService::get_setting(&db, "app_description") | |
| 227 | + .await? | |
| 228 | + .map(|s| s.value) | |
| 229 | + .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 230 | + | |
| 231 | + let layout = crate::templates::LayoutContext::new("", ""); | |
| 232 | + | |
| 200 | 233 | let template = HomeTemplate { |
| 201 | 234 | page_title: layout.page_title.clone(), |
| 202 | 235 | csrf_token: layout.csrf_token.clone(), |
| 203 | 236 | app_name: layout.app_name.clone(), |
| 204 | 237 | app_version: layout.app_version.clone(), |
| 205 | - app_domain: "localhost".to_string(), | |
| 206 | - app_description: "Secure password manager".to_string(), | |
| 207 | - username: user_model.username.clone(), | |
| 208 | - email: user_model.email.clone(), | |
| 209 | - initials, | |
| 210 | - avatar_color, | |
| 211 | - roles_text: roles.join(", "), | |
| 238 | + app_domain, | |
| 239 | + app_description, | |
| 240 | + username: user.username.clone(), | |
| 241 | + email: user.email.clone(), | |
| 242 | + initials: user.initials.clone(), | |
| 243 | + avatar_color: user.avatar_color.clone(), | |
| 244 | + roles_text: user.roles.join(", "), | |
| 212 | 245 | nav_items, |
| 213 | 246 | has_pending_password_change: false, |
| 214 | 247 | welcome_message, |
| 248 | + home_subtitle, | |
| 249 | + home_my_account, | |
| 250 | + home_upcoming_features, | |
| 251 | + home_did_you_know, | |
| 252 | + home_did_you_know_text, | |
| 215 | 253 | account_stats, |
| 216 | 254 | upcoming_features, |
| 217 | - user_menu_profile: "Profil".to_string(), | |
| 218 | - user_menu_logout: "Déconnexion".to_string(), | |
| 219 | - user_menu_settings: "Paramètres".to_string(), | |
| 255 | + user_menu_profile, | |
| 256 | + user_menu_logout, | |
| 257 | + user_menu_settings, | |
| 220 | 258 | }; |
| 221 | 259 | |
| 222 | 260 | Ok(template) |
M
src/api/handlers/login_handler.rs
+320
-56
@@ -4,13 +4,15 @@
| 4 | 4 | /// Handles the login page display (GET /login). |
| 5 | 5 | use askama::Template; |
| 6 | 6 | use axum::{ |
| 7 | - extract::State, | |
| 7 | + extract::{Extension, State}, | |
| 8 | 8 | http::HeaderMap, |
| 9 | 9 | response::IntoResponse, |
| 10 | 10 | }; |
| 11 | 11 | use axum_extra::extract::CookieJar; |
| 12 | 12 | use sea_orm::DatabaseConnection; |
| 13 | +use std::sync::Arc; | |
| 13 | 14 | |
| 15 | +use crate::i18n::service::I18nService; | |
| 14 | 16 | use crate::templates::LayoutContext; |
| 15 | 17 | |
| 16 | 18 | /// Login page template |
@@ -21,7 +23,6 @@ struct LoginTemplate {
| 21 | 23 | app_version: String, |
| 22 | 24 | current_locale: String, |
| 23 | 25 | supported_languages: Vec<(String, String, bool)>, |
| 24 | - // Translated strings (hardcoded FR) | |
| 25 | 26 | page_title: String, |
| 26 | 27 | page_description: String, |
| 27 | 28 | subtitle: String, |
@@ -65,6 +66,17 @@ struct ResendVerificationTemplate {
| 65 | 66 | app_version: String, |
| 66 | 67 | app_domain: String, |
| 67 | 68 | app_description: String, |
| 69 | + page_title: String, | |
| 70 | + title: String, | |
| 71 | + subtitle: String, | |
| 72 | + email_label: String, | |
| 73 | + email_placeholder: String, | |
| 74 | + submit: String, | |
| 75 | + back_to_login: String, | |
| 76 | + error_default: String, | |
| 77 | + success_default: String, | |
| 78 | + error_missing_email: String, | |
| 79 | + error_cannot_send: String, | |
| 68 | 80 | } |
| 69 | 81 | |
| 70 | 82 | /// Reset password page template |
@@ -77,13 +89,23 @@ struct ResetPasswordTemplate {
| 77 | 89 | app_description: String, |
| 78 | 90 | page_title: String, |
| 79 | 91 | reset_password_title: String, |
| 92 | + subtitle: String, | |
| 80 | 93 | new_password_label: String, |
| 81 | 94 | new_password_placeholder: String, |
| 95 | + confirm_password_label: String, | |
| 96 | + confirm_password_placeholder: String, | |
| 82 | 97 | password_requirements: String, |
| 83 | 98 | submit_button: String, |
| 84 | 99 | back_to_login: String, |
| 85 | 100 | success_message: String, |
| 86 | 101 | error_message: String, |
| 102 | + missing_token_url: String, | |
| 103 | + missing_token: String, | |
| 104 | + missing_new_password: String, | |
| 105 | + passwords_not_match: String, | |
| 106 | + reset_success_message: String, | |
| 107 | + error_link_invalid: String, | |
| 108 | + error_generic: String, | |
| 87 | 109 | } |
| 88 | 110 | |
| 89 | 111 | crate::impl_template_response!(LoginTemplate); |
@@ -93,6 +115,7 @@ crate::impl_template_response!(ResetPasswordTemplate);
| 93 | 115 | /// Handler for GET /login - Login page (PUBLIC) |
| 94 | 116 | pub async fn login_page( |
| 95 | 117 | headers: HeaderMap, |
| 118 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 96 | 119 | ) -> Result<impl IntoResponse, crate::error::AppError> { |
| 97 | 120 | tracing::debug!("Rendering login page"); |
| 98 | 121 |
@@ -110,12 +133,113 @@ pub async fn login_page(
| 110 | 133 | .unwrap_or_else(|| "fr".to_string()); |
| 111 | 134 | |
| 112 | 135 | let supported_languages: Vec<(String, String, bool)> = vec![ |
| 113 | - ("fr".to_string(), "Français".to_string(), current_locale == "fr"), | |
| 114 | - ("en".to_string(), "English".to_string(), current_locale == "en"), | |
| 115 | - ("de".to_string(), "Deutsch".to_string(), current_locale == "de"), | |
| 116 | - ("es".to_string(), "Español".to_string(), current_locale == "es"), | |
| 136 | + ( | |
| 137 | + "fr".to_string(), | |
| 138 | + "Français".to_string(), | |
| 139 | + current_locale == "fr", | |
| 140 | + ), | |
| 141 | + ( | |
| 142 | + "en".to_string(), | |
| 143 | + "English".to_string(), | |
| 144 | + current_locale == "en", | |
| 145 | + ), | |
| 146 | + ( | |
| 147 | + "de".to_string(), | |
| 148 | + "Deutsch".to_string(), | |
| 149 | + current_locale == "de", | |
| 150 | + ), | |
| 151 | + ( | |
| 152 | + "es".to_string(), | |
| 153 | + "Español".to_string(), | |
| 154 | + current_locale == "es", | |
| 155 | + ), | |
| 117 | 156 | ]; |
| 118 | 157 | |
| 158 | + let locale_str = ¤t_locale; | |
| 159 | + let page_title = i18n_service.translate("login.page_title", locale_str).await; | |
| 160 | + let page_description = i18n_service | |
| 161 | + .translate("login.page_description", locale_str) | |
| 162 | + .await; | |
| 163 | + let subtitle = i18n_service.translate("login.subtitle", locale_str).await; | |
| 164 | + let tab_login = i18n_service.translate("login.tab_login", locale_str).await; | |
| 165 | + let tab_register = i18n_service | |
| 166 | + .translate("login.tab_register", locale_str) | |
| 167 | + .await; | |
| 168 | + let login_title = i18n_service.translate("login.title", locale_str).await; | |
| 169 | + let register_title = i18n_service | |
| 170 | + .translate("login.register_title", locale_str) | |
| 171 | + .await; | |
| 172 | + let username_label = i18n_service.translate("login.username", locale_str).await; | |
| 173 | + let password_label = i18n_service.translate("login.password", locale_str).await; | |
| 174 | + let email_label = i18n_service.translate("login.email_label", locale_str).await; | |
| 175 | + let username_placeholder = i18n_service | |
| 176 | + .translate("login.username_placeholder", locale_str) | |
| 177 | + .await; | |
| 178 | + let password_placeholder = i18n_service | |
| 179 | + .translate("login.password_placeholder", locale_str) | |
| 180 | + .await; | |
| 181 | + let username_placeholder_register = i18n_service | |
| 182 | + .translate("login.username_placeholder_register", locale_str) | |
| 183 | + .await; | |
| 184 | + let password_placeholder_register = i18n_service | |
| 185 | + .translate("login.password_placeholder_register", locale_str) | |
| 186 | + .await; | |
| 187 | + let email_placeholder = i18n_service | |
| 188 | + .translate("login.email_placeholder", locale_str) | |
| 189 | + .await; | |
| 190 | + let submit_button = i18n_service.translate("login.submit", locale_str).await; | |
| 191 | + let register_submit = i18n_service | |
| 192 | + .translate("login.register_submit", locale_str) | |
| 193 | + .await; | |
| 194 | + let remember_me = i18n_service | |
| 195 | + .translate("login.remember_me", locale_str) | |
| 196 | + .await; | |
| 197 | + let forgot_password = i18n_service | |
| 198 | + .translate("login.forgot_password", locale_str) | |
| 199 | + .await; | |
| 200 | + let email_not_verified = i18n_service | |
| 201 | + .translate("login.email_not_verified", locale_str) | |
| 202 | + .await; | |
| 203 | + let resend_verification = i18n_service | |
| 204 | + .translate("login.resend_verification", locale_str) | |
| 205 | + .await; | |
| 206 | + let already_have_account = i18n_service | |
| 207 | + .translate("login.already_have_account", locale_str) | |
| 208 | + .await; | |
| 209 | + let sign_in_link = i18n_service | |
| 210 | + .translate("login.sign_in_link", locale_str) | |
| 211 | + .await; | |
| 212 | + let footer_text = i18n_service | |
| 213 | + .translate("login.footer_text", locale_str) | |
| 214 | + .await; | |
| 215 | + let success_message = i18n_service | |
| 216 | + .translate("login.success_message", locale_str) | |
| 217 | + .await; | |
| 218 | + let error_message = i18n_service | |
| 219 | + .translate("login.error_message", locale_str) | |
| 220 | + .await; | |
| 221 | + let email_unverified_message = i18n_service | |
| 222 | + .translate("login.email_unverified_message", locale_str) | |
| 223 | + .await; | |
| 224 | + let resend_button = i18n_service | |
| 225 | + .translate("login.resend_button", locale_str) | |
| 226 | + .await; | |
| 227 | + let resend_success = i18n_service | |
| 228 | + .translate("login.resend_success", locale_str) | |
| 229 | + .await; | |
| 230 | + let resend_error = i18n_service | |
| 231 | + .translate("login.resend_error", locale_str) | |
| 232 | + .await; | |
| 233 | + let resend_prompt = i18n_service | |
| 234 | + .translate("login.resend_prompt", locale_str) | |
| 235 | + .await; | |
| 236 | + let register_success = i18n_service | |
| 237 | + .translate("login.register_success", locale_str) | |
| 238 | + .await; | |
| 239 | + let register_error = i18n_service | |
| 240 | + .translate("login.register_error", locale_str) | |
| 241 | + .await; | |
| 242 | + | |
| 119 | 243 | let layout = LayoutContext::new("", ""); |
| 120 | 244 | |
| 121 | 245 | let template = LoginTemplate { |
@@ -123,39 +247,39 @@ pub async fn login_page(
| 123 | 247 | app_version: layout.app_version.clone(), |
| 124 | 248 | current_locale, |
| 125 | 249 | supported_languages, |
| 126 | - page_title: "Connexion - RustWarden".to_string(), | |
| 127 | - page_description: "Gestionnaire de mots de passe sécurisé".to_string(), | |
| 128 | - subtitle: "Connectez-vous à votre compte".to_string(), | |
| 129 | - tab_login: "Connexion".to_string(), | |
| 130 | - tab_register: "Inscription".to_string(), | |
| 131 | - login_title: "Connexion".to_string(), | |
| 132 | - register_title: "Créer un compte".to_string(), | |
| 133 | - username_label: "Nom d'utilisateur".to_string(), | |
| 134 | - password_label: "Mot de passe".to_string(), | |
| 135 | - email_label: "Email".to_string(), | |
| 136 | - username_placeholder: "Entrez votre nom d'utilisateur".to_string(), | |
| 137 | - password_placeholder: "Entrez votre mot de passe".to_string(), | |
| 138 | - username_placeholder_register: "Choisissez un nom d'utilisateur".to_string(), | |
| 139 | - password_placeholder_register: "Choisissez un mot de passe".to_string(), | |
| 140 | - email_placeholder: "votre@email.com".to_string(), | |
| 141 | - submit_button: "Se connecter".to_string(), | |
| 142 | - register_submit: "S'inscrire".to_string(), | |
| 143 | - remember_me: "Se souvenir de moi".to_string(), | |
| 144 | - forgot_password: "Mot de passe oublié ?".to_string(), | |
| 145 | - email_not_verified: "Votre email n'est pas vérifié".to_string(), | |
| 146 | - resend_verification: "Renvoyer l'email de vérification".to_string(), | |
| 147 | - already_have_account: "Vous avez déjà un compte ?".to_string(), | |
| 148 | - sign_in_link: "Se connecter".to_string(), | |
| 149 | - footer_text: "RustWarden - Gestionnaire de mots de passe sécurisé".to_string(), | |
| 150 | - success_message: "Connexion réussie ! Redirection...".to_string(), | |
| 151 | - error_message: "Identifiants invalides".to_string(), | |
| 152 | - email_unverified_message: "Votre email n'est pas vérifié. Veuillez vérifier votre boîte de réception.".to_string(), | |
| 153 | - resend_button: "Renvoyer".to_string(), | |
| 154 | - resend_success: "Email de vérification renvoyé avec succès".to_string(), | |
| 155 | - resend_error: "Erreur lors de l'envoi de l'email".to_string(), | |
| 156 | - resend_prompt: "Veuillez entrer votre email pour renvoyer le lien de vérification".to_string(), | |
| 157 | - register_success: "Inscription réussie ! Veuillez vérifier votre email.".to_string(), | |
| 158 | - register_error: "Erreur lors de l'inscription".to_string(), | |
| 250 | + page_title, | |
| 251 | + page_description, | |
| 252 | + subtitle, | |
| 253 | + tab_login, | |
| 254 | + tab_register, | |
| 255 | + login_title, | |
| 256 | + register_title, | |
| 257 | + username_label, | |
| 258 | + password_label, | |
| 259 | + email_label, | |
| 260 | + username_placeholder, | |
| 261 | + password_placeholder, | |
| 262 | + username_placeholder_register, | |
| 263 | + password_placeholder_register, | |
| 264 | + email_placeholder, | |
| 265 | + submit_button, | |
| 266 | + register_submit, | |
| 267 | + remember_me, | |
| 268 | + forgot_password, | |
| 269 | + email_not_verified, | |
| 270 | + resend_verification, | |
| 271 | + already_have_account, | |
| 272 | + sign_in_link, | |
| 273 | + footer_text, | |
| 274 | + success_message, | |
| 275 | + error_message, | |
| 276 | + email_unverified_message, | |
| 277 | + resend_button, | |
| 278 | + resend_success, | |
| 279 | + resend_error, | |
| 280 | + resend_prompt, | |
| 281 | + register_success, | |
| 282 | + register_error, | |
| 159 | 283 | }; |
| 160 | 284 | |
| 161 | 285 | Ok(template) |
@@ -163,17 +287,66 @@ pub async fn login_page(
| 163 | 287 | |
| 164 | 288 | /// Handler for GET /resend-verification - Resend email verification page (PUBLIC) |
| 165 | 289 | pub async fn resend_verification_page( |
| 166 | - State(_db): State<DatabaseConnection>, | |
| 290 | + headers: HeaderMap, | |
| 291 | + State(db): State<DatabaseConnection>, | |
| 292 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 167 | 293 | ) -> Result<impl IntoResponse, crate::error::AppError> { |
| 168 | 294 | tracing::debug!("Rendering resend verification page"); |
| 169 | 295 | |
| 170 | - let layout = LayoutContext::new("Renvoyer l'email de validation", ""); | |
| 296 | + let jar = CookieJar::from_headers(&headers); | |
| 297 | + let current_locale = jar | |
| 298 | + .get("locale") | |
| 299 | + .and_then(|c| { | |
| 300 | + let value = c.value(); | |
| 301 | + if matches!(value, "fr" | "en" | "de" | "es") { | |
| 302 | + Some(value.to_string()) | |
| 303 | + } else { | |
| 304 | + None | |
| 305 | + } | |
| 306 | + }) | |
| 307 | + .unwrap_or_else(|| "fr".to_string()); | |
| 308 | + | |
| 309 | + let locale_str = ¤t_locale; | |
| 310 | + let page_title = i18n_service.translate("resend_verification.page_title", locale_str).await; | |
| 311 | + let title = i18n_service.translate("resend_verification.title", locale_str).await; | |
| 312 | + let subtitle = i18n_service.translate("resend_verification.subtitle", locale_str).await; | |
| 313 | + let email_label = i18n_service.translate("resend_verification.email_label", locale_str).await; | |
| 314 | + let email_placeholder = i18n_service.translate("resend_verification.email_placeholder", locale_str).await; | |
| 315 | + let submit = i18n_service.translate("resend_verification.submit", locale_str).await; | |
| 316 | + let back_to_login = i18n_service.translate("resend_verification.back_to_login", locale_str).await; | |
| 317 | + let error_default = i18n_service.translate("resend_verification.error_default", locale_str).await; | |
| 318 | + let success_default = i18n_service.translate("resend_verification.success_default", locale_str).await; | |
| 319 | + let error_missing_email = i18n_service.translate("resend_verification.error_missing_email", locale_str).await; | |
| 320 | + let error_cannot_send = i18n_service.translate("resend_verification.error_cannot_send", locale_str).await; | |
| 321 | + | |
| 322 | + let layout = LayoutContext::new("", ""); | |
| 323 | + | |
| 324 | + use crate::services::app_settings_service::AppSettingsService; | |
| 325 | + let app_domain = AppSettingsService::get_setting(&db, "app_domain") | |
| 326 | + .await? | |
| 327 | + .map(|s| s.value) | |
| 328 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 329 | + let app_description = AppSettingsService::get_setting(&db, "app_description") | |
| 330 | + .await? | |
| 331 | + .map(|s| s.value) | |
| 332 | + .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 171 | 333 | |
| 172 | 334 | let template = ResendVerificationTemplate { |
| 173 | 335 | app_name: layout.app_name.clone(), |
| 174 | 336 | app_version: layout.app_version.clone(), |
| 175 | - app_domain: "localhost".to_string(), | |
| 176 | - app_description: "Secure password manager".to_string(), | |
| 337 | + app_domain, | |
| 338 | + app_description, | |
| 339 | + page_title, | |
| 340 | + title, | |
| 341 | + subtitle, | |
| 342 | + email_label, | |
| 343 | + email_placeholder, | |
| 344 | + submit, | |
| 345 | + back_to_login, | |
| 346 | + error_default, | |
| 347 | + success_default, | |
| 348 | + error_missing_email, | |
| 349 | + error_cannot_send, | |
| 177 | 350 | }; |
| 178 | 351 | |
| 179 | 352 | Ok(template) |
@@ -188,28 +361,119 @@ pub struct ResetPasswordQuery {
| 188 | 361 | |
| 189 | 362 | /// Handler for GET /reset-password?token={token} - Reset password page (PUBLIC) |
| 190 | 363 | pub async fn reset_password_page( |
| 191 | - _headers: HeaderMap, | |
| 192 | - State(_db): State<DatabaseConnection>, | |
| 364 | + headers: HeaderMap, | |
| 365 | + State(db): State<DatabaseConnection>, | |
| 366 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 193 | 367 | axum::extract::Query(_params): axum::extract::Query<ResetPasswordQuery>, |
| 194 | 368 | ) -> Result<impl IntoResponse, crate::error::AppError> { |
| 195 | 369 | tracing::debug!("Rendering reset password page"); |
| 196 | 370 | |
| 371 | + let jar = CookieJar::from_headers(&headers); | |
| 372 | + let current_locale = jar | |
| 373 | + .get("locale") | |
| 374 | + .and_then(|c| { | |
| 375 | + let value = c.value(); | |
| 376 | + if matches!(value, "fr" | "en" | "de" | "es") { | |
| 377 | + Some(value.to_string()) | |
| 378 | + } else { | |
| 379 | + None | |
| 380 | + } | |
| 381 | + }) | |
| 382 | + .unwrap_or_else(|| "fr".to_string()); | |
| 383 | + | |
| 384 | + let locale_str = ¤t_locale; | |
| 385 | + let page_title = i18n_service | |
| 386 | + .translate("password_reset.page_title", locale_str) | |
| 387 | + .await; | |
| 388 | + let reset_password_title = i18n_service | |
| 389 | + .translate("password_reset.title", locale_str) | |
| 390 | + .await; | |
| 391 | + let subtitle = i18n_service | |
| 392 | + .translate("password_reset.subtitle", locale_str) | |
| 393 | + .await; | |
| 394 | + let new_password_label = i18n_service | |
| 395 | + .translate("password_reset.new_password_label", locale_str) | |
| 396 | + .await; | |
| 397 | + let new_password_placeholder = i18n_service | |
| 398 | + .translate("password_reset.new_password_placeholder", locale_str) | |
| 399 | + .await; | |
| 400 | + let confirm_password_label = i18n_service | |
| 401 | + .translate("password_reset.confirm_password_label", locale_str) | |
| 402 | + .await; | |
| 403 | + let confirm_password_placeholder = i18n_service | |
| 404 | + .translate("password_reset.confirm_password_placeholder", locale_str) | |
| 405 | + .await; | |
| 406 | + let password_requirements = i18n_service | |
| 407 | + .translate("password_reset.requirements", locale_str) | |
| 408 | + .await; | |
| 409 | + let submit_button = i18n_service.translate("login.submit", locale_str).await; | |
| 410 | + let back_to_login = i18n_service | |
| 411 | + .translate("login.sign_in_link", locale_str) | |
| 412 | + .await; | |
| 413 | + let success_message = i18n_service | |
| 414 | + .translate("login.success_message", locale_str) | |
| 415 | + .await; | |
| 416 | + let error_message = i18n_service | |
| 417 | + .translate("login.error_message", locale_str) | |
| 418 | + .await; | |
| 419 | + let missing_token_url = i18n_service | |
| 420 | + .translate("password_reset.missing_token_url", locale_str) | |
| 421 | + .await; | |
| 422 | + let missing_token = i18n_service | |
| 423 | + .translate("password_reset.missing_token", locale_str) | |
| 424 | + .await; | |
| 425 | + let missing_new_password = i18n_service | |
| 426 | + .translate("password_reset.missing_new_password", locale_str) | |
| 427 | + .await; | |
| 428 | + let passwords_not_match = i18n_service | |
| 429 | + .translate("password_reset.passwords_not_match", locale_str) | |
| 430 | + .await; | |
| 431 | + let reset_success_message = i18n_service | |
| 432 | + .translate("password_reset.success_message", locale_str) | |
| 433 | + .await; | |
| 434 | + let error_link_invalid = i18n_service | |
| 435 | + .translate("password_reset.error_link_invalid", locale_str) | |
| 436 | + .await; | |
| 437 | + let error_generic = i18n_service | |
| 438 | + .translate("password_reset.error_generic", locale_str) | |
| 439 | + .await; | |
| 440 | + | |
| 197 | 441 | let layout = LayoutContext::new("", ""); |
| 198 | 442 | |
| 443 | + use crate::services::app_settings_service::AppSettingsService; | |
| 444 | + let app_domain = AppSettingsService::get_setting(&db, "app_domain") | |
| 445 | + .await? | |
| 446 | + .map(|s| s.value) | |
| 447 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 448 | + let app_description = AppSettingsService::get_setting(&db, "app_description") | |
| 449 | + .await? | |
| 450 | + .map(|s| s.value) | |
| 451 | + .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 452 | + | |
| 199 | 453 | let template = ResetPasswordTemplate { |
| 200 | 454 | app_name: layout.app_name.clone(), |
| 201 | 455 | app_version: layout.app_version.clone(), |
| 202 | - app_domain: "localhost".to_string(), | |
| 203 | - app_description: "Secure password manager".to_string(), | |
| 204 | - page_title: "Réinitialiser le mot de passe".to_string(), | |
| 205 | - reset_password_title: "Réinitialiser le mot de passe".to_string(), | |
| 206 | - new_password_label: "Nouveau mot de passe".to_string(), | |
| 207 | - new_password_placeholder: "Entrez le nouveau mot de passe".to_string(), | |
| 208 | - password_requirements: "Le mot de passe doit contenir au moins 8 caractères".to_string(), | |
| 209 | - submit_button: "Réinitialiser".to_string(), | |
| 210 | - back_to_login: "Retour à la connexion".to_string(), | |
| 211 | - success_message: "Mot de passe réinitialisé avec succès".to_string(), | |
| 212 | - error_message: "Erreur lors de la réinitialisation".to_string(), | |
| 456 | + app_domain, | |
| 457 | + app_description, | |
| 458 | + page_title, | |
| 459 | + reset_password_title, | |
| 460 | + subtitle, | |
| 461 | + new_password_label, | |
| 462 | + new_password_placeholder, | |
| 463 | + confirm_password_label, | |
| 464 | + confirm_password_placeholder, | |
| 465 | + password_requirements, | |
| 466 | + submit_button, | |
| 467 | + back_to_login, | |
| 468 | + success_message, | |
| 469 | + error_message, | |
| 470 | + missing_token_url, | |
| 471 | + missing_token, | |
| 472 | + missing_new_password, | |
| 473 | + passwords_not_match, | |
| 474 | + reset_success_message, | |
| 475 | + error_link_invalid, | |
| 476 | + error_generic, | |
| 213 | 477 | }; |
| 214 | 478 | |
| 215 | 479 | Ok(template) |
M
src/api/handlers/settings_handler.rs
+340
-266
@@ -10,11 +10,17 @@ use axum::{
| 10 | 10 | }; |
| 11 | 11 | use sea_orm::DatabaseConnection; |
| 12 | 12 | use serde::Deserialize; |
| 13 | +use std::sync::Arc; | |
| 13 | 14 | use tower_sessions::Session; |
| 14 | 15 | |
| 16 | +use crate::api::dto::navigation_dto::NavigationDTO; | |
| 17 | +use crate::api::dto::user_profile_dto::UserProfileDTO; | |
| 15 | 18 | use crate::auth::jwt::Claims; |
| 16 | 19 | use crate::config::email::EmailConfig; |
| 17 | 20 | use crate::error::AppError; |
| 21 | +use crate::i18n::extractor::Locale; | |
| 22 | +use crate::i18n::service::I18nService; | |
| 23 | +use crate::services::app_settings_service::AppSettingsService; | |
| 18 | 24 | use crate::services::refresh_token_service::RefreshTokenService; |
| 19 | 25 | use crate::services::user_service::UserService; |
| 20 | 26 | use chrono::Utc; |
@@ -69,98 +75,24 @@ struct NavItem {
| 69 | 75 | has_disabled_message: bool, |
| 70 | 76 | } |
| 71 | 77 | |
| 72 | -fn build_nav_items(roles: &[String], active_id: &str) -> Vec<NavItem> { | |
| 73 | - let mut items = vec![ | |
| 74 | - NavItem { | |
| 75 | - id: "home".to_string(), | |
| 76 | - label: "Accueil".to_string(), | |
| 77 | - icon: "home".to_string(), | |
| 78 | - url: "/home".to_string(), | |
| 79 | - active: active_id == "home", | |
| 80 | - enabled: true, | |
| 81 | - badge: String::new(), | |
| 82 | - has_badge: false, | |
| 83 | - disabled_message: String::new(), | |
| 84 | - has_disabled_message: false, | |
| 85 | - }, | |
| 86 | - NavItem { | |
| 87 | - id: "settings".to_string(), | |
| 88 | - label: "Paramètres".to_string(), | |
| 89 | - icon: "settings".to_string(), | |
| 90 | - url: "/settings".to_string(), | |
| 91 | - active: active_id == "settings", | |
| 92 | - enabled: true, | |
| 93 | - badge: String::new(), | |
| 94 | - has_badge: false, | |
| 95 | - disabled_message: String::new(), | |
| 96 | - has_disabled_message: false, | |
| 97 | - }, | |
| 98 | - ]; | |
| 99 | - | |
| 100 | - if roles.contains(&"admin".to_string()) { | |
| 101 | - items.push(NavItem { | |
| 102 | - id: "admin".to_string(), | |
| 103 | - label: "Administration".to_string(), | |
| 104 | - icon: "shield".to_string(), | |
| 105 | - url: "/admin".to_string(), | |
| 106 | - active: active_id == "admin", | |
| 107 | - enabled: true, | |
| 108 | - badge: String::new(), | |
| 109 | - has_badge: false, | |
| 110 | - disabled_message: String::new(), | |
| 111 | - has_disabled_message: false, | |
| 112 | - }); | |
| 113 | - } | |
| 114 | - | |
| 115 | - items | |
| 116 | -} | |
| 117 | - | |
| 118 | -/// Helper to build a simple UserProfile from user model + roles | |
| 119 | -struct SimpleUserProfile { | |
| 120 | - id: uuid::Uuid, | |
| 121 | - username: String, | |
| 122 | - email: String, | |
| 123 | - initials: String, | |
| 124 | - avatar_color: String, | |
| 125 | - roles: Vec<String>, | |
| 126 | - created_at: chrono::DateTime<chrono::Utc>, | |
| 127 | -} | |
| 128 | - | |
| 129 | -impl SimpleUserProfile { | |
| 130 | - fn from_model(model: &crate::models::user::Model, roles: Vec<String>) -> Self { | |
| 131 | - let initials = { | |
| 132 | - let parts: Vec<&str> = model.username.split_whitespace().collect(); | |
| 133 | - match parts.len() { | |
| 134 | - 0 => "??".to_string(), | |
| 135 | - 1 => model.username.chars().take(2).collect::<String>().to_uppercase(), | |
| 136 | - _ => format!( | |
| 137 | - "{}{}", | |
| 138 | - parts[0].chars().next().unwrap_or('?'), | |
| 139 | - parts[1].chars().next().unwrap_or('?') | |
| 140 | - ).to_uppercase(), | |
| 141 | - } | |
| 142 | - }; | |
| 143 | - | |
| 144 | - let avatar_color = { | |
| 145 | - let hash: u32 = model.username | |
| 146 | - .bytes() | |
| 147 | - .fold(0u32, |acc, b| acc.wrapping_mul(31).wrapping_add(b as u32)); | |
| 148 | - let hue = hash % 180; | |
| 149 | - let sat = 60 + (hash % 20) as u8; | |
| 150 | - let light = 45 + (hash % 15) as u8; | |
| 151 | - format!("hsl({}, {}%, {}%)", hue, sat, light) | |
| 152 | - }; | |
| 153 | - | |
| 154 | - Self { | |
| 155 | - id: model.id, | |
| 156 | - username: model.username.clone(), | |
| 157 | - email: model.email.clone(), | |
| 158 | - initials, | |
| 159 | - avatar_color, | |
| 160 | - roles, | |
| 161 | - created_at: model.created_at, | |
| 162 | - } | |
| 163 | - } | |
| 78 | +/// Helper to build NavItems from NavigationDTO | |
| 79 | +fn build_nav_items_from_dto(navigation: &NavigationDTO, active_id: &str) -> Vec<NavItem> { | |
| 80 | + navigation | |
| 81 | + .items | |
| 82 | + .iter() | |
| 83 | + .map(|item| NavItem { | |
| 84 | + id: item.id.clone(), | |
| 85 | + label: item.label.clone(), | |
| 86 | + icon: item.icon.clone(), | |
| 87 | + url: item.url.clone(), | |
| 88 | + active: item.id == active_id, | |
| 89 | + enabled: item.enabled, | |
| 90 | + badge: item.badge.clone(), | |
| 91 | + has_badge: item.has_badge, | |
| 92 | + disabled_message: item.disabled_message.clone(), | |
| 93 | + has_disabled_message: item.has_disabled_message, | |
| 94 | + }) | |
| 95 | + .collect() | |
| 164 | 96 | } |
| 165 | 97 | |
| 166 | 98 | /// Settings page template |
@@ -184,6 +116,21 @@ struct SettingsTemplate {
| 184 | 116 | user_menu_profile: String, |
| 185 | 117 | user_menu_logout: String, |
| 186 | 118 | user_menu_settings: String, |
| 119 | + settings_title: String, | |
| 120 | + settings_subtitle: String, | |
| 121 | + profile_title: String, | |
| 122 | + profile_desc: String, | |
| 123 | + password_title: String, | |
| 124 | + password_desc: String, | |
| 125 | + sessions_title: String, | |
| 126 | + sessions_desc: String, | |
| 127 | + preferences_title: String, | |
| 128 | + preferences_desc: String, | |
| 129 | + access_button: String, | |
| 130 | + info_title: String, | |
| 131 | + info_desc: String, | |
| 132 | + breadcrumb_home: String, | |
| 133 | + breadcrumb_settings: String, | |
| 187 | 134 | } |
| 188 | 135 | |
| 189 | 136 | crate::impl_template_response!(SettingsTemplate); |
@@ -191,6 +138,8 @@ crate::impl_template_response!(SettingsTemplate);
| 191 | 138 | /// Handler for GET /settings - Settings page (PROTECTED) |
| 192 | 139 | pub async fn settings( |
| 193 | 140 | Extension(claims): Extension<Claims>, |
| 141 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 142 | + Extension(locale): Extension<Locale>, | |
| 194 | 143 | State(db): State<DatabaseConnection>, |
| 195 | 144 | ) -> Result<impl IntoResponse, AppError> { |
| 196 | 145 | let username = claims.username.clone(); |
@@ -204,18 +153,52 @@ pub async fn settings(
| 204 | 153 | })?; |
| 205 | 154 | |
| 206 | 155 | let roles = UserService::get_user_roles(&db, user_model.id).await?; |
| 207 | - let user = SimpleUserProfile::from_model(&user_model, roles.clone()); | |
| 208 | - | |
| 209 | - let layout = crate::templates::LayoutContext::new("Paramètres", ""); | |
| 210 | - let nav_items = build_nav_items(&roles, "settings"); | |
| 156 | + let user = UserProfileDTO::new(user_model, roles.clone()); | |
| 157 | + | |
| 158 | + let locale_str = locale.as_str(); | |
| 159 | + | |
| 160 | + let user_menu_profile = i18n_service.translate("user_menu.profile", locale_str).await; | |
| 161 | + let user_menu_logout = i18n_service.translate("user_menu.logout", locale_str).await; | |
| 162 | + let user_menu_settings = i18n_service.translate("user_menu.settings", locale_str).await; | |
| 163 | + | |
| 164 | + let settings_page_title = i18n_service.translate("settings.page_title", locale_str).await; | |
| 165 | + let settings_title = i18n_service.translate("settings.title", locale_str).await; | |
| 166 | + let settings_subtitle = i18n_service.translate("settings.subtitle", locale_str).await; | |
| 167 | + let profile_title = i18n_service.translate("settings.profile_title", locale_str).await; | |
| 168 | + let profile_desc = i18n_service.translate("settings.profile_desc", locale_str).await; | |
| 169 | + let password_title = i18n_service.translate("settings.password_title", locale_str).await; | |
| 170 | + let password_desc = i18n_service.translate("settings.password_desc", locale_str).await; | |
| 171 | + let sessions_title = i18n_service.translate("settings.sessions_title", locale_str).await; | |
| 172 | + let sessions_desc = i18n_service.translate("settings.sessions_desc", locale_str).await; | |
| 173 | + let preferences_title = i18n_service.translate("settings.preferences_title", locale_str).await; | |
| 174 | + let preferences_desc = i18n_service.translate("settings.preferences_desc", locale_str).await; | |
| 175 | + let access_button = i18n_service.translate("settings.access", locale_str).await; | |
| 176 | + let info_title = i18n_service.translate("settings.info_title", locale_str).await; | |
| 177 | + let info_desc = i18n_service.translate("settings.info_desc", locale_str).await; | |
| 178 | + let breadcrumb_home = i18n_service.translate("breadcrumb.home", locale_str).await; | |
| 179 | + let breadcrumb_settings = i18n_service.translate("breadcrumb.settings", locale_str).await; | |
| 180 | + | |
| 181 | + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await; | |
| 182 | + let nav_items = build_nav_items_from_dto(&navigation, "settings"); | |
| 183 | + | |
| 184 | + let layout = crate::templates::LayoutContext::new("", ""); | |
| 185 | + | |
| 186 | + let app_domain = AppSettingsService::get_setting(&db, "app_domain") | |
| 187 | + .await? | |
| 188 | + .map(|s| s.value) | |
| 189 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 190 | + let app_description = AppSettingsService::get_setting(&db, "app_description") | |
| 191 | + .await? | |
| 192 | + .map(|s| s.value) | |
| 193 | + .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 211 | 194 | |
| 212 | 195 | let template = SettingsTemplate { |
| 213 | - page_title: layout.page_title.clone(), | |
| 196 | + page_title: settings_page_title, | |
| 214 | 197 | csrf_token: layout.csrf_token.clone(), |
| 215 | 198 | app_name: layout.app_name.clone(), |
| 216 | 199 | app_version: layout.app_version.clone(), |
| 217 | - app_domain: "localhost".to_string(), | |
| 218 | - app_description: "Secure password manager".to_string(), | |
| 200 | + app_domain, | |
| 201 | + app_description, | |
| 219 | 202 | username: user.username.clone(), |
| 220 | 203 | email: user.email.clone(), |
| 221 | 204 | initials: user.initials.clone(), |
@@ -223,9 +206,24 @@ pub async fn settings(
| 223 | 206 | roles_text: user.roles.join(", "), |
| 224 | 207 | nav_items, |
| 225 | 208 | has_pending_password_change: false, |
| 226 | - user_menu_profile: "Profil".to_string(), | |
| 227 | - user_menu_logout: "Déconnexion".to_string(), | |
| 228 | - user_menu_settings: "Paramètres".to_string(), | |
| 209 | + user_menu_profile, | |
| 210 | + user_menu_logout, | |
| 211 | + user_menu_settings, | |
| 212 | + settings_title, | |
| 213 | + settings_subtitle, | |
| 214 | + profile_title, | |
| 215 | + profile_desc, | |
| 216 | + password_title, | |
| 217 | + password_desc, | |
| 218 | + sessions_title, | |
| 219 | + sessions_desc, | |
| 220 | + preferences_title, | |
| 221 | + preferences_desc, | |
| 222 | + access_button, | |
| 223 | + info_title, | |
| 224 | + info_desc, | |
| 225 | + breadcrumb_home, | |
| 226 | + breadcrumb_settings, | |
| 229 | 227 | }; |
| 230 | 228 | |
| 231 | 229 | Ok(template) |
@@ -644,7 +642,7 @@ pub async fn update_preferences(
| 644 | 642 | } |
| 645 | 643 | |
| 646 | 644 | // ============================================================================ |
| 647 | -// GET Handlers for HTML Pages (simplified - no i18n) | |
| 645 | +// GET Handlers for HTML Pages (with i18n) | |
| 648 | 646 | // ============================================================================ |
| 649 | 647 | |
| 650 | 648 | /// Custom filters for Askama templates |
@@ -949,6 +947,8 @@ crate::impl_template_response!(PreferencesTemplate);
| 949 | 947 | /// Handler for GET /profile - Display profile page (PROTECTED) |
| 950 | 948 | pub async fn get_profile_page( |
| 951 | 949 | Extension(claims): Extension<Claims>, |
| 950 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 951 | + Extension(locale): Extension<Locale>, | |
| 952 | 952 | State(db): State<DatabaseConnection>, |
| 953 | 953 | session: Session, |
| 954 | 954 | ) -> Result<impl IntoResponse, AppError> { |
@@ -957,22 +957,34 @@ pub async fn get_profile_page(
| 957 | 957 | .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; |
| 958 | 958 | |
| 959 | 959 | let roles = UserService::get_user_roles(&db, user_model.id).await?; |
| 960 | - let user = SimpleUserProfile::from_model(&user_model, roles.clone()); | |
| 960 | + let user = UserProfileDTO::new(user_model, roles.clone()); | |
| 961 | + | |
| 962 | + let locale_str = locale.as_str(); | |
| 961 | 963 | |
| 962 | 964 | let layout = crate::templates::LayoutContext::new("Profil", ""); |
| 963 | 965 | |
| 964 | 966 | use crate::templates::helpers::csrf_token_helper; |
| 965 | 967 | let csrf_token = csrf_token_helper(&session).await; |
| 966 | 968 | |
| 967 | - let nav_items = build_nav_items(&roles, "settings"); | |
| 969 | + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await; | |
| 970 | + let nav_items = build_nav_items_from_dto(&navigation, "settings"); | |
| 971 | + | |
| 972 | + let app_domain = AppSettingsService::get_setting(&db, "app_domain") | |
| 973 | + .await? | |
| 974 | + .map(|s| s.value) | |
| 975 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 976 | + let app_description = AppSettingsService::get_setting(&db, "app_description") | |
| 977 | + .await? | |
| 978 | + .map(|s| s.value) | |
| 979 | + .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 968 | 980 | |
| 969 | 981 | let template = ProfileTemplate { |
| 970 | 982 | page_title: layout.page_title.clone(), |
| 971 | 983 | csrf_token, |
| 972 | 984 | app_name: layout.app_name.clone(), |
| 973 | 985 | app_version: layout.app_version.clone(), |
| 974 | - app_domain: "localhost".to_string(), | |
| 975 | - app_description: "Secure password manager".to_string(), | |
| 986 | + app_domain, | |
| 987 | + app_description, | |
| 976 | 988 | username: user.username.clone(), |
| 977 | 989 | email: user.email.clone(), |
| 978 | 990 | user_id: user.id.to_string(), |
@@ -983,30 +995,30 @@ pub async fn get_profile_page(
| 983 | 995 | avatar_color: user.avatar_color.clone(), |
| 984 | 996 | nav_items, |
| 985 | 997 | has_pending_password_change: false, |
| 986 | - user_menu_profile: "Profil".to_string(), | |
| 987 | - user_menu_logout: "Déconnexion".to_string(), | |
| 988 | - user_menu_settings: "Paramètres".to_string(), | |
| 989 | - title: "Profil".to_string(), | |
| 990 | - subtitle: "Gérez vos informations personnelles".to_string(), | |
| 991 | - account_info: "Informations du compte".to_string(), | |
| 992 | - username_label: "Nom d'utilisateur".to_string(), | |
| 993 | - username_public: "Visible publiquement".to_string(), | |
| 994 | - username_placeholder: "Entrez votre nom d'utilisateur".to_string(), | |
| 995 | - username_pattern: "Lettres, chiffres, tirets et underscores".to_string(), | |
| 996 | - username_help: "3 à 30 caractères".to_string(), | |
| 997 | - email_label: "Email".to_string(), | |
| 998 | - email_used_for: "Utilisé pour les notifications".to_string(), | |
| 999 | - email_warning: "La modification de l'email nécessite une vérification".to_string(), | |
| 1000 | - email_placeholder: "votre@email.com".to_string(), | |
| 1001 | - email_change_info: "Un email de vérification sera envoyé à la nouvelle adresse".to_string(), | |
| 1002 | - save_button: "Enregistrer".to_string(), | |
| 1003 | - cancel: "Annuler".to_string(), | |
| 1004 | - additional_info: "Informations complémentaires".to_string(), | |
| 1005 | - account_created: "Compte créé le".to_string(), | |
| 1006 | - roles_label: "Rôles".to_string(), | |
| 1007 | - breadcrumb_home: "Accueil".to_string(), | |
| 1008 | - breadcrumb_settings: "Paramètres".to_string(), | |
| 1009 | - breadcrumb_profile: "Profil".to_string(), | |
| 998 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 999 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 1000 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 1001 | + title: i18n_service.translate("profile.title", locale_str).await, | |
| 1002 | + subtitle: i18n_service.translate("profile.subtitle", locale_str).await, | |
| 1003 | + account_info: i18n_service.translate("profile.account_info", locale_str).await, | |
| 1004 | + username_label: i18n_service.translate("profile.username", locale_str).await, | |
| 1005 | + username_public: i18n_service.translate("profile.username_public", locale_str).await, | |
| 1006 | + username_placeholder: i18n_service.translate("profile.username_placeholder", locale_str).await, | |
| 1007 | + username_pattern: i18n_service.translate("profile.username_pattern", locale_str).await, | |
| 1008 | + username_help: i18n_service.translate("profile.username_help", locale_str).await, | |
| 1009 | + email_label: i18n_service.translate("profile.email_label", locale_str).await, | |
| 1010 | + email_used_for: i18n_service.translate("profile.email_used_for", locale_str).await, | |
| 1011 | + email_warning: i18n_service.translate("profile.email_warning", locale_str).await, | |
| 1012 | + email_placeholder: i18n_service.translate("profile.email_placeholder", locale_str).await, | |
| 1013 | + email_change_info: i18n_service.translate("profile.email_change_info", locale_str).await, | |
| 1014 | + save_button: i18n_service.translate("profile.save", locale_str).await, | |
| 1015 | + cancel: i18n_service.translate("common.cancel", locale_str).await, | |
| 1016 | + additional_info: i18n_service.translate("profile.additional_info", locale_str).await, | |
| 1017 | + account_created: i18n_service.translate("profile.account_created", locale_str).await, | |
| 1018 | + roles_label: i18n_service.translate("profile.roles", locale_str).await, | |
| 1019 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 1020 | + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await, | |
| 1021 | + breadcrumb_profile: i18n_service.translate("breadcrumb.profile", locale_str).await, | |
| 1010 | 1022 | }; |
| 1011 | 1023 | |
| 1012 | 1024 | Ok(template) |
@@ -1015,6 +1027,8 @@ pub async fn get_profile_page(
| 1015 | 1027 | /// Handler for GET /settings/password - Display password change page (PROTECTED) |
| 1016 | 1028 | pub async fn get_password_page( |
| 1017 | 1029 | Extension(claims): Extension<Claims>, |
| 1030 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 1031 | + Extension(locale): Extension<Locale>, | |
| 1018 | 1032 | State(db): State<DatabaseConnection>, |
| 1019 | 1033 | session: Session, |
| 1020 | 1034 | Query(query): Query<PasswordPageQuery>, |
@@ -1024,11 +1038,13 @@ pub async fn get_password_page(
| 1024 | 1038 | .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; |
| 1025 | 1039 | |
| 1026 | 1040 | let roles = UserService::get_user_roles(&db, user_model.id).await?; |
| 1027 | - let user = SimpleUserProfile::from_model(&user_model, roles.clone()); | |
| 1041 | + let user = UserProfileDTO::new(user_model.clone(), roles.clone()); | |
| 1042 | + | |
| 1043 | + let locale_str = locale.as_str(); | |
| 1028 | 1044 | |
| 1029 | 1045 | let must_change = query.must_change.as_deref() == Some("true"); |
| 1030 | 1046 | let must_change_message = if must_change { |
| 1031 | - "Vous devez changer votre mot de passe avant de continuer.".to_string() | |
| 1047 | + i18n_service.translate("password.must_change_message", locale_str).await | |
| 1032 | 1048 | } else { |
| 1033 | 1049 | String::new() |
| 1034 | 1050 | }; |
@@ -1041,76 +1057,102 @@ pub async fn get_password_page(
| 1041 | 1057 | let has_pending_request = pending_request.is_some(); |
| 1042 | 1058 | let has_pending_password_change = has_pending_request; |
| 1043 | 1059 | |
| 1060 | + let pending_request_message = if has_pending_request { | |
| 1061 | + i18n_service.translate("password.pending_request_message", locale_str).await | |
| 1062 | + } else { | |
| 1063 | + String::new() | |
| 1064 | + }; | |
| 1065 | + let pending_request_info = if has_pending_request { | |
| 1066 | + i18n_service.translate("password.pending_request_info", locale_str).await | |
| 1067 | + } else { | |
| 1068 | + String::new() | |
| 1069 | + }; | |
| 1070 | + let pending_request_cancel = i18n_service.translate("password.pending_request_cancel", locale_str).await; | |
| 1071 | + | |
| 1044 | 1072 | let layout = crate::templates::LayoutContext::new("Mot de passe", ""); |
| 1045 | 1073 | use crate::templates::helpers::csrf_token_helper; |
| 1046 | 1074 | let csrf_token = csrf_token_helper(&session).await; |
| 1047 | - let nav_items = build_nav_items(&roles, "settings"); | |
| 1075 | + | |
| 1076 | + let mut navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await; | |
| 1077 | + navigation.set_pending_password_change(has_pending_password_change); | |
| 1078 | + let nav_items = build_nav_items_from_dto(&navigation, "settings"); | |
| 1079 | + | |
| 1080 | + let app_domain = AppSettingsService::get_setting(&db, "app_domain") | |
| 1081 | + .await? | |
| 1082 | + .map(|s| s.value) | |
| 1083 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 1084 | + let app_description = AppSettingsService::get_setting(&db, "app_description") | |
| 1085 | + .await? | |
| 1086 | + .map(|s| s.value) | |
| 1087 | + .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 1088 | + | |
| 1089 | + let page_title = i18n_service.translate("password.page_title", locale_str).await; | |
| 1048 | 1090 | |
| 1049 | 1091 | let template = PasswordTemplate { |
| 1050 | - page_title: "Changer le mot de passe".to_string(), | |
| 1092 | + page_title, | |
| 1051 | 1093 | csrf_token, |
| 1052 | 1094 | app_name: layout.app_name.clone(), |
| 1053 | 1095 | app_version: layout.app_version.clone(), |
| 1054 | - app_domain: "localhost".to_string(), | |
| 1055 | - app_description: "Secure password manager".to_string(), | |
| 1096 | + app_domain, | |
| 1097 | + app_description, | |
| 1056 | 1098 | username: user.username.clone(), |
| 1057 | 1099 | email: user.email.clone(), |
| 1058 | 1100 | roles_text: user.roles.join(", "), |
| 1059 | 1101 | initials: user.initials.clone(), |
| 1060 | 1102 | avatar_color: user.avatar_color.clone(), |
| 1061 | 1103 | nav_items, |
| 1062 | - user_menu_profile: "Profil".to_string(), | |
| 1063 | - user_menu_logout: "Déconnexion".to_string(), | |
| 1064 | - user_menu_settings: "Paramètres".to_string(), | |
| 1104 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 1105 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 1106 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 1065 | 1107 | must_change, |
| 1066 | 1108 | must_change_message, |
| 1067 | 1109 | has_pending_request, |
| 1068 | - pending_request_message: if has_pending_request { "Une demande de changement de mot de passe est en attente de confirmation.".to_string() } else { String::new() }, | |
| 1069 | - pending_request_info: if has_pending_request { "Vérifiez votre boîte de réception pour le lien de confirmation.".to_string() } else { String::new() }, | |
| 1070 | - pending_request_cancel: "Annuler la demande".to_string(), | |
| 1071 | - title: "Changer le mot de passe".to_string(), | |
| 1072 | - subtitle: "Modifiez votre mot de passe de connexion".to_string(), | |
| 1073 | - new_password_label: "Nouveau mot de passe".to_string(), | |
| 1074 | - current_password_label: "Mot de passe actuel".to_string(), | |
| 1075 | - current_password_placeholder: "Entrez votre mot de passe actuel".to_string(), | |
| 1076 | - new_password_placeholder: "Entrez le nouveau mot de passe".to_string(), | |
| 1077 | - required: "Requis".to_string(), | |
| 1078 | - requirements: "Au moins 8 caractères, une majuscule, une minuscule et un chiffre".to_string(), | |
| 1079 | - strength_label: "Force du mot de passe".to_string(), | |
| 1080 | - strength_weak: "Faible".to_string(), | |
| 1081 | - strength_medium: "Moyen".to_string(), | |
| 1082 | - strength_strong: "Fort".to_string(), | |
| 1083 | - strength_too_short: "Trop court".to_string(), | |
| 1084 | - strength_missing_lowercase: "Ajoutez une minuscule".to_string(), | |
| 1085 | - strength_missing_uppercase: "Ajoutez une majuscule".to_string(), | |
| 1086 | - strength_missing_digit: "Ajoutez un chiffre".to_string(), | |
| 1087 | - strength_missing_special: "Ajoutez un caractère spécial".to_string(), | |
| 1088 | - confirm_password_label: "Confirmer le mot de passe".to_string(), | |
| 1089 | - confirm_password_placeholder: "Confirmez le nouveau mot de passe".to_string(), | |
| 1090 | - passwords_match: "Les mots de passe correspondent".to_string(), | |
| 1091 | - passwords_not_match: "Les mots de passe ne correspondent pas".to_string(), | |
| 1092 | - change_button: "Changer le mot de passe".to_string(), | |
| 1093 | - changing: "Changement en cours...".to_string(), | |
| 1094 | - logout_warning_title: "Attention".to_string(), | |
| 1095 | - logout_warning_desc: "Changer votre mot de passe invalidera toutes vos sessions actives.".to_string(), | |
| 1096 | - saved: "Mot de passe modifié avec succès".to_string(), | |
| 1097 | - error_all_fields_required: "Tous les champs sont requis".to_string(), | |
| 1098 | - error_empty_data: "Les données sont vides".to_string(), | |
| 1099 | - error_invalid_data: "Données invalides".to_string(), | |
| 1100 | - error_incorrect_current: "Le mot de passe actuel est incorrect".to_string(), | |
| 1101 | - error_reading_response: "Erreur de lecture de la réponse".to_string(), | |
| 1102 | - error_415: "Type de contenu non supporté".to_string(), | |
| 1103 | - network_error: "Erreur réseau".to_string(), | |
| 1104 | - security_tips_title: "Conseils de sécurité".to_string(), | |
| 1105 | - security_tip_unique: "Utilisez un mot de passe unique pour chaque service".to_string(), | |
| 1106 | - security_tip_share: "Ne partagez jamais votre mot de passe".to_string(), | |
| 1107 | - security_tip_regular: "Changez régulièrement votre mot de passe".to_string(), | |
| 1108 | - security_tip_2fa: "Activez l'authentification à deux facteurs si disponible".to_string(), | |
| 1109 | - security_tip_manager: "Utilisez un gestionnaire de mots de passe".to_string(), | |
| 1110 | - breadcrumb_home: "Accueil".to_string(), | |
| 1111 | - breadcrumb_settings: "Paramètres".to_string(), | |
| 1112 | - breadcrumb_password: "Mot de passe".to_string(), | |
| 1113 | - cancel: "Annuler".to_string(), | |
| 1110 | + pending_request_message, | |
| 1111 | + pending_request_info, | |
| 1112 | + pending_request_cancel, | |
| 1113 | + title: i18n_service.translate("password.title", locale_str).await, | |
| 1114 | + subtitle: i18n_service.translate("password.subtitle", locale_str).await, | |
| 1115 | + new_password_label: i18n_service.translate("password.new_password", locale_str).await, | |
| 1116 | + current_password_label: i18n_service.translate("password.current_password", locale_str).await, | |
| 1117 | + current_password_placeholder: i18n_service.translate("password.current_password_placeholder", locale_str).await, | |
| 1118 | + new_password_placeholder: i18n_service.translate("password.new_password_placeholder", locale_str).await, | |
| 1119 | + required: i18n_service.translate("password.required", locale_str).await, | |
| 1120 | + requirements: i18n_service.translate("password.requirements", locale_str).await, | |
| 1121 | + strength_label: i18n_service.translate("password.strength", locale_str).await, | |
| 1122 | + strength_weak: i18n_service.translate("password.strength_weak", locale_str).await, | |
| 1123 | + strength_medium: i18n_service.translate("password.strength_medium", locale_str).await, | |
| 1124 | + strength_strong: i18n_service.translate("password.strength_strong", locale_str).await, | |
| 1125 | + strength_too_short: i18n_service.translate("password.strength_too_short", locale_str).await, | |
| 1126 | + strength_missing_lowercase: i18n_service.translate("password.strength_missing_lowercase", locale_str).await, | |
| 1127 | + strength_missing_uppercase: i18n_service.translate("password.strength_missing_uppercase", locale_str).await, | |
| 1128 | + strength_missing_digit: i18n_service.translate("password.strength_missing_digit", locale_str).await, | |
| 1129 | + strength_missing_special: i18n_service.translate("password.strength_missing_special", locale_str).await, | |
| 1130 | + confirm_password_label: i18n_service.translate("password.confirm_password", locale_str).await, | |
| 1131 | + confirm_password_placeholder: i18n_service.translate("password.confirm_password_placeholder", locale_str).await, | |
| 1132 | + passwords_match: i18n_service.translate("password.passwords_match", locale_str).await, | |
| 1133 | + passwords_not_match: i18n_service.translate("password.passwords_not_match", locale_str).await, | |
| 1134 | + change_button: i18n_service.translate("password.change_button", locale_str).await, | |
| 1135 | + changing: i18n_service.translate("password.changing", locale_str).await, | |
| 1136 | + logout_warning_title: i18n_service.translate("password.logout_warning_title", locale_str).await, | |
| 1137 | + logout_warning_desc: i18n_service.translate("password.logout_warning_desc", locale_str).await, | |
| 1138 | + saved: i18n_service.translate("password.saved", locale_str).await, | |
| 1139 | + error_all_fields_required: i18n_service.translate("password.error_all_fields_required", locale_str).await, | |
| 1140 | + error_empty_data: i18n_service.translate("password.error_empty_data", locale_str).await, | |
| 1141 | + error_invalid_data: i18n_service.translate("password.error_invalid_data", locale_str).await, | |
| 1142 | + error_incorrect_current: i18n_service.translate("password.error_incorrect_current", locale_str).await, | |
| 1143 | + error_reading_response: i18n_service.translate("password.error_reading_response", locale_str).await, | |
| 1144 | + error_415: i18n_service.translate("password.error_415", locale_str).await, | |
| 1145 | + network_error: i18n_service.translate("password.network_error", locale_str).await, | |
| 1146 | + security_tips_title: i18n_service.translate("password.security_tips_title", locale_str).await, | |
| 1147 | + security_tip_unique: i18n_service.translate("password.security_tip_unique", locale_str).await, | |
| 1148 | + security_tip_share: i18n_service.translate("password.security_tip_share", locale_str).await, | |
| 1149 | + security_tip_regular: i18n_service.translate("password.security_tip_regular", locale_str).await, | |
| 1150 | + security_tip_2fa: i18n_service.translate("password.security_tip_2fa", locale_str).await, | |
| 1151 | + security_tip_manager: i18n_service.translate("password.security_tip_manager", locale_str).await, | |
| 1152 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 1153 | + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await, | |
| 1154 | + breadcrumb_password: i18n_service.translate("breadcrumb.password", locale_str).await, | |
| 1155 | + cancel: i18n_service.translate("common.cancel", locale_str).await, | |
| 1114 | 1156 | has_pending_password_change, |
| 1115 | 1157 | }; |
| 1116 | 1158 |
@@ -1120,6 +1162,8 @@ pub async fn get_password_page(
| 1120 | 1162 | /// Handler for GET /settings/sessions - Display sessions page (PROTECTED) |
| 1121 | 1163 | pub async fn get_sessions_page( |
| 1122 | 1164 | Extension(claims): Extension<Claims>, |
| 1165 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 1166 | + Extension(locale): Extension<Locale>, | |
| 1123 | 1167 | State(db): State<DatabaseConnection>, |
| 1124 | 1168 | session: Session, |
| 1125 | 1169 | ) -> Result<impl IntoResponse, AppError> { |
@@ -1128,7 +1172,9 @@ pub async fn get_sessions_page(
| 1128 | 1172 | .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; |
| 1129 | 1173 | |
| 1130 | 1174 | let roles = UserService::get_user_roles(&db, user_model.id).await?; |
| 1131 | - let user = SimpleUserProfile::from_model(&user_model, roles.clone()); | |
| 1175 | + let user = UserProfileDTO::new(user_model, roles.clone()); | |
| 1176 | + | |
| 1177 | + let locale_str = locale.as_str(); | |
| 1132 | 1178 | |
| 1133 | 1179 | let current_token_id: Option<uuid::Uuid> = session.get("refresh_token_id").await.ok().flatten(); |
| 1134 | 1180 |
@@ -1158,15 +1204,28 @@ pub async fn get_sessions_page(
| 1158 | 1204 | let layout = crate::templates::LayoutContext::new("Sessions", ""); |
| 1159 | 1205 | use crate::templates::helpers::csrf_token_helper; |
| 1160 | 1206 | let csrf_token = csrf_token_helper(&session).await; |
| 1161 | - let nav_items = build_nav_items(&roles, "settings"); | |
| 1207 | + | |
| 1208 | + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await; | |
| 1209 | + let nav_items = build_nav_items_from_dto(&navigation, "settings"); | |
| 1210 | + | |
| 1211 | + let app_domain = AppSettingsService::get_setting(&db, "app_domain") | |
| 1212 | + .await? | |
| 1213 | + .map(|s| s.value) | |
| 1214 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 1215 | + let app_description = AppSettingsService::get_setting(&db, "app_description") | |
| 1216 | + .await? | |
| 1217 | + .map(|s| s.value) | |
| 1218 | + .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 1219 | + | |
| 1220 | + let page_title = i18n_service.translate("sessions.page_title", locale_str).await; | |
| 1162 | 1221 | |
| 1163 | 1222 | let template = SessionsTemplate { |
| 1164 | - page_title: "Sessions actives".to_string(), | |
| 1223 | + page_title, | |
| 1165 | 1224 | csrf_token, |
| 1166 | 1225 | app_name: layout.app_name.clone(), |
| 1167 | 1226 | app_version: layout.app_version.clone(), |
| 1168 | - app_domain: "localhost".to_string(), | |
| 1169 | - app_description: "Secure password manager".to_string(), | |
| 1227 | + app_domain, | |
| 1228 | + app_description, | |
| 1170 | 1229 | username: user.username.clone(), |
| 1171 | 1230 | email: user.email.clone(), |
| 1172 | 1231 | roles_text: user.roles.join(", "), |
@@ -1175,36 +1234,36 @@ pub async fn get_sessions_page(
| 1175 | 1234 | sessions, |
| 1176 | 1235 | nav_items, |
| 1177 | 1236 | has_pending_password_change: false, |
| 1178 | - user_menu_profile: "Profil".to_string(), | |
| 1179 | - user_menu_logout: "Déconnexion".to_string(), | |
| 1180 | - user_menu_settings: "Paramètres".to_string(), | |
| 1181 | - title: "Sessions actives".to_string(), | |
| 1182 | - subtitle: "Gérez vos sessions de connexion".to_string(), | |
| 1183 | - current_session: "Session actuelle".to_string(), | |
| 1184 | - current_badge: "Actuelle".to_string(), | |
| 1185 | - unknown_device: "Appareil inconnu".to_string(), | |
| 1186 | - ip_address: "Adresse IP".to_string(), | |
| 1187 | - created_at: "Créée le".to_string(), | |
| 1188 | - last_activity: "Dernière activité".to_string(), | |
| 1189 | - session_id: "ID de session".to_string(), | |
| 1190 | - revoke: "Révoquer".to_string(), | |
| 1191 | - revoke_confirm_template: "Êtes-vous sûr de vouloir révoquer cette session ?".to_string(), | |
| 1192 | - revoke_success: "Session révoquée avec succès".to_string(), | |
| 1193 | - revoke_error: "Erreur lors de la révocation".to_string(), | |
| 1194 | - no_sessions: "Aucune session active".to_string(), | |
| 1195 | - no_sessions_desc: "Vous n'avez pas de sessions actives".to_string(), | |
| 1196 | - danger_zone: "Zone de danger".to_string(), | |
| 1197 | - danger_zone_desc: "Révoquer toutes les sessions vous déconnectera de tous vos appareils".to_string(), | |
| 1198 | - revoke_all: "Révoquer toutes les sessions".to_string(), | |
| 1199 | - revoke_all_confirm: "Êtes-vous sûr de vouloir révoquer toutes les sessions ?".to_string(), | |
| 1200 | - revoke_all_success: "Toutes les sessions ont été révoquées".to_string(), | |
| 1201 | - revoke_all_error: "Erreur lors de la révocation des sessions".to_string(), | |
| 1202 | - about_title: "À propos des sessions".to_string(), | |
| 1203 | - about_desc: "Les sessions représentent vos connexions actives sur différents appareils".to_string(), | |
| 1204 | - back_to_settings: "Retour aux paramètres".to_string(), | |
| 1205 | - breadcrumb_home: "Accueil".to_string(), | |
| 1206 | - breadcrumb_settings: "Paramètres".to_string(), | |
| 1207 | - breadcrumb_sessions: "Sessions".to_string(), | |
| 1237 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 1238 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 1239 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 1240 | + title: i18n_service.translate("sessions.title", locale_str).await, | |
| 1241 | + subtitle: i18n_service.translate("sessions.subtitle", locale_str).await, | |
| 1242 | + current_session: i18n_service.translate("sessions.current_session", locale_str).await, | |
| 1243 | + current_badge: i18n_service.translate("sessions.current_badge", locale_str).await, | |
| 1244 | + unknown_device: i18n_service.translate("sessions.unknown_device", locale_str).await, | |
| 1245 | + ip_address: i18n_service.translate("sessions.ip_address", locale_str).await, | |
| 1246 | + created_at: i18n_service.translate("sessions.created_at", locale_str).await, | |
| 1247 | + last_activity: i18n_service.translate("sessions.last_activity", locale_str).await, | |
| 1248 | + session_id: i18n_service.translate("sessions.session_id", locale_str).await, | |
| 1249 | + revoke: i18n_service.translate("sessions.revoke", locale_str).await, | |
| 1250 | + revoke_confirm_template: i18n_service.translate("sessions.revoke_confirm", locale_str).await, | |
| 1251 | + revoke_success: i18n_service.translate("sessions.revoke_success", locale_str).await, | |
| 1252 | + revoke_error: i18n_service.translate("sessions.revoke_error", locale_str).await, | |
| 1253 | + no_sessions: i18n_service.translate("sessions.no_sessions", locale_str).await, | |
| 1254 | + no_sessions_desc: i18n_service.translate("sessions.no_sessions_desc", locale_str).await, | |
| 1255 | + danger_zone: i18n_service.translate("sessions.danger_zone", locale_str).await, | |
| 1256 | + danger_zone_desc: i18n_service.translate("sessions.danger_zone_desc", locale_str).await, | |
| 1257 | + revoke_all: i18n_service.translate("sessions.revoke_all", locale_str).await, | |
| 1258 | + revoke_all_confirm: i18n_service.translate("sessions.revoke_all_confirm", locale_str).await, | |
| 1259 | + revoke_all_success: i18n_service.translate("sessions.revoke_all_success", locale_str).await, | |
| 1260 | + revoke_all_error: i18n_service.translate("sessions.revoke_all_error", locale_str).await, | |
| 1261 | + about_title: i18n_service.translate("sessions.about_title", locale_str).await, | |
| 1262 | + about_desc: i18n_service.translate("sessions.about_desc", locale_str).await, | |
| 1263 | + back_to_settings: i18n_service.translate("sessions.back_to_settings", locale_str).await, | |
| 1264 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 1265 | + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await, | |
| 1266 | + breadcrumb_sessions: i18n_service.translate("breadcrumb.sessions", locale_str).await, | |
| 1208 | 1267 | }; |
| 1209 | 1268 | |
| 1210 | 1269 | Ok(template) |
@@ -1213,6 +1272,8 @@ pub async fn get_sessions_page(
| 1213 | 1272 | /// Handler for GET /settings/preferences - Display preferences page (PROTECTED) |
| 1214 | 1273 | pub async fn get_preferences_page( |
| 1215 | 1274 | Extension(claims): Extension<Claims>, |
| 1275 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 1276 | + Extension(locale): Extension<Locale>, | |
| 1216 | 1277 | State(db): State<DatabaseConnection>, |
| 1217 | 1278 | session: Session, |
| 1218 | 1279 | ) -> Result<impl IntoResponse, AppError> { |
@@ -1221,7 +1282,9 @@ pub async fn get_preferences_page(
| 1221 | 1282 | .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; |
| 1222 | 1283 | |
| 1223 | 1284 | let roles = UserService::get_user_roles(&db, user_model.id).await?; |
| 1224 | - let user = SimpleUserProfile::from_model(&user_model, roles.clone()); | |
| 1285 | + let user = UserProfileDTO::new(user_model, roles.clone()); | |
| 1286 | + | |
| 1287 | + let locale_str = locale.as_str(); | |
| 1225 | 1288 | |
| 1226 | 1289 | use crate::services::user_preferences_service::UserPreferencesService; |
| 1227 | 1290 | let preferences = match UserPreferencesService::get_preferences(&db, claims.sub).await? { |
@@ -1229,7 +1292,7 @@ pub async fn get_preferences_page(
| 1229 | 1292 | None => UserPreferencesService::create_default_preferences(&db, claims.sub).await?, |
| 1230 | 1293 | }; |
| 1231 | 1294 | |
| 1232 | - let current_locale = preferences.language.clone(); | |
| 1295 | + let current_locale = locale_str.to_string(); | |
| 1233 | 1296 | let supported_languages: Vec<(String, String, bool)> = vec![ |
| 1234 | 1297 | ("fr".to_string(), "Français".to_string(), current_locale == "fr"), |
| 1235 | 1298 | ("en".to_string(), "English".to_string(), current_locale == "en"), |
@@ -1240,13 +1303,24 @@ pub async fn get_preferences_page(
| 1240 | 1303 | let layout = crate::templates::LayoutContext::new("Préférences", ""); |
| 1241 | 1304 | use crate::templates::helpers::csrf_token_helper; |
| 1242 | 1305 | let csrf_token = csrf_token_helper(&session).await; |
| 1243 | - let nav_items = build_nav_items(&roles, "settings"); | |
| 1244 | 1306 | |
| 1245 | - let save_button = "Enregistrer".to_string(); | |
| 1246 | - let saved_success = "Préférences enregistrées avec succès".to_string(); | |
| 1247 | - let saved_reload = "Préférences enregistrées. Rechargement en cours...".to_string(); | |
| 1248 | - let error_prefix = "Erreur : ".to_string(); | |
| 1249 | - let network_error = "Erreur réseau".to_string(); | |
| 1307 | + let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await; | |
| 1308 | + let nav_items = build_nav_items_from_dto(&navigation, "settings"); | |
| 1309 | + | |
| 1310 | + let app_domain = AppSettingsService::get_setting(&db, "app_domain") | |
| 1311 | + .await? | |
| 1312 | + .map(|s| s.value) | |
| 1313 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 1314 | + let app_description = AppSettingsService::get_setting(&db, "app_description") | |
| 1315 | + .await? | |
| 1316 | + .map(|s| s.value) | |
| 1317 | + .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 1318 | + | |
| 1319 | + let save_button = i18n_service.translate("preferences.save", locale_str).await; | |
| 1320 | + let saved_success = i18n_service.translate("preferences.saved_success", locale_str).await; | |
| 1321 | + let saved_reload = i18n_service.translate("preferences.saved_reload", locale_str).await; | |
| 1322 | + let error_prefix = i18n_service.translate("preferences.error", locale_str).await; | |
| 1323 | + let network_error = i18n_service.translate("preferences.network_error", locale_str).await; | |
| 1250 | 1324 | |
| 1251 | 1325 | let saved_reload_json = serde_json::to_string(&saved_reload).unwrap_or_else(|_| "\"\"".to_string()); |
| 1252 | 1326 | let saved_success_json = serde_json::to_string(&saved_success).unwrap_or_else(|_| "\"\"".to_string()); |
@@ -1259,8 +1333,8 @@ pub async fn get_preferences_page(
| 1259 | 1333 | csrf_token, |
| 1260 | 1334 | app_name: layout.app_name.clone(), |
| 1261 | 1335 | app_version: layout.app_version.clone(), |
| 1262 | - app_domain: "localhost".to_string(), | |
| 1263 | - app_description: "Secure password manager".to_string(), | |
| 1336 | + app_domain, | |
| 1337 | + app_description, | |
| 1264 | 1338 | username: user.username.clone(), |
| 1265 | 1339 | email: user.email.clone(), |
| 1266 | 1340 | roles_text: user.roles.join(", "), |
@@ -1279,34 +1353,34 @@ pub async fn get_preferences_page(
| 1279 | 1353 | supported_languages, |
| 1280 | 1354 | nav_items, |
| 1281 | 1355 | has_pending_password_change: false, |
| 1282 | - user_menu_profile: "Profil".to_string(), | |
| 1283 | - user_menu_logout: "Déconnexion".to_string(), | |
| 1284 | - user_menu_settings: "Paramètres".to_string(), | |
| 1285 | - title: "Préférences".to_string(), | |
| 1286 | - subtitle: "Personnalisez votre expérience".to_string(), | |
| 1287 | - theme_label: "Thème".to_string(), | |
| 1288 | - theme_light: "Clair".to_string(), | |
| 1289 | - theme_light_desc: "Interface lumineuse".to_string(), | |
| 1290 | - theme_dark: "Sombre".to_string(), | |
| 1291 | - theme_dark_desc: "Interface sombre".to_string(), | |
| 1292 | - theme_auto: "Automatique".to_string(), | |
| 1293 | - theme_auto_desc: "Suit les préférences système".to_string(), | |
| 1294 | - theme_preview: "Aperçu du thème".to_string(), | |
| 1295 | - language_label: "Langue".to_string(), | |
| 1296 | - language_fr: "Français".to_string(), | |
| 1297 | - language_fr_desc: "Interface en français".to_string(), | |
| 1298 | - language_en: "English".to_string(), | |
| 1299 | - language_en_desc: "Interface in English".to_string(), | |
| 1300 | - language_de: "Deutsch".to_string(), | |
| 1301 | - language_de_desc: "Benutzeroberfläche auf Deutsch".to_string(), | |
| 1302 | - language_es: "Español".to_string(), | |
| 1303 | - language_es_desc: "Interfaz en español".to_string(), | |
| 1304 | - translation_warning: "Certaines traductions peuvent être incomplètes".to_string(), | |
| 1305 | - modified: "Modifié".to_string(), | |
| 1306 | - modified_desc: "Vous avez des modifications non enregistrées".to_string(), | |
| 1356 | + user_menu_profile: i18n_service.translate("user_menu.profile", locale_str).await, | |
| 1357 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 1358 | + user_menu_settings: i18n_service.translate("user_menu.settings", locale_str).await, | |
| 1359 | + title: i18n_service.translate("preferences.title", locale_str).await, | |
| 1360 | + subtitle: i18n_service.translate("preferences.subtitle", locale_str).await, | |
| 1361 | + theme_label: i18n_service.translate("preferences.theme", locale_str).await, | |
| 1362 | + theme_light: i18n_service.translate("preferences.theme_light", locale_str).await, | |
| 1363 | + theme_light_desc: i18n_service.translate("preferences.theme_light_desc", locale_str).await, | |
| 1364 | + theme_dark: i18n_service.translate("preferences.theme_dark", locale_str).await, | |
| 1365 | + theme_dark_desc: i18n_service.translate("preferences.theme_dark_desc", locale_str).await, | |
| 1366 | + theme_auto: i18n_service.translate("preferences.theme_auto", locale_str).await, | |
| 1367 | + theme_auto_desc: i18n_service.translate("preferences.theme_auto_desc", locale_str).await, | |
| 1368 | + theme_preview: i18n_service.translate("preferences.theme_preview", locale_str).await, | |
| 1369 | + language_label: i18n_service.translate("preferences.language", locale_str).await, | |
| 1370 | + language_fr: i18n_service.translate("preferences.language_fr", locale_str).await, | |
| 1371 | + language_fr_desc: i18n_service.translate("preferences.language_fr_desc", locale_str).await, | |
| 1372 | + language_en: i18n_service.translate("preferences.language_en", locale_str).await, | |
| 1373 | + language_en_desc: i18n_service.translate("preferences.language_en_desc", locale_str).await, | |
| 1374 | + language_de: i18n_service.translate("preferences.language_de", locale_str).await, | |
| 1375 | + language_de_desc: i18n_service.translate("preferences.language_de_desc", locale_str).await, | |
| 1376 | + language_es: i18n_service.translate("preferences.language_es", locale_str).await, | |
| 1377 | + language_es_desc: i18n_service.translate("preferences.language_es_desc", locale_str).await, | |
| 1378 | + translation_warning: i18n_service.translate("preferences.translation_warning", locale_str).await, | |
| 1379 | + modified: i18n_service.translate("preferences.modified", locale_str).await, | |
| 1380 | + modified_desc: i18n_service.translate("preferences.modified_desc", locale_str).await, | |
| 1307 | 1381 | save_button, |
| 1308 | - saving: "Enregistrement...".to_string(), | |
| 1309 | - cancel: "Annuler".to_string(), | |
| 1382 | + saving: i18n_service.translate("preferences.saving", locale_str).await, | |
| 1383 | + cancel: i18n_service.translate("preferences.cancel", locale_str).await, | |
| 1310 | 1384 | saved_success, |
| 1311 | 1385 | saved_reload, |
| 1312 | 1386 | saved_reload_json, |
@@ -1316,14 +1390,14 @@ pub async fn get_preferences_page(
| 1316 | 1390 | network_error, |
| 1317 | 1391 | network_error_json, |
| 1318 | 1392 | save_button_json, |
| 1319 | - sync_title: "Synchronisation".to_string(), | |
| 1320 | - sync_desc: "Les préférences sont synchronisées sur tous vos appareils".to_string(), | |
| 1321 | - preview_dark: "Aperçu sombre".to_string(), | |
| 1322 | - preview_light: "Aperçu clair".to_string(), | |
| 1323 | - preview_auto: "Aperçu automatique".to_string(), | |
| 1324 | - breadcrumb_home: "Accueil".to_string(), | |
| 1325 | - breadcrumb_settings: "Paramètres".to_string(), | |
| 1326 | - breadcrumb_preferences: "Préférences".to_string(), | |
| 1393 | + sync_title: i18n_service.translate("preferences.sync_title", locale_str).await, | |
| 1394 | + sync_desc: i18n_service.translate("preferences.sync_desc", locale_str).await, | |
| 1395 | + preview_dark: i18n_service.translate("preferences.preview_dark", locale_str).await, | |
| 1396 | + preview_light: i18n_service.translate("preferences.preview_light", locale_str).await, | |
| 1397 | + preview_auto: i18n_service.translate("preferences.preview_auto", locale_str).await, | |
| 1398 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 1399 | + breadcrumb_settings: i18n_service.translate("breadcrumb.settings", locale_str).await, | |
| 1400 | + breadcrumb_preferences: i18n_service.translate("breadcrumb.preferences", locale_str).await, | |
| 1327 | 1401 | }; |
| 1328 | 1402 | |
| 1329 | 1403 | Ok(template) |
M
src/main.rs
+74
-0
@@ -1,10 +1,51 @@
| 1 | +use axum::{extract::Request, middleware::Next, response::Response}; | |
| 1 | 2 | use rustwarden::auth::session::SessionConfig; |
| 2 | 3 | use rustwarden::config::auth::AuthConfig; |
| 3 | 4 | use rustwarden::config::database; |
| 4 | 5 | use rustwarden::config::email::EmailConfig; |
| 5 | 6 | use rustwarden::config::server::ServerConfig; |
| 7 | +use rustwarden::i18n::extractor::Locale; | |
| 6 | 8 | use sea_orm::DatabaseConnection; |
| 7 | 9 | |
| 10 | +/// Middleware that extracts the user's locale from the request and injects it as an Extension. | |
| 11 | +/// Priority: cookie "locale" -> Accept-Language header -> default "fr" | |
| 12 | +async fn locale_middleware(request: Request, next: Next) -> Response { | |
| 13 | + let (mut parts, body) = request.into_parts(); | |
| 14 | + | |
| 15 | + let cookie_jar = axum_extra::extract::CookieJar::from_headers(&parts.headers); | |
| 16 | + let locale_str = cookie_jar | |
| 17 | + .get("locale") | |
| 18 | + .and_then(|c| { | |
| 19 | + let v = c.value(); | |
| 20 | + if matches!(v, "fr" | "en" | "de" | "es") { | |
| 21 | + Some(v.to_string()) | |
| 22 | + } else { | |
| 23 | + None | |
| 24 | + } | |
| 25 | + }) | |
| 26 | + .or_else(|| { | |
| 27 | + parts | |
| 28 | + .headers | |
| 29 | + .get("accept-language") | |
| 30 | + .and_then(|h| h.to_str().ok()) | |
| 31 | + .and_then(|h| { | |
| 32 | + h.split(',') | |
| 33 | + .next() | |
| 34 | + .and_then(|s| s.split(';').next()) | |
| 35 | + .map(|s| s.trim()) | |
| 36 | + .filter(|s| s.len() >= 2) | |
| 37 | + .map(|s| s[..2].to_string()) | |
| 38 | + .filter(|s| matches!(s.as_str(), "fr" | "en" | "de" | "es")) | |
| 39 | + }) | |
| 40 | + }) | |
| 41 | + .unwrap_or_else(|| "fr".to_string()); | |
| 42 | + | |
| 43 | + parts.extensions.insert(Locale(locale_str)); | |
| 44 | + | |
| 45 | + let request = Request::from_parts(parts, body); | |
| 46 | + next.run(request).await | |
| 47 | +} | |
| 48 | + | |
| 8 | 49 | #[tokio::main] |
| 9 | 50 | async fn main() { |
| 10 | 51 | dotenvy::dotenv().ok(); |
@@ -127,6 +168,37 @@ async fn main() {
| 127 | 168 | }); |
| 128 | 169 | tracing::info!("JWT blacklist cleanup task started (runs every hour)"); |
| 129 | 170 | |
| 171 | + // Initialize I18nService and load translations from YAML files | |
| 172 | + let i18n_service = { | |
| 173 | + use rustwarden::i18n::service::I18nService; | |
| 174 | + use rustwarden::i18n::loader::load_translations_from_directory; | |
| 175 | + use std::sync::Arc; | |
| 176 | + | |
| 177 | + let service = I18nService::new(); | |
| 178 | + match load_translations_from_directory("locales").await { | |
| 179 | + Ok(all_translations) => { | |
| 180 | + let mut locale_count = 0; | |
| 181 | + let mut key_count = 0; | |
| 182 | + for (locale, translations) in all_translations { | |
| 183 | + key_count += translations.len(); | |
| 184 | + locale_count += 1; | |
| 185 | + if let Err(e) = service.load_translations(locale.clone(), translations).await { | |
| 186 | + tracing::error!("Failed to load translations for locale {}: {}", locale, e); | |
| 187 | + } | |
| 188 | + } | |
| 189 | + tracing::info!( | |
| 190 | + "I18n service initialized: {} locales, {} translation keys", | |
| 191 | + locale_count, | |
| 192 | + key_count | |
| 193 | + ); | |
| 194 | + } | |
| 195 | + Err(e) => { | |
| 196 | + tracing::warn!("Failed to load translations: {}. I18n will use fallback keys.", e); | |
| 197 | + } | |
| 198 | + } | |
| 199 | + Arc::new(service) | |
| 200 | + }; | |
| 201 | + | |
| 130 | 202 | let router = rustwarden::api::routes::create_router(); |
| 131 | 203 | |
| 132 | 204 | use rustwarden::api::middleware::admin_service::AdminService; |
@@ -141,6 +213,8 @@ async fn main() {
| 141 | 213 | .layer_fn(move |service| AdminService::new(service, db_for_admin.clone())) |
| 142 | 214 | .layer_fn(move |service| AuthService::new(service, db_for_auth.clone())) |
| 143 | 215 | .layer(session_layer) |
| 216 | + .layer(axum::middleware::from_fn(locale_middleware)) | |
| 217 | + .layer(axum::Extension(i18n_service)) | |
| 144 | 218 | .into_inner(), |
| 145 | 219 | ); |
| 146 | 220 |
M
templates/admin/dashboard.html
+28
-29
@@ -7,23 +7,23 @@
| 7 | 7 | <!-- Breadcrumb --> |
| 8 | 8 | <div class="text-sm breadcrumbs mb-3"> |
| 9 | 9 | <ul> |
| 10 | - <li><a href="/">Accueil</a></li> | |
| 11 | - <li>Administration</li> | |
| 10 | + <li><a href="/">{{ breadcrumb_home }}</a></li> | |
| 11 | + <li>{{ breadcrumb_admin }}</li> | |
| 12 | 12 | </ul> |
| 13 | 13 | </div> |
| 14 | 14 | |
| 15 | - <h1 class="text-2xl font-bold mb-4">Tableau de bord Administrateur</h1> | |
| 15 | + <h1 class="text-2xl font-bold mb-4">{{ heading }}</h1> | |
| 16 | 16 | |
| 17 | 17 | <!-- Statistics Cards --> |
| 18 | 18 | <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4"> |
| 19 | 19 | <!-- Total Users Card --> |
| 20 | 20 | <div class="card bg-base-100 shadow-md"> |
| 21 | 21 | <div class="card-body p-4"> |
| 22 | - <h2 class="card-title text-primary text-base">👥 Utilisateurs</h2> | |
| 23 | - <p class="text-xs text-base-content/70 mb-1">Total utilisateurs</p> | |
| 22 | + <h2 class="card-title text-primary text-base">👥 {{ users_card_title }}</h2> | |
| 23 | + <p class="text-xs text-base-content/70 mb-1">{{ users_card_subtitle }}</p> | |
| 24 | 24 | <p class="text-2xl font-bold">{{ total_users }}</p> |
| 25 | 25 | <div class="card-actions justify-end mt-2"> |
| 26 | - <a href="/admin/users" class="btn btn-primary btn-sm">Accéder</a> | |
| 26 | + <a href="/admin/users" class="btn btn-primary btn-sm">{{ access_button }}</a> | |
| 27 | 27 | </div> |
| 28 | 28 | </div> |
| 29 | 29 | </div> |
@@ -31,8 +31,8 @@
| 31 | 31 | <!-- Active Sessions Card --> |
| 32 | 32 | <div class="card bg-base-100 shadow-md"> |
| 33 | 33 | <div class="card-body p-4"> |
| 34 | - <h2 class="card-title text-secondary text-base">🔐 Sessions actives</h2> | |
| 35 | - <p class="text-xs text-base-content/70 mb-1">Sessions en cours</p> | |
| 34 | + <h2 class="card-title text-secondary text-base">🔐 {{ sessions_card_title }}</h2> | |
| 35 | + <p class="text-xs text-base-content/70 mb-1">{{ sessions_card_subtitle }}</p> | |
| 36 | 36 | <p class="text-2xl font-bold">{{ active_sessions }}</p> |
| 37 | 37 | </div> |
| 38 | 38 | </div> |
@@ -40,8 +40,8 @@
| 40 | 40 | <!-- Admins Card --> |
| 41 | 41 | <div class="card bg-base-100 shadow-md"> |
| 42 | 42 | <div class="card-body p-4"> |
| 43 | - <h2 class="card-title text-accent text-base">👑 Administrateurs</h2> | |
| 44 | - <p class="text-xs text-base-content/70 mb-1">Comptes admin</p> | |
| 43 | + <h2 class="card-title text-accent text-base">👑 {{ admins_card_title }}</h2> | |
| 44 | + <p class="text-xs text-base-content/70 mb-1">{{ admins_card_subtitle }}</p> | |
| 45 | 45 | <p class="text-2xl font-bold">{{ total_admins }}</p> |
| 46 | 46 | </div> |
| 47 | 47 | </div> |
@@ -49,8 +49,8 @@
| 49 | 49 | <!-- Last Activity Card --> |
| 50 | 50 | <div class="card bg-base-100 shadow-md"> |
| 51 | 51 | <div class="card-body p-4"> |
| 52 | - <h2 class="card-title text-info text-base">⏰ Dernière activité</h2> | |
| 53 | - <p class="text-xs text-base-content/70 mb-1">Dernière connexion</p> | |
| 52 | + <h2 class="card-title text-info text-base">⏰ {{ last_activity_card_title }}</h2> | |
| 53 | + <p class="text-xs text-base-content/70 mb-1">{{ last_activity_card_subtitle }}</p> | |
| 54 | 54 | <p class="text-base font-semibold">{{ last_activity }}</p> |
| 55 | 55 | </div> |
| 56 | 56 | </div> |
@@ -60,7 +60,7 @@
| 60 | 60 | {% if has_audit_logs %} |
| 61 | 61 | <div class="card bg-base-100 shadow-md mb-4"> |
| 62 | 62 | <div class="card-body p-4"> |
| 63 | - <h2 class="card-title text-lg mb-3">📊 Statistiques des logs d'audit</h2> | |
| 63 | + <h2 class="card-title text-lg mb-3">📊 {{ audit_section_title }}</h2> | |
| 64 | 64 | <div class="grid grid-cols-1 lg:grid-cols-2 gap-4"> |
| 65 | 65 | <!-- Histogram Chart --> |
| 66 | 66 | <div> |
@@ -70,16 +70,16 @@
| 70 | 70 | <div class="space-y-3"> |
| 71 | 71 | <div class="grid grid-cols-2 gap-3"> |
| 72 | 72 | <div> |
| 73 | - <p class="text-xs text-base-content/70">Total des logs</p> | |
| 73 | + <p class="text-xs text-base-content/70">{{ audit_total_logs_label }}</p> | |
| 74 | 74 | <p class="text-xl font-bold">{{ audit_total_logs }}</p> |
| 75 | 75 | </div> |
| 76 | 76 | <div> |
| 77 | - <p class="text-xs text-base-content/70">Dernières 24h</p> | |
| 77 | + <p class="text-xs text-base-content/70">{{ audit_last_24h }}</p> | |
| 78 | 78 | <p class="text-xl font-bold">{{ audit_logs_last_24h }}</p> |
| 79 | 79 | </div> |
| 80 | 80 | </div> |
| 81 | 81 | <div> |
| 82 | - <p class="text-xs text-base-content/70 mb-2">Par niveau</p> | |
| 82 | + <p class="text-xs text-base-content/70 mb-2">{{ audit_by_level }}</p> | |
| 83 | 83 | <div class="flex flex-wrap gap-2"> |
| 84 | 84 | <span class="badge badge-info badge-sm">DEBUG: {{ audit_debug_count }}</span> |
| 85 | 85 | <span class="badge badge-success badge-sm">INFO: {{ audit_info_count }}</span> |
@@ -97,40 +97,40 @@
| 97 | 97 | <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> |
| 98 | 98 | <a href="/admin/settings" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow"> |
| 99 | 99 | <div class="card-body p-4"> |
| 100 | - <h2 class="card-title text-secondary text-base">⚙️ Paramètres Application</h2> | |
| 101 | - <p class="text-sm">Configurer les paramètres globaux de l'application</p> | |
| 100 | + <h2 class="card-title text-secondary text-base">⚙️ {{ settings_card_title }}</h2> | |
| 101 | + <p class="text-sm">{{ settings_card_desc }}</p> | |
| 102 | 102 | <div class="card-actions justify-end mt-2"> |
| 103 | - <button class="btn btn-secondary btn-sm">Accéder</button> | |
| 103 | + <button class="btn btn-secondary btn-sm">{{ access_button }}</button> | |
| 104 | 104 | </div> |
| 105 | 105 | </div> |
| 106 | 106 | </a> |
| 107 | 107 | |
| 108 | 108 | <a href="/admin/apps" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow"> |
| 109 | 109 | <div class="card-body p-4"> |
| 110 | - <h2 class="card-title text-accent text-base">📦 Gestion des Applications</h2> | |
| 111 | - <p class="text-sm">Activer ou désactiver les applications modulaires</p> | |
| 110 | + <h2 class="card-title text-accent text-base">📦 {{ apps_card_title }}</h2> | |
| 111 | + <p class="text-sm">{{ apps_card_desc }}</p> | |
| 112 | 112 | <div class="card-actions justify-end mt-2"> |
| 113 | - <button class="btn btn-accent btn-sm">Accéder</button> | |
| 113 | + <button class="btn btn-accent btn-sm">{{ access_button }}</button> | |
| 114 | 114 | </div> |
| 115 | 115 | </div> |
| 116 | 116 | </a> |
| 117 | 117 | |
| 118 | 118 | <a href="/admin/organisations" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow"> |
| 119 | 119 | <div class="card-body p-4"> |
| 120 | - <h2 class="card-title text-info text-base">🏢 Gestion des Organisations</h2> | |
| 121 | - <p class="text-sm">Créer et gérer les organisations et leurs membres</p> | |
| 120 | + <h2 class="card-title text-info text-base">🏢 {{ orgs_card_title }}</h2> | |
| 121 | + <p class="text-sm">{{ orgs_card_desc }}</p> | |
| 122 | 122 | <div class="stats stats-vertical lg:stats-horizontal shadow-sm mt-2"> |
| 123 | 123 | <div class="stat py-1 px-2"> |
| 124 | - <div class="stat-title text-xs">Organisations</div> | |
| 124 | + <div class="stat-title text-xs">{{ orgs_stat_title }}</div> | |
| 125 | 125 | <div class="stat-value text-base">{{ total_organisations }}</div> |
| 126 | 126 | </div> |
| 127 | 127 | <div class="stat py-1 px-2"> |
| 128 | - <div class="stat-title text-xs">Membres totaux</div> | |
| 128 | + <div class="stat-title text-xs">{{ orgs_members_stat_title }}</div> | |
| 129 | 129 | <div class="stat-value text-base">{{ total_organisation_members }}</div> |
| 130 | 130 | </div> |
| 131 | 131 | </div> |
| 132 | 132 | <div class="card-actions justify-end mt-2"> |
| 133 | - <button class="btn btn-info btn-sm">Accéder</button> | |
| 133 | + <button class="btn btn-info btn-sm">{{ access_button }}</button> | |
| 134 | 134 | </div> |
| 135 | 135 | </div> |
| 136 | 136 | </a> |
@@ -150,7 +150,7 @@ document.addEventListener('DOMContentLoaded', function() {
| 150 | 150 | data: { |
| 151 | 151 | labels: ['DEBUG', 'INFO', 'WARN', 'ERROR'], |
| 152 | 152 | datasets: [{ |
| 153 | - label: 'Logs par niveau', | |
| 153 | + label: '{{ chart_label }}', | |
| 154 | 154 | data: [ |
| 155 | 155 | {{ audit_debug_count }}, |
| 156 | 156 | {{ audit_info_count }}, |
@@ -192,4 +192,3 @@ document.addEventListener('DOMContentLoaded', function() {
| 192 | 192 | </script> |
| 193 | 193 | {% endif %} |
| 194 | 194 | {% endblock %} |
| 195 | - |
M
templates/admin/errors.html
+18
-22
@@ -7,29 +7,26 @@
| 7 | 7 | <!-- Breadcrumb --> |
| 8 | 8 | <div class="text-sm breadcrumbs mb-3"> |
| 9 | 9 | <ul> |
| 10 | - <li><a href="/">Accueil</a></li> | |
| 11 | - <li><a href="/admin">Administration</a></li> | |
| 12 | - <li>Codes d'Erreur</li> | |
| 10 | + <li><a href="/">{{ breadcrumb_home }}</a></li> | |
| 11 | + <li><a href="/admin">{{ breadcrumb_admin }}</a></li> | |
| 12 | + <li>{{ breadcrumb_errors }}</li> | |
| 13 | 13 | </ul> |
| 14 | 14 | </div> |
| 15 | 15 | |
| 16 | - <h1 class="text-2xl font-bold mb-4">Codes d'Erreur</h1> | |
| 17 | - <p class="text-base-content/70 mb-6"> | |
| 18 | - Documentation des codes d'erreur de l'application. Chaque code d'erreur est associé à un middleware | |
| 19 | - et fournit des informations de contexte pour le débogage. | |
| 20 | - </p> | |
| 16 | + <h1 class="text-2xl font-bold mb-4">{{ title }}</h1> | |
| 17 | + <p class="text-base-content/70 mb-6">{{ description }}</p> | |
| 21 | 18 | |
| 22 | 19 | <!-- Error Codes Table --> |
| 23 | 20 | <div class="overflow-x-auto"> |
| 24 | 21 | <table class="table table-zebra w-full"> |
| 25 | 22 | <thead> |
| 26 | 23 | <tr> |
| 27 | - <th>Code</th> | |
| 28 | - <th>Description</th> | |
| 29 | - <th>Catégorie</th> | |
| 30 | - <th>Middleware</th> | |
| 31 | - <th>Contexte</th> | |
| 32 | - <th>Exemple</th> | |
| 24 | + <th>{{ table_code }}</th> | |
| 25 | + <th>{{ table_description }}</th> | |
| 26 | + <th>{{ table_category }}</th> | |
| 27 | + <th>{{ table_middleware }}</th> | |
| 28 | + <th>{{ table_context }}</th> | |
| 29 | + <th>{{ table_example }}</th> | |
| 33 | 30 | </tr> |
| 34 | 31 | </thead> |
| 35 | 32 | <tbody> |
@@ -50,17 +47,16 @@
| 50 | 47 | <!-- Legend --> |
| 51 | 48 | <div class="mt-6 card bg-base-200"> |
| 52 | 49 | <div class="card-body"> |
| 53 | - <h2 class="card-title text-lg">Légende</h2> | |
| 50 | + <h2 class="card-title text-lg">{{ legend_title }}</h2> | |
| 54 | 51 | <ul class="list-disc list-inside space-y-2 text-sm"> |
| 55 | - <li><strong>Code</strong> : Identifiant unique de l'erreur (format: CATEGORY_NUMBER)</li> | |
| 56 | - <li><strong>Description</strong> : Description en français de l'erreur</li> | |
| 57 | - <li><strong>Catégorie</strong> : Type d'erreur (Authentification, CSRF, Rate Limiting, etc.)</li> | |
| 58 | - <li><strong>Middleware</strong> : Middleware responsable de générer cette erreur</li> | |
| 59 | - <li><strong>Contexte</strong> : Explication détaillée de quand cette erreur se produit</li> | |
| 60 | - <li><strong>Exemple</strong> : Exemple concret de situation générant cette erreur</li> | |
| 52 | + <li><strong>{{ table_code }}</strong> : {{ legend_code }}</li> | |
| 53 | + <li><strong>{{ table_description }}</strong> : {{ legend_description }}</li> | |
| 54 | + <li><strong>{{ table_category }}</strong> : {{ legend_category }}</li> | |
| 55 | + <li><strong>{{ table_middleware }}</strong> : {{ legend_middleware }}</li> | |
| 56 | + <li><strong>{{ table_context }}</strong> : {{ legend_context }}</li> | |
| 57 | + <li><strong>{{ table_example }}</strong> : {{ legend_example }}</li> | |
| 61 | 58 | </ul> |
| 62 | 59 | </div> |
| 63 | 60 | </div> |
| 64 | 61 | </div> |
| 65 | 62 | {% endblock %} |
| 66 | - |
M
templates/admin/settings.html
+34
-40
@@ -112,14 +112,14 @@
| 112 | 112 | <div class="card-body"> |
| 113 | 113 | <h2 class="card-title text-xl mb-4"> |
| 114 | 114 | <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg> |
| 115 | - Sécurité des Mots de Passe | |
| 115 | + {{ password_security }} | |
| 116 | 116 | </h2> |
| 117 | 117 | |
| 118 | 118 | <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 119 | 119 | <!-- Min Length --> |
| 120 | 120 | <div class="form-control"> |
| 121 | 121 | <label class="label"> |
| 122 | - <span class="label-text font-semibold">Longueur minimale</span> | |
| 122 | + <span class="label-text font-semibold">{{ password_min_length }}</span> | |
| 123 | 123 | </label> |
| 124 | 124 | <input |
| 125 | 125 | type="number" |
@@ -130,14 +130,14 @@
| 130 | 130 | max="128" |
| 131 | 131 | > |
| 132 | 132 | <label class="label"> |
| 133 | - <span class="label-text-alt">Entre 6 et 128 caractères</span> | |
| 133 | + <span class="label-text-alt">{{ password_min_length_hint }}</span> | |
| 134 | 134 | </label> |
| 135 | 135 | </div> |
| 136 | 136 | |
| 137 | 137 | <!-- Complexity Requirements --> |
| 138 | 138 | <div class="space-y-2"> |
| 139 | 139 | <label class="label"> |
| 140 | - <span class="label-text font-semibold">Exigences de complexité</span> | |
| 140 | + <span class="label-text font-semibold">{{ password_complexity }}</span> | |
| 141 | 141 | </label> |
| 142 | 142 | <div class="form-control"> |
| 143 | 143 | <label class="label cursor-pointer justify-start gap-3 py-1"> |
@@ -148,7 +148,7 @@
| 148 | 148 | class="checkbox checkbox-sm" |
| 149 | 149 | {% if settings.password_require_uppercase == "true" %}checked{% endif %} |
| 150 | 150 | > |
| 151 | - <span class="label-text">Majuscule requise</span> | |
| 151 | + <span class="label-text">{{ require_uppercase }}</span> | |
| 152 | 152 | </label> |
| 153 | 153 | </div> |
| 154 | 154 | <div class="form-control"> |
@@ -160,7 +160,7 @@
| 160 | 160 | class="checkbox checkbox-sm" |
| 161 | 161 | {% if settings.password_require_lowercase == "true" %}checked{% endif %} |
| 162 | 162 | > |
| 163 | - <span class="label-text">Minuscule requise</span> | |
| 163 | + <span class="label-text">{{ require_lowercase }}</span> | |
| 164 | 164 | </label> |
| 165 | 165 | </div> |
| 166 | 166 | <div class="form-control"> |
@@ -172,7 +172,7 @@
| 172 | 172 | class="checkbox checkbox-sm" |
| 173 | 173 | {% if settings.password_require_digits == "true" %}checked{% endif %} |
| 174 | 174 | > |
| 175 | - <span class="label-text">Chiffre requis</span> | |
| 175 | + <span class="label-text">{{ require_digits }}</span> | |
| 176 | 176 | </label> |
| 177 | 177 | </div> |
| 178 | 178 | <div class="form-control"> |
@@ -184,7 +184,7 @@
| 184 | 184 | class="checkbox checkbox-sm" |
| 185 | 185 | {% if settings.password_require_special == "true" %}checked{% endif %} |
| 186 | 186 | > |
| 187 | - <span class="label-text">Caractère spécial requis</span> | |
| 187 | + <span class="label-text">{{ require_special }}</span> | |
| 188 | 188 | </label> |
| 189 | 189 | </div> |
| 190 | 190 | </div> |
@@ -201,8 +201,8 @@
| 201 | 201 | {% if settings.password_expiration_enabled == "true" %}checked{% endif %} |
| 202 | 202 | > |
| 203 | 203 | <div> |
| 204 | - <span class="label-text font-semibold">Expiration des mots de passe</span> | |
| 205 | - <span class="label-text-alt block">Forcer le changement périodique</span> | |
| 204 | + <span class="label-text font-semibold">{{ password_expiration }}</span> | |
| 205 | + <span class="label-text-alt block">{{ password_expiration_desc }}</span> | |
| 206 | 206 | </div> |
| 207 | 207 | </label> |
| 208 | 208 | </div> |
@@ -210,7 +210,7 @@
| 210 | 210 | <div id="expirationSettings" class="space-y-3"> |
| 211 | 211 | <div class="form-control"> |
| 212 | 212 | <label class="label"> |
| 213 | - <span class="label-text">Expiration après (jours)</span> | |
| 213 | + <span class="label-text">{{ expiration_days }}</span> | |
| 214 | 214 | </label> |
| 215 | 215 | <input |
| 216 | 216 | type="number" |
@@ -231,13 +231,13 @@
| 231 | 231 | class="checkbox checkbox-sm" |
| 232 | 232 | {% if settings.password_expiration_alert_enabled == "true" %}checked{% endif %} |
| 233 | 233 | > |
| 234 | - <span class="label-text">Alerte email avant expiration</span> | |
| 234 | + <span class="label-text">{{ expiration_alert }}</span> | |
| 235 | 235 | </label> |
| 236 | 236 | </div> |
| 237 | 237 | |
| 238 | 238 | <div class="form-control"> |
| 239 | 239 | <label class="label"> |
| 240 | - <span class="label-text">Alerte (jours avant)</span> | |
| 240 | + <span class="label-text">{{ expiration_alert_days }}</span> | |
| 241 | 241 | </label> |
| 242 | 242 | <input |
| 243 | 243 | type="number" |
@@ -258,33 +258,33 @@
| 258 | 258 | <div class="card-body"> |
| 259 | 259 | <h2 class="card-title text-xl mb-4"> |
| 260 | 260 | <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /></svg> |
| 261 | - Journalisation des Audits | |
| 261 | + {{ audit_logging }} | |
| 262 | 262 | </h2> |
| 263 | 263 | |
| 264 | 264 | <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| 265 | 265 | <!-- Log Level --> |
| 266 | 266 | <div class="form-control"> |
| 267 | 267 | <label class="label"> |
| 268 | - <span class="label-text font-semibold">Niveau de log</span> | |
| 268 | + <span class="label-text font-semibold">{{ log_level }}</span> | |
| 269 | 269 | </label> |
| 270 | 270 | <select |
| 271 | 271 | name="audit_log_level" |
| 272 | 272 | class="select select-bordered w-full" |
| 273 | 273 | > |
| 274 | - <option value="DEBUG" {% if settings.audit_log_level == "DEBUG" %}selected{% endif %}>DEBUG - Tous les détails</option> | |
| 275 | - <option value="INFO" {% if settings.audit_log_level == "INFO" %}selected{% endif %}>INFO - Informations normales</option> | |
| 276 | - <option value="WARN" {% if settings.audit_log_level == "WARN" %}selected{% endif %}>WARN - Avertissements uniquement</option> | |
| 277 | - <option value="ERROR" {% if settings.audit_log_level == "ERROR" %}selected{% endif %}>ERROR - Erreurs uniquement</option> | |
| 274 | + <option value="DEBUG" {% if settings.audit_log_level == "DEBUG" %}selected{% endif %}>{{ log_level_debug }}</option> | |
| 275 | + <option value="INFO" {% if settings.audit_log_level == "INFO" %}selected{% endif %}>{{ log_level_info }}</option> | |
| 276 | + <option value="WARN" {% if settings.audit_log_level == "WARN" %}selected{% endif %}>{{ log_level_warn }}</option> | |
| 277 | + <option value="ERROR" {% if settings.audit_log_level == "ERROR" %}selected{% endif %}>{{ log_level_error }}</option> | |
| 278 | 278 | </select> |
| 279 | 279 | <label class="label"> |
| 280 | - <span class="label-text-alt">Contrôle la verbosité des logs d'audit</span> | |
| 280 | + <span class="label-text-alt">{{ log_level_hint }}</span> | |
| 281 | 281 | </label> |
| 282 | 282 | </div> |
| 283 | 283 | |
| 284 | 284 | <!-- Retention --> |
| 285 | 285 | <div class="form-control"> |
| 286 | 286 | <label class="label"> |
| 287 | - <span class="label-text font-semibold">Rétention des logs (jours)</span> | |
| 287 | + <span class="label-text font-semibold">{{ log_retention }}</span> | |
| 288 | 288 | </label> |
| 289 | 289 | <input |
| 290 | 290 | type="number" |
@@ -295,14 +295,14 @@
| 295 | 295 | max="365" |
| 296 | 296 | > |
| 297 | 297 | <label class="label"> |
| 298 | - <span class="label-text-alt">Durée de conservation des logs (1-365 jours)</span> | |
| 298 | + <span class="label-text-alt">{{ log_retention_hint }}</span> | |
| 299 | 299 | </label> |
| 300 | 300 | </div> |
| 301 | 301 | </div> |
| 302 | 302 | |
| 303 | 303 | <div class="alert alert-info mt-4"> |
| 304 | 304 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> |
| 305 | - <span class="text-sm">Les logs d'audit enregistrent toutes les actions administratives (création, modification, suppression d'utilisateurs).</span> | |
| 305 | + <span class="text-sm">{{ audit_info_message }}</span> | |
| 306 | 306 | </div> |
| 307 | 307 | </div> |
| 308 | 308 | </div> |
@@ -312,14 +312,14 @@
| 312 | 312 | <div class="card-body"> |
| 313 | 313 | <div class="flex justify-between items-center"> |
| 314 | 314 | <div> |
| 315 | - <h3 class="font-semibold">Sauvegarder les modifications</h3> | |
| 316 | - <p class="text-sm text-base-content/70">Les paramètres seront appliqués immédiatement</p> | |
| 315 | + <h3 class="font-semibold">{{ save_changes }}</h3> | |
| 316 | + <p class="text-sm text-base-content/70">{{ save_changes_desc }}</p> | |
| 317 | 317 | </div> |
| 318 | 318 | <div class="flex gap-2"> |
| 319 | - <a href="/admin" class="btn btn-ghost">Annuler</a> | |
| 319 | + <a href="/admin" class="btn btn-ghost">{{ cancel }}</a> | |
| 320 | 320 | <button type="submit" class="btn btn-primary"> |
| 321 | 321 | <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> |
| 322 | - Enregistrer les paramètres | |
| 322 | + {{ save_button }} | |
| 323 | 323 | </button> |
| 324 | 324 | </div> |
| 325 | 325 | </div> |
@@ -329,7 +329,6 @@
| 329 | 329 | </div> |
| 330 | 330 | |
| 331 | 331 | <script> |
| 332 | -// Toggle expiration settings visibility | |
| 333 | 332 | const expirationCheckbox = document.getElementById('expirationEnabled'); |
| 334 | 333 | const expirationSettings = document.getElementById('expirationSettings'); |
| 335 | 334 |
@@ -343,10 +342,9 @@ function toggleExpirationSettings() {
| 343 | 342 | |
| 344 | 343 | if (expirationCheckbox) { |
| 345 | 344 | expirationCheckbox.addEventListener('change', toggleExpirationSettings); |
| 346 | - toggleExpirationSettings(); // Initialize on load | |
| 345 | + toggleExpirationSettings(); | |
| 347 | 346 | } |
| 348 | 347 | |
| 349 | -// Form submission | |
| 350 | 348 | document.getElementById('settingsForm').addEventListener('submit', async function(e) { |
| 351 | 349 | e.preventDefault(); |
| 352 | 350 |
@@ -354,7 +352,7 @@ document.getElementById('settingsForm').addEventListener('submit', async functio
| 354 | 352 | const submitBtn = this.querySelector('button[type="submit"]'); |
| 355 | 353 | |
| 356 | 354 | submitBtn.disabled = true; |
| 357 | - submitBtn.innerHTML = '<span class="loading loading-spinner"></span> Enregistrement...'; | |
| 355 | + submitBtn.innerHTML = '<span class="loading loading-spinner"></span> {{ saving }}'; | |
| 358 | 356 | |
| 359 | 357 | try { |
| 360 | 358 | const response = await fetch('/admin/settings', { |
@@ -368,23 +366,19 @@ document.getElementById('settingsForm').addEventListener('submit', async functio
| 368 | 366 | const result = await response.json(); |
| 369 | 367 | |
| 370 | 368 | if (response.ok) { |
| 371 | - alert('✅ Paramètres enregistrés avec succès !'); | |
| 369 | + alert('✅ {{ save_success }}'); | |
| 372 | 370 | window.location.reload(); |
| 373 | 371 | } else { |
| 374 | - alert('❌ Erreur : ' + (result.error || result.message || 'Échec de l\'enregistrement')); | |
| 372 | + alert('❌ {{ save_error_prefix }} : ' + (result.error || result.message || '')); | |
| 375 | 373 | submitBtn.disabled = false; |
| 376 | - submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> Enregistrer les paramètres'; | |
| 374 | + submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> {{ save_button }}'; | |
| 377 | 375 | } |
| 378 | 376 | } catch (error) { |
| 379 | 377 | console.error('Error:', error); |
| 380 | - alert('❌ Erreur réseau. Veuillez réessayer.'); | |
| 378 | + alert('❌ {{ network_error }}'); | |
| 381 | 379 | submitBtn.disabled = false; |
| 382 | - submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> Enregistrer les paramètres'; | |
| 380 | + submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> {{ save_button }}'; | |
| 383 | 381 | } |
| 384 | 382 | }); |
| 385 | 383 | </script> |
| 386 | 384 | {% endblock %} |
| 387 | - | |
| 388 | - | |
| 389 | - | |
| 390 | - |
M
templates/admin/user_form.html
+21
-37
@@ -7,32 +7,20 @@
| 7 | 7 | <!-- Breadcrumb --> |
| 8 | 8 | <div class="text-sm breadcrumbs mb-6"> |
| 9 | 9 | <ul> |
| 10 | - <li><a href="/">Accueil</a></li> | |
| 11 | - <li><a href="/admin">Administration</a></li> | |
| 12 | - <li><a href="/admin/users">Utilisateurs</a></li> | |
| 13 | - <li> | |
| 14 | - {% if mode == "create" %} | |
| 15 | - Créer | |
| 16 | - {% else %} | |
| 17 | - Modifier | |
| 18 | - {% endif %} | |
| 19 | - </li> | |
| 10 | + <li><a href="/">{{ breadcrumb_home }}</a></li> | |
| 11 | + <li><a href="/admin">{{ breadcrumb_admin }}</a></li> | |
| 12 | + <li><a href="/admin/users">{{ breadcrumb_users }}</a></li> | |
| 13 | + <li>{{ breadcrumb_mode }}</li> | |
| 20 | 14 | </ul> |
| 21 | 15 | </div> |
| 22 | 16 | |
| 23 | 17 | <div class="flex justify-between items-center mb-6"> |
| 24 | - <h1 class="text-3xl font-bold"> | |
| 25 | - {% if mode == "create" %} | |
| 26 | - Créer un utilisateur | |
| 27 | - {% else %} | |
| 28 | - Modifier un utilisateur | |
| 29 | - {% endif %} | |
| 30 | - </h1> | |
| 18 | + <h1 class="text-3xl font-bold">{{ form_title }}</h1> | |
| 31 | 19 | <a href="/admin/users" class="btn btn-ghost"> |
| 32 | 20 | <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 33 | 21 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" /> |
| 34 | 22 | </svg> |
| 35 | - Retour à la liste | |
| 23 | + {{ back_to_list }} | |
| 36 | 24 | </a> |
| 37 | 25 | </div> |
| 38 | 26 |
@@ -68,7 +56,7 @@
| 68 | 56 | <!-- Username Field --> |
| 69 | 57 | <div class="form-control"> |
| 70 | 58 | <label class="label"> |
| 71 | - <span class="label-text font-semibold">Nom d'utilisateur</span> | |
| 59 | + <span class="label-text font-semibold">{{ username_label }}</span> | |
| 72 | 60 | <span class="label-text-alt text-error">*</span> |
| 73 | 61 | </label> |
| 74 | 62 | {% if mode == "create" %} |
@@ -76,7 +64,7 @@
| 76 | 64 | type="text" |
| 77 | 65 | name="username" |
| 78 | 66 | value="{{ form_username }}" |
| 79 | - placeholder="nom_utilisateur" | |
| 67 | + placeholder="{{ username_placeholder }}" | |
| 80 | 68 | class="input input-bordered w-full" |
| 81 | 69 | required |
| 82 | 70 | > |
@@ -90,21 +78,21 @@
| 90 | 78 | <input type="hidden" name="username" value="{{ form_username }}"> |
| 91 | 79 | {% endif %} |
| 92 | 80 | <label class="label"> |
| 93 | - <span class="label-text-alt">Le nom d'utilisateur ne peut pas être modifié après création</span> | |
| 81 | + <span class="label-text-alt">{{ username_readonly_hint }}</span> | |
| 94 | 82 | </label> |
| 95 | 83 | </div> |
| 96 | 84 | |
| 97 | 85 | <!-- Email Field --> |
| 98 | 86 | <div class="form-control"> |
| 99 | 87 | <label class="label"> |
| 100 | - <span class="label-text font-semibold">Email</span> | |
| 88 | + <span class="label-text font-semibold">{{ email_label }}</span> | |
| 101 | 89 | <span class="label-text-alt text-error">*</span> |
| 102 | 90 | </label> |
| 103 | 91 | <input |
| 104 | 92 | type="email" |
| 105 | 93 | name="email" |
| 106 | 94 | value="{{ form_email }}" |
| 107 | - placeholder="utilisateur@example.com" | |
| 95 | + placeholder="{{ email_placeholder }}" | |
| 108 | 96 | class="input input-bordered w-full" |
| 109 | 97 | required |
| 110 | 98 | > |
@@ -114,17 +102,17 @@
| 114 | 102 | {% if mode == "create" %} |
| 115 | 103 | <div class="form-control"> |
| 116 | 104 | <label class="label"> |
| 117 | - <span class="label-text font-semibold">Mot de passe</span> | |
| 118 | - <span class="label-text-alt">Optionnel</span> | |
| 105 | + <span class="label-text font-semibold">{{ password_label }}</span> | |
| 106 | + <span class="label-text-alt">{{ password_optional }}</span> | |
| 119 | 107 | </label> |
| 120 | 108 | <input |
| 121 | 109 | type="password" |
| 122 | 110 | name="password" |
| 123 | - placeholder="Laisser vide pour générer un mot de passe aléatoire" | |
| 111 | + placeholder="{{ password_placeholder }}" | |
| 124 | 112 | class="input input-bordered w-full" |
| 125 | 113 | > |
| 126 | 114 | <label class="label"> |
| 127 | - <span class="label-text-alt">Si laissé vide, un mot de passe sécurisé sera généré automatiquement</span> | |
| 115 | + <span class="label-text-alt">{{ password_hint }}</span> | |
| 128 | 116 | </label> |
| 129 | 117 | </div> |
| 130 | 118 | {% endif %} |
@@ -132,7 +120,7 @@
| 132 | 120 | <!-- Roles Field --> |
| 133 | 121 | <div class="form-control"> |
| 134 | 122 | <label class="label"> |
| 135 | - <span class="label-text font-semibold">Rôles</span> | |
| 123 | + <span class="label-text font-semibold">{{ roles_label }}</span> | |
| 136 | 124 | <span class="label-text-alt text-error">*</span> |
| 137 | 125 | </label> |
| 138 | 126 | <div class="space-y-2"> |
@@ -155,7 +143,7 @@
| 155 | 143 | {% endfor %} |
| 156 | 144 | </div> |
| 157 | 145 | <label class="label"> |
| 158 | - <span class="label-text-alt">Sélectionnez au moins un rôle</span> | |
| 146 | + <span class="label-text-alt">{{ roles_hint }}</span> | |
| 159 | 147 | </label> |
| 160 | 148 | </div> |
| 161 | 149 |
@@ -171,8 +159,8 @@
| 171 | 159 | {% if must_change_password %}checked{% endif %} |
| 172 | 160 | > |
| 173 | 161 | <div class="flex-1"> |
| 174 | - <span class="label-text font-medium">Forcer le changement de mot de passe</span> | |
| 175 | - <p class="text-sm text-base-content/70">L'utilisateur devra changer son mot de passe à la prochaine connexion</p> | |
| 162 | + <span class="label-text font-medium">{{ must_change_password_label }}</span> | |
| 163 | + <p class="text-sm text-base-content/70">{{ must_change_password_desc }}</p> | |
| 176 | 164 | </div> |
| 177 | 165 | </label> |
| 178 | 166 | </div> |
@@ -180,14 +168,10 @@
| 180 | 168 | |
| 181 | 169 | <!-- Form Actions --> |
| 182 | 170 | <div class="flex justify-end gap-4 pt-4 border-t"> |
| 183 | - <a href="/admin/users" class="btn btn-ghost">Annuler</a> | |
| 171 | + <a href="/admin/users" class="btn btn-ghost">{{ cancel }}</a> | |
| 184 | 172 | <button type="submit" class="btn btn-primary" id="submit-btn"> |
| 185 | 173 | <span id="form-loading" class="htmx-indicator loading loading-spinner loading-sm mr-2"></span> |
| 186 | - {% if mode == "create" %} | |
| 187 | - Créer l'utilisateur | |
| 188 | - {% else %} | |
| 189 | - Enregistrer les modifications | |
| 190 | - {% endif %} | |
| 174 | + {{ submit_label }} | |
| 191 | 175 | </button> |
| 192 | 176 | </div> |
| 193 | 177 | </form> |
M
templates/admin/users_list.html
+11
-11
@@ -355,7 +355,7 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
| 355 | 355 | const submitBtn = this.querySelector('button[type="submit"]'); |
| 356 | 356 | const originalBtnText = submitBtn.innerHTML; |
| 357 | 357 | submitBtn.disabled = true; |
| 358 | - submitBtn.innerHTML = '<span class="loading loading-spinner"></span> Réinitialisation...'; | |
| 358 | + submitBtn.innerHTML = '<span class="loading loading-spinner"></span> {{ resetting }}'; | |
| 359 | 359 | |
| 360 | 360 | try { |
| 361 | 361 | let response; |
@@ -399,11 +399,11 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
| 399 | 399 | successModal.className = 'modal modal-open'; |
| 400 | 400 | successModal.innerHTML = ` |
| 401 | 401 | <div class="modal-box"> |
| 402 | - <h3 class="font-bold text-lg mb-4">Réinitialisation par email</h3> | |
| 402 | + <h3 class="font-bold text-lg mb-4">{{ reset_by_email_title }}</h3> | |
| 403 | 403 | <div class="mb-4">${message.innerHTML}</div> |
| 404 | 404 | <div class="modal-action"> |
| 405 | 405 | <button type="button" class="btn btn-primary" onclick="this.closest('.modal').remove(); document.getElementById('resetPasswordModal').close();"> |
| 406 | - Fermer | |
| 406 | + {{ close }} | |
| 407 | 407 | </button> |
| 408 | 408 | </div> |
| 409 | 409 | </div> |
@@ -427,16 +427,16 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
| 427 | 427 | passwordDisplay.className = 'modal modal-open'; |
| 428 | 428 | passwordDisplay.innerHTML = ` |
| 429 | 429 | <div class="modal-box"> |
| 430 | - <h3 class="font-bold text-lg mb-4">✅ Mot de passe réinitialisé</h3> | |
| 430 | + <h3 class="font-bold text-lg mb-4">✅ {{ password_reset_success_title }}</h3> | |
| 431 | 431 | <div class="alert alert-warning mb-4"> |
| 432 | 432 | <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"> |
| 433 | 433 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> |
| 434 | 434 | </svg> |
| 435 | - <span>⚠️ Ce mot de passe ne sera affiché qu'une seule fois. Copiez-le maintenant !</span> | |
| 435 | + <span>⚠️ {{ password_warning_once }}</span> | |
| 436 | 436 | </div> |
| 437 | 437 | <div class="form-control mb-4"> |
| 438 | 438 | <label class="label"> |
| 439 | - <span class="label-text font-semibold">Nouveau mot de passe</span> | |
| 439 | + <span class="label-text font-semibold">{{ new_password_label }}</span> | |
| 440 | 440 | </label> |
| 441 | 441 | <div class="input-group"> |
| 442 | 442 | <input |
@@ -454,13 +454,13 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
| 454 | 454 | <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 455 | 455 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" /> |
| 456 | 456 | </svg> |
| 457 | - Copier | |
| 457 | + {{ copy }} | |
| 458 | 458 | </button> |
| 459 | 459 | </div> |
| 460 | 460 | </div> |
| 461 | 461 | <div class="modal-action"> |
| 462 | 462 | <button type="button" class="btn btn-primary" onclick="this.closest('.modal').remove(); document.getElementById('resetPasswordModal').close(); window.location.reload();"> |
| 463 | - Fermer | |
| 463 | + {{ close }} | |
| 464 | 464 | </button> |
| 465 | 465 | </div> |
| 466 | 466 | </div> |
@@ -472,18 +472,18 @@ document.getElementById('resetPasswordForm').addEventListener('submit', async fu
| 472 | 472 | const passwordInput = document.getElementById('displayPassword'); |
| 473 | 473 | passwordInput.select(); |
| 474 | 474 | document.execCommand('copy'); |
| 475 | - alert('✅ Mot de passe copié dans le presse-papiers'); | |
| 475 | + alert('✅ {{ password_copied }}'); | |
| 476 | 476 | }; |
| 477 | 477 | } else { |
| 478 | 478 | // No password to display (custom password was set) |
| 479 | - alert('✅ Mot de passe réinitialisé avec succès'); | |
| 479 | + alert('✅ {{ password_set_success }}'); | |
| 480 | 480 | document.getElementById('resetPasswordModal').close(); |
| 481 | 481 | window.location.reload(); |
| 482 | 482 | } |
| 483 | 483 | } else { |
| 484 | 484 | const errorText = await response.text(); |
| 485 | 485 | console.error('Error:', errorText); |
| 486 | - let errorMessage = '❌ Erreur lors de la réinitialisation du mot de passe'; | |
| 486 | + let errorMessage = '❌ {{ reset_error }}'; | |
| 487 | 487 | try { |
| 488 | 488 | const errorJson = JSON.parse(errorText); |
| 489 | 489 | if (errorJson.message) { |
M
templates/base.html
+1
-1
@@ -3,7 +3,7 @@
| 3 | 3 | <head> |
| 4 | 4 | <meta charset="UTF-8"> |
| 5 | 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 6 | - <meta name="description" content="{% if app_description != "" %}{{ app_description }}{% else %}RustWarden - Cloud storage solution written in Rust{% endif %}"> | |
| 6 | + <meta name="description" content="{% if app_description != "" %}{{ app_description }}{% else %}RustWarden - web boilerplate solution written in Rust{% endif %}"> | |
| 7 | 7 | <title>{% block title %}{{ page_title }} - {{ app_name }}{% endblock %}</title> |
| 8 | 8 | |
| 9 | 9 | <!-- Favicon --> |
M
templates/home.html
+7
-15
@@ -8,12 +8,12 @@
| 8 | 8 | <!-- Welcome Section --> |
| 9 | 9 | <div class="mb-8"> |
| 10 | 10 | <h1 class="text-3xl font-bold text-base-content mb-2">{{ welcome_message }}</h1> |
| 11 | - <p class="text-base-content/70">Bienvenue sur votre espace cloud personnel</p> | |
| 11 | + <p class="text-base-content/70">{{ home_subtitle }}</p> | |
| 12 | 12 | </div> |
| 13 | 13 | |
| 14 | 14 | <!-- Account Stats --> |
| 15 | 15 | <div class="mb-12"> |
| 16 | - <h2 class="text-xl font-semibold text-base-content mb-4">Mon Compte</h2> | |
| 16 | + <h2 class="text-xl font-semibold text-base-content mb-4">{{ home_my_account }}</h2> | |
| 17 | 17 | <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"> |
| 18 | 18 | {% for stat in account_stats %} |
| 19 | 19 | <div class="card bg-base-100 shadow-md hover:shadow-xl transition-shadow"> |
@@ -37,7 +37,7 @@
| 37 | 37 | |
| 38 | 38 | <!-- Upcoming Features --> |
| 39 | 39 | <div> |
| 40 | - <h2 class="text-xl font-semibold text-base-content mb-4">Fonctionnalités à Venir</h2> | |
| 40 | + <h2 class="text-xl font-semibold text-base-content mb-4">{{ home_upcoming_features }}</h2> | |
| 41 | 41 | <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> |
| 42 | 42 | {% for feature in upcoming_features %} |
| 43 | 43 | <div class="card bg-base-100 shadow-md hover:shadow-xl transition-shadow"> |
@@ -56,12 +56,7 @@
| 56 | 56 | </div> |
| 57 | 57 | <div> |
| 58 | 58 | <h3 class="card-title text-lg">{{ feature.name }}</h3> |
| 59 | - <span class="badge | |
| 60 | - {% if feature.badge == "Bientôt" %} | |
| 61 | - badge-warning | |
| 62 | - {% else %} | |
| 63 | - badge-info | |
| 64 | - {% endif %}"> | |
| 59 | + <span class="badge badge-warning"> | |
| 65 | 60 | {{ feature.badge }} |
| 66 | 61 | </span> |
| 67 | 62 | </div> |
@@ -79,14 +74,11 @@
| 79 | 74 | </div> |
| 80 | 75 | </div> |
| 81 | 76 | |
| 82 | -<!-- Quick Actions (Optional) --> | |
| 77 | +<!-- Quick Actions --> | |
| 83 | 78 | <div class="mt-12 card bg-info/10 border border-info/20"> |
| 84 | 79 | <div class="card-body p-6"> |
| 85 | - <h3 class="card-title text-lg mb-2">💡 Le Saviez-Vous ?</h3> | |
| 86 | - <p class="text-base-content/80"> | |
| 87 | - RustWarden est un fork de Nextcloud écrit en Rust pour offrir de meilleures performances | |
| 88 | - et une sécurité renforcée. Restez à l'écoute pour les nouvelles fonctionnalités ! | |
| 89 | - </p> | |
| 80 | + <h3 class="card-title text-lg mb-2">💡 {{ home_did_you_know }}</h3> | |
| 81 | + <p class="text-base-content/80">{{ home_did_you_know_text }}</p> | |
| 90 | 82 | </div> |
| 91 | 83 | </div> |
| 92 | 84 | {% endblock %} |
M
templates/resend_verification.html
+18
-14
@@ -1,14 +1,14 @@
| 1 | 1 | {% extends "base.html" %} |
| 2 | 2 | |
| 3 | -{% block title %}Renvoyer l'email de validation - RustWarden{% endblock %} | |
| 3 | +{% block title %}{{ page_title }}{% endblock %} | |
| 4 | 4 | |
| 5 | 5 | {% block content %} |
| 6 | 6 | <div class="min-h-screen flex items-center justify-center bg-base-200 px-4"> |
| 7 | 7 | <div class="max-w-lg w-full space-y-6"> |
| 8 | 8 | <div class="text-center"> |
| 9 | - <h1 class="text-3xl font-bold text-base-content mb-2">Renvoyer l'email de validation</h1> | |
| 9 | + <h1 class="text-3xl font-bold text-base-content mb-2">{{ title }}</h1> | |
| 10 | 10 | <p class="text-base-content/70"> |
| 11 | - Entrez votre adresse email pour recevoir un nouveau lien de validation. | |
| 11 | + {{ subtitle }} | |
| 12 | 12 | </p> |
| 13 | 13 | </div> |
| 14 | 14 |
@@ -26,7 +26,7 @@
| 26 | 26 | <form id="resend-form" class="space-y-4"> |
| 27 | 27 | <div class="form-control"> |
| 28 | 28 | <label for="email" class="label"> |
| 29 | - <span class="label-text">Adresse email</span> | |
| 29 | + <span class="label-text">{{ email_label }}</span> | |
| 30 | 30 | </label> |
| 31 | 31 | <input |
| 32 | 32 | type="email" |
@@ -35,24 +35,29 @@
| 35 | 35 | required |
| 36 | 36 | autocomplete="email" |
| 37 | 37 | class="input input-bordered w-full" |
| 38 | - placeholder="vous@example.com"> | |
| 38 | + placeholder="{{ email_placeholder }}"> | |
| 39 | 39 | </div> |
| 40 | 40 | |
| 41 | 41 | <div class="form-control mt-6"> |
| 42 | 42 | <button type="submit" class="btn btn-primary w-full"> |
| 43 | - Envoyer le lien de validation | |
| 43 | + {{ submit }} | |
| 44 | 44 | </button> |
| 45 | 45 | </div> |
| 46 | 46 | </form> |
| 47 | 47 | |
| 48 | 48 | <div class="mt-4 text-sm text-base-content/70 text-center space-y-2"> |
| 49 | - <a href="/login" class="text-primary hover:underline">Revenir à la connexion</a> | |
| 49 | + <a href="/login" class="text-primary hover:underline">{{ back_to_login }}</a> | |
| 50 | 50 | </div> |
| 51 | 51 | </div> |
| 52 | 52 | </div> |
| 53 | 53 | </div> |
| 54 | 54 | |
| 55 | 55 | <script> |
| 56 | + const MSG_ERROR_DEFAULT = "{{ error_default }}"; | |
| 57 | + const MSG_SUCCESS_DEFAULT = "{{ success_default }}"; | |
| 58 | + const MSG_ERROR_MISSING_EMAIL = "{{ error_missing_email }}"; | |
| 59 | + const MSG_ERROR_CANNOT_SEND = "{{ error_cannot_send }}"; | |
| 60 | + | |
| 56 | 61 | const form = document.getElementById('resend-form'); |
| 57 | 62 | const alertError = document.getElementById('alert-error'); |
| 58 | 63 | const alertErrorText = document.getElementById('alert-error-text'); |
@@ -61,7 +66,7 @@
| 61 | 66 | |
| 62 | 67 | function showError(msg) { |
| 63 | 68 | if (alertError && alertErrorText) { |
| 64 | - alertErrorText.textContent = msg || "Impossible d'envoyer la demande."; | |
| 69 | + alertErrorText.textContent = msg || MSG_ERROR_DEFAULT; | |
| 65 | 70 | alertError.classList.remove('hidden'); |
| 66 | 71 | } |
| 67 | 72 | if (alertSuccess) alertSuccess.classList.add('hidden'); |
@@ -69,7 +74,7 @@
| 69 | 74 | |
| 70 | 75 | function showSuccess(msg) { |
| 71 | 76 | if (alertSuccess && alertSuccessText) { |
| 72 | - alertSuccessText.textContent = msg || "Si le compte existe et n'est pas vérifié, un email a été envoyé."; | |
| 77 | + alertSuccessText.textContent = msg || MSG_SUCCESS_DEFAULT; | |
| 73 | 78 | alertSuccess.classList.remove('hidden'); |
| 74 | 79 | } |
| 75 | 80 | if (alertError) alertError.classList.add('hidden'); |
@@ -79,7 +84,7 @@
| 79 | 84 | e.preventDefault(); |
| 80 | 85 | const email = document.getElementById('email')?.value; |
| 81 | 86 | if (!email) { |
| 82 | - showError("Veuillez renseigner votre email."); | |
| 87 | + showError(MSG_ERROR_MISSING_EMAIL); | |
| 83 | 88 | return; |
| 84 | 89 | } |
| 85 | 90 | fetch('/api/v1/auth/resend-verification', { |
@@ -95,13 +100,12 @@
| 95 | 100 | msg = data.message || data.error || msg; |
| 96 | 101 | } catch (_) {} |
| 97 | 102 | if (resp.ok) { |
| 98 | - showSuccess(msg || "Si le compte existe et n'est pas vérifié, un email a été envoyé."); | |
| 103 | + showSuccess(msg || MSG_SUCCESS_DEFAULT); | |
| 99 | 104 | } else { |
| 100 | - showError(msg || "Impossible d'envoyer la demande pour l'instant."); | |
| 105 | + showError(msg || MSG_ERROR_CANNOT_SEND); | |
| 101 | 106 | } |
| 102 | 107 | }) |
| 103 | - .catch(() => showError("Impossible d'envoyer la demande pour l'instant.")); | |
| 108 | + .catch(() => showError(MSG_ERROR_CANNOT_SEND)); | |
| 104 | 109 | }); |
| 105 | 110 | </script> |
| 106 | 111 | {% endblock %} |
| 107 | - |
M
templates/reset_password.html
+22
-15
@@ -11,7 +11,7 @@
| 11 | 11 | </div> |
| 12 | 12 | <h1 class="text-3xl font-bold text-base-content mb-2">{{ reset_password_title }}</h1> |
| 13 | 13 | <p class="text-base-content/70"> |
| 14 | - Entrez votre nouveau mot de passe ci-dessous. | |
| 14 | + {{ subtitle }} | |
| 15 | 15 | </p> |
| 16 | 16 | </div> |
| 17 | 17 |
@@ -48,7 +48,7 @@
| 48 | 48 | |
| 49 | 49 | <div class="form-control"> |
| 50 | 50 | <label for="confirm_password" class="label"> |
| 51 | - <span class="label-text">Confirmer le mot de passe</span> | |
| 51 | + <span class="label-text">{{ confirm_password_label }}</span> | |
| 52 | 52 | </label> |
| 53 | 53 | <input |
| 54 | 54 | type="password" |
@@ -57,7 +57,7 @@
| 57 | 57 | required |
| 58 | 58 | autocomplete="new-password" |
| 59 | 59 | class="input input-bordered w-full" |
| 60 | - placeholder="Confirmez votre nouveau mot de passe"> | |
| 60 | + placeholder="{{ confirm_password_placeholder }}"> | |
| 61 | 61 | </div> |
| 62 | 62 | |
| 63 | 63 | <div class="form-control mt-6"> |
@@ -75,14 +75,23 @@
| 75 | 75 | </div> |
| 76 | 76 | |
| 77 | 77 | <script> |
| 78 | - // Extract token from URL query parameter | |
| 78 | + const MSG_MISSING_TOKEN_URL = "{{ missing_token_url }}"; | |
| 79 | + const MSG_MISSING_TOKEN = "{{ missing_token }}"; | |
| 80 | + const MSG_MISSING_NEW_PASSWORD = "{{ missing_new_password }}"; | |
| 81 | + const MSG_PASSWORDS_NOT_MATCH = "{{ passwords_not_match }}"; | |
| 82 | + const MSG_RESET_SUCCESS = "{{ reset_success_message }}"; | |
| 83 | + const MSG_ERROR_LINK_INVALID = "{{ error_link_invalid }}"; | |
| 84 | + const MSG_ERROR_GENERIC = "{{ error_generic }}"; | |
| 85 | + const MSG_ERROR_DEFAULT = "{{ error_message }}"; | |
| 86 | + const MSG_SUCCESS_DEFAULT = "{{ success_message }}"; | |
| 87 | + | |
| 79 | 88 | const urlParams = new URLSearchParams(window.location.search); |
| 80 | 89 | const token = urlParams.get('token'); |
| 81 | 90 | |
| 82 | 91 | if (token) { |
| 83 | 92 | document.getElementById('token').value = token; |
| 84 | 93 | } else { |
| 85 | - showError("Token de réinitialisation manquant dans l'URL."); | |
| 94 | + showError(MSG_MISSING_TOKEN_URL); | |
| 86 | 95 | } |
| 87 | 96 | |
| 88 | 97 | const form = document.getElementById('reset-form'); |
@@ -93,7 +102,7 @@
| 93 | 102 | |
| 94 | 103 | function showError(msg) { |
| 95 | 104 | if (alertError && alertErrorText) { |
| 96 | - alertErrorText.textContent = msg || "{{ error_message }}"; | |
| 105 | + alertErrorText.textContent = msg || MSG_ERROR_DEFAULT; | |
| 97 | 106 | alertError.classList.remove('hidden'); |
| 98 | 107 | } |
| 99 | 108 | if (alertSuccess) alertSuccess.classList.add('hidden'); |
@@ -101,7 +110,7 @@
| 101 | 110 | |
| 102 | 111 | function showSuccess(msg) { |
| 103 | 112 | if (alertSuccess && alertSuccessText) { |
| 104 | - alertSuccessText.textContent = msg || "{{ success_message }}"; | |
| 113 | + alertSuccessText.textContent = msg || MSG_SUCCESS_DEFAULT; | |
| 105 | 114 | alertSuccess.classList.remove('hidden'); |
| 106 | 115 | } |
| 107 | 116 | if (alertError) alertError.classList.add('hidden'); |
@@ -114,17 +123,17 @@
| 114 | 123 | const confirmPassword = document.getElementById('confirm_password')?.value; |
| 115 | 124 | |
| 116 | 125 | if (!token) { |
| 117 | - showError("Token de réinitialisation manquant."); | |
| 126 | + showError(MSG_MISSING_TOKEN); | |
| 118 | 127 | return; |
| 119 | 128 | } |
| 120 | 129 | |
| 121 | 130 | if (!newPassword) { |
| 122 | - showError("Veuillez entrer un nouveau mot de passe."); | |
| 131 | + showError(MSG_MISSING_NEW_PASSWORD); | |
| 123 | 132 | return; |
| 124 | 133 | } |
| 125 | 134 | |
| 126 | 135 | if (newPassword !== confirmPassword) { |
| 127 | - showError("Les mots de passe ne correspondent pas."); | |
| 136 | + showError(MSG_PASSWORDS_NOT_MATCH); | |
| 128 | 137 | return; |
| 129 | 138 | } |
| 130 | 139 |
@@ -141,17 +150,15 @@
| 141 | 150 | msg = data.message || data.error || msg; |
| 142 | 151 | } catch (_) {} |
| 143 | 152 | if (resp.ok) { |
| 144 | - showSuccess(msg || "Votre mot de passe a été réinitialisé avec succès. Vous pouvez maintenant vous connecter."); | |
| 145 | - // Redirect to login after 3 seconds | |
| 153 | + showSuccess(msg || MSG_RESET_SUCCESS); | |
| 146 | 154 | setTimeout(() => { |
| 147 | 155 | window.location.href = '/login'; |
| 148 | 156 | }, 3000); |
| 149 | 157 | } else { |
| 150 | - showError(msg || "Impossible de réinitialiser le mot de passe. Le lien peut être expiré ou invalide."); | |
| 158 | + showError(msg || MSG_ERROR_LINK_INVALID); | |
| 151 | 159 | } |
| 152 | 160 | }) |
| 153 | - .catch(() => showError("Impossible de réinitialiser le mot de passe pour l'instant.")); | |
| 161 | + .catch(() => showError(MSG_ERROR_GENERIC)); | |
| 154 | 162 | }); |
| 155 | 163 | </script> |
| 156 | 164 | {% endblock %} |
| 157 | - |
M
templates/settings.html
+19
-22
@@ -1,6 +1,6 @@
| 1 | 1 | {% extends "base.html" %} |
| 2 | 2 | |
| 3 | -{% block title %}Paramètres - RustWarden{% endblock %} | |
| 3 | +{% block title %}{{ page_title }}{% endblock %} | |
| 4 | 4 | |
| 5 | 5 | {% block navbar %} |
| 6 | 6 | {% include "components/navbar.html" %} |
@@ -11,15 +11,15 @@
| 11 | 11 | <!-- Breadcrumb --> |
| 12 | 12 | <div class="text-sm breadcrumbs mb-6"> |
| 13 | 13 | <ul> |
| 14 | - <li><a href="/">Accueil</a></li> | |
| 15 | - <li>Paramètres</li> | |
| 14 | + <li><a href="/">{{ breadcrumb_home }}</a></li> | |
| 15 | + <li>{{ breadcrumb_settings }}</li> | |
| 16 | 16 | </ul> |
| 17 | 17 | </div> |
| 18 | 18 | |
| 19 | 19 | <!-- Page Header --> |
| 20 | 20 | <div class="mb-8"> |
| 21 | - <h1 class="text-3xl font-bold mb-2">⚙️ Paramètres</h1> | |
| 22 | - <p class="text-base-content/70">Gérez vos préférences et paramètres de compte</p> | |
| 21 | + <h1 class="text-3xl font-bold mb-2">⚙️ {{ settings_title }}</h1> | |
| 22 | + <p class="text-base-content/70">{{ settings_subtitle }}</p> | |
| 23 | 23 | </div> |
| 24 | 24 | |
| 25 | 25 | <!-- Settings Navigation Cards --> |
@@ -31,11 +31,11 @@
| 31 | 31 | <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 32 | 32 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /> |
| 33 | 33 | </svg> |
| 34 | - Mon Profil | |
| 34 | + {{ profile_title }} | |
| 35 | 35 | </h2> |
| 36 | - <p class="text-sm text-base-content/70">Modifiez vos informations personnelles (nom d'utilisateur, email)</p> | |
| 36 | + <p class="text-sm text-base-content/70">{{ profile_desc }}</p> | |
| 37 | 37 | <div class="card-actions justify-end"> |
| 38 | - <button class="btn btn-primary btn-sm">Accéder</button> | |
| 38 | + <button class="btn btn-primary btn-sm">{{ access_button }}</button> | |
| 39 | 39 | </div> |
| 40 | 40 | </div> |
| 41 | 41 | </a> |
@@ -47,11 +47,11 @@
| 47 | 47 | <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 48 | 48 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /> |
| 49 | 49 | </svg> |
| 50 | - Mot de passe | |
| 50 | + {{ password_title }} | |
| 51 | 51 | </h2> |
| 52 | - <p class="text-sm text-base-content/70">Changez votre mot de passe pour sécuriser votre compte</p> | |
| 52 | + <p class="text-sm text-base-content/70">{{ password_desc }}</p> | |
| 53 | 53 | <div class="card-actions justify-end"> |
| 54 | - <button class="btn btn-secondary btn-sm">Accéder</button> | |
| 54 | + <button class="btn btn-secondary btn-sm">{{ access_button }}</button> | |
| 55 | 55 | </div> |
| 56 | 56 | </div> |
| 57 | 57 | </a> |
@@ -63,11 +63,11 @@
| 63 | 63 | <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 64 | 64 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> |
| 65 | 65 | </svg> |
| 66 | - Sessions actives | |
| 66 | + {{ sessions_title }} | |
| 67 | 67 | </h2> |
| 68 | - <p class="text-sm text-base-content/70">Gérez vos sessions actives et révoquez l'accès depuis d'autres appareils</p> | |
| 68 | + <p class="text-sm text-base-content/70">{{ sessions_desc }}</p> | |
| 69 | 69 | <div class="card-actions justify-end"> |
| 70 | - <button class="btn btn-accent btn-sm">Accéder</button> | |
| 70 | + <button class="btn btn-accent btn-sm">{{ access_button }}</button> | |
| 71 | 71 | </div> |
| 72 | 72 | </div> |
| 73 | 73 | </a> |
@@ -79,11 +79,11 @@
| 79 | 79 | <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"> |
| 80 | 80 | <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" /> |
| 81 | 81 | </svg> |
| 82 | - Préférences | |
| 82 | + {{ preferences_title }} | |
| 83 | 83 | </h2> |
| 84 | - <p class="text-sm text-base-content/70">Personnalisez votre expérience (thème, langue)</p> | |
| 84 | + <p class="text-sm text-base-content/70">{{ preferences_desc }}</p> | |
| 85 | 85 | <div class="card-actions justify-end"> |
| 86 | - <button class="btn btn-info btn-sm">Accéder</button> | |
| 86 | + <button class="btn btn-info btn-sm">{{ access_button }}</button> | |
| 87 | 87 | </div> |
| 88 | 88 | </div> |
| 89 | 89 | </a> |
@@ -92,11 +92,8 @@
| 92 | 92 | <!-- Quick Info --> |
| 93 | 93 | <div class="mt-8 card bg-base-200 shadow-xl"> |
| 94 | 94 | <div class="card-body"> |
| 95 | - <h3 class="card-title text-lg">ℹ️ Informations</h3> | |
| 96 | - <p class="text-sm text-base-content/70"> | |
| 97 | - Utilisez les sections ci-dessus pour gérer différents aspects de votre compte. | |
| 98 | - Toutes les modifications sont sécurisées et nécessitent votre authentification. | |
| 99 | - </p> | |
| 95 | + <h3 class="card-title text-lg">ℹ️ {{ info_title }}</h3> | |
| 96 | + <p class="text-sm text-base-content/70">{{ info_desc }}</p> | |
| 100 | 97 | </div> |
| 101 | 98 | </div> |
| 102 | 99 | </div> |
GitRust