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