rustfmt
[recipes.git] / backend / src / services.rs
1 use std::collections::HashMap;
2
3 use actix_web::{
4 cookie::Cookie,
5 get,
6 http::{header, header::ContentType, StatusCode},
7 post, web, HttpRequest, HttpResponse, Responder,
8 };
9 use askama_actix::{Template, TemplateToResponse};
10 use chrono::Duration;
11 use log::{debug, error, info, log_enabled, Level};
12 use serde::Deserialize;
13
14 use crate::{
15 config::Config,
16 consts,
17 data::{asynchronous, db},
18 email, model, utils,
19 };
20
21 mod api;
22
23 ///// UTILS /////
24
25 fn get_ip_and_user_agent(req: &HttpRequest) -> (String, String) {
26 let ip = match req.headers().get(consts::REVERSE_PROXY_IP_HTTP_FIELD) {
27 Some(v) => v.to_str().unwrap_or_default().to_string(),
28 None => req
29 .peer_addr()
30 .map(|addr| addr.ip().to_string())
31 .unwrap_or_default(),
32 };
33
34 let user_agent = req
35 .headers()
36 .get(header::USER_AGENT)
37 .map(|v| v.to_str().unwrap_or_default())
38 .unwrap_or_default()
39 .to_string();
40
41 (ip, user_agent)
42 }
43
44 async fn get_current_user(
45 req: &HttpRequest,
46 connection: web::Data<db::Connection>,
47 ) -> Option<model::User> {
48 let (client_ip, client_user_agent) = get_ip_and_user_agent(req);
49
50 match req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) {
51 Some(token_cookie) => match connection
52 .authentication_async(token_cookie.value(), &client_ip, &client_user_agent)
53 .await
54 {
55 Ok(db::AuthenticationResult::NotValidToken) =>
56 // TODO: remove cookie?
57 {
58 None
59 }
60 Ok(db::AuthenticationResult::Ok(user_id)) => {
61 match connection.load_user_async(user_id).await {
62 Ok(user) => Some(user),
63 Err(error) => {
64 error!("Error during authentication: {}", error);
65 None
66 }
67 }
68 }
69 Err(error) => {
70 error!("Error during authentication: {}", error);
71 None
72 }
73 },
74 None => None,
75 }
76 }
77
78 type Result<T> = std::result::Result<T, ServiceError>;
79
80 ///// ERROR /////
81
82 #[derive(Debug)]
83 pub struct ServiceError {
84 status_code: StatusCode,
85 message: Option<String>,
86 }
87
88 impl From<asynchronous::DBAsyncError> for ServiceError {
89 fn from(error: asynchronous::DBAsyncError) -> Self {
90 ServiceError {
91 status_code: StatusCode::INTERNAL_SERVER_ERROR,
92 message: Some(format!("{:?}", error)),
93 }
94 }
95 }
96
97 impl From<email::Error> for ServiceError {
98 fn from(error: email::Error) -> Self {
99 ServiceError {
100 status_code: StatusCode::INTERNAL_SERVER_ERROR,
101 message: Some(format!("{:?}", error)),
102 }
103 }
104 }
105
106 impl From<actix_web::error::BlockingError> for ServiceError {
107 fn from(error: actix_web::error::BlockingError) -> Self {
108 ServiceError {
109 status_code: StatusCode::INTERNAL_SERVER_ERROR,
110 message: Some(format!("{:?}", error)),
111 }
112 }
113 }
114
115 impl From<ron::error::SpannedError> for ServiceError {
116 fn from(error: ron::error::SpannedError) -> Self {
117 ServiceError {
118 status_code: StatusCode::INTERNAL_SERVER_ERROR,
119 message: Some(format!("{:?}", error)),
120 }
121 }
122 }
123
124 impl std::fmt::Display for ServiceError {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
126 if let Some(ref m) = self.message {
127 write!(f, "**{}**\n\n", m)?;
128 }
129 write!(f, "Code: {}", self.status_code)
130 }
131 }
132
133 impl actix_web::error::ResponseError for ServiceError {
134 fn error_response(&self) -> HttpResponse {
135 MessageBaseTemplate {
136 message: &self.to_string(),
137 }
138 .to_response()
139 }
140
141 fn status_code(&self) -> StatusCode {
142 self.status_code
143 }
144 }
145
146 ///// HOME /////
147
148 #[derive(Template)]
149 #[template(path = "home.html")]
150 struct HomeTemplate {
151 user: Option<model::User>,
152 recipes: Vec<(i64, String)>,
153 current_recipe_id: Option<i64>,
154 }
155
156 #[get("/")]
157 pub async fn home_page(
158 req: HttpRequest,
159 connection: web::Data<db::Connection>,
160 ) -> Result<HttpResponse> {
161 let user = get_current_user(&req, connection.clone()).await;
162 let recipes = connection.get_all_recipe_titles_async().await?;
163
164 Ok(HomeTemplate {
165 user,
166 current_recipe_id: None,
167 recipes,
168 }
169 .to_response())
170 }
171
172 ///// VIEW RECIPE /////
173
174 #[derive(Template)]
175 #[template(path = "view_recipe.html")]
176 struct ViewRecipeTemplate {
177 user: Option<model::User>,
178 recipes: Vec<(i64, String)>,
179 current_recipe_id: Option<i64>,
180
181 current_recipe: model::Recipe,
182 }
183
184 #[get("/recipe/view/{id}")]
185 pub async fn view_recipe(
186 req: HttpRequest,
187 path: web::Path<(i64,)>,
188 connection: web::Data<db::Connection>,
189 ) -> Result<HttpResponse> {
190 let (id,) = path.into_inner();
191 let user = get_current_user(&req, connection.clone()).await;
192 let recipes = connection.get_all_recipe_titles_async().await?;
193 let recipe = connection.get_recipe_async(id).await?;
194
195 Ok(ViewRecipeTemplate {
196 user,
197 current_recipe_id: Some(recipe.id),
198 recipes,
199 current_recipe: recipe,
200 }
201 .to_response())
202 }
203
204 ///// EDIT/NEW RECIPE /////
205
206 #[derive(Template)]
207 #[template(path = "edit_recipe.html")]
208 struct EditRecipeTemplate {
209 user: Option<model::User>,
210 recipes: Vec<(i64, String)>,
211 current_recipe_id: Option<i64>,
212
213 current_recipe: model::Recipe,
214 }
215
216 #[get("/recipe/edit/{id}")]
217 pub async fn edit_recipe(
218 req: HttpRequest,
219 path: web::Path<(i64,)>,
220 connection: web::Data<db::Connection>,
221 ) -> Result<HttpResponse> {
222 let (id,) = path.into_inner();
223 let user = match get_current_user(&req, connection.clone()).await {
224 Some(u) => u,
225 None => {
226 return Ok(MessageTemplate {
227 user: None,
228 message: "Cannot edit a recipe without being logged in",
229 }
230 .to_response())
231 }
232 };
233
234 let recipe = connection.get_recipe_async(id).await?;
235
236 if recipe.user_id != user.id {
237 return Ok(MessageTemplate {
238 message: "Cannot edit a recipe you don't own",
239 user: Some(user),
240 }
241 .to_response());
242 }
243
244 let recipes = connection.get_all_recipe_titles_async().await?;
245
246 Ok(EditRecipeTemplate {
247 user: Some(user),
248 current_recipe_id: Some(recipe.id),
249 recipes,
250 current_recipe: recipe,
251 }
252 .to_response())
253 }
254
255 #[get("/recipe/new")]
256 pub async fn new_recipe(
257 req: HttpRequest,
258 path: web::Path<(i64,)>,
259 connection: web::Data<db::Connection>,
260 ) -> Result<HttpResponse> {
261 let user = match get_current_user(&req, connection.clone()).await {
262 Some(u) => u,
263 None => {
264 return Ok(MessageTemplate {
265 message: "Cannot create a recipe without being logged in",
266 user: None,
267 }
268 .to_response())
269 }
270 };
271
272 let recipe_id = connection.create_recipe_async(user.id).await?;
273 let recipes = connection.get_all_recipe_titles_async().await?;
274 let user_id = user.id;
275
276 Ok(EditRecipeTemplate {
277 user: Some(user),
278 current_recipe_id: Some(recipe_id),
279 recipes,
280 current_recipe: model::Recipe::empty(recipe_id, user_id),
281 }
282 .to_response())
283 }
284
285 ///// MESSAGE /////
286
287 #[derive(Template)]
288 #[template(path = "message_base.html")]
289 struct MessageBaseTemplate<'a> {
290 message: &'a str,
291 }
292
293 #[derive(Template)]
294 #[template(path = "message.html")]
295 struct MessageTemplate<'a> {
296 user: Option<model::User>,
297 message: &'a str,
298 }
299
300 //// SIGN UP /////
301
302 #[derive(Template)]
303 #[template(path = "sign_up_form.html")]
304 struct SignUpFormTemplate {
305 user: Option<model::User>,
306 email: String,
307 message: String,
308 message_email: String,
309 message_password: String,
310 }
311
312 #[get("/signup")]
313 pub async fn sign_up_get(
314 req: HttpRequest,
315 connection: web::Data<db::Connection>,
316 ) -> impl Responder {
317 let user = get_current_user(&req, connection.clone()).await;
318 SignUpFormTemplate {
319 user,
320 email: String::new(),
321 message: String::new(),
322 message_email: String::new(),
323 message_password: String::new(),
324 }
325 }
326
327 #[derive(Deserialize)]
328 pub struct SignUpFormData {
329 email: String,
330 password_1: String,
331 password_2: String,
332 }
333
334 enum SignUpError {
335 InvalidEmail,
336 PasswordsNotEqual,
337 InvalidPassword,
338 UserAlreadyExists,
339 DatabaseError,
340 UnableSendEmail,
341 }
342
343 #[post("/signup")]
344 pub async fn sign_up_post(
345 req: HttpRequest,
346 form: web::Form<SignUpFormData>,
347 connection: web::Data<db::Connection>,
348 config: web::Data<Config>,
349 ) -> Result<HttpResponse> {
350 fn error_response(
351 error: SignUpError,
352 form: &web::Form<SignUpFormData>,
353 user: Option<model::User>,
354 ) -> Result<HttpResponse> {
355 Ok(SignUpFormTemplate {
356 user,
357 email: form.email.clone(),
358 message_email: match error {
359 SignUpError::InvalidEmail => "Invalid email",
360 _ => "",
361 }
362 .to_string(),
363 message_password: match error {
364 SignUpError::PasswordsNotEqual => "Passwords don't match",
365 SignUpError::InvalidPassword => "Password must have at least eight characters",
366 _ => "",
367 }
368 .to_string(),
369 message: match error {
370 SignUpError::UserAlreadyExists => "This email is already taken",
371 SignUpError::DatabaseError => "Database error",
372 SignUpError::UnableSendEmail => "Unable to send the validation email",
373 _ => "",
374 }
375 .to_string(),
376 }
377 .to_response())
378 }
379
380 let user = get_current_user(&req, connection.clone()).await;
381
382 // Validation of email and password.
383 if let common::utils::EmailValidation::NotValid = common::utils::validate_email(&form.email) {
384 return error_response(SignUpError::InvalidEmail, &form, user);
385 }
386
387 if form.password_1 != form.password_2 {
388 return error_response(SignUpError::PasswordsNotEqual, &form, user);
389 }
390
391 if let common::utils::PasswordValidation::TooShort =
392 common::utils::validate_password(&form.password_1)
393 {
394 return error_response(SignUpError::InvalidPassword, &form, user);
395 }
396
397 match connection
398 .sign_up_async(&form.email, &form.password_1)
399 .await
400 {
401 Ok(db::SignUpResult::UserAlreadyExists) => {
402 error_response(SignUpError::UserAlreadyExists, &form, user)
403 }
404 Ok(db::SignUpResult::UserCreatedWaitingForValidation(token)) => {
405 let url = {
406 let host = req
407 .headers()
408 .get(header::HOST)
409 .map(|v| v.to_str().unwrap_or_default())
410 .unwrap_or_default();
411 let port: Option<u16> = 'p: {
412 let split_port: Vec<&str> = host.split(':').collect();
413 if split_port.len() == 2 {
414 if let Ok(p) = split_port[1].parse::<u16>() {
415 break 'p Some(p);
416 }
417 }
418 None
419 };
420 format!(
421 "http{}://{}",
422 if port.is_some() && port.unwrap() != 443 {
423 ""
424 } else {
425 "s"
426 },
427 host
428 )
429 };
430
431 let email = form.email.clone();
432
433 match web::block(move || {
434 email::send_validation(
435 &url,
436 &email,
437 &token,
438 &config.smtp_login,
439 &config.smtp_password,
440 )
441 })
442 .await?
443 {
444 Ok(()) => Ok(HttpResponse::Found()
445 .insert_header((header::LOCATION, "/signup_check_email"))
446 .finish()),
447 Err(error) => {
448 error!("Email validation error: {}", error);
449 error_response(SignUpError::UnableSendEmail, &form, user)
450 }
451 }
452 }
453 Err(error) => {
454 error!("Signup database error: {}", error);
455 error_response(SignUpError::DatabaseError, &form, user)
456 }
457 }
458 }
459
460 #[get("/signup_check_email")]
461 pub async fn sign_up_check_email(
462 req: HttpRequest,
463 connection: web::Data<db::Connection>,
464 ) -> impl Responder {
465 let user = get_current_user(&req, connection.clone()).await;
466 MessageTemplate {
467 user,
468 message: "An email has been sent, follow the link to validate your account.",
469 }
470 }
471
472 #[get("/validation")]
473 pub async fn sign_up_validation(
474 req: HttpRequest,
475 query: web::Query<HashMap<String, String>>,
476 connection: web::Data<db::Connection>,
477 ) -> Result<HttpResponse> {
478 let (client_ip, client_user_agent) = get_ip_and_user_agent(&req);
479 let user = get_current_user(&req, connection.clone()).await;
480
481 match query.get("token") {
482 Some(token) => {
483 match connection
484 .validation_async(
485 token,
486 Duration::seconds(consts::VALIDATION_TOKEN_DURATION),
487 &client_ip,
488 &client_user_agent,
489 )
490 .await?
491 {
492 db::ValidationResult::Ok(token, user_id) => {
493 let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
494 let user = match connection.load_user(user_id) {
495 Ok(user) => Some(user),
496 Err(error) => {
497 error!("Error retrieving user by id: {}", error);
498 None
499 }
500 };
501
502 let mut response = MessageTemplate {
503 user,
504 message: "Email validation successful, your account has been created",
505 }
506 .to_response();
507
508 if let Err(error) = response.add_cookie(&cookie) {
509 error!("Unable to set cookie after validation: {}", error);
510 };
511
512 Ok(response)
513 }
514 db::ValidationResult::ValidationExpired => Ok(MessageTemplate {
515 user,
516 message: "The validation has expired. Try to sign up again.",
517 }
518 .to_response()),
519 db::ValidationResult::UnknownUser => Ok(MessageTemplate {
520 user,
521 message: "Validation error.",
522 }
523 .to_response()),
524 }
525 }
526 None => Ok(MessageTemplate {
527 user,
528 message: &format!("No token provided"),
529 }
530 .to_response()),
531 }
532 }
533
534 ///// SIGN IN /////
535
536 #[derive(Template)]
537 #[template(path = "sign_in_form.html")]
538 struct SignInFormTemplate {
539 user: Option<model::User>,
540 email: String,
541 message: String,
542 }
543
544 #[get("/signin")]
545 pub async fn sign_in_get(
546 req: HttpRequest,
547 connection: web::Data<db::Connection>,
548 ) -> impl Responder {
549 let user = get_current_user(&req, connection.clone()).await;
550 SignInFormTemplate {
551 user,
552 email: String::new(),
553 message: String::new(),
554 }
555 }
556
557 #[derive(Deserialize)]
558 pub struct SignInFormData {
559 email: String,
560 password: String,
561 }
562
563 enum SignInError {
564 AccountNotValidated,
565 AuthenticationFailed,
566 }
567
568 #[post("/signin")]
569 pub async fn sign_in_post(
570 req: HttpRequest,
571 form: web::Form<SignInFormData>,
572 connection: web::Data<db::Connection>,
573 ) -> Result<HttpResponse> {
574 fn error_response(
575 error: SignInError,
576 form: &web::Form<SignInFormData>,
577 user: Option<model::User>,
578 ) -> Result<HttpResponse> {
579 Ok(SignInFormTemplate {
580 user,
581 email: form.email.clone(),
582 message: match error {
583 SignInError::AccountNotValidated => "This account must be validated first",
584 SignInError::AuthenticationFailed => "Wrong email or password",
585 }
586 .to_string(),
587 }
588 .to_response())
589 }
590
591 let user = get_current_user(&req, connection.clone()).await;
592 let (client_ip, client_user_agent) = get_ip_and_user_agent(&req);
593
594 match connection
595 .sign_in_async(&form.email, &form.password, &client_ip, &client_user_agent)
596 .await
597 {
598 Ok(db::SignInResult::AccountNotValidated) => {
599 error_response(SignInError::AccountNotValidated, &form, user)
600 }
601 Ok(db::SignInResult::UserNotFound) | Ok(db::SignInResult::WrongPassword) => {
602 error_response(SignInError::AuthenticationFailed, &form, user)
603 }
604 Ok(db::SignInResult::Ok(token, user_id)) => {
605 let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
606 let mut response = HttpResponse::Found()
607 .insert_header((header::LOCATION, "/"))
608 .finish();
609 if let Err(error) = response.add_cookie(&cookie) {
610 error!("Unable to set cookie after sign in: {}", error);
611 };
612 Ok(response)
613 }
614 Err(error) => {
615 error!("Signin error: {}", error);
616 error_response(SignInError::AuthenticationFailed, &form, user)
617 }
618 }
619 }
620
621 ///// SIGN OUT /////
622
623 #[get("/signout")]
624 pub async fn sign_out(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
625 let mut response = HttpResponse::Found()
626 .insert_header((header::LOCATION, "/"))
627 .finish();
628
629 if let Some(token_cookie) = req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) {
630 if let Err(error) = connection.sign_out_async(token_cookie.value()).await {
631 error!("Unable to sign out: {}", error);
632 };
633
634 if let Err(error) =
635 response.add_removal_cookie(&Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, ""))
636 {
637 error!("Unable to set a removal cookie after sign out: {}", error);
638 };
639 };
640 response
641 }
642
643 pub async fn not_found(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
644 let user = get_current_user(&req, connection.clone()).await;
645 MessageTemplate {
646 user,
647 message: "404: Not found",
648 }
649 }