Sign up form and other stuff
[recipes.git] / backend / src / main.rs
1 use std::{collections::HashMap, net::ToSocketAddrs};
2
3 use actix_files as fs;
4 use actix_web::{http::header, get, post, web, Responder, middleware, App, HttpServer, HttpRequest, HttpResponse};
5 use askama_actix::{Template, TemplateToResponse};
6 use chrono::{prelude::*, Duration};
7 use clap::Parser;
8 use serde::Deserialize;
9
10 use config::Config;
11
12 mod consts;
13 mod db;
14 mod hash;
15 mod model;
16 mod user;
17 mod email;
18 mod config;
19
20 #[derive(Template)]
21 #[template(path = "home.html")]
22 struct HomeTemplate {
23 recipes: Vec<(i32, String)>,
24 }
25
26 #[derive(Template)]
27 #[template(path = "sign_in_form.html")]
28 struct SignInFormTemplate {
29 }
30
31 #[derive(Template)]
32 #[template(path = "view_recipe.html")]
33 struct ViewRecipeTemplate {
34 recipes: Vec<(i32, String)>,
35 current_recipe: model::Recipe,
36 }
37
38 #[derive(Template)]
39 #[template(path = "message.html")]
40 struct MessageTemplate {
41 recipes: Vec<(i32, String)>,
42 message: String,
43 }
44
45 #[get("/")]
46 async fn home_page(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
47 HomeTemplate { recipes: connection.get_all_recipe_titles().unwrap_or_default() }
48 }
49
50 //// SIGN UP /////
51
52 #[derive(Template)]
53 #[template(path = "sign_up_form.html")]
54 struct SignUpFormTemplate {
55 email: String,
56 message: String,
57 message_email: String,
58 message_password: String,
59 }
60
61 impl SignUpFormTemplate {
62 fn new() -> Self {
63 SignUpFormTemplate { email: String::new(), message: String::new(), message_email: String::new(), message_password: String::new() }
64 }
65 }
66
67 enum SignUpError {
68 InvalidEmail,
69 PasswordsNotEqual,
70 InvalidPassword,
71 UserAlreadyExists,
72 DatabaseError,
73 UnableSendEmail,
74 }
75
76 #[get("/signup")]
77 async fn sign_up_get(req: HttpRequest, query: web::Query<HashMap<String, String>>, connection: web::Data<db::Connection>) -> impl Responder {
78 SignUpFormTemplate::new()
79 }
80
81 #[derive(Deserialize)]
82 struct SignUpFormData {
83 email: String,
84 password_1: String,
85 password_2: String,
86 }
87
88 #[post("/signup")]
89 async fn sign_up_post(req: HttpRequest, form: web::Form<SignUpFormData>, connection: web::Data<db::Connection>, config: web::Data<Config>) -> impl Responder {
90 println!("Sign Up, email: {}, passwords: {}/{}", form.email, form.password_1, form.password_2);
91
92 fn error_response(error: SignUpError, form: &web::Form<SignUpFormData>) -> HttpResponse {
93 SignUpFormTemplate {
94 email: form.email.clone(),
95 message_email:
96 match error {
97 SignUpError::InvalidEmail => "Invalid email",
98 _ => "",
99 }.to_string(),
100 message_password:
101 match error {
102 SignUpError::PasswordsNotEqual => "Passwords don't match",
103 SignUpError::InvalidPassword => "Password must have at least eight characters",
104 _ => "",
105 }.to_string(),
106 message:
107 match error {
108 SignUpError::UserAlreadyExists => "This email is already taken",
109 SignUpError::DatabaseError => "Database error",
110 SignUpError::UnableSendEmail => "Unable to send the validation email",
111 _ => "",
112 }.to_string(),
113 }.to_response()
114 }
115
116 // Validation of email and password.
117 if let common::utils::EmailValidation::NotValid = common::utils::validate_email(&form.email) {
118 return error_response(SignUpError::InvalidEmail, &form);
119 }
120
121 if form.password_1 != form.password_2 {
122 return error_response(SignUpError::PasswordsNotEqual, &form);
123 }
124
125 if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form.password_1) {
126 return error_response(SignUpError::InvalidPassword, &form);
127 }
128
129 match connection.sign_up(&form.email, &form.password_1) {
130 Ok(db::SignUpResult::UserAlreadyExists) => {
131 error_response(SignUpError::UserAlreadyExists, &form)
132 },
133 Ok(db::SignUpResult::UserCreatedWaitingForValidation(token)) => {
134 let url = {
135 let host = req.headers().get(header::HOST).map(|v| v.to_str().unwrap_or_default()).unwrap_or_default();
136 let port: Option<i32> = 'p: {
137 let split_port: Vec<&str> = host.split(':').collect();
138 if split_port.len() == 2 {
139 if let Ok(p) = split_port[1].parse::<i32>() {
140 break 'p Some(p)
141 }
142 }
143 None
144 };
145 format!("http{}://{}", if port.is_some() && port.unwrap() != 443 { "" } else { "s" }, host)
146 };
147 match email::send_validation(&url, &form.email, &token, &config.smtp_login, &config.smtp_password) {
148 Ok(()) =>
149 HttpResponse::Found()
150 .insert_header((header::LOCATION, "/signup_check_email"))
151 .finish(),
152 Err(error) => {
153 eprintln!("Email validation error: {:?}", error);
154 error_response(SignUpError::UnableSendEmail, &form)
155 },
156 }
157 },
158 Err(error) => {
159 eprintln!("Signup database error: {:?}", error);
160 error_response(SignUpError::DatabaseError, &form)
161 },
162 }
163 }
164
165 #[get("/signup_check_email")]
166 async fn sign_up_check_email(connection: web::Data<db::Connection>) -> impl Responder {
167 let recipes = connection.get_all_recipe_titles().unwrap_or_default();
168 MessageTemplate {
169 recipes,
170 message: "An email has been sent, follow the link to validate your account.".to_string(),
171 }
172 }
173
174 #[get("/validation")]
175 async fn sign_up_validation(req: HttpRequest, query: web::Query<HashMap<String, String>>, connection: web::Data<db::Connection>) -> impl Responder {
176
177 println!("req:\n{:#?}", req);
178
179 let client_user_agent = req.headers().get(header::USER_AGENT).map(|v| v.to_str().unwrap_or_default()).unwrap_or_default();
180 let client_ip = req.peer_addr().map(|addr| addr.ip().to_string()).unwrap_or_default();
181
182 let recipes = connection.get_all_recipe_titles().unwrap_or_default();
183 match query.get("token") {
184 Some(token) => {
185 match connection.validation(token, Duration::seconds(consts::VALIDATION_TOKEN_DURATION), &client_ip, client_user_agent).unwrap() {
186 db::ValidationResult::Ok(token, user_id) =>
187 // TODO: set token to cookie.
188 MessageTemplate {
189 recipes,
190 message: "Email validation successful, your account has been created".to_string(),
191 },
192 db::ValidationResult::ValidationExpired =>
193 MessageTemplate {
194 recipes,
195 message: "The validation has expired. Try to sign up again.".to_string(),
196 },
197 db::ValidationResult::UnknownUser =>
198 MessageTemplate {
199 recipes,
200 message: "Validation error.".to_string(),
201 },
202 }
203 },
204 None => {
205 MessageTemplate {
206 recipes,
207 message: format!("No token provided"),
208 }
209 },
210 }
211 }
212
213 ///// SIGN IN /////
214
215 #[get("/signinform")]
216 async fn sign_in_form(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
217 SignInFormTemplate {
218 }
219 }
220
221 #[post("/signin")]
222 async fn sign_in(req: HttpRequest) -> impl Responder {
223 "todo"
224 }
225
226 #[get("/recipe/view/{id}")]
227 async fn view_recipe(req: HttpRequest, path: web::Path<(i32,)>, connection: web::Data<db::Connection>) -> impl Responder {
228 let (id,)= path.into_inner();
229 let recipes = connection.get_all_recipe_titles().unwrap_or_default();
230 println!("{:?}", recipes);
231 match connection.get_recipe(id) {
232 Ok(recipe) =>
233 ViewRecipeTemplate {
234 recipes,
235 current_recipe: recipe,
236 }.to_response(),
237 Err(_error) =>
238 MessageTemplate {
239 recipes,
240 message: format!("Unable to get recipe #{}", id),
241 }.to_response(),
242 }
243 }
244
245 async fn not_found(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
246 let recipes = connection.get_all_recipe_titles().unwrap_or_default();
247 MessageTemplate {
248 recipes,
249 message: "404: Not found".to_string(),
250 }
251 }
252
253 fn get_exe_name() -> String {
254 let first_arg = std::env::args().nth(0).unwrap();
255 let sep: &[_] = &['\\', '/'];
256 first_arg[first_arg.rfind(sep).unwrap()+1..].to_string()
257 }
258
259 #[actix_web::main]
260 async fn main() -> std::io::Result<()> {
261 if process_args() { return Ok(()) }
262
263 std::env::set_var("RUST_LOG", "actix_web=debug");
264 env_logger::init();
265
266 println!("Starting Recipes as web server...");
267
268 let config = web::Data::new(config::load());
269 let port = config.as_ref().port;
270
271 println!("Configuration: {:?}", config);
272
273 let db_connection = web::Data::new(db::Connection::new().unwrap());
274
275 std::env::set_var("RUST_LOG", "actix_web=info");
276
277 let server =
278 HttpServer::new(move || {
279 App::new()
280 .wrap(middleware::Logger::default())
281 .wrap(middleware::Compress::default())
282 .app_data(db_connection.clone())
283 .app_data(config.clone())
284 .service(home_page)
285 .service(sign_up_get)
286 .service(sign_up_post)
287 .service(sign_up_check_email)
288 .service(sign_up_validation)
289 .service(sign_in_form)
290 .service(sign_in)
291 .service(view_recipe)
292 .service(fs::Files::new("/static", "static"))
293 .default_service(web::to(not_found))
294 //.default_service(not_found)
295 });
296
297 server.bind(&format!("0.0.0.0:{}", port))?.run().await
298 }
299
300 #[derive(Parser, Debug)]
301 struct Args {
302 #[arg(long)]
303 dbtest: bool
304 }
305
306 fn process_args() -> bool {
307 let args = Args::parse();
308
309 if args.dbtest {
310 match db::Connection::new() {
311 Ok(con) => {
312 if let Err(error) = con.execute_file("sql/data_test.sql") {
313 println!("Error: {:?}", error);
314 }
315 // Set the creation datetime to 'now'.
316 con.execute_sql("UPDATE [User] SET [creation_datetime] = ?1 WHERE [email] = 'paul@test.org'", [Utc::now()]).unwrap();
317 },
318 Err(error) => {
319 println!("Error: {:?}", error)
320 },
321 }
322
323 return true;
324 }
325
326 false
327
328 /*
329
330
331 fn print_usage() {
332 println!("Usage:");
333 println!(" {} [--help] [--test]", get_exe_name());
334 }
335
336 let args: Vec<String> = args().collect();
337
338 if args.iter().any(|arg| arg == "--help") {
339 print_usage();
340 return true
341 } else if args.iter().any(|arg| arg == "--test") {
342 match db::Connection::new() {
343 Ok(_) => (),
344 Err(error) => println!("Error: {:?}", error)
345 }
346 return true
347 }
348 false
349 */
350 }