use axum::extract::{Form, State}; use axum::response::{Html, IntoResponse, Response}; use http::StatusCode; use minijinja::context; use serde::{Deserialize, Serialize}; use std::fmt; use crate::db::{Database, DatabaseInterface, User, error::BoxedError}; use crate::http::{HttpSession, HttpState}; #[derive(Debug, Serialize)] pub struct SettingsFlash { success: bool, message: String, } impl SettingsFlash { pub fn success(message: String) -> Self { Self { success: true, message, } } pub fn error(message: String) -> Self { Self { success: false, message, } } } pub fn user_settings_page( state: &HttpState, session: &HttpSession, flash: Option<&SettingsFlash>, ) -> Response { let ctx = context! { flash => flash, user => session.user, }; let page = state .templates .get_template("user_settings.html") .unwrap() .render(ctx) .unwrap(); (StatusCode::OK, Html(page)).into_response() } pub async fn get_user_settings(State(state): State, session: HttpSession) -> Response { user_settings_page(&state, &session, None) } #[derive(Debug, Serialize)] pub enum PasswordChangeError { InvalidPreviousPassword, PasswordsDoNotMatch, EmptyPassword, } impl fmt::Display for PasswordChangeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, "{}", match self { Self::InvalidPreviousPassword => "The current password is wrong", Self::PasswordsDoNotMatch => "Provided new passwords do not match", Self::EmptyPassword => "You cannot set an empty password!", } ) } } impl std::error::Error for PasswordChangeError {} impl From for SettingsFlash { fn from(e: PasswordChangeError) -> Self { Self::error(e.to_string()) } } #[derive(Clone, Debug, Deserialize)] pub struct PasswordChangeForm { current_password: String, new_password: String, new_password_confirm: String, } impl PasswordChangeForm { pub async fn validate( &self, db: &Database, user: &User, ) -> Result, BoxedError> { if self.new_password.trim().is_empty() { return Ok(Err(PasswordChangeError::EmptyPassword)); } if self.new_password != self.new_password_confirm { return Ok(Err(PasswordChangeError::PasswordsDoNotMatch)); } // TODO: implement minimum password security (eg. minimum length) if db .check_password(&user.user_ref(), &self.current_password) .await? { Ok(Ok(())) } else { Ok(Err(PasswordChangeError::InvalidPreviousPassword)) } } } pub async fn post_change_password( State(mut state): State, session: HttpSession, Form(form): Form, ) -> Response { match form.validate(&state.db, &session.user).await { Ok(Ok(())) => { if let Err(e) = state .db .change_password(&session.user, form.new_password) .await { return format!("Database error: {e}").into_response(); } user_settings_page( &state, &session, Some(&SettingsFlash::success( "Your password was changed successfully".to_string(), )), ) } Ok(Err(e)) => user_settings_page(&state, &session, Some(&e.into())), Err(e) => format!("Database error: {e}").into_response(), } }