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)
}