diff --git a/backend/src/consts.rs b/backend/src/consts.rs index c6749e3..8733910 100644 --- a/backend/src/consts.rs +++ b/backend/src/consts.rs @@ -38,6 +38,8 @@ pub const COOKIE_LANG_NAME: &str = "lang"; /// (cookie authentication, password reset, validation token). pub const TOKEN_SIZE: usize = 32; +pub const EMAIL_ADDRESS: &str = "recipes@recipes.gburri.org"; + /// When sending a validation email, /// the server has this duration to wait for a response from the SMTP server. pub const SEND_EMAIL_TIMEOUT: Duration = Duration::from_secs(60); diff --git a/backend/src/data/db/user.rs b/backend/src/data/db/user.rs index bde7fe8..fcca817 100644 --- a/backend/src/data/db/user.rs +++ b/backend/src/data/db/user.rs @@ -1,6 +1,7 @@ use chrono::{Duration, prelude::*}; use rand::distr::{Alphanumeric, SampleString}; use sqlx::Sqlite; +use strum_macros::Display; use super::{Connection, DBError, Result}; use crate::{ @@ -9,27 +10,27 @@ use crate::{ hash::{hash, verify_password}, }; -#[derive(Debug)] +#[derive(Debug, Display)] pub enum SignUpResult { UserAlreadyExists, UserCreatedWaitingForValidation(String), // Validation token. } -#[derive(Debug)] +#[derive(Debug, Display)] pub enum UpdateUserResult { EmailAlreadyTaken, UserUpdatedWaitingForRevalidation(String), // Validation token. Ok, } -#[derive(Debug)] +#[derive(Debug, Display)] pub enum ValidationResult { UnknownUser, ValidationExpired, Ok(String, i64), // Returns token and user id. } -#[derive(Debug)] +#[derive(Debug, Display)] pub enum SignInResult { UserNotFound, WrongPassword, @@ -37,20 +38,20 @@ pub enum SignInResult { Ok(String, i64), // Returns token and user id. } -#[derive(Debug)] +#[derive(Debug, Display)] pub enum AuthenticationResult { NotValidToken, Ok(i64), // Returns user id. } -#[derive(Debug)] +#[derive(Debug, Display)] pub enum GetTokenResetPasswordResult { PasswordAlreadyReset, EmailUnknown, Ok(String), } -#[derive(Debug)] +#[derive(Debug, Display)] pub enum ResetPasswordResult { ResetTokenExpired, Ok, diff --git a/backend/src/email.rs b/backend/src/email.rs index 96f0b40..73c09b2 100644 --- a/backend/src/email.rs +++ b/backend/src/email.rs @@ -30,7 +30,7 @@ pub async fn send_email( ) -> Result<(), Error> { let email = Message::builder() .message_id(None) - .from("recipes@recipes.gburri.org".parse()?) + .from(consts::EMAIL_ADDRESS.parse()?) .to(email.parse()?) .subject(title) .body(message.to_string())?; diff --git a/backend/src/log.rs b/backend/src/log.rs index 054be4d..a2dbe30 100644 --- a/backend/src/log.rs +++ b/backend/src/log.rs @@ -30,7 +30,7 @@ const TRACING_DISPLAY_THREAD: bool = false; #[derive(Clone)] pub struct Log { - guard: Arc, + _guard: Arc, directory: PathBuf, } @@ -69,7 +69,7 @@ impl Log { .init(); Log { - guard: Arc::new(guard), + _guard: Arc::new(guard), directory: directory.as_ref().to_path_buf(), } } diff --git a/backend/src/services/user.rs b/backend/src/services/user.rs index 815dc16..1e96c63 100644 --- a/backend/src/services/user.rs +++ b/backend/src/services/user.rs @@ -16,6 +16,7 @@ use axum_extra::extract::{ use chrono::Duration; use lettre::Address; use serde::Deserialize; +use strum_macros::Display; use tracing::{Level, event}; use crate::{ @@ -23,6 +24,8 @@ use crate::{ translation::Sentence, utils, }; +const VALIDATION_TOKEN_KEY: &str = "validation_token"; + /// SIGN UP /// #[debug_handler] @@ -62,6 +65,7 @@ pub struct SignUpFormData { password_2: String, } +#[derive(Display)] enum SignUpError { InvalidEmail, PasswordsNotEqual, @@ -84,6 +88,13 @@ pub async fn sign_up_post( form_data: &SignUpFormData, context: Context, ) -> Result { + event!( + Level::WARN, + "[Sign up] Unable to sign up with email {}: {}", + form_data.email, + error + ); + let invalid_password_mess = &context.tr.tp( Sentence::InvalidPassword, &[Box::new(common::consts::MIN_PASSWORD_SIZE)], @@ -160,8 +171,8 @@ pub async fn sign_up_post( &context.tr.tp( Sentence::SignUpFollowEmailLink, &[Box::new(format!( - "{}/validation?validation_token={}", - url, token + "{}/validation?{}={}", + url, VALIDATION_TOKEN_KEY, token ))], ), &config.smtp_relay_address, @@ -179,16 +190,10 @@ pub async fn sign_up_post( .render()?, ) .into_response()), - Err(_) => { - // error!("Email validation error: {}", error); // TODO: log - error_response(SignUpError::UnableSendEmail, &form_data, context) - } + Err(_) => error_response(SignUpError::UnableSendEmail, &form_data, context), } } - Err(_) => { - // error!("Signup database error: {}", error); // TODO: log - error_response(SignUpError::DatabaseError, &form_data, context) - } + Err(_) => error_response(SignUpError::DatabaseError, &form_data, context), } } @@ -201,7 +206,12 @@ pub async fn sign_up_validation( headers: HeaderMap, ) -> Result<(CookieJar, impl IntoResponse)> { let mut jar = CookieJar::from_headers(&headers); - if context.user.is_some() { + if let Some(ref user) = context.user { + event!( + Level::WARN, + "[Sign up] Unable to validate: user already logged. Email: {}", + user.email + ); return Ok(( jar, Html( @@ -215,7 +225,7 @@ pub async fn sign_up_validation( )); } let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr); - match query.get("validation_token") { + match query.get(VALIDATION_TOKEN_KEY) { // 'validation_token' exists only when a user tries to validate a new account. Some(token) => { match connection @@ -244,41 +254,61 @@ pub async fn sign_up_validation( ), )) } - db::user::ValidationResult::ValidationExpired => Ok(( - jar, - Html( - MessageTemplate::new_with_user( - context.tr.t(Sentence::SignUpValidationExpired), - context.tr, - context.user, - ) - .render()?, - ), - )), - db::user::ValidationResult::UnknownUser => Ok(( - jar, - Html( - MessageTemplate::new_with_user( - context.tr.t(Sentence::SignUpValidationErrorTryAgain), - context.tr, - context.user, - ) - .render()?, - ), - )), + db::user::ValidationResult::ValidationExpired => { + event!( + Level::WARN, + "[Sign up] Unable to validate: validation expired. Token: {}", + token + ); + Ok(( + jar, + Html( + MessageTemplate::new_with_user( + context.tr.t(Sentence::SignUpValidationExpired), + context.tr, + context.user, + ) + .render()?, + ), + )) + } + db::user::ValidationResult::UnknownUser => { + event!( + Level::WARN, + "[Sign up] Unable to validate: unknown user. Token: {}", + token + ); + Ok(( + jar, + Html( + MessageTemplate::new_with_user( + context.tr.t(Sentence::SignUpValidationErrorTryAgain), + context.tr, + context.user, + ) + .render()?, + ), + )) + } } } - None => Ok(( - jar, - Html( - MessageTemplate::new_with_user( - context.tr.t(Sentence::ValidationError), - context.tr, - context.user, - ) - .render()?, - ), - )), + None => { + event!( + Level::WARN, + "[Sign up] Unable to validate: no token provided" + ); + Ok(( + jar, + Html( + MessageTemplate::new_with_user( + context.tr.t(Sentence::ValidationError), + context.tr, + context.user, + ) + .render()?, + ), + )) + } } } @@ -322,30 +352,46 @@ pub async fn sign_in_post( ) .await? { - db::user::SignInResult::AccountNotValidated => Ok(( - jar, - Html( - SignInFormTemplate { - email: &form_data.email, - message: context.tr.t(Sentence::AccountMustBeValidatedFirst), - context, - } - .render()?, - ) - .into_response(), - )), - db::user::SignInResult::UserNotFound | db::user::SignInResult::WrongPassword => Ok(( - jar, - Html( - SignInFormTemplate { - email: &form_data.email, - message: context.tr.t(Sentence::WrongEmailOrPassword), - context, - } - .render()?, - ) - .into_response(), - )), + error @ db::user::SignInResult::AccountNotValidated => { + event!( + Level::WARN, + "[Sign in] Account not validated, email: {}: {}", + form_data.email, + error + ); + Ok(( + jar, + Html( + SignInFormTemplate { + email: &form_data.email, + message: context.tr.t(Sentence::AccountMustBeValidatedFirst), + context, + } + .render()?, + ) + .into_response(), + )) + } + error @ (db::user::SignInResult::UserNotFound | db::user::SignInResult::WrongPassword) => { + event!( + Level::WARN, + "[Sign in] Email: {}: {}", + form_data.email, + error + ); + Ok(( + jar, + Html( + SignInFormTemplate { + email: &form_data.email, + message: context.tr.t(Sentence::WrongEmailOrPassword), + context, + } + .render()?, + ) + .into_response(), + )) + } db::user::SignInResult::Ok(token, _user_id) => { let cookie = Cookie::build((consts::COOKIE_AUTH_TOKEN_NAME, token)) .same_site(cookie::SameSite::Strict); @@ -586,6 +632,7 @@ pub struct ResetPasswordForm { reset_token: String, } +#[derive(Display)] enum ResetPasswordError { PasswordsNotEqual, InvalidPassword, @@ -604,6 +651,16 @@ pub async fn reset_password_post( form_data: &ResetPasswordForm, context: Context, ) -> Result { + event!( + Level::WARN, + "[Reset password] Email: {}: {}", + if let Some(ref user) = context.user { + &user.email + } else { + "" + }, + error + ); let reset_password_mess = &context.tr.tp( Sentence::InvalidPassword, &[Box::new(common::consts::MIN_PASSWORD_SIZE)], @@ -700,6 +757,7 @@ pub struct EditUserForm { password_2: String, } +#[derive(Display)] enum ProfileUpdateError { InvalidEmail, EmailAlreadyTaken, @@ -709,7 +767,6 @@ enum ProfileUpdateError { 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, @@ -718,17 +775,22 @@ pub async fn edit_user_post( Extension(context): Extension, Form(form_data): Form, ) -> Result { - event!( - Level::DEBUG, - "First day of the week: {:?}", - form_data.first_day_of_the_week - ); if let Some(ref user) = context.user { fn error_response( error: ProfileUpdateError, form_data: &EditUserForm, context: Context, ) -> Result { + event!( + Level::WARN, + "[Edit user] Email: {}: {}", + if let Some(ref user) = context.user { + &user.email + } else { + "" + }, + error + ); let invalid_password_mess = &context.tr.tp( Sentence::InvalidPassword, &[Box::new(common::consts::MIN_PASSWORD_SIZE)], @@ -810,8 +872,8 @@ pub async fn edit_user_post( &context.tr.tp( Sentence::ProfileFollowEmailLink, &[Box::new(format!( - "{}/revalidation?validation_token={}", - url, token + "{}/revalidation?{}={}", + url, VALIDATION_TOKEN_KEY, token ))], ), &config.smtp_relay_address, @@ -888,7 +950,7 @@ pub async fn email_revalidation( )); } let (client_ip, client_user_agent) = utils::get_ip_and_user_agent(&headers, addr); - match query.get("validation_token") { + match query.get(VALIDATION_TOKEN_KEY) { // 'validation_token' exists only when a user must validate a new email. Some(token) => { match connection @@ -917,28 +979,44 @@ pub async fn email_revalidation( ), )) } - db::user::ValidationResult::ValidationExpired => Ok(( - jar, - Html( - MessageTemplate::new_with_user( - context.tr.t(Sentence::ValidationExpired), - context.tr, - context.user, - ) - .render()?, - ), - )), - db::user::ValidationResult::UnknownUser => Ok(( - jar, - Html( - MessageTemplate::new_with_user( - context.tr.t(Sentence::ValidationErrorTryToSignUpAgain), - context.tr, - context.user, - ) - .render()?, - ), - )), + error @ db::user::ValidationResult::ValidationExpired => { + event!( + Level::WARN, + "[Email revalidation] Token: {}: {}", + token, + error + ); + Ok(( + jar, + Html( + MessageTemplate::new_with_user( + context.tr.t(Sentence::ValidationExpired), + context.tr, + context.user, + ) + .render()?, + ), + )) + } + error @ db::user::ValidationResult::UnknownUser => { + event!( + Level::WARN, + "[Email revalidation] Email: {}: {}", + token, + error + ); + Ok(( + jar, + Html( + MessageTemplate::new_with_user( + context.tr.t(Sentence::ValidationErrorTryToSignUpAgain), + context.tr, + context.user, + ) + .render()?, + ), + )) + } } } None => Ok(( diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 230b40a..63d397a 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -51,14 +51,14 @@ pub fn main() -> Result<(), JsValue> { .unwrap_or(chrono::Weekday::Mon); match path[..] { - ["recipe", "edit", id] => { - let id = id.parse::().unwrap(); // TODO: remove unwrap. - pages::recipe_edit::setup_page(id) - } - ["recipe", "view", id] => { - let id = id.parse::().unwrap(); // TODO: remove unwrap. - pages::recipe_view::setup_page(id, is_user_logged, first_day_of_the_week) - } + ["recipe", "edit", id] => match id.parse::() { + Ok(id) => pages::recipe_edit::setup_page(id), + Err(error) => log!(format!("Error parsing recipe id: {}", error)), + }, + ["recipe", "view", id] => match id.parse::() { + Ok(id) => pages::recipe_view::setup_page(id, is_user_logged, first_day_of_the_week), + Err(error) => log!(format!("Error parsing recipe id: {}", error)), + }, ["dev_panel"] => pages::dev_panel::setup_page(), // Home. [""] => pages::home::setup_page(is_user_logged, first_day_of_the_week),