use std::{collections::HashMap, net::SocketAddr}; use askama::Template; use axum::{ body::Body, debug_handler, extract::{ConnectInfo, Extension, Host, Path, Query, Request, State}, http::{HeaderMap, StatusCode}, response::{IntoResponse, Redirect, Response, Result}, Form, }; use axum_extra::extract::cookie::{Cookie, CookieJar}; use chrono::Duration; use serde::Deserialize; use tracing::{event, Level}; use crate::{config::Config, consts, data::db, email, model, utils, AppState}; pub mod ron; impl axum::response::IntoResponse for db::DBError { fn into_response(self) -> Response { let body = MessageTemplate { user: None, message: &self.to_string(), }; (StatusCode::INTERNAL_SERVER_ERROR, body).into_response() } } ///// HOME ///// #[derive(Template)] #[template(path = "home.html")] struct HomeTemplate { user: Option, recipes: Vec<(i64, String)>, current_recipe_id: Option, } #[debug_handler] pub async fn home_page( State(connection): State, Extension(user): Extension>, ) -> Result { let recipes = connection.get_all_recipe_titles().await?; Ok(HomeTemplate { user, current_recipe_id: None, recipes, }) } ///// VIEW RECIPE ///// #[derive(Template)] #[template(path = "view_recipe.html")] struct ViewRecipeTemplate { user: Option, recipes: Vec<(i64, String)>, current_recipe_id: Option, current_recipe: model::Recipe, } #[debug_handler] pub async fn view_recipe( State(connection): State, Extension(user): Extension>, Path(recipe_id): Path, ) -> Result { let recipes = connection.get_all_recipe_titles().await?; match connection.get_recipe(recipe_id).await? { Some(recipe) => Ok(ViewRecipeTemplate { user, current_recipe_id: Some(recipe.id), recipes, current_recipe: recipe, } .into_response()), None => Ok(MessageTemplate { user, message: &format!("Cannot find the recipe {}", recipe_id), } .into_response()), } } ///// EDIT/NEW RECIPE ///// // #[derive(Template)] // #[template(path = "edit_recipe.html")] // struct EditRecipeTemplate { // user: Option, // recipes: Vec<(i64, String)>, // current_recipe_id: Option, // current_recipe: model::Recipe, // } // #[get("/recipe/edit/{id}")] // pub async fn edit_recipe( // req: HttpRequest, // path: web::Path<(i64,)>, // connection: web::Data, // ) -> Result { // let (id,) = path.into_inner(); // let user = match get_current_user(&req, connection.clone()).await { // Some(u) => u, // None => { // return Ok(MessageTemplate { // user: None, // message: "Cannot edit a recipe without being logged in", // } // .to_response()) // } // }; // let recipe = connection.get_recipe_async(id).await?; // if recipe.user_id != user.id { // return Ok(MessageTemplate { // message: "Cannot edit a recipe you don't own", // user: Some(user), // } // .to_response()); // } // let recipes = connection.get_all_recipe_titles_async().await?; // Ok(EditRecipeTemplate { // user: Some(user), // current_recipe_id: Some(recipe.id), // recipes, // current_recipe: recipe, // } // .to_response()) // } // #[get("/recipe/new")] // pub async fn new_recipe( // req: HttpRequest, // connection: web::Data, // ) -> Result { // let user = match get_current_user(&req, connection.clone()).await { // Some(u) => u, // None => { // return Ok(MessageTemplate { // message: "Cannot create a recipe without being logged in", // user: None, // } // .to_response()) // } // }; // let recipe_id = connection.create_recipe_async(user.id).await?; // let recipes = connection.get_all_recipe_titles_async().await?; // let user_id = user.id; // Ok(EditRecipeTemplate { // user: Some(user), // current_recipe_id: Some(recipe_id), // recipes, // current_recipe: model::Recipe::empty(recipe_id, user_id), // } // .to_response()) // } ///// MESSAGE ///// #[derive(Template)] #[template(path = "message_without_user.html")] struct MessageWithoutUser<'a> { message: &'a str, } #[derive(Template)] #[template(path = "message.html")] struct MessageTemplate<'a> { user: Option, message: &'a str, } //// SIGN UP ///// #[derive(Template)] #[template(path = "sign_up_form.html")] struct SignUpFormTemplate { user: Option, email: String, message: String, message_email: String, message_password: String, } #[debug_handler] pub async fn sign_up_get( Extension(user): Extension>, ) -> Result { Ok(SignUpFormTemplate { user, email: String::new(), message: String::new(), message_email: String::new(), message_password: String::new(), }) } #[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>, Form(form_data): Form, ) -> Result { fn error_response( error: SignUpError, form_data: &SignUpFormData, user: Option, ) -> Result { Ok(SignUpFormTemplate { user, email: form_data.email.clone(), message_email: match error { SignUpError::InvalidEmail => "Invalid email", _ => "", } .to_string(), message_password: match error { SignUpError::PasswordsNotEqual => "Passwords don't match", SignUpError::InvalidPassword => "Password must have at least eight characters", _ => "", } .to_string(), message: match error { SignUpError::UserAlreadyExists => "This email is not available", SignUpError::DatabaseError => "Database error", SignUpError::UnableSendEmail => "Unable to send the validation email", _ => "", } .to_string(), } .into_response()) } // Validation of email and password. if let common::utils::EmailValidation::NotValid = common::utils::validate_email(&form_data.email) { return error_response(SignUpError::InvalidEmail, &form_data, user); } if form_data.password_1 != form_data.password_2 { return error_response(SignUpError::PasswordsNotEqual, &form_data, user); } if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form_data.password_1) { return error_response(SignUpError::InvalidPassword, &form_data, user); } match connection .sign_up(&form_data.email, &form_data.password_1) .await { Ok(db::SignUpResult::UserAlreadyExists) => { error_response(SignUpError::UserAlreadyExists, &form_data, user) } Ok(db::SignUpResult::UserCreatedWaitingForValidation(token)) => { let url = utils::get_url_from_host(&host); let email = form_data.email.clone(); match email::send_email( &email, &format!( "Follow this link to confirm your inscription: {}/validation?validation_token={}", url, token ), &config.smtp_relay_address, &config.smtp_login, &config.smtp_password, ) .await { Ok(()) => Ok(MessageTemplate { user, message: "An email has been sent, follow the link to validate your account.", } .into_response()), Err(_) => { // error!("Email validation error: {}", error); // TODO: log error_response(SignUpError::UnableSendEmail, &form_data, user) } } } Err(_) => { // error!("Signup database error: {}", error); error_response(SignUpError::DatabaseError, &form_data, user) } } } #[debug_handler] pub async fn sign_up_validation( State(connection): State, Extension(user): 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, MessageTemplate { user, message: "User already exists", }, )); } 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::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, MessageTemplate { user, message: "Email validation successful, your account has been created", }, )) } db::ValidationResult::ValidationExpired => Ok(( jar, MessageTemplate { user, message: "The validation has expired. Try to sign up again", }, )), db::ValidationResult::UnknownUser => Ok(( jar, MessageTemplate { user, message: "Validation error. Try to sign up again", }, )), } } None => Ok(( jar, MessageTemplate { user, message: "Validation error", }, )), } } ///// SIGN IN ///// #[derive(Template)] #[template(path = "sign_in_form.html")] struct SignInFormTemplate { user: Option, email: String, message: String, } #[debug_handler] pub async fn sign_in_get( Extension(user): Extension>, ) -> Result { Ok(SignInFormTemplate { user, email: String::new(), message: String::new(), }) } #[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>, 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::SignInResult::AccountNotValidated => Ok(( jar, SignInFormTemplate { user, email: form_data.email, message: "This account must be validated first".to_string(), } .into_response(), )), db::SignInResult::UserNotFound | db::SignInResult::WrongPassword => Ok(( jar, SignInFormTemplate { user, email: form_data.email, message: "Wrong email or password".to_string(), } .into_response(), )), db::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 ///// #[derive(Template)] #[template(path = "ask_reset_password.html")] struct AskResetPasswordTemplate { user: Option, email: String, message: String, message_email: String, } #[debug_handler] pub async fn ask_reset_password_get( Extension(user): Extension>, ) -> Result { if user.is_some() { Ok(MessageTemplate { user, message: "Can't ask to reset password when already logged in", } .into_response()) } else { Ok(AskResetPasswordTemplate { user, email: String::new(), message: String::new(), message_email: String::new(), } .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>, Form(form_data): Form, ) -> Result { fn error_response( error: AskResetPasswordError, email: &str, user: Option, ) -> Result { Ok(AskResetPasswordTemplate { user, email: email.to_string(), message_email: match error { AskResetPasswordError::InvalidEmail => "Invalid email", _ => "", } .to_string(), message: match error { AskResetPasswordError::EmailAlreadyReset => { "The password has already been reset for this email" } AskResetPasswordError::EmailUnknown => "Email unknown", AskResetPasswordError::UnableSendEmail => "Unable to send the reset password email", AskResetPasswordError::DatabaseError => "Database error", _ => "", } .to_string(), } .into_response()) } // Validation of email. if let common::utils::EmailValidation::NotValid = common::utils::validate_email(&form_data.email) { return error_response(AskResetPasswordError::InvalidEmail, &form_data.email, user); } match connection .get_token_reset_password( &form_data.email, Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION), ) .await { Ok(db::GetTokenResetPasswordResult::PasswordAlreadyReset) => error_response( AskResetPasswordError::EmailAlreadyReset, &form_data.email, user, ), Ok(db::GetTokenResetPasswordResult::EmailUnknown) => { error_response(AskResetPasswordError::EmailUnknown, &form_data.email, user) } Ok(db::GetTokenResetPasswordResult::Ok(token)) => { let url = utils::get_url_from_host(&host); match email::send_email( &form_data.email, &format!( "Follow this link to reset your password: {}/reset_password?reset_token={}", url, token ), &config.smtp_relay_address, &config.smtp_login, &config.smtp_password, ) .await { Ok(()) => Ok(MessageTemplate { user, message: "An email has been sent, follow the link to reset your password.", } .into_response()), Err(_) => { // error!("Email validation error: {}", error); // TODO: log error_response( AskResetPasswordError::UnableSendEmail, &form_data.email, user, ) } } } Err(error) => { event!(Level::ERROR, "{}", error); error_response(AskResetPasswordError::DatabaseError, &form_data.email, user) } } } #[derive(Template)] #[template(path = "reset_password.html")] struct ResetPasswordTemplate { user: Option, reset_token: String, message: String, message_password: String, } #[debug_handler] pub async fn reset_password_get( Extension(user): Extension>, Query(query): Query>, ) -> Result { if let Some(reset_token) = query.get("reset_token") { Ok(ResetPasswordTemplate { user, reset_token: reset_token.to_string(), message: String::new(), message_password: String::new(), } .into_response()) } else { Ok(MessageTemplate { user, message: "Reset token missing", } .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>, Form(form_data): Form, ) -> Result { fn error_response( error: ResetPasswordError, form_data: &ResetPasswordForm, user: Option, ) -> Result { Ok(ResetPasswordTemplate { user, reset_token: form_data.reset_token.clone(), message_password: match error { ResetPasswordError::PasswordsNotEqual => "Passwords don't match", ResetPasswordError::InvalidPassword => { "Password must have at least eight characters" } _ => "", } .to_string(), message: match error { ResetPasswordError::TokenExpired => "Token expired, try to reset password again", ResetPasswordError::DatabaseError => "Database error", _ => "", } .to_string(), } .into_response()) } if form_data.password_1 != form_data.password_2 { return error_response(ResetPasswordError::PasswordsNotEqual, &form_data, user); } if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form_data.password_1) { return error_response(ResetPasswordError::InvalidPassword, &form_data, user); } match connection .reset_password( &form_data.password_1, &form_data.reset_token, Duration::seconds(consts::VALIDATION_PASSWORD_RESET_TOKEN_DURATION), ) .await { Ok(db::ResetPasswordResult::Ok) => Ok(MessageTemplate { user, message: "Your password has been reset", } .into_response()), Ok(db::ResetPasswordResult::ResetTokenExpired) => { error_response(ResetPasswordError::TokenExpired, &form_data, user) } Err(_) => error_response(ResetPasswordError::DatabaseError, &form_data, user), } } ///// EDIT PROFILE ///// #[derive(Template)] #[template(path = "profile.html")] struct ProfileTemplate { user: Option, } #[debug_handler] pub async fn edit_user( State(connection): State, Extension(user): Extension>, ) -> Response { if user.is_some() { ProfileTemplate { user }.into_response() } else { MessageTemplate { user: None, message: "Not logged in", } .into_response() } } ///// 404 ///// #[debug_handler] pub async fn not_found() -> Result { Ok(MessageWithoutUser { message: "404: Not found", }) }