use std::collections::HashMap; use actix_web::{http::{header, header::ContentType, StatusCode}, get, post, web, Responder, HttpRequest, HttpResponse, cookie::Cookie}; use askama_actix::{Template, TemplateToResponse}; use chrono::Duration; use serde::Deserialize; use log::{debug, error, log_enabled, info, Level}; use crate::utils; use crate::email; use crate::consts; use crate::config::Config; use crate::user::User; use crate::model; use crate::data::{db, asynchronous}; ///// UTILS ///// fn get_ip_and_user_agent(req: &HttpRequest) -> (String, String) { let ip = match req.headers().get(consts::REVERSE_PROXY_IP_HTTP_FIELD) { Some(v) => v.to_str().unwrap_or_default().to_string(), None => req.peer_addr().map(|addr| addr.ip().to_string()).unwrap_or_default() }; let user_agent = req.headers().get(header::USER_AGENT).map(|v| v.to_str().unwrap_or_default()).unwrap_or_default().to_string(); (ip, user_agent) } async fn get_current_user(req: &HttpRequest, connection: web::Data) -> Option { let (client_ip, client_user_agent) = get_ip_and_user_agent(req); match req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) { Some(token_cookie) => match connection.authentication_async(token_cookie.value(), &client_ip, &client_user_agent).await { Ok(db::AuthenticationResult::NotValidToken) => // TODO: remove cookie? None, Ok(db::AuthenticationResult::Ok(user_id)) => match connection.load_user_async(user_id).await { Ok(user) => Some(user), Err(error) => { error!("Error during authentication: {}", error); None } }, Err(error) => { error!("Error during authentication: {}", error); None }, }, None => None } } type Result = std::result::Result; ///// ERROR ///// #[derive(Debug)] pub struct ServiceError { status_code: StatusCode, message: Option, } impl From for ServiceError { fn from(error: asynchronous::DBAsyncError) -> Self { ServiceError { status_code: StatusCode::INTERNAL_SERVER_ERROR, message: Some(format!("{:?}", error)), } } } impl From for ServiceError { fn from(error: email::Error) -> Self { ServiceError { status_code: StatusCode::INTERNAL_SERVER_ERROR, message: Some(format!("{:?}", error)), } } } impl From for ServiceError { fn from(error: actix_web::error::BlockingError) -> Self { ServiceError { status_code: StatusCode::INTERNAL_SERVER_ERROR, message: Some(format!("{:?}", error)), } } } impl std::fmt::Display for ServiceError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { if let Some(ref m) = self.message { write!(f, "**{}**\n\n", m)?; } write!(f, "Code: {}", self.status_code) } } impl actix_web::error::ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { MessageBaseTemplate { message: &self.to_string(), }.to_response() } fn status_code(&self) -> StatusCode { self.status_code } } ///// HOME ///// #[derive(Template)] #[template(path = "home.html")] struct HomeTemplate { user: Option, recipes: Vec<(i32, String)>, current_recipe_id: Option, } #[get("/")] pub async fn home_page(req: HttpRequest, connection: web::Data) -> Result { let user = get_current_user(&req, connection.clone()).await; let recipes = connection.get_all_recipe_titles_async().await?; Ok(HomeTemplate { user, current_recipe_id: None, recipes }.to_response()) } ///// VIEW RECIPE ///// #[derive(Template)] #[template(path = "view_recipe.html")] struct ViewRecipeTemplate { user: Option, recipes: Vec<(i32, String)>, current_recipe_id: Option, current_recipe: model::Recipe, } #[get("/recipe/view/{id}")] pub async fn view_recipe(req: HttpRequest, path: web::Path<(i32,)>, connection: web::Data) -> Result { let (id,)= path.into_inner(); let user = get_current_user(&req, connection.clone()).await; let recipes = connection.get_all_recipe_titles_async().await?; let recipe = connection.get_recipe_async(id).await?; Ok(ViewRecipeTemplate { user, current_recipe_id: Some(recipe.id), recipes, current_recipe: recipe, }.to_response()) } ///// MESSAGE ///// #[derive(Template)] #[template(path = "message_base.html")] struct MessageBaseTemplate<'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, } #[get("/signup")] pub async fn sign_up_get(req: HttpRequest, connection: web::Data) -> impl Responder { let user = get_current_user(&req, connection.clone()).await; SignUpFormTemplate { user, email: String::new(), message: String::new(), message_email: String::new(), message_password: String::new() } } #[derive(Deserialize)] pub struct SignUpFormData { email: String, password_1: String, password_2: String, } enum SignUpError { InvalidEmail, PasswordsNotEqual, InvalidPassword, UserAlreadyExists, DatabaseError, UnableSendEmail, } #[post("/signup")] pub async fn sign_up_post(req: HttpRequest, form: web::Form, connection: web::Data, config: web::Data) -> Result { fn error_response(error: SignUpError, form: &web::Form, user: Option) -> Result { Ok(SignUpFormTemplate { user, email: form.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 already taken", SignUpError::DatabaseError => "Database error", SignUpError::UnableSendEmail => "Unable to send the validation email", _ => "", }.to_string(), }.to_response()) } let user = get_current_user(&req, connection.clone()).await; // Validation of email and password. if let common::utils::EmailValidation::NotValid = common::utils::validate_email(&form.email) { return error_response(SignUpError::InvalidEmail, &form, user); } if form.password_1 != form.password_2 { return error_response(SignUpError::PasswordsNotEqual, &form, user); } if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form.password_1) { return error_response(SignUpError::InvalidPassword, &form, user); } match connection.sign_up_async(&form.email, &form.password_1).await { Ok(db::SignUpResult::UserAlreadyExists) => { error_response(SignUpError::UserAlreadyExists, &form, user) }, Ok(db::SignUpResult::UserCreatedWaitingForValidation(token)) => { let url = { let host = req.headers().get(header::HOST).map(|v| v.to_str().unwrap_or_default()).unwrap_or_default(); let port: Option = 'p: { let split_port: Vec<&str> = host.split(':').collect(); if split_port.len() == 2 { if let Ok(p) = split_port[1].parse::() { break 'p Some(p) } } None }; format!("http{}://{}", if port.is_some() && port.unwrap() != 443 { "" } else { "s" }, host) }; let email = form.email.clone(); match web::block(move || { email::send_validation(&url, &email, &token, &config.smtp_login, &config.smtp_password) }).await? { Ok(()) => Ok(HttpResponse::Found() .insert_header((header::LOCATION, "/signup_check_email")) .finish()), Err(error) => { error!("Email validation error: {}", error); error_response(SignUpError::UnableSendEmail, &form, user) }, } }, Err(error) => { error!("Signup database error: {}", error); error_response(SignUpError::DatabaseError, &form, user) }, } } #[get("/signup_check_email")] pub async fn sign_up_check_email(req: HttpRequest, connection: web::Data) -> impl Responder { let user = get_current_user(&req, connection.clone()).await; MessageTemplate { user, message: "An email has been sent, follow the link to validate your account.", } } #[get("/validation")] pub async fn sign_up_validation(req: HttpRequest, query: web::Query>, connection: web::Data) -> Result { let (client_ip, client_user_agent) = get_ip_and_user_agent(&req); let user = get_current_user(&req, connection.clone()).await; match query.get("token") { Some(token) => { match connection.validation_async(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); let user = match connection.load_user(user_id) { Ok(user) => Some(user), Err(error) => { error!("Error retrieving user by id: {}", error); None } }; let mut response = MessageTemplate { user, message: "Email validation successful, your account has been created", }.to_response(); if let Err(error) = response.add_cookie(&cookie) { error!("Unable to set cookie after validation: {}", error); }; Ok(response) }, db::ValidationResult::ValidationExpired => Ok(MessageTemplate { user, message: "The validation has expired. Try to sign up again.", }.to_response()), db::ValidationResult::UnknownUser => Ok(MessageTemplate { user, message: "Validation error.", }.to_response()), } }, None => { Ok(MessageTemplate { user, message: &format!("No token provided"), }.to_response()) }, } } ///// SIGN IN ///// #[derive(Template)] #[template(path = "sign_in_form.html")] struct SignInFormTemplate { user: Option, email: String, message: String, } #[get("/signin")] pub async fn sign_in_get(req: HttpRequest, connection: web::Data) -> impl Responder { let user = get_current_user(&req, connection.clone()).await; SignInFormTemplate { user, email: String::new(), message: String::new(), } } #[derive(Deserialize)] pub struct SignInFormData { email: String, password: String, } enum SignInError { AccountNotValidated, AuthenticationFailed, } #[post("/signin")] pub async fn sign_in_post(req: HttpRequest, form: web::Form, connection: web::Data) -> Result { fn error_response(error: SignInError, form: &web::Form, user: Option) -> Result { Ok(SignInFormTemplate { user, email: form.email.clone(), message: match error { SignInError::AccountNotValidated => "This account must be validated first", SignInError::AuthenticationFailed => "Wrong email or password", }.to_string(), }.to_response()) } let user = get_current_user(&req, connection.clone()).await; let (client_ip, client_user_agent) = get_ip_and_user_agent(&req); match connection.sign_in_async(&form.email, &form.password, &client_ip, &client_user_agent).await { Ok(db::SignInResult::AccountNotValidated) => error_response(SignInError::AccountNotValidated, &form, user), Ok(db::SignInResult::UserNotFound) | Ok(db::SignInResult::WrongPassword) => { error_response(SignInError::AuthenticationFailed, &form, user) }, Ok(db::SignInResult::Ok(token, user_id)) => { let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token); let mut response = HttpResponse::Found() .insert_header((header::LOCATION, "/")) .finish(); if let Err(error) = response.add_cookie(&cookie) { error!("Unable to set cookie after sign in: {}", error); }; Ok(response) }, Err(error) => { error!("Signin error: {}", error); error_response(SignInError::AuthenticationFailed, &form, user) }, } } ///// SIGN OUT ///// #[get("/signout")] pub async fn sign_out(req: HttpRequest, connection: web::Data) -> impl Responder { let mut response = HttpResponse::Found() .insert_header((header::LOCATION, "/")) .finish(); if let Some(token_cookie) = req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) { if let Err(error) = connection.sign_out_async(token_cookie.value()).await { error!("Unable to sign out: {}", error); }; if let Err(error) = response.add_removal_cookie(&Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, "")) { error!("Unable to set a removal cookie after sign out: {}", error); }; }; response } pub async fn not_found(req: HttpRequest, connection: web::Data) -> impl Responder { let user = get_current_user(&req, connection.clone()).await; MessageTemplate { user, message: "404: Not found", } }