llldap/src/http/user_settings.rs

145 lines
3.7 KiB
Rust
Raw Normal View History

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<HttpState>, 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<PasswordChangeError> 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<Result<(), PasswordChangeError>, 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<HttpState>,
session: HttpSession,
Form(form): Form<PasswordChangeForm>,
) -> 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(),
}
}