preferences.rs
458 lignes · 16439 octets
//! Settings - User preferences (theme, language). use askama::Template; use axum::{Extension, Json, extract::State, response::IntoResponse}; use sea_orm::DatabaseConnection; use serde::Deserialize; use std::sync::Arc; use tower_sessions::Session; use super::{NavItem, build_nav_items_from_dto}; use crate::dto::navigation_dto::NavigationDTO; use crate::dto::user_profile_dto::UserProfileDTO; use rustwarden_core::auth::jwt::Claims; use rustwarden_core::error::AppError; use rustwarden_core::i18n::extractor::Locale; use rustwarden_core::i18n::service::I18nService; use rustwarden_core::services::app_settings_service::AppSettingsService; use rustwarden_core::services::user_service::UserService; /// Inline DTO: Update preferences request #[derive(Debug, Deserialize)] pub struct UpdatePreferencesRequest { /// UI theme preference (e.g. `"dark"`, `"light"`). pub theme: Option<String>, /// Interface language code (e.g. `"en"`, `"fr"`). pub language: Option<String>, } /// GET /settings/preferences - Display user preferences (JSON) pub async fn get_preferences( State(db): State<DatabaseConnection>, Extension(claims): Extension<rustwarden_core::auth::jwt::Claims>, ) -> Result<impl IntoResponse, AppError> { use rustwarden_core::services::user_preferences_service::UserPreferencesService; let preferences = match UserPreferencesService::get_preferences(&db, claims.sub).await? { Some(prefs) => prefs, None => UserPreferencesService::create_default_preferences(&db, claims.sub).await?, }; Ok(Json(serde_json::json!({ "theme": preferences.theme, "language": preferences.language, }))) } /// PUT /settings/preferences - Update user preferences pub async fn update_preferences( State(db): State<DatabaseConnection>, Extension(claims): Extension<rustwarden_core::auth::jwt::Claims>, axum::extract::Form(form): axum::extract::Form<UpdatePreferencesRequest>, ) -> Result<impl IntoResponse, AppError> { use rustwarden_core::services::user_preferences_service::UserPreferencesService; let current_prefs = match UserPreferencesService::get_preferences(&db, claims.sub).await? { Some(prefs) => prefs, None => UserPreferencesService::create_default_preferences(&db, claims.sub).await?, }; let theme = form.theme.unwrap_or(current_prefs.theme.clone()); let language = form.language.unwrap_or(current_prefs.language.clone()); let language_changed = language != current_prefs.language; let updated_prefs = UserPreferencesService::update_preferences(&db, claims.sub, theme, language.clone()) .await?; tracing::info!( "User {} updated preferences: theme={}, language={}", claims.sub, updated_prefs.theme, updated_prefs.language ); let json_response = Json(serde_json::json!({ "message": "Preferences updated successfully", "theme": updated_prefs.theme, "language": updated_prefs.language, "language_changed": language_changed, "reload_required": language_changed, })); use axum::http::header; use time::Duration as TimeDuration; let theme_cookie = axum_extra::extract::cookie::Cookie::build(("theme", updated_prefs.theme.clone())) .http_only(false) .same_site(axum_extra::extract::cookie::SameSite::Lax) .path("/") .max_age(TimeDuration::seconds(31536000)); let mut response = json_response.into_response(); if let Ok(hv) = theme_cookie.to_string().parse() { response.headers_mut().append(header::SET_COOKIE, hv); } if language_changed && matches!(language.as_str(), "fr" | "en" | "de" | "es" | "it" | "pt") { let locale_cookie = axum_extra::extract::cookie::Cookie::build(("locale", language)) .http_only(false) .same_site(axum_extra::extract::cookie::SameSite::Lax) .path("/") .max_age(TimeDuration::seconds(31536000)); if let Ok(hv) = locale_cookie.to_string().parse() { response.headers_mut().append(header::SET_COOKIE, hv); } } Ok(response) } // ============================================================================ // GET Handlers for HTML Pages (with i18n) // ============================================================================ #[derive(Template)] #[template(path = "settings/preferences.html")] #[allow(dead_code)] struct PreferencesTemplate { page_title: String, csrf_token: String, app_name: String, app_version: String, app_domain: String, app_description: String, analytics_opt_out: crate::analytics::AnalyticsOptOutContext, username: String, email: String, roles_text: String, initials: String, avatar_color: String, theme: String, is_theme_light: bool, is_theme_dark: bool, is_theme_auto: bool, language: String, is_language_fr: bool, is_language_en: bool, is_language_de: bool, is_language_es: bool, is_language_it: bool, is_language_pt: bool, current_locale: String, supported_languages: Vec<(String, String, bool)>, nav_items: Vec<NavItem>, has_pending_password_change: bool, user_menu_profile: String, user_menu_logout: String, // Translated strings (hardcoded FR) title: String, subtitle: String, theme_label: String, theme_light: String, theme_light_desc: String, theme_dark: String, theme_dark_desc: String, theme_auto: String, theme_auto_desc: String, theme_preview: String, language_label: String, language_fr: String, language_fr_desc: String, language_en: String, language_en_desc: String, language_de: String, language_de_desc: String, language_es: String, language_es_desc: String, language_it: String, language_it_desc: String, language_pt: String, language_pt_desc: String, translation_warning: String, modified: String, modified_desc: String, save_button: String, saving: String, cancel: String, saved_success: String, saved_reload: String, saved_reload_json: String, saved_success_json: String, error_prefix: String, error_prefix_json: String, network_error: String, network_error_json: String, save_button_json: String, sync_title: String, sync_desc: String, preview_dark: String, preview_light: String, preview_auto: String, breadcrumb_home: String, breadcrumb_settings: String, breadcrumb_preferences: String, } rustwarden_core::impl_template_response!(PreferencesTemplate); /// Handler for GET /profile - Display profile page (PROTECTED) pub async fn get_preferences_page( Extension(claims): Extension<Claims>, Extension(i18n_service): Extension<Arc<I18nService>>, Extension(locale): Extension<Locale>, analytics: rustwarden_core::config::analytics::AnalyticsContext, State(db): State<DatabaseConnection>, session: Session, ) -> Result<impl IntoResponse, AppError> { let user_model = UserService::get_user_by_username(&db, &claims.username) .await? .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; let roles = UserService::get_user_roles(&db, user_model.id).await?; let user = UserProfileDTO::new(user_model, roles.clone()); let locale_str = locale.as_str(); use rustwarden_core::services::user_preferences_service::UserPreferencesService; let preferences = match UserPreferencesService::get_preferences(&db, claims.sub).await? { Some(prefs) => prefs, None => UserPreferencesService::create_default_preferences(&db, claims.sub).await?, }; let current_locale = locale_str.to_string(); let supported_languages: Vec<(String, String, bool)> = vec![ ( "fr".to_string(), "Français".to_string(), current_locale == "fr", ), ( "en".to_string(), "English".to_string(), current_locale == "en", ), ( "de".to_string(), "Deutsch".to_string(), current_locale == "de", ), ( "es".to_string(), "Español".to_string(), current_locale == "es", ), ( "it".to_string(), "Italiano".to_string(), current_locale == "it", ), ( "pt".to_string(), "Português".to_string(), current_locale == "pt", ), ]; let layout = rustwarden_core::templates::LayoutContext::new("Préférences", ""); use crate::template_helpers::csrf_token_helper; let csrf_token = csrf_token_helper(&session).await; let navigation = NavigationDTO::for_user(&roles, &i18n_service, &locale, Some(&db)).await; let nav_items = build_nav_items_from_dto(&navigation, "settings"); let app_domain = AppSettingsService::get_setting(&db, "app_domain") .await? .map(|s| s.value) .unwrap_or_else(|| "localhost".to_string()); let app_description = AppSettingsService::get_setting(&db, "app_description") .await? .map(|s| s.value) .unwrap_or_else(|| "User accounts and authentication".to_string()); let save_button = i18n_service.translate("preferences.save", locale_str).await; let saved_success = i18n_service .translate("preferences.saved_success", locale_str) .await; let saved_reload = i18n_service .translate("preferences.saved_reload", locale_str) .await; let error_prefix = i18n_service .translate("preferences.error", locale_str) .await; let network_error = i18n_service .translate("preferences.network_error", locale_str) .await; let saved_reload_json = serde_json::to_string(&saved_reload).unwrap_or_else(|_| "\"\"".to_string()); let saved_success_json = serde_json::to_string(&saved_success).unwrap_or_else(|_| "\"\"".to_string()); let error_prefix_json = serde_json::to_string(&error_prefix).unwrap_or_else(|_| "\"\"".to_string()); let network_error_json = serde_json::to_string(&network_error).unwrap_or_else(|_| "\"\"".to_string()); let save_button_json = serde_json::to_string(&save_button).unwrap_or_else(|_| "\"\"".to_string()); let template = PreferencesTemplate { page_title: layout.page_title.clone(), csrf_token, app_name: layout.app_name.clone(), app_version: layout.app_version.clone(), app_domain, app_description, analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( &analytics, &i18n_service, locale.as_str(), ) .await, username: user.username.clone(), email: user.email.clone(), roles_text: user.roles.join(", "), initials: user.initials.clone(), avatar_color: user.avatar_color.clone(), theme: preferences.theme.clone(), is_theme_light: preferences.theme == "light", is_theme_dark: preferences.theme == "dark", is_theme_auto: preferences.theme == "auto", language: preferences.language.clone(), is_language_fr: preferences.language == "fr", is_language_en: preferences.language == "en", is_language_de: preferences.language == "de", is_language_es: preferences.language == "es", is_language_it: preferences.language == "it", is_language_pt: preferences.language == "pt", current_locale, supported_languages, nav_items, has_pending_password_change: false, user_menu_profile: i18n_service .translate("user_menu.profile", locale_str) .await, user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, title: i18n_service .translate("preferences.title", locale_str) .await, subtitle: i18n_service .translate("preferences.subtitle", locale_str) .await, theme_label: i18n_service .translate("preferences.theme", locale_str) .await, theme_light: i18n_service .translate("preferences.theme_light", locale_str) .await, theme_light_desc: i18n_service .translate("preferences.theme_light_desc", locale_str) .await, theme_dark: i18n_service .translate("preferences.theme_dark", locale_str) .await, theme_dark_desc: i18n_service .translate("preferences.theme_dark_desc", locale_str) .await, theme_auto: i18n_service .translate("preferences.theme_auto", locale_str) .await, theme_auto_desc: i18n_service .translate("preferences.theme_auto_desc", locale_str) .await, theme_preview: i18n_service .translate("preferences.theme_preview", locale_str) .await, language_label: i18n_service .translate("preferences.language", locale_str) .await, language_fr: i18n_service .translate("preferences.language_fr", locale_str) .await, language_fr_desc: i18n_service .translate("preferences.language_fr_desc", locale_str) .await, language_en: i18n_service .translate("preferences.language_en", locale_str) .await, language_en_desc: i18n_service .translate("preferences.language_en_desc", locale_str) .await, language_de: i18n_service .translate("preferences.language_de", locale_str) .await, language_de_desc: i18n_service .translate("preferences.language_de_desc", locale_str) .await, language_es: i18n_service .translate("preferences.language_es", locale_str) .await, language_es_desc: i18n_service .translate("preferences.language_es_desc", locale_str) .await, language_it: i18n_service .translate("preferences.language_it", locale_str) .await, language_it_desc: i18n_service .translate("preferences.language_it_desc", locale_str) .await, language_pt: i18n_service .translate("preferences.language_pt", locale_str) .await, language_pt_desc: i18n_service .translate("preferences.language_pt_desc", locale_str) .await, translation_warning: i18n_service .translate("preferences.translation_warning", locale_str) .await, modified: i18n_service .translate("preferences.modified", locale_str) .await, modified_desc: i18n_service .translate("preferences.modified_desc", locale_str) .await, save_button, saving: i18n_service .translate("preferences.saving", locale_str) .await, cancel: i18n_service .translate("preferences.cancel", locale_str) .await, saved_success, saved_reload, saved_reload_json, saved_success_json, error_prefix, error_prefix_json, network_error, network_error_json, save_button_json, sync_title: i18n_service .translate("preferences.sync_title", locale_str) .await, sync_desc: i18n_service .translate("preferences.sync_desc", locale_str) .await, preview_dark: i18n_service .translate("preferences.preview_dark", locale_str) .await, preview_light: i18n_service .translate("preferences.preview_light", locale_str) .await, preview_auto: i18n_service .translate("preferences.preview_auto", locale_str) .await, breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, breadcrumb_settings: i18n_service .translate("breadcrumb.settings", locale_str) .await, breadcrumb_preferences: i18n_service .translate("breadcrumb.preferences", locale_str) .await, }; Ok(template) }
GitRust