login_handler.rs
745 lignes · 24840 octets
/// Login Page Handler /// Feature 004: UI Post-Auth /// /// Handles the login page display (GET /login). use askama::Template; use axum::{ extract::{Extension, State}, http::HeaderMap, response::IntoResponse, }; use axum_extra::extract::CookieJar; use sea_orm::DatabaseConnection; use std::sync::Arc; use rustwarden_core::i18n::service::I18nService; use rustwarden_core::templates::LayoutContext; /// Login page template #[derive(Template)] #[template(path = "login.html")] struct LoginTemplate { app_name: String, app_version: String, current_locale: String, supported_languages: Vec<(String, String, bool)>, page_title: String, page_description: String, subtitle: String, tab_login: String, tab_register: String, login_title: String, register_title: String, username_label: String, password_label: String, email_label: String, username_placeholder: String, password_placeholder: String, username_placeholder_register: String, username_pattern_register: String, password_placeholder_register: String, email_placeholder: String, submit_button: String, register_submit: String, remember_me: String, forgot_password: String, email_not_verified: String, resend_verification: String, already_have_account: String, sign_in_link: String, footer_text: String, success_message: String, error_message: String, email_unverified_message: String, resend_button: String, resend_success: String, resend_error: String, resend_prompt: String, register_success: String, register_error: String, pw_min_length: usize, } /// Resend verification template #[derive(Template)] #[template(path = "resend_verification.html")] struct ResendVerificationTemplate { app_name: String, app_version: String, app_domain: String, app_description: String, analytics_opt_out: crate::analytics::AnalyticsOptOutContext, page_title: String, #[allow(dead_code)] // utilise par base.html (template parent) csrf_token: String, title: String, subtitle: String, email_label: String, email_placeholder: String, submit: String, back_to_login: String, error_default: String, success_default: String, error_missing_email: String, error_cannot_send: String, } /// Reset password page template #[derive(Template)] #[template(path = "reset_password.html")] struct ResetPasswordTemplate { app_name: String, app_version: String, app_domain: String, app_description: String, analytics_opt_out: crate::analytics::AnalyticsOptOutContext, page_title: String, #[allow(dead_code)] // utilise par base.html (template parent) csrf_token: String, reset_password_title: String, subtitle: String, new_password_label: String, new_password_placeholder: String, confirm_password_label: String, confirm_password_placeholder: String, password_requirements: String, submit_button: String, back_to_login: String, success_message: String, error_message: String, missing_token_url: String, missing_token: String, missing_new_password: String, passwords_not_match: String, reset_success_message: String, error_link_invalid: String, error_generic: String, } rustwarden_core::impl_template_response!(LoginTemplate); rustwarden_core::impl_template_response!(ResendVerificationTemplate); rustwarden_core::impl_template_response!(ForgotPasswordTemplate); rustwarden_core::impl_template_response!(ResetPasswordTemplate); /// Handler for GET /login - Login page (PUBLIC) pub async fn login_page( headers: HeaderMap, State(db): State<DatabaseConnection>, Extension(i18n_service): Extension<Arc<I18nService>>, ) -> Result<impl IntoResponse, rustwarden_core::error::AppError> { tracing::debug!("Rendering login page"); let jar = CookieJar::from_headers(&headers); let current_locale = jar .get("locale") .and_then(|c| { let value = c.value(); if matches!(value, "fr" | "en" | "de" | "es" | "it" | "pt") { Some(value.to_string()) } else { None } }) .unwrap_or_else(|| "fr".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 locale_str = ¤t_locale; let page_title = i18n_service.translate("login.page_title", locale_str).await; let page_description = i18n_service .translate("login.page_description", locale_str) .await; let subtitle = i18n_service.translate("login.subtitle", locale_str).await; let tab_login = i18n_service.translate("login.tab_login", locale_str).await; let tab_register = i18n_service .translate("login.tab_register", locale_str) .await; let login_title = i18n_service.translate("login.title", locale_str).await; let register_title = i18n_service .translate("login.register_title", locale_str) .await; let username_label = i18n_service.translate("login.username", locale_str).await; let password_label = i18n_service.translate("login.password", locale_str).await; let email_label = i18n_service .translate("login.email_label", locale_str) .await; let username_placeholder = i18n_service .translate("login.username_placeholder", locale_str) .await; let password_placeholder = i18n_service .translate("login.password_placeholder", locale_str) .await; let username_placeholder_register = i18n_service .translate("login.username_placeholder_register", locale_str) .await; let username_pattern_register = i18n_service .translate("login.username_pattern_register", locale_str) .await; let password_placeholder_register = i18n_service .translate("login.password_placeholder_register", locale_str) .await; let email_placeholder = i18n_service .translate("login.email_placeholder", locale_str) .await; let submit_button = i18n_service.translate("login.submit", locale_str).await; let register_submit = i18n_service .translate("login.register_submit", locale_str) .await; let remember_me = i18n_service .translate("login.remember_me", locale_str) .await; let forgot_password = i18n_service .translate("login.forgot_password", locale_str) .await; let email_not_verified = i18n_service .translate("login.email_not_verified", locale_str) .await; let resend_verification = i18n_service .translate("login.resend_verification", locale_str) .await; let already_have_account = i18n_service .translate("login.already_have_account", locale_str) .await; let sign_in_link = i18n_service .translate("login.sign_in_link", locale_str) .await; let footer_text = i18n_service .translate("login.footer_text", locale_str) .await; let success_message = i18n_service .translate("login.success_message", locale_str) .await; let error_message = i18n_service .translate("login.error_message", locale_str) .await; let email_unverified_message = i18n_service .translate("login.email_unverified_message", locale_str) .await; let resend_button = i18n_service .translate("login.resend_button", locale_str) .await; let resend_success = i18n_service .translate("login.resend_success", locale_str) .await; let resend_error = i18n_service .translate("login.resend_error", locale_str) .await; let resend_prompt = i18n_service .translate("login.resend_prompt", locale_str) .await; let register_success = i18n_service .translate("login.register_success", locale_str) .await; let register_error = i18n_service .translate("login.register_error", locale_str) .await; let layout = LayoutContext::new("", ""); let template = LoginTemplate { app_name: layout.app_name.clone(), app_version: layout.app_version.clone(), current_locale, supported_languages, page_title, page_description, subtitle, tab_login, tab_register, login_title, register_title, username_label, password_label, email_label, username_placeholder, password_placeholder, username_placeholder_register, username_pattern_register, password_placeholder_register, email_placeholder, submit_button, register_submit, remember_me, forgot_password, email_not_verified, resend_verification, already_have_account, sign_in_link, footer_text, success_message, error_message, email_unverified_message, resend_button, resend_success, resend_error, resend_prompt, register_success, register_error, pw_min_length: rustwarden_core::middleware::password_complexity::PasswordRules::from_db( &db, ) .await .min_length, }; Ok(template) } /// Page asking for an email so the API can send a password-reset link. #[derive(Template)] #[template(path = "forgot_password.html")] struct ForgotPasswordTemplate { app_name: String, app_version: String, app_domain: String, app_description: String, analytics_opt_out: crate::analytics::AnalyticsOptOutContext, page_title: String, #[allow(dead_code)] // used by the parent base.html csrf_token: String, title: String, subtitle: String, email_label: String, email_placeholder: String, submit: String, back_to_login: String, error_default: String, success_default: String, error_missing_email: String, error_cannot_send: String, } /// Handler for GET /forgot-password (PUBLIC). /// /// The form posts to `POST /api/v1/auth/forgot-password`, which always /// answers 200 so the page does not reveal whether the address exists. pub async fn forgot_password_page( headers: HeaderMap, State(db): State<DatabaseConnection>, Extension(i18n_service): Extension<Arc<I18nService>>, analytics: rustwarden_core::config::analytics::AnalyticsContext, ) -> Result<impl IntoResponse, rustwarden_core::error::AppError> { let jar = CookieJar::from_headers(&headers); let current_locale = jar .get("locale") .and_then(|c| { let value = c.value(); if matches!(value, "fr" | "en" | "de" | "es" | "it" | "pt") { Some(value.to_string()) } else { None } }) .unwrap_or_else(|| "fr".to_string()); let locale_str = current_locale.as_str(); let layout = LayoutContext::new("", ""); let app_domain = rustwarden_core::services::app_settings_service::AppSettingsService::get_setting( &db, "app_domain", ) .await? .map(|s| s.value) .unwrap_or_else(|| "localhost".to_string()); let app_description = rustwarden_core::services::app_settings_service::AppSettingsService::get_setting( &db, "app_description", ) .await? .map(|s| s.value) .unwrap_or_else(|| "User accounts and authentication".to_string()); Ok(ForgotPasswordTemplate { 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_str, ) .await, page_title: i18n_service .translate("forgot_password.page_title", locale_str) .await, csrf_token: String::new(), title: i18n_service .translate("forgot_password.title", locale_str) .await, subtitle: i18n_service .translate("forgot_password.subtitle", locale_str) .await, email_label: i18n_service .translate("forgot_password.email_label", locale_str) .await, email_placeholder: i18n_service .translate("forgot_password.email_placeholder", locale_str) .await, submit: i18n_service .translate("forgot_password.submit", locale_str) .await, back_to_login: i18n_service .translate("forgot_password.back_to_login", locale_str) .await, error_default: i18n_service .translate("forgot_password.error_default", locale_str) .await, success_default: i18n_service .translate("forgot_password.success_default", locale_str) .await, error_missing_email: i18n_service .translate("forgot_password.error_missing_email", locale_str) .await, error_cannot_send: i18n_service .translate("forgot_password.error_cannot_send", locale_str) .await, }) } /// Handler for GET /resend-verification - Resend email verification page (PUBLIC) pub async fn resend_verification_page( headers: HeaderMap, State(db): State<DatabaseConnection>, Extension(i18n_service): Extension<Arc<I18nService>>, analytics: rustwarden_core::config::analytics::AnalyticsContext, ) -> Result<impl IntoResponse, rustwarden_core::error::AppError> { tracing::debug!("Rendering resend verification page"); let jar = CookieJar::from_headers(&headers); let current_locale = jar .get("locale") .and_then(|c| { let value = c.value(); if matches!(value, "fr" | "en" | "de" | "es" | "it" | "pt") { Some(value.to_string()) } else { None } }) .unwrap_or_else(|| "fr".to_string()); let locale_str = ¤t_locale; let page_title = i18n_service .translate("resend_verification.page_title", locale_str) .await; let title = i18n_service .translate("resend_verification.title", locale_str) .await; let subtitle = i18n_service .translate("resend_verification.subtitle", locale_str) .await; let email_label = i18n_service .translate("resend_verification.email_label", locale_str) .await; let email_placeholder = i18n_service .translate("resend_verification.email_placeholder", locale_str) .await; let submit = i18n_service .translate("resend_verification.submit", locale_str) .await; let back_to_login = i18n_service .translate("resend_verification.back_to_login", locale_str) .await; let error_default = i18n_service .translate("resend_verification.error_default", locale_str) .await; let success_default = i18n_service .translate("resend_verification.success_default", locale_str) .await; let error_missing_email = i18n_service .translate("resend_verification.error_missing_email", locale_str) .await; let error_cannot_send = i18n_service .translate("resend_verification.error_cannot_send", locale_str) .await; let layout = LayoutContext::new("", ""); use rustwarden_core::services::app_settings_service::AppSettingsService; 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 template = ResendVerificationTemplate { 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, current_locale.as_str(), ) .await, page_title, csrf_token: String::new(), title, subtitle, email_label, email_placeholder, submit, back_to_login, error_default, success_default, error_missing_email, error_cannot_send, }; Ok(template) } /// Query parameters for reset password page #[derive(serde::Deserialize)] pub struct ResetPasswordQuery { #[allow(dead_code)] token: Option<String>, } /// Handler for GET /reset-password?token={token} - Reset password page (PUBLIC) pub async fn reset_password_page( headers: HeaderMap, State(db): State<DatabaseConnection>, Extension(i18n_service): Extension<Arc<I18nService>>, analytics: rustwarden_core::config::analytics::AnalyticsContext, axum::extract::Query(_params): axum::extract::Query<ResetPasswordQuery>, ) -> Result<impl IntoResponse, rustwarden_core::error::AppError> { tracing::debug!("Rendering reset password page"); let jar = CookieJar::from_headers(&headers); let current_locale = jar .get("locale") .and_then(|c| { let value = c.value(); if matches!(value, "fr" | "en" | "de" | "es" | "it" | "pt") { Some(value.to_string()) } else { None } }) .unwrap_or_else(|| "fr".to_string()); let locale_str = ¤t_locale; let page_title = i18n_service .translate("password_reset.page_title", locale_str) .await; let reset_password_title = i18n_service .translate("password_reset.title", locale_str) .await; let subtitle = i18n_service .translate("password_reset.subtitle", locale_str) .await; let new_password_label = i18n_service .translate("password_reset.new_password_label", locale_str) .await; let new_password_placeholder = i18n_service .translate("password_reset.new_password_placeholder", locale_str) .await; let confirm_password_label = i18n_service .translate("password_reset.confirm_password_label", locale_str) .await; let confirm_password_placeholder = i18n_service .translate("password_reset.confirm_password_placeholder", locale_str) .await; let password_requirements = i18n_service .translate("password_reset.requirements", locale_str) .await; let submit_button = i18n_service.translate("login.submit", locale_str).await; let back_to_login = i18n_service .translate("login.sign_in_link", locale_str) .await; let success_message = i18n_service .translate("login.success_message", locale_str) .await; let error_message = i18n_service .translate("login.error_message", locale_str) .await; let missing_token_url = i18n_service .translate("password_reset.missing_token_url", locale_str) .await; let missing_token = i18n_service .translate("password_reset.missing_token", locale_str) .await; let missing_new_password = i18n_service .translate("password_reset.missing_new_password", locale_str) .await; let passwords_not_match = i18n_service .translate("password_reset.passwords_not_match", locale_str) .await; let reset_success_message = i18n_service .translate("password_reset.success_message", locale_str) .await; let error_link_invalid = i18n_service .translate("password_reset.error_link_invalid", locale_str) .await; let error_generic = i18n_service .translate("password_reset.error_generic", locale_str) .await; let layout = LayoutContext::new("", ""); use rustwarden_core::services::app_settings_service::AppSettingsService; 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 template = ResetPasswordTemplate { 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, current_locale.as_str(), ) .await, page_title, csrf_token: String::new(), reset_password_title, subtitle, new_password_label, new_password_placeholder, confirm_password_label, confirm_password_placeholder, password_requirements, submit_button, back_to_login, success_message, error_message, missing_token_url, missing_token, missing_new_password, passwords_not_match, reset_success_message, error_link_invalid, error_generic, }; Ok(template) } /// Login 2FA page template #[derive(Template)] #[template(path = "login_2fa.html")] #[allow(dead_code)] struct Login2faTemplate { app_name: String, app_version: String, page_title: String, title: String, subtitle: String, code_label: String, backup_code_label: String, verify_button: String, use_backup_code: String, use_totp_code: String, back_to_login: String, error_invalid_code: String, error_network: String, } rustwarden_core::impl_template_response!(Login2faTemplate); /// GET /login/2fa — 2FA verification page pub async fn login_2fa_page( headers: HeaderMap, Extension(i18n_service): Extension<Arc<I18nService>>, ) -> Result<impl IntoResponse, rustwarden_core::error::AppError> { let jar = CookieJar::from_headers(&headers); let current_locale = jar .get("locale") .and_then(|c| { let value = c.value(); if matches!(value, "fr" | "en" | "de" | "es" | "it" | "pt") { Some(value.to_string()) } else { None } }) .unwrap_or_else(|| "fr".to_string()); let locale_str = ¤t_locale; let layout = LayoutContext::new("", ""); let template = Login2faTemplate { app_name: layout.app_name.clone(), app_version: layout.app_version.clone(), page_title: i18n_service .translate("login_2fa.page_title", locale_str) .await, title: i18n_service.translate("login_2fa.title", locale_str).await, subtitle: i18n_service .translate("login_2fa.subtitle", locale_str) .await, code_label: i18n_service .translate("login_2fa.code_label", locale_str) .await, backup_code_label: i18n_service .translate("login_2fa.backup_code_label", locale_str) .await, verify_button: i18n_service .translate("login_2fa.verify_button", locale_str) .await, use_backup_code: i18n_service .translate("login_2fa.use_backup_code", locale_str) .await, use_totp_code: i18n_service .translate("login_2fa.use_totp_code", locale_str) .await, back_to_login: i18n_service .translate("login_2fa.back_to_login", locale_str) .await, error_invalid_code: i18n_service .translate("login_2fa.error_invalid_code", locale_str) .await, error_network: i18n_service .translate("login_2fa.error_network", locale_str) .await, }; Ok(template) }
GitRust