use std::{collections::HashMap, net::SocketAddr}; use axum::{ Form, body::Body, debug_handler, extract::{ConnectInfo, Extension, Request, State}, http::HeaderMap, response::{Html, IntoResponse, Redirect, Response}, }; use axum_extra::extract::{ Host, Query, cookie::{Cookie, CookieJar}, }; use chrono::Duration; use lettre::Address; use rinja::Template; use serde::Deserialize; use tracing::{Level, event}; use crate::{ AppState, Result, config::Config, consts, data::{db, model}, email, html_templates::*, translation::{self, Sentence}, utils, }; /// SIGN UP /// #[debug_handler] pub async fn sign_up_get( State(connection): State, Extension(user): Extension>, Extension(tr): Extension, ) -> Result { if connection.get_new_user_registration_enabled().await? { Ok(Html( SignUpFormTemplate { user, tr, email: String::new(), message: "", message_email: "", message_password: "", } .render()?, ) .into_response()) } else { Ok( Html(MessageTemplate::new_with_user(tr.t(Sentence::SignUpClosed), tr, user).render()?) .into_response(), ) } } #[derive(Deserialize, Debug)] pub struct SignUpFormData { email: String, password_1: String, password_2: String, } enum SignUpError { InvalidEmail, PasswordsNotEqual, InvalidPassword, UserAlreadyExists, DatabaseError, UnableSendEmail, } #[debug_handler(state = AppState)] pub async fn sign_up_post( Host(host): Host, State(connection): State, State(config): State, Extension(user): Extension>, Extension(tr): Extension, Form(form_data): Form, ) -> Result { fn error_response( error: SignUpError, form_data: &SignUpFormData, user: Option, tr: translation::Tr, ) -> Result { let invalid_password_mess = &tr.tp( Sentence::InvalidPassword, &[Box::new(common::consts::MIN_PASSWORD_SIZE)], ); Ok(Html( SignUpFormTemplate { user, email: form_data.email.clone(), message_email: match error { SignUpError::InvalidEmail => tr.t(Sentence::InvalidEmail), _ => "", }, message_password: match error { SignUpError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch), SignUpError::InvalidPassword => invalid_password_mess, _ => "", }, message: match error { SignUpError::UserAlreadyExists => tr.t(Sentence::EmailAlreadyTaken), SignUpError::DatabaseError => tr.t(Sentence::DatabaseError), SignUpError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail), _ => "", }, tr, } .render()?, ) .into_response()) } if !connection.get_new_user_registration_enabled().await? { return Ok(Html( MessageTemplate::new_with_user(tr.t(Sentence::SignUpClosed), tr, user).render()?, ) .into_response()); } // Validation of email and password. if form_data.email.parse::
().is_err() { return error_response(SignUpError::InvalidEmail, &form_data, user, tr); } if form_data.password_1 != form_data.password_2 { return error_response(SignUpError::PasswordsNotEqual, &form_data, user, tr); } if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form_data.password_1) { return error_response(SignUpError::InvalidPassword, &form_data, user, tr); } match connection .sign_up(&form_data.email, &form_data.password_1) .await { Ok(db::user::SignUpResult::UserAlreadyExists) => { error_response(SignUpError::UserAlreadyExists, &form_data, user, tr) } Ok(db::user::SignUpResult::UserCreatedWaitingForValidation(token)) => { let url = utils::get_url_from_host(&host); let email = form_data.email.clone(); match email::send_email( &email, &tr.tp( Sentence::SignUpFollowEmailLink, &[Box::new(format!( "{}/validation?validation_token={}", url, token ))], ), &config.smtp_relay_address, &config.smtp_login, &config.smtp_password, ) .await { Ok(()) => Ok(Html( MessageTemplate::new_with_user(tr.t(Sentence::SignUpEmailSent), tr, user) .render()?, ) .into_response()), Err(_) => { // error!("Email validation error: {}", error); // TODO: log error_response(SignUpError::UnableSendEmail, &form_data, user, tr) } } } Err(_) => { // error!("Signup database error: {}", error); // TODO: log error_response(SignUpError::DatabaseError, &form_data, user, tr) } } } #[debug_handler] pub async fn sign_up_validation( State(connection): State, Extension(user): Extension>, Extension(tr): Extension, ConnectInfo(addr): ConnectInfo, Query(query): Query>, headers: HeaderMap, ) -> Result<(CookieJar, impl IntoResponse)> { let mut jar = CookieJar::from_headers(&headers); if user.is_some() { return Ok(( jar, Html( MessageTemplate::new_with_user( tr.t(Sentence::ValidationUserAlreadyExists), tr, user, ) .render()?, ), )); } let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr); match query.get("validation_token") { // 'validation_token' exists only when a user tries to validate a new account. Some(token) => { match connection .validation( token, Duration::seconds(consts::VALIDATION_TOKEN_DURATION), &client_ip, &client_user_agent, ) .await? { db::user::ValidationResult::Ok(token, user_id) => { let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token); jar = jar.add(cookie); let user = connection.load_user(user_id).await?; Ok(( jar, Html( MessageTemplate::new_with_user( tr.t(Sentence::SignUpEmailValidationSuccess), tr, user, ) .render()?, ), )) } db::user::ValidationResult::ValidationExpired => Ok(( jar, Html( MessageTemplate::new_with_user( tr.t(Sentence::SignUpValidationExpired), tr, user, ) .render()?, ), )), db::user::ValidationResult::UnknownUser => Ok(( jar, Html( MessageTemplate::new_with_user( tr.t(Sentence::SignUpValidationErrorTryAgain), tr, user, ) .render()?, ), )), } } None => Ok(( jar, Html( MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user) .render()?, ), )), } } /// SIGN IN /// #[debug_handler] pub async fn sign_in_get( Extension(user): Extension>, Extension(tr): Extension, ) -> Result { Ok(Html( SignInFormTemplate { user, tr, email: "", message: "", } .render()?, )) } #[derive(Deserialize, Debug)] pub struct SignInFormData { email: String, password: String, } #[debug_handler] pub async fn sign_in_post( ConnectInfo(addr): ConnectInfo, State(connection): State, Extension(user): Extension>, Extension(tr): Extension, headers: HeaderMap, Form(form_data): Form, ) -> Result<(CookieJar, Response)> { let jar = CookieJar::from_headers(&headers); let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr); match connection .sign_in( &form_data.email, &form_data.password, &client_ip, &client_user_agent, ) .await? { db::user::SignInResult::AccountNotValidated => Ok(( jar, Html( SignInFormTemplate { user, email: &form_data.email, message: tr.t(Sentence::AccountMustBeValidatedFirst), tr, } .render()?, ) .into_response(), )), db::user::SignInResult::UserNotFound | db::user::SignInResult::WrongPassword => Ok(( jar, Html( SignInFormTemplate { user, email: &form_data.email, message: tr.t(Sentence::WrongEmailOrPassword), tr, } .render()?, ) .into_response(), )), db::user::SignInResult::Ok(token, _user_id) => { let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token); Ok((jar.add(cookie), Redirect::to("/").into_response())) } } } /// SIGN OUT /// #[debug_handler] pub async fn sign_out( State(connection): State, req: Request, ) -> Result<(CookieJar, Redirect)> { let mut jar = CookieJar::from_headers(req.headers()); if let Some(token_cookie) = jar.get(consts::COOKIE_AUTH_TOKEN_NAME) { let token = token_cookie.value().to_string(); jar = jar.remove(consts::COOKIE_AUTH_TOKEN_NAME); connection.sign_out(&token).await?; } Ok((jar, Redirect::to("/"))) } /// RESET PASSWORD /// #[debug_handler] pub async fn ask_reset_password_get( Extension(user): Extension>, Extension(tr): Extension, ) -> Result { if user.is_some() { Ok(Html( MessageTemplate::new_with_user(tr.t(Sentence::AskResetAlreadyLoggedInError), tr, user) .render()?, ) .into_response()) } else { Ok(Html( AskResetPasswordTemplate { user, tr, email: "", message: "", message_email: "", } .render()?, ) .into_response()) } } #[derive(Deserialize, Debug)] pub struct AskResetPasswordForm { email: String, } enum AskResetPasswordError { InvalidEmail, EmailAlreadyReset, EmailUnknown, UnableSendEmail, DatabaseError, } #[debug_handler(state = AppState)] pub async fn ask_reset_password_post( Host(host): Host, State(connection): State, State(config): State, Extension(user): Extension>, Extension(tr): Extension, Form(form_data): Form, ) -> Result { fn error_response( error: AskResetPasswordError, email: &str, user: Option, tr: translation::Tr, ) -> Result { Ok(Html( AskResetPasswordTemplate { user, email, message_email: match error { AskResetPasswordError::InvalidEmail => tr.t(Sentence::InvalidEmail), _ => "", }, message: match error { AskResetPasswordError::EmailAlreadyReset => { tr.t(Sentence::AskResetEmailAlreadyResetError) } AskResetPasswordError::EmailUnknown => tr.t(Sentence::EmailUnknown), AskResetPasswordError::UnableSendEmail => { tr.t(Sentence::UnableToSendResetEmail) } AskResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError), _ => "", }, tr, } .render()?, ) .into_response()) } // Validation of email. if form_data.email.parse::
().is_err() { return error_response( AskResetPasswordError::InvalidEmail, &form_data.email, user, tr, ); } match connection .get_token_reset_password( &form_data.email, Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION), ) .await { Ok(db::user::GetTokenResetPasswordResult::PasswordAlreadyReset) => error_response( AskResetPasswordError::EmailAlreadyReset, &form_data.email, user, tr, ), Ok(db::user::GetTokenResetPasswordResult::EmailUnknown) => error_response( AskResetPasswordError::EmailUnknown, &form_data.email, user, tr, ), Ok(db::user::GetTokenResetPasswordResult::Ok(token)) => { let url = utils::get_url_from_host(&host); match email::send_email( &form_data.email, &tr.tp( Sentence::AskResetFollowEmailLink, &[Box::new(format!( "{}/reset_password?reset_token={}", url, token ))], ), &config.smtp_relay_address, &config.smtp_login, &config.smtp_password, ) .await { Ok(()) => Ok(Html( MessageTemplate::new_with_user(tr.t(Sentence::AskResetEmailSent), tr, user) .render()?, ) .into_response()), Err(_) => { // error!("Email validation error: {}", error); // TODO: log error_response( AskResetPasswordError::UnableSendEmail, &form_data.email, user, tr, ) } } } Err(error) => { event!(Level::ERROR, "{}", error); error_response( AskResetPasswordError::DatabaseError, &form_data.email, user, tr, ) } } } #[debug_handler] pub async fn reset_password_get( State(connection): State, Extension(user): Extension>, Extension(tr): Extension, Query(query): Query>, ) -> Result { if let Some(reset_token) = query.get("reset_token") { // Check if the token is valid. if connection .is_reset_password_token_valid( reset_token, Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION), ) .await? { Ok(Html( ResetPasswordTemplate { user, tr, reset_token, message: "", message_password: "", } .render()?, ) .into_response()) } else { Ok(Html( MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user) .render()?, ) .into_response()) } } else { Ok(Html( MessageTemplate::new_with_user(tr.t(Sentence::AskResetTokenMissing), tr, user) .render()?, ) .into_response()) } } #[derive(Deserialize, Debug)] pub struct ResetPasswordForm { password_1: String, password_2: String, reset_token: String, } enum ResetPasswordError { PasswordsNotEqual, InvalidPassword, TokenExpired, DatabaseError, } #[debug_handler] pub async fn reset_password_post( State(connection): State, Extension(user): Extension>, Extension(tr): Extension, Form(form_data): Form, ) -> Result { fn error_response( error: ResetPasswordError, form_data: &ResetPasswordForm, user: Option, tr: translation::Tr, ) -> Result { let reset_password_mess = &tr.tp( Sentence::InvalidPassword, &[Box::new(common::consts::MIN_PASSWORD_SIZE)], ); Ok(Html( ResetPasswordTemplate { user, reset_token: &form_data.reset_token, message_password: match error { ResetPasswordError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch), ResetPasswordError::InvalidPassword => reset_password_mess, _ => "", }, message: match error { ResetPasswordError::TokenExpired => tr.t(Sentence::AskResetTokenExpired), ResetPasswordError::DatabaseError => tr.t(Sentence::DatabaseError), _ => "", }, tr, } .render()?, ) .into_response()) } if form_data.password_1 != form_data.password_2 { return error_response(ResetPasswordError::PasswordsNotEqual, &form_data, user, tr); } if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form_data.password_1) { return error_response(ResetPasswordError::InvalidPassword, &form_data, user, tr); } match connection .reset_password( &form_data.password_1, &form_data.reset_token, Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION), ) .await { Ok(db::user::ResetPasswordResult::Ok) => Ok(Html( MessageTemplate::new_with_user(tr.t(Sentence::PasswordReset), tr, user).render()?, ) .into_response()), Ok(db::user::ResetPasswordResult::ResetTokenExpired) => { error_response(ResetPasswordError::TokenExpired, &form_data, user, tr) } Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, user, tr), } } /// EDIT PROFILE /// #[debug_handler] pub async fn edit_user_get( Extension(user): Extension>, Extension(tr): Extension, ) -> Result { Ok(if let Some(user) = user { Html( ProfileTemplate { username: &user.name, email: &user.email, default_servings: user.default_servings, message: "", message_email: "", message_password: "", user: Some(user.clone()), tr, } .render()?, ) .into_response() } else { Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response() }) } #[derive(Deserialize, Debug)] pub struct EditUserForm { name: String, email: String, default_servings: u32, password_1: String, password_2: String, } enum ProfileUpdateError { InvalidEmail, EmailAlreadyTaken, PasswordsNotEqual, InvalidPassword, DatabaseError, UnableSendEmail, } // TODO: A lot of code are similar to 'sign_up_post', maybe find a way to factorize some. #[debug_handler(state = AppState)] pub async fn edit_user_post( Host(host): Host, State(connection): State, State(config): State, Extension(user): Extension>, Extension(tr): Extension, Form(form_data): Form, ) -> Result { if let Some(user) = user { fn error_response( error: ProfileUpdateError, form_data: &EditUserForm, user: model::User, tr: translation::Tr, ) -> Result { let invalid_password_mess = &tr.tp( Sentence::InvalidPassword, &[Box::new(common::consts::MIN_PASSWORD_SIZE)], ); Ok(Html( ProfileTemplate { user: Some(user), username: &form_data.name, email: &form_data.email, default_servings: form_data.default_servings, message_email: match error { ProfileUpdateError::InvalidEmail => tr.t(Sentence::InvalidEmail), ProfileUpdateError::EmailAlreadyTaken => tr.t(Sentence::EmailAlreadyTaken), _ => "", }, message_password: match error { ProfileUpdateError::PasswordsNotEqual => tr.t(Sentence::PasswordDontMatch), ProfileUpdateError::InvalidPassword => invalid_password_mess, _ => "", }, message: match error { ProfileUpdateError::DatabaseError => tr.t(Sentence::DatabaseError), ProfileUpdateError::UnableSendEmail => tr.t(Sentence::UnableToSendEmail), _ => "", }, tr, } .render()?, ) .into_response()) } if form_data.email.parse::
().is_err() { return error_response(ProfileUpdateError::InvalidEmail, &form_data, user, tr); } let new_password = if !form_data.password_1.is_empty() || !form_data.password_2.is_empty() { if form_data.password_1 != form_data.password_2 { return error_response(ProfileUpdateError::PasswordsNotEqual, &form_data, user, tr); } if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form_data.password_1) { return error_response(ProfileUpdateError::InvalidPassword, &form_data, user, tr); } Some(form_data.password_1.as_ref()) } else { None }; let email_trimmed = form_data.email.trim(); let message: &str; match connection .update_user( user.id, Some(email_trimmed), Some(&form_data.name), Some(form_data.default_servings), new_password, ) .await { Ok(db::user::UpdateUserResult::EmailAlreadyTaken) => { return error_response(ProfileUpdateError::EmailAlreadyTaken, &form_data, user, tr); } Ok(db::user::UpdateUserResult::UserUpdatedWaitingForRevalidation(token)) => { let url = utils::get_url_from_host(&host); let email = form_data.email.clone(); match email::send_email( &email, &tr.tp( Sentence::ProfileFollowEmailLink, &[Box::new(format!( "{}/revalidation?validation_token={}", url, token ))], ), &config.smtp_relay_address, &config.smtp_login, &config.smtp_password, ) .await { Ok(()) => { message = tr.t(Sentence::ProfileEmailSent); } Err(_) => { // error!("Email validation error: {}", error); // TODO: log return error_response( ProfileUpdateError::UnableSendEmail, &form_data, user, tr, ); } } } Ok(db::user::UpdateUserResult::Ok) => { message = tr.t(Sentence::ProfileSaved); } Err(_) => { return error_response(ProfileUpdateError::DatabaseError, &form_data, user, tr); } } // Reload after update. let user = connection.load_user(user.id).await?; Ok(Html( ProfileTemplate { user, username: &form_data.name, email: &form_data.email, default_servings: form_data.default_servings, message, message_email: "", message_password: "", tr, } .render()?, ) .into_response()) } else { Ok(Html(MessageTemplate::new(tr.t(Sentence::NotLoggedIn), tr).render()?).into_response()) } } #[debug_handler] pub async fn email_revalidation( State(connection): State, Extension(user): Extension>, Extension(tr): Extension, ConnectInfo(addr): ConnectInfo, Query(query): Query>, headers: HeaderMap, ) -> Result<(CookieJar, impl IntoResponse)> { let mut jar = CookieJar::from_headers(&headers); if user.is_some() { return Ok(( jar, Html( MessageTemplate::new_with_user( tr.t(Sentence::ValidationUserAlreadyExists), tr, user, ) .render()?, ), )); } let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr); match query.get("validation_token") { // 'validation_token' exists only when a user must validate a new email. Some(token) => { match connection .validation( token, Duration::seconds(consts::VALIDATION_TOKEN_DURATION), &client_ip, &client_user_agent, ) .await? { db::user::ValidationResult::Ok(token, user_id) => { let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token); jar = jar.add(cookie); let user = connection.load_user(user_id).await?; Ok(( jar, Html( MessageTemplate::new_with_user( tr.t(Sentence::ValidationSuccessful), tr, user, ) .render()?, ), )) } db::user::ValidationResult::ValidationExpired => Ok(( jar, Html( MessageTemplate::new_with_user(tr.t(Sentence::ValidationExpired), tr, user) .render()?, ), )), db::user::ValidationResult::UnknownUser => Ok(( jar, Html( MessageTemplate::new_with_user( tr.t(Sentence::ValidationErrorTryToSignUpAgain), tr, user, ) .render()?, ), )), } } None => Ok(( jar, Html( MessageTemplate::new_with_user(tr.t(Sentence::ValidationError), tr, user) .render()?, ), )), } }