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).
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);

View file

@ -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,

View file

@ -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())?;

View file

@ -30,7 +30,7 @@ const TRACING_DISPLAY_THREAD: bool = false;
#[derive(Clone)]
pub struct Log {
guard: Arc<WorkerGuard>,
_guard: Arc<WorkerGuard>,
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(),
}
}

View file

@ -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<Response> {
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<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(
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<Context>,
Form(form_data): Form<EditUserForm>,
) -> Result<Response> {
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<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(
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((

View file

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