Log errors in the user module

This commit is contained in:
Greg Burri 2025-04-27 02:57:08 +02:00
parent b0f0633338
commit 7b9df97a32
6 changed files with 199 additions and 118 deletions

View file

@ -38,6 +38,8 @@ pub const COOKIE_LANG_NAME: &str = "lang";
/// (cookie authentication, password reset, validation token). /// (cookie authentication, password reset, validation token).
pub const TOKEN_SIZE: usize = 32; pub const TOKEN_SIZE: usize = 32;
pub const EMAIL_ADDRESS: &str = "recipes@recipes.gburri.org";
/// When sending a validation email, /// When sending a validation email,
/// the server has this duration to wait for a response from the SMTP server. /// the server has this duration to wait for a response from the SMTP server.
pub const SEND_EMAIL_TIMEOUT: Duration = Duration::from_secs(60); pub const SEND_EMAIL_TIMEOUT: Duration = Duration::from_secs(60);

View file

@ -1,6 +1,7 @@
use chrono::{Duration, prelude::*}; use chrono::{Duration, prelude::*};
use rand::distr::{Alphanumeric, SampleString}; use rand::distr::{Alphanumeric, SampleString};
use sqlx::Sqlite; use sqlx::Sqlite;
use strum_macros::Display;
use super::{Connection, DBError, Result}; use super::{Connection, DBError, Result};
use crate::{ use crate::{
@ -9,27 +10,27 @@ use crate::{
hash::{hash, verify_password}, hash::{hash, verify_password},
}; };
#[derive(Debug)] #[derive(Debug, Display)]
pub enum SignUpResult { pub enum SignUpResult {
UserAlreadyExists, UserAlreadyExists,
UserCreatedWaitingForValidation(String), // Validation token. UserCreatedWaitingForValidation(String), // Validation token.
} }
#[derive(Debug)] #[derive(Debug, Display)]
pub enum UpdateUserResult { pub enum UpdateUserResult {
EmailAlreadyTaken, EmailAlreadyTaken,
UserUpdatedWaitingForRevalidation(String), // Validation token. UserUpdatedWaitingForRevalidation(String), // Validation token.
Ok, Ok,
} }
#[derive(Debug)] #[derive(Debug, Display)]
pub enum ValidationResult { pub enum ValidationResult {
UnknownUser, UnknownUser,
ValidationExpired, ValidationExpired,
Ok(String, i64), // Returns token and user id. Ok(String, i64), // Returns token and user id.
} }
#[derive(Debug)] #[derive(Debug, Display)]
pub enum SignInResult { pub enum SignInResult {
UserNotFound, UserNotFound,
WrongPassword, WrongPassword,
@ -37,20 +38,20 @@ pub enum SignInResult {
Ok(String, i64), // Returns token and user id. Ok(String, i64), // Returns token and user id.
} }
#[derive(Debug)] #[derive(Debug, Display)]
pub enum AuthenticationResult { pub enum AuthenticationResult {
NotValidToken, NotValidToken,
Ok(i64), // Returns user id. Ok(i64), // Returns user id.
} }
#[derive(Debug)] #[derive(Debug, Display)]
pub enum GetTokenResetPasswordResult { pub enum GetTokenResetPasswordResult {
PasswordAlreadyReset, PasswordAlreadyReset,
EmailUnknown, EmailUnknown,
Ok(String), Ok(String),
} }
#[derive(Debug)] #[derive(Debug, Display)]
pub enum ResetPasswordResult { pub enum ResetPasswordResult {
ResetTokenExpired, ResetTokenExpired,
Ok, Ok,

View file

@ -30,7 +30,7 @@ pub async fn send_email(
) -> Result<(), Error> { ) -> Result<(), Error> {
let email = Message::builder() let email = Message::builder()
.message_id(None) .message_id(None)
.from("recipes@recipes.gburri.org".parse()?) .from(consts::EMAIL_ADDRESS.parse()?)
.to(email.parse()?) .to(email.parse()?)
.subject(title) .subject(title)
.body(message.to_string())?; .body(message.to_string())?;

View file

@ -30,7 +30,7 @@ const TRACING_DISPLAY_THREAD: bool = false;
#[derive(Clone)] #[derive(Clone)]
pub struct Log { pub struct Log {
guard: Arc<WorkerGuard>, _guard: Arc<WorkerGuard>,
directory: PathBuf, directory: PathBuf,
} }
@ -69,7 +69,7 @@ impl Log {
.init(); .init();
Log { Log {
guard: Arc::new(guard), _guard: Arc::new(guard),
directory: directory.as_ref().to_path_buf(), directory: directory.as_ref().to_path_buf(),
} }
} }

View file

@ -16,6 +16,7 @@ use axum_extra::extract::{
use chrono::Duration; use chrono::Duration;
use lettre::Address; use lettre::Address;
use serde::Deserialize; use serde::Deserialize;
use strum_macros::Display;
use tracing::{Level, event}; use tracing::{Level, event};
use crate::{ use crate::{
@ -23,6 +24,8 @@ use crate::{
translation::Sentence, utils, translation::Sentence, utils,
}; };
const VALIDATION_TOKEN_KEY: &str = "validation_token";
/// SIGN UP /// /// SIGN UP ///
#[debug_handler] #[debug_handler]
@ -62,6 +65,7 @@ pub struct SignUpFormData {
password_2: String, password_2: String,
} }
#[derive(Display)]
enum SignUpError { enum SignUpError {
InvalidEmail, InvalidEmail,
PasswordsNotEqual, PasswordsNotEqual,
@ -84,6 +88,13 @@ pub async fn sign_up_post(
form_data: &SignUpFormData, form_data: &SignUpFormData,
context: Context, context: Context,
) -> Result<Response> { ) -> Result<Response> {
event!(
Level::WARN,
"[Sign up] Unable to sign up with email {}: {}",
form_data.email,
error
);
let invalid_password_mess = &context.tr.tp( let invalid_password_mess = &context.tr.tp(
Sentence::InvalidPassword, Sentence::InvalidPassword,
&[Box::new(common::consts::MIN_PASSWORD_SIZE)], &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
@ -160,8 +171,8 @@ pub async fn sign_up_post(
&context.tr.tp( &context.tr.tp(
Sentence::SignUpFollowEmailLink, Sentence::SignUpFollowEmailLink,
&[Box::new(format!( &[Box::new(format!(
"{}/validation?validation_token={}", "{}/validation?{}={}",
url, token url, VALIDATION_TOKEN_KEY, token
))], ))],
), ),
&config.smtp_relay_address, &config.smtp_relay_address,
@ -179,16 +190,10 @@ pub async fn sign_up_post(
.render()?, .render()?,
) )
.into_response()), .into_response()),
Err(_) => { Err(_) => error_response(SignUpError::UnableSendEmail, &form_data, context),
// error!("Email validation error: {}", error); // TODO: log
error_response(SignUpError::UnableSendEmail, &form_data, context)
}
} }
} }
Err(_) => { Err(_) => error_response(SignUpError::DatabaseError, &form_data, context),
// error!("Signup database error: {}", error); // TODO: log
error_response(SignUpError::DatabaseError, &form_data, context)
}
} }
} }
@ -201,7 +206,12 @@ pub async fn sign_up_validation(
headers: HeaderMap, headers: HeaderMap,
) -> Result<(CookieJar, impl IntoResponse)> { ) -> Result<(CookieJar, impl IntoResponse)> {
let mut jar = CookieJar::from_headers(&headers); 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(( return Ok((
jar, jar,
Html( 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); 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. // 'validation_token' exists only when a user tries to validate a new account.
Some(token) => { Some(token) => {
match connection match connection
@ -244,41 +254,61 @@ pub async fn sign_up_validation(
), ),
)) ))
} }
db::user::ValidationResult::ValidationExpired => Ok(( db::user::ValidationResult::ValidationExpired => {
jar, event!(
Html( Level::WARN,
MessageTemplate::new_with_user( "[Sign up] Unable to validate: validation expired. Token: {}",
context.tr.t(Sentence::SignUpValidationExpired), token
context.tr, );
context.user, Ok((
) jar,
.render()?, Html(
), MessageTemplate::new_with_user(
)), context.tr.t(Sentence::SignUpValidationExpired),
db::user::ValidationResult::UnknownUser => Ok(( context.tr,
jar, context.user,
Html( )
MessageTemplate::new_with_user( .render()?,
context.tr.t(Sentence::SignUpValidationErrorTryAgain), ),
context.tr, ))
context.user, }
) db::user::ValidationResult::UnknownUser => {
.render()?, 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(( None => {
jar, event!(
Html( Level::WARN,
MessageTemplate::new_with_user( "[Sign up] Unable to validate: no token provided"
context.tr.t(Sentence::ValidationError), );
context.tr, Ok((
context.user, jar,
) Html(
.render()?, 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? .await?
{ {
db::user::SignInResult::AccountNotValidated => Ok(( error @ db::user::SignInResult::AccountNotValidated => {
jar, event!(
Html( Level::WARN,
SignInFormTemplate { "[Sign in] Account not validated, email: {}: {}",
email: &form_data.email, form_data.email,
message: context.tr.t(Sentence::AccountMustBeValidatedFirst), error
context, );
} Ok((
.render()?, jar,
) Html(
.into_response(), SignInFormTemplate {
)), email: &form_data.email,
db::user::SignInResult::UserNotFound | db::user::SignInResult::WrongPassword => Ok(( message: context.tr.t(Sentence::AccountMustBeValidatedFirst),
jar, context,
Html( }
SignInFormTemplate { .render()?,
email: &form_data.email, )
message: context.tr.t(Sentence::WrongEmailOrPassword), .into_response(),
context, ))
} }
.render()?, error @ (db::user::SignInResult::UserNotFound | db::user::SignInResult::WrongPassword) => {
) event!(
.into_response(), 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) => { db::user::SignInResult::Ok(token, _user_id) => {
let cookie = Cookie::build((consts::COOKIE_AUTH_TOKEN_NAME, token)) let cookie = Cookie::build((consts::COOKIE_AUTH_TOKEN_NAME, token))
.same_site(cookie::SameSite::Strict); .same_site(cookie::SameSite::Strict);
@ -586,6 +632,7 @@ pub struct ResetPasswordForm {
reset_token: String, reset_token: String,
} }
#[derive(Display)]
enum ResetPasswordError { enum ResetPasswordError {
PasswordsNotEqual, PasswordsNotEqual,
InvalidPassword, InvalidPassword,
@ -604,6 +651,16 @@ pub async fn reset_password_post(
form_data: &ResetPasswordForm, form_data: &ResetPasswordForm,
context: Context, context: Context,
) -> Result<Response> { ) -> Result<Response> {
event!(
Level::WARN,
"[Reset password] Email: {}: {}",
if let Some(ref user) = context.user {
&user.email
} else {
"<Unknown user>"
},
error
);
let reset_password_mess = &context.tr.tp( let reset_password_mess = &context.tr.tp(
Sentence::InvalidPassword, Sentence::InvalidPassword,
&[Box::new(common::consts::MIN_PASSWORD_SIZE)], &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
@ -700,6 +757,7 @@ pub struct EditUserForm {
password_2: String, password_2: String,
} }
#[derive(Display)]
enum ProfileUpdateError { enum ProfileUpdateError {
InvalidEmail, InvalidEmail,
EmailAlreadyTaken, EmailAlreadyTaken,
@ -709,7 +767,6 @@ enum ProfileUpdateError {
UnableSendEmail, UnableSendEmail,
} }
// TODO: A lot of code are similar to 'sign_up_post', maybe find a way to factorize some.
#[debug_handler(state = AppState)] #[debug_handler(state = AppState)]
pub async fn edit_user_post( pub async fn edit_user_post(
Host(host): Host, Host(host): Host,
@ -718,17 +775,22 @@ pub async fn edit_user_post(
Extension(context): Extension<Context>, Extension(context): Extension<Context>,
Form(form_data): Form<EditUserForm>, Form(form_data): Form<EditUserForm>,
) -> Result<Response> { ) -> Result<Response> {
event!(
Level::DEBUG,
"First day of the week: {:?}",
form_data.first_day_of_the_week
);
if let Some(ref user) = context.user { if let Some(ref user) = context.user {
fn error_response( fn error_response(
error: ProfileUpdateError, error: ProfileUpdateError,
form_data: &EditUserForm, form_data: &EditUserForm,
context: Context, context: Context,
) -> Result<Response> { ) -> Result<Response> {
event!(
Level::WARN,
"[Edit user] Email: {}: {}",
if let Some(ref user) = context.user {
&user.email
} else {
"<Unknown user>"
},
error
);
let invalid_password_mess = &context.tr.tp( let invalid_password_mess = &context.tr.tp(
Sentence::InvalidPassword, Sentence::InvalidPassword,
&[Box::new(common::consts::MIN_PASSWORD_SIZE)], &[Box::new(common::consts::MIN_PASSWORD_SIZE)],
@ -810,8 +872,8 @@ pub async fn edit_user_post(
&context.tr.tp( &context.tr.tp(
Sentence::ProfileFollowEmailLink, Sentence::ProfileFollowEmailLink,
&[Box::new(format!( &[Box::new(format!(
"{}/revalidation?validation_token={}", "{}/revalidation?{}={}",
url, token url, VALIDATION_TOKEN_KEY, token
))], ))],
), ),
&config.smtp_relay_address, &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); 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. // 'validation_token' exists only when a user must validate a new email.
Some(token) => { Some(token) => {
match connection match connection
@ -917,28 +979,44 @@ pub async fn email_revalidation(
), ),
)) ))
} }
db::user::ValidationResult::ValidationExpired => Ok(( error @ db::user::ValidationResult::ValidationExpired => {
jar, event!(
Html( Level::WARN,
MessageTemplate::new_with_user( "[Email revalidation] Token: {}: {}",
context.tr.t(Sentence::ValidationExpired), token,
context.tr, error
context.user, );
) Ok((
.render()?, jar,
), Html(
)), MessageTemplate::new_with_user(
db::user::ValidationResult::UnknownUser => Ok(( context.tr.t(Sentence::ValidationExpired),
jar, context.tr,
Html( context.user,
MessageTemplate::new_with_user( )
context.tr.t(Sentence::ValidationErrorTryToSignUpAgain), .render()?,
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(( None => Ok((

View file

@ -51,14 +51,14 @@ pub fn main() -> Result<(), JsValue> {
.unwrap_or(chrono::Weekday::Mon); .unwrap_or(chrono::Weekday::Mon);
match path[..] { match path[..] {
["recipe", "edit", id] => { ["recipe", "edit", id] => match id.parse::<i64>() {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap. Ok(id) => pages::recipe_edit::setup_page(id),
pages::recipe_edit::setup_page(id) Err(error) => log!(format!("Error parsing recipe id: {}", error)),
} },
["recipe", "view", id] => { ["recipe", "view", id] => match id.parse::<i64>() {
let id = id.parse::<i64>().unwrap(); // TODO: remove unwrap. Ok(id) => pages::recipe_view::setup_page(id, is_user_logged, first_day_of_the_week),
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(), ["dev_panel"] => pages::dev_panel::setup_page(),
// Home. // Home.
[""] => pages::home::setup_page(is_user_logged, first_day_of_the_week), [""] => pages::home::setup_page(is_user_logged, first_day_of_the_week),