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