X-Git-Url: http://git.euphorik.ch/?p=recipes.git;a=blobdiff_plain;f=backend%2Fsrc%2Fmain.rs;fp=backend%2Fsrc%2Fmain.rs;h=141c87fef46ff6b8e16d30eaa53ce638e34f20d4;hp=98bebc9ba1778bc33ff5e1e6a32839a6af7b006e;hb=45d4867cb37ce8d7007c4d98de70d81d0b705b92;hpb=b1ffd1a04a55d6653ed55ea99f09550e5a8a9a96 diff --git a/backend/src/main.rs b/backend/src/main.rs index 98bebc9..141c87f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,19 +1,21 @@ -use std::fs::File; -use std::sync::Mutex; +use std::{collections::HashMap, net::ToSocketAddrs}; use actix_files as fs; -use actix_web::{get, web, Responder, middleware, App, HttpServer, HttpRequest}; -use askama_actix::Template; -use chrono::prelude::*; +use actix_web::{http::header, get, post, web, Responder, middleware, App, HttpServer, HttpRequest, HttpResponse}; +use askama_actix::{Template, TemplateToResponse}; +use chrono::{prelude::*, Duration}; use clap::Parser; -use ron::de::from_reader; use serde::Deserialize; +use config::Config; + mod consts; mod db; mod hash; mod model; mod user; +mod email; +mod config; #[derive(Template)] #[template(path = "home.html")] @@ -21,6 +23,11 @@ struct HomeTemplate { recipes: Vec<(i32, String)>, } +#[derive(Template)] +#[template(path = "sign_in_form.html")] +struct SignInFormTemplate { +} + #[derive(Template)] #[template(path = "view_recipe.html")] struct ViewRecipeTemplate { @@ -28,27 +35,219 @@ struct ViewRecipeTemplate { current_recipe: model::Recipe, } -#[derive(Deserialize)] -pub struct Request { - m: Option +#[derive(Template)] +#[template(path = "message.html")] +struct MessageTemplate { + recipes: Vec<(i32, String)>, + message: String, } #[get("/")] async fn home_page(req: HttpRequest, connection: web::Data) -> impl Responder { - HomeTemplate { recipes: connection.get_all_recipe_titles().unwrap() } // TODO: unwrap. + HomeTemplate { recipes: connection.get_all_recipe_titles().unwrap_or_default() } +} + +//// SIGN UP ///// + +#[derive(Template)] +#[template(path = "sign_up_form.html")] +struct SignUpFormTemplate { + email: String, + message: String, + message_email: String, + message_password: String, +} + +impl SignUpFormTemplate { + fn new() -> Self { + SignUpFormTemplate { email: String::new(), message: String::new(), message_email: String::new(), message_password: String::new() } + } +} + +enum SignUpError { + InvalidEmail, + PasswordsNotEqual, + InvalidPassword, + UserAlreadyExists, + DatabaseError, + UnableSendEmail, +} + +#[get("/signup")] +async fn sign_up_get(req: HttpRequest, query: web::Query>, connection: web::Data) -> impl Responder { + SignUpFormTemplate::new() +} + +#[derive(Deserialize)] +struct SignUpFormData { + email: String, + password_1: String, + password_2: String, +} + +#[post("/signup")] +async fn sign_up_post(req: HttpRequest, form: web::Form, connection: web::Data, config: web::Data) -> impl Responder { + println!("Sign Up, email: {}, passwords: {}/{}", form.email, form.password_1, form.password_2); + + fn error_response(error: SignUpError, form: &web::Form) -> HttpResponse { + SignUpFormTemplate { + 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() + } + + // Validation of email and password. + if let common::utils::EmailValidation::NotValid = common::utils::validate_email(&form.email) { + return error_response(SignUpError::InvalidEmail, &form); + } + + if form.password_1 != form.password_2 { + return error_response(SignUpError::PasswordsNotEqual, &form); + } + + if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form.password_1) { + return error_response(SignUpError::InvalidPassword, &form); + } + + match connection.sign_up(&form.email, &form.password_1) { + Ok(db::SignUpResult::UserAlreadyExists) => { + error_response(SignUpError::UserAlreadyExists, &form) + }, + 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) + }; + match email::send_validation(&url, &form.email, &token, &config.smtp_login, &config.smtp_password) { + Ok(()) => + HttpResponse::Found() + .insert_header((header::LOCATION, "/signup_check_email")) + .finish(), + Err(error) => { + eprintln!("Email validation error: {:?}", error); + error_response(SignUpError::UnableSendEmail, &form) + }, + } + }, + Err(error) => { + eprintln!("Signup database error: {:?}", error); + error_response(SignUpError::DatabaseError, &form) + }, + } +} + +#[get("/signup_check_email")] +async fn sign_up_check_email(connection: web::Data) -> impl Responder { + let recipes = connection.get_all_recipe_titles().unwrap_or_default(); + MessageTemplate { + recipes, + message: "An email has been sent, follow the link to validate your account.".to_string(), + } +} + +#[get("/validation")] +async fn sign_up_validation(req: HttpRequest, query: web::Query>, connection: web::Data) -> impl Responder { + + println!("req:\n{:#?}", req); + + let client_user_agent = req.headers().get(header::USER_AGENT).map(|v| v.to_str().unwrap_or_default()).unwrap_or_default(); + let client_ip = req.peer_addr().map(|addr| addr.ip().to_string()).unwrap_or_default(); + + let recipes = connection.get_all_recipe_titles().unwrap_or_default(); + match query.get("token") { + Some(token) => { + match connection.validation(token, Duration::seconds(consts::VALIDATION_TOKEN_DURATION), &client_ip, client_user_agent).unwrap() { + db::ValidationResult::Ok(token, user_id) => + // TODO: set token to cookie. + MessageTemplate { + recipes, + message: "Email validation successful, your account has been created".to_string(), + }, + db::ValidationResult::ValidationExpired => + MessageTemplate { + recipes, + message: "The validation has expired. Try to sign up again.".to_string(), + }, + db::ValidationResult::UnknownUser => + MessageTemplate { + recipes, + message: "Validation error.".to_string(), + }, + } + }, + None => { + MessageTemplate { + recipes, + message: format!("No token provided"), + } + }, + } +} + +///// SIGN IN ///// + +#[get("/signinform")] +async fn sign_in_form(req: HttpRequest, connection: web::Data) -> impl Responder { + SignInFormTemplate { + } +} + +#[post("/signin")] +async fn sign_in(req: HttpRequest) -> impl Responder { + "todo" } #[get("/recipe/view/{id}")] async fn view_recipe(req: HttpRequest, path: web::Path<(i32,)>, connection: web::Data) -> impl Responder { - ViewRecipeTemplate { - recipes: connection.get_all_recipe_titles().unwrap(), - current_recipe: connection.get_recipe(path.0).unwrap(), + let (id,)= path.into_inner(); + let recipes = connection.get_all_recipe_titles().unwrap_or_default(); + println!("{:?}", recipes); + match connection.get_recipe(id) { + Ok(recipe) => + ViewRecipeTemplate { + recipes, + current_recipe: recipe, + }.to_response(), + Err(_error) => + MessageTemplate { + recipes, + message: format!("Unable to get recipe #{}", id), + }.to_response(), } } -#[derive(Debug, Deserialize)] -struct Config { - port: u16 +async fn not_found(req: HttpRequest, connection: web::Data) -> impl Responder { + let recipes = connection.get_all_recipe_titles().unwrap_or_default(); + MessageTemplate { + recipes, + message: "404: Not found".to_string(), + } } fn get_exe_name() -> String { @@ -66,36 +265,36 @@ async fn main() -> std::io::Result<()> { println!("Starting Recipes as web server..."); - let config: Config = { - let f = File::open(consts::FILE_CONF).unwrap_or_else(|_| panic!("Failed to open configuration file {}", consts::FILE_CONF)); - match from_reader(f) { - Ok(c) => c, - Err(e) => panic!("Failed to load config: {}", e) - } - }; + let config = web::Data::new(config::load()); + let port = config.as_ref().port; println!("Configuration: {:?}", config); - let db_connection = web::Data::new(db::Connection::new().unwrap()); // TODO: remove unwrap. + let db_connection = web::Data::new(db::Connection::new().unwrap()); std::env::set_var("RUST_LOG", "actix_web=info"); - let mut server = - HttpServer::new( - move || { - App::new() - .wrap(middleware::Logger::default()) - .wrap(middleware::Compress::default()) - .app_data(db_connection.clone()) - .service(home_page) - .service(view_recipe) - .service(fs::Files::new("/static", "static").show_files_listing()) - } - ); - - server = server.bind(&format!("0.0.0.0:{}", config.port)).unwrap(); + let server = + HttpServer::new(move || { + App::new() + .wrap(middleware::Logger::default()) + .wrap(middleware::Compress::default()) + .app_data(db_connection.clone()) + .app_data(config.clone()) + .service(home_page) + .service(sign_up_get) + .service(sign_up_post) + .service(sign_up_check_email) + .service(sign_up_validation) + .service(sign_in_form) + .service(sign_in) + .service(view_recipe) + .service(fs::Files::new("/static", "static")) + .default_service(web::to(not_found)) + //.default_service(not_found) + }); - server.run().await + server.bind(&format!("0.0.0.0:{}", port))?.run().await } #[derive(Parser, Debug)]