-use std::collections::HashMap;
+use std::path::Path;
use actix_files as fs;
-use actix_web::{http::header, get, post, web, Responder, middleware, App, HttpServer, HttpRequest, HttpResponse, cookie::Cookie};
-use askama_actix::{Template, TemplateToResponse};
-use chrono::{prelude::*, Duration};
+use actix_web::{middleware, web, App, HttpServer};
+use chrono::prelude::*;
use clap::Parser;
-use serde::Deserialize;
-use log::{debug, error, log_enabled, info, Level};
-use config::Config;
-use user::User;
+use data::db;
+mod config;
mod consts;
-mod db;
+mod data;
+mod email;
mod hash;
mod model;
+mod services;
mod user;
-mod email;
-mod config;
-
-const COOKIE_AUTH_TOKEN_NAME: &str = "auth_token";
-
-///// 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)
-}
-
-fn get_current_user(req: &HttpRequest, connection: &web::Data<db::Connection>) -> Option<User> {
- let (client_ip, client_user_agent) = get_ip_and_user_agent(req);
-
- match req.cookie(COOKIE_AUTH_TOKEN_NAME) {
- Some(token_cookie) =>
- match connection.authentication(token_cookie.value(), &client_ip, &client_user_agent) {
- Ok(db::AuthenticationResult::NotValidToken) =>
- // TODO: remove cookie?
- None,
- Ok(db::AuthenticationResult::Ok(user_id)) =>
- match connection.load_user(user_id) {
- Ok(user) =>
- Some(user),
- Err(error) => {
- error!("Error during authentication: {}", error);
- None
- }
- },
- Err(error) => {
- error!("Error during authentication: {}", error);
- None
- },
- },
- None => None
- }
-}
-
-///// HOME /////
-
-#[derive(Template)]
-#[template(path = "home.html")]
-struct HomeTemplate {
- user: Option<user::User>,
- recipes: Vec<(i32, String)>,
-}
-
-#[get("/")]
-async fn home_page(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
- HomeTemplate { user: get_current_user(&req, &connection), recipes: connection.get_all_recipe_titles().unwrap_or_default() }
-}
-
-///// VIEW RECIPE /////
-
-#[derive(Template)]
-#[template(path = "view_recipe.html")]
-struct ViewRecipeTemplate {
- user: Option<user::User>,
- recipes: Vec<(i32, String)>,
- current_recipe: model::Recipe,
-}
-
-#[get("/recipe/view/{id}")]
-async fn view_recipe(req: HttpRequest, path: web::Path<(i32,)>, connection: web::Data<db::Connection>) -> impl Responder {
- let (id,)= path.into_inner();
- let recipes = connection.get_all_recipe_titles().unwrap_or_default();
- let user = get_current_user(&req, &connection);
-
- match connection.get_recipe(id) {
- Ok(recipe) =>
- ViewRecipeTemplate {
- user,
- recipes,
- current_recipe: recipe,
- }.to_response(),
- Err(_error) =>
- MessageTemplate {
- user,
- recipes,
- message: format!("Unable to get recipe #{}", id),
- }.to_response(),
- }
-}
-
-///// MESSAGE /////
-
-#[derive(Template)]
-#[template(path = "message.html")]
-struct MessageTemplate {
- user: Option<user::User>,
- recipes: Vec<(i32, String)>,
- message: String,
-}
-
-//// SIGN UP /////
-
-#[derive(Template)]
-#[template(path = "sign_up_form.html")]
-struct SignUpFormTemplate {
- user: Option<user::User>,
- email: String,
- message: String,
- message_email: String,
- message_password: String,
-}
-
-#[get("/signup")]
-async fn sign_up_get(req: HttpRequest, query: web::Query<HashMap<String, String>>, connection: web::Data<db::Connection>) -> impl Responder {
- SignUpFormTemplate { user: get_current_user(&req, &connection), email: String::new(), message: String::new(), message_email: String::new(), message_password: String::new() }
-}
-
-#[derive(Deserialize)]
-struct SignUpFormData {
- email: String,
- password_1: String,
- password_2: String,
-}
-
-enum SignUpError {
- InvalidEmail,
- PasswordsNotEqual,
- InvalidPassword,
- UserAlreadyExists,
- DatabaseError,
- UnableSendEmail,
-}
-
-#[post("/signup")]
-async fn sign_up_post(req: HttpRequest, form: web::Form<SignUpFormData>, connection: web::Data<db::Connection>, config: web::Data<Config>) -> impl Responder {
- fn error_response(error: SignUpError, form: &web::Form<SignUpFormData>, user: Option<User>) -> HttpResponse {
- 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);
-
- // 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(&form.email, &form.password_1) {
- 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<i32> = 'p: {
- let split_port: Vec<&str> = host.split(':').collect();
- if split_port.len() == 2 {
- if let Ok(p) = split_port[1].parse::<i32>() {
- 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) => {
- 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")]
-async fn sign_up_check_email(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
- let recipes = connection.get_all_recipe_titles().unwrap_or_default();
- MessageTemplate {
- user: get_current_user(&req, &connection),
- 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<HashMap<String, String>>, connection: web::Data<db::Connection>) -> impl Responder {
- let (client_ip, client_user_agent) = get_ip_and_user_agent(&req);
- let user = get_current_user(&req, &connection);
-
- 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) => {
- let cookie = Cookie::new(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,
- recipes,
- message: "Email validation successful, your account has been created".to_string(),
- }.to_response();
-
- if let Err(error) = response.add_cookie(&cookie) {
- error!("Unable to set cookie after validation: {}", error);
- };
-
- response
- },
- db::ValidationResult::ValidationExpired =>
- MessageTemplate {
- user,
- recipes,
- message: "The validation has expired. Try to sign up again.".to_string(),
- }.to_response(),
- db::ValidationResult::UnknownUser =>
- MessageTemplate {
- user,
- recipes,
- message: "Validation error.".to_string(),
- }.to_response(),
- }
- },
- None => {
- MessageTemplate {
- user,
- recipes,
- message: format!("No token provided"),
- }.to_response()
- },
- }
-}
-
-///// SIGN IN /////
-
-#[derive(Template)]
-#[template(path = "sign_in_form.html")]
-struct SignInFormTemplate {
- user: Option<user::User>,
- email: String,
- message: String,
-}
-
-#[get("/signin")]
-async fn sign_in_get(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
- SignInFormTemplate {
- user: get_current_user(&req, &connection),
- email: String::new(),
- message: String::new(),
- }
-}
-
-#[derive(Deserialize)]
-struct SignInFormData {
- email: String,
- password: String,
-}
-
-enum SignInError {
- AccountNotValidated,
- AuthenticationFailed,
-}
-
-#[post("/signin")]
-async fn sign_in_post(req: HttpRequest, form: web::Form<SignInFormData>, connection: web::Data<db::Connection>) -> impl Responder {
- fn error_response(error: SignInError, form: &web::Form<SignInFormData>, user: Option<User>) -> HttpResponse {
- 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);
- let (client_ip, client_user_agent) = get_ip_and_user_agent(&req);
-
- match connection.sign_in(&form.email, &form.password, &client_ip, &client_user_agent) {
- 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(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);
- };
- response
- },
- Err(error) => {
- error!("Signin error: {}", error);
- error_response(SignInError::AuthenticationFailed, &form, user)
- },
- }
-}
-
-
-///// SIGN OUT /////
-
-#[get("/signout")]
-async fn sign_out(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
- let mut response =
- HttpResponse::Found()
- .insert_header((header::LOCATION, "/"))
- .finish();
-
- if let Some(token_cookie) = req.cookie(COOKIE_AUTH_TOKEN_NAME) {
- if let Err(error) = connection.sign_out(token_cookie.value()) {
- error!("Unable to sign out: {}", error);
- };
-
- if let Err(error) = response.add_removal_cookie(&Cookie::new(COOKIE_AUTH_TOKEN_NAME, "")) {
- error!("Unable to set a removal cookie after sign out: {}", error);
- };
- };
- response
-}
-
-async fn not_found(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
- let recipes = connection.get_all_recipe_titles().unwrap_or_default();
- MessageTemplate {
- user: get_current_user(&req, &connection),
- recipes,
- message: "404: Not found".to_string(),
- }
-}
+mod utils;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
- if process_args() { return Ok(()) }
+ if process_args() {
+ return Ok(());
+ }
std::env::set_var("RUST_LOG", "info,actix_web=info");
env_logger::init();
let db_connection = web::Data::new(db::Connection::new().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_get)
- .service(sign_in_post)
- .service(sign_out)
- .service(view_recipe)
- .service(fs::Files::new("/static", "static"))
- .default_service(web::to(not_found))
- });
+ 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(services::home_page)
+ .service(services::sign_up_get)
+ .service(services::sign_up_post)
+ .service(services::sign_up_check_email)
+ .service(services::sign_up_validation)
+ .service(services::sign_in_get)
+ .service(services::sign_in_post)
+ .service(services::sign_out)
+ .service(services::view_recipe)
+ .service(services::edit_recipe)
+ .service(fs::Files::new("/static", "static"))
+ .default_service(web::to(services::not_found))
+ });
+ //.workers(1);
server.bind(&format!("0.0.0.0:{}", port))?.run().await
}
#[derive(Parser, Debug)]
struct Args {
+ /// Will clear the database and insert some test data. (A backup is made first).
#[arg(long)]
- dbtest: bool
+ dbtest: bool,
}
fn process_args() -> bool {
let args = Args::parse();
if args.dbtest {
+ // Make a backup of the database.
+ let db_path = Path::new(consts::DB_DIRECTORY).join(consts::DB_FILENAME);
+ if db_path.exists() {
+ let db_path_bckup = (1..)
+ .find_map(|n| {
+ let p = db_path.with_extension(format!("sqlite.bckup{:03}", n));
+ if p.exists() {
+ None
+ } else {
+ Some(p)
+ }
+ })
+ .unwrap();
+ std::fs::copy(&db_path, &db_path_bckup).expect(&format!(
+ "Unable to make backup of {:?} to {:?}",
+ &db_path, &db_path_bckup
+ ));
+ std::fs::remove_file(&db_path)
+ .expect(&format!("Unable to remove db file: {:?}", &db_path));
+ }
+
match db::Connection::new() {
Ok(con) => {
if let Err(error) = con.execute_file("sql/data_test.sql") {
- error!("{}", error);
+ eprintln!("{}", error);
}
// Set the creation datetime to 'now'.
- con.execute_sql("UPDATE [User] SET [creation_datetime] = ?1 WHERE [email] = 'paul@test.org'", [Utc::now()]).unwrap();
- },
+ con.execute_sql(
+ "UPDATE [User] SET [creation_datetime] = ?1 WHERE [email] = 'paul@test.org'",
+ [Utc::now()],
+ )
+ .unwrap();
+ }
Err(error) => {
- error!("Error: {}", error)
- },
+ eprintln!("{}", error);
+ }
}
return true;