X-Git-Url: http://git.euphorik.ch/?a=blobdiff_plain;f=backend%2Fsrc%2Fmain.rs;h=141c87fef46ff6b8e16d30eaa53ce638e34f20d4;hb=45d4867cb37ce8d7007c4d98de70d81d0b705b92;hp=299f5c55c36ac60b35416d327fe41b2d59e22175;hpb=eab43f8995eff5b8a4f6c4ded6a655866feddedb;p=recipes.git diff --git a/backend/src/main.rs b/backend/src/main.rs index 299f5c5..141c87f 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,42 +1,253 @@ -use std::io::prelude::*; -use std::{fs::File, env::args}; +use std::{collections::HashMap, net::ToSocketAddrs}; use actix_files as fs; -use actix_web::{get, web, Responder, middleware, App, HttpServer, HttpResponse, web::Query, middleware::Logger}; - -use askama::Template; -use listenfd::ListenFd; -use ron::de::from_reader; +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 serde::Deserialize; -use env_logger; -use itertools::Itertools; +use config::Config; mod consts; mod db; +mod hash; +mod model; +mod user; +mod email; +mod config; + +#[derive(Template)] +#[template(path = "home.html")] +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 { + recipes: Vec<(i32, String)>, + current_recipe: model::Recipe, +} #[derive(Template)] -#[template(path = "main.html")] -struct MainTemplate<'a> { - test: &'a str, +#[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_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)] -pub struct Request { - m: Option +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"), + } + }, + } } -fn main_page(query: Query) -> HttpResponse { +///// SIGN IN ///// - let main_template = MainTemplate { test: &"*** test ***" }; +#[get("/signinform")] +async fn sign_in_form(req: HttpRequest, connection: web::Data) -> impl Responder { + SignInFormTemplate { + } +} - let s = main_template.render().unwrap(); - HttpResponse::Ok().content_type("text/html").body(s) +#[post("/signin")] +async fn sign_in(req: HttpRequest) -> impl Responder { + "todo" } -#[derive(Debug, Deserialize)] -struct Config { - port: u16 +#[get("/recipe/view/{id}")] +async fn view_recipe(req: HttpRequest, path: web::Path<(i32,)>, connection: web::Data) -> impl Responder { + 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(), + } +} + +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 { @@ -45,51 +256,78 @@ fn get_exe_name() -> String { first_arg[first_arg.rfind(sep).unwrap()+1..].to_string() } -#[actix_rt::main] +#[actix_web::main] async fn main() -> std::io::Result<()> { if process_args() { return Ok(()) } - println!("Starting RUP as web server..."); + std::env::set_var("RUST_LOG", "actix_web=debug"); + env_logger::init(); - 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) - } - }; + println!("Starting Recipes as web server..."); + + let config = web::Data::new(config::load()); + let port = config.as_ref().port; println!("Configuration: {:?}", config); - // let database_connection = db::create_or_update(); + let db_connection = web::Data::new(db::Connection::new().unwrap()); std::env::set_var("RUST_LOG", "actix_web=info"); - env_logger::init(); - let mut listenfd = ListenFd::from_env(); - let mut server = - HttpServer::new( - || { - App::new() - .wrap(middleware::Compress::default()) - .wrap(Logger::default()) - .wrap(Logger::new("%a %{User-Agent}i")) - .service(web::resource("/").to(main_page)) - .service(fs::Files::new("/static", "static").show_files_listing()) - } - ); + 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 = - if let Some(l) = listenfd.take_tcp_listener(0).unwrap() { - server.listen(l).unwrap() - } else { - server.bind(&format!("0.0.0.0:{}", config.port)).unwrap() - }; + server.bind(&format!("0.0.0.0:{}", port))?.run().await +} - server.run().await +#[derive(Parser, Debug)] +struct Args { + #[arg(long)] + dbtest: bool } fn process_args() -> bool { + let args = Args::parse(); + + if args.dbtest { + match db::Connection::new() { + Ok(con) => { + if let Err(error) = con.execute_file("sql/data_test.sql") { + println!("Error: {:?}", error); + } + // Set the creation datetime to 'now'. + con.execute_sql("UPDATE [User] SET [creation_datetime] = ?1 WHERE [email] = 'paul@test.org'", [Utc::now()]).unwrap(); + }, + Err(error) => { + println!("Error: {:?}", error) + }, + } + + return true; + } + + false + + /* + + fn print_usage() { println!("Usage:"); println!(" {} [--help] [--test]", get_exe_name()); @@ -101,10 +339,12 @@ fn process_args() -> bool { print_usage(); return true } else if args.iter().any(|arg| arg == "--test") { - let db_connection = db::Connection::new(); - db_connection.create_or_update(); + match db::Connection::new() { + Ok(_) => (), + Err(error) => println!("Error: {:?}", error) + } return true } - false + */ }