X-Git-Url: http://git.euphorik.ch/index.cgi?a=blobdiff_plain;f=backend%2Fsrc%2Fservices.rs;h=ce64899dc368d1127e199c9dfaa107c34ace400c;hb=HEAD;hp=2d69cc5fafdf31d843c0370697768bb50f3c760c;hpb=642dd8a80ce2e1212b8e30c1edabb32bdb416cfc;p=recipes.git diff --git a/backend/src/services.rs b/backend/src/services.rs index 2d69cc5..ce64899 100644 --- a/backend/src/services.rs +++ b/backend/src/services.rs @@ -1,59 +1,77 @@ use std::collections::HashMap; -use actix_web::{http::{header, header::ContentType, StatusCode}, get, post, web, Responder, HttpRequest, HttpResponse, cookie::Cookie}; +use actix_web::{ + cookie::Cookie, + get, + http::{header, header::ContentType, StatusCode}, + post, web, HttpRequest, HttpResponse, Responder, +}; use askama_actix::{Template, TemplateToResponse}; use chrono::Duration; +use log::{debug, error, info, log_enabled, Level}; 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}; +use crate::{ + config::Config, + consts, + data::{asynchronous, db}, + email, model, utils, +}; mod api; ///// 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 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(); + 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 { +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 + 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, } } @@ -67,7 +85,7 @@ pub struct ServiceError { message: Option, } -impl From for ServiceError { +impl From for ServiceError { fn from(error: asynchronous::DBAsyncError) -> Self { ServiceError { status_code: StatusCode::INTERNAL_SERVER_ERROR, @@ -76,7 +94,7 @@ impl From for ServiceError { } } -impl From for ServiceError { +impl From for ServiceError { fn from(error: email::Error) -> Self { ServiceError { status_code: StatusCode::INTERNAL_SERVER_ERROR, @@ -85,7 +103,7 @@ impl From for ServiceError { } } -impl From for ServiceError { +impl From for ServiceError { fn from(error: actix_web::error::BlockingError) -> Self { ServiceError { status_code: StatusCode::INTERNAL_SERVER_ERROR, @@ -94,6 +112,15 @@ impl From for ServiceError { } } +impl From for ServiceError { + fn from(error: ron::error::SpannedError) -> 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 { @@ -107,7 +134,8 @@ impl actix_web::error::ResponseError for ServiceError { fn error_response(&self) -> HttpResponse { MessageBaseTemplate { message: &self.to_string(), - }.to_response() + } + .to_response() } fn status_code(&self) -> StatusCode { @@ -120,17 +148,25 @@ impl actix_web::error::ResponseError for ServiceError { #[derive(Template)] #[template(path = "home.html")] struct HomeTemplate { - user: Option, + user: Option, recipes: Vec<(i64, String)>, current_recipe_id: Option, } #[get("/")] -pub async fn home_page(req: HttpRequest, connection: web::Data) -> Result { +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()) + Ok(HomeTemplate { + user, + current_recipe_id: None, + recipes, + } + .to_response()) } ///// VIEW RECIPE ///// @@ -138,15 +174,20 @@ pub async fn home_page(req: HttpRequest, connection: web::Data) #[derive(Template)] #[template(path = "view_recipe.html")] struct ViewRecipeTemplate { - user: Option, + user: Option, recipes: Vec<(i64, String)>, current_recipe_id: Option, + current_recipe: model::Recipe, } #[get("/recipe/view/{id}")] -pub async fn view_recipe(req: HttpRequest, path: web::Path<(i64,)>, connection: web::Data) -> Result { - let (id,)= path.into_inner(); +pub async fn view_recipe( + req: HttpRequest, + path: web::Path<(i64,)>, + 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?; @@ -156,7 +197,89 @@ pub async fn view_recipe(req: HttpRequest, path: web::Path<(i64,)>, connection: current_recipe_id: Some(recipe.id), recipes, current_recipe: recipe, - }.to_response()) + } + .to_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, + path: web::Path<(i64,)>, + 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 ///// @@ -170,7 +293,7 @@ struct MessageBaseTemplate<'a> { #[derive(Template)] #[template(path = "message.html")] struct MessageTemplate<'a> { - user: Option, + user: Option, message: &'a str, } @@ -179,7 +302,7 @@ struct MessageTemplate<'a> { #[derive(Template)] #[template(path = "sign_up_form.html")] struct SignUpFormTemplate { - user: Option, + user: Option, email: String, message: String, message_email: String, @@ -187,9 +310,18 @@ struct SignUpFormTemplate { } #[get("/signup")] -pub async fn sign_up_get(req: HttpRequest, connection: web::Data) -> impl Responder { +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() } + SignUpFormTemplate { + user, + email: String::new(), + message: String::new(), + message_email: String::new(), + message_password: String::new(), + } } #[derive(Deserialize)] @@ -209,30 +341,40 @@ enum SignUpError { } #[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 { +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()) + 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; @@ -246,51 +388,80 @@ pub async fn sign_up_post(req: HttpRequest, form: web::Form, con return error_response(SignUpError::PasswordsNotEqual, &form, user); } - if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form.password_1) { + 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 { + 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 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) + break 'p Some(p); } } None }; - format!("http{}://{}", if port.is_some() && port.unwrap() != 443 { "" } else { "s" }, host) + 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()), + 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 { +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, @@ -299,55 +470,64 @@ pub async fn sign_up_check_email(req: HttpRequest, connection: web::Data>, connection: web::Data) -> Result { +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? { + 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(); + 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()), + } + 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()) - }, + } + None => Ok(MessageTemplate { + user, + message: &format!("No token provided"), + } + .to_response()), } } @@ -356,13 +536,16 @@ pub async fn sign_up_validation(req: HttpRequest, query: web::Query, + user: Option, email: String, message: String, } #[get("/signin")] -pub async fn sign_in_get(req: HttpRequest, connection: web::Data) -> impl Responder { +pub async fn sign_in_get( + req: HttpRequest, + connection: web::Data, +) -> impl Responder { let user = get_current_user(&req, connection.clone()).await; SignInFormTemplate { user, @@ -383,62 +566,74 @@ enum SignInError { } #[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 { +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()) + 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), + 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(); + 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(); + 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, "")) { + 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); }; };