1 use std
::collections
::HashMap
;
6 http
::{header
, header
::ContentType
, StatusCode
},
7 post
, web
, HttpRequest
, HttpResponse
, Responder
,
9 use askama_actix
::{Template
, TemplateToResponse
};
11 use log
::{debug
, error
, info
, log_enabled
, Level
};
12 use serde
::Deserialize
;
17 data
::{asynchronous
, db
},
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(),
30 .map(|addr
| addr
.ip().to_string())
36 .get(header
::USER_AGENT
)
37 .map(|v
| v
.to_str().unwrap_or_default())
44 async
fn get_current_user(
46 connection
: web
::Data
<db
::Connection
>,
47 ) -> Option
<model
::User
> {
48 let (client_ip
, client_user_agent
) = get_ip_and_user_agent(req
);
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
)
55 Ok(db
::AuthenticationResult
::NotValidToken
) =>
56 // TODO: remove cookie?
60 Ok(db
::AuthenticationResult
::Ok(user_id
)) => {
61 match connection
.load_user_async(user_id
).await
{
62 Ok(user
) => Some(user
),
64 error!("Error during authentication: {}", error
);
70 error!("Error during authentication: {}", error
);
78 type Result
<T
> = std
::result
::Result
<T
, ServiceError
>;
83 pub struct ServiceError
{
84 status_code
: StatusCode
,
85 message
: Option
<String
>,
88 impl From
<asynchronous
::DBAsyncError
> for ServiceError
{
89 fn from(error
: asynchronous
::DBAsyncError
) -> Self {
91 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
92 message
: Some(format!("{:?}", error
)),
97 impl From
<email
::Error
> for ServiceError
{
98 fn from(error
: email
::Error
) -> Self {
100 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
101 message
: Some(format!("{:?}", error
)),
106 impl From
<actix_web
::error
::BlockingError
> for ServiceError
{
107 fn from(error
: actix_web
::error
::BlockingError
) -> Self {
109 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
110 message
: Some(format!("{:?}", error
)),
115 impl From
<ron
::error
::SpannedError
> for ServiceError
{
116 fn from(error
: ron
::error
::SpannedError
) -> Self {
118 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
119 message
: Some(format!("{:?}", error
)),
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
)?
;
129 write!(f
, "Code: {}", self.status_code
)
133 impl actix_web
::error
::ResponseError
for ServiceError
{
134 fn error_response(&self) -> HttpResponse
{
135 MessageBaseTemplate
{
136 message
: &self.to_string(),
141 fn status_code(&self) -> StatusCode
{
149 #[template(path = "home.html")]
150 struct HomeTemplate
{
151 user
: Option
<model
::User
>,
152 recipes
: Vec
<(i64, String
)>,
153 current_recipe_id
: Option
<i64>,
157 pub async
fn home_page(
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?
;
166 current_recipe_id
: None
,
172 ///// VIEW RECIPE /////
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>,
181 current_recipe
: model
::Recipe
,
184 #[get("/recipe/view/{id}")]
185 pub async
fn view_recipe(
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?
;
195 Ok(ViewRecipeTemplate
{
197 current_recipe_id
: Some(recipe
.id
),
199 current_recipe
: recipe
,
204 ///// EDIT/NEW RECIPE /////
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>,
213 current_recipe
: model
::Recipe
,
216 #[get("/recipe/edit/{id}")]
217 pub async
fn edit_recipe(
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
{
226 return Ok(MessageTemplate
{
228 message
: "Cannot edit a recipe without being logged in",
234 let recipe
= connection
.get_recipe_async(id
).await?
;
236 if recipe
.user_id
!= user
.id
{
237 return Ok(MessageTemplate
{
238 message
: "Cannot edit a recipe you don't own",
244 let recipes
= connection
.get_all_recipe_titles_async().await?
;
246 Ok(EditRecipeTemplate
{
248 current_recipe_id
: Some(recipe
.id
),
250 current_recipe
: recipe
,
255 #[get("/recipe/new")]
256 pub async
fn new_recipe(
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
{
264 return Ok(MessageTemplate
{
265 message
: "Cannot create a recipe without being logged in",
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
;
276 Ok(EditRecipeTemplate
{
278 current_recipe_id
: Some(recipe_id
),
280 current_recipe
: model
::Recipe
::empty(recipe_id
, user_id
),
288 #[template(path = "message_base.html")]
289 struct MessageBaseTemplate
<'a
> {
294 #[template(path = "message.html")]
295 struct MessageTemplate
<'a
> {
296 user
: Option
<model
::User
>,
303 #[template(path = "sign_up_form.html")]
304 struct SignUpFormTemplate
{
305 user
: Option
<model
::User
>,
308 message_email
: String
,
309 message_password
: String
,
313 pub async
fn sign_up_get(
315 connection
: web
::Data
<db
::Connection
>,
316 ) -> impl Responder
{
317 let user
= get_current_user(&req
, connection
.clone()).await
;
320 email
: String
::new(),
321 message
: String
::new(),
322 message_email
: String
::new(),
323 message_password
: String
::new(),
327 #[derive(Deserialize)]
328 pub struct SignUpFormData
{
344 pub async
fn sign_up_post(
346 form
: web
::Form
<SignUpFormData
>,
347 connection
: web
::Data
<db
::Connection
>,
348 config
: web
::Data
<Config
>,
349 ) -> Result
<HttpResponse
> {
352 form
: &web
::Form
<SignUpFormData
>,
353 user
: Option
<model
::User
>,
354 ) -> Result
<HttpResponse
> {
355 Ok(SignUpFormTemplate
{
357 email
: form
.email
.clone(),
358 message_email
: match error
{
359 SignUpError
::InvalidEmail
=> "Invalid email",
363 message_password
: match error
{
364 SignUpError
::PasswordsNotEqual
=> "Passwords don't match",
365 SignUpError
::InvalidPassword
=> "Password must have at least eight characters",
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",
380 let user
= get_current_user(&req
, connection
.clone()).await
;
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
);
387 if form
.password_1
!= form
.password_2
{
388 return error_response(SignUpError
::PasswordsNotEqual
, &form
, user
);
391 if let common
::utils
::PasswordValidation
::TooShort
=
392 common
::utils
::validate_password(&form
.password_1
)
394 return error_response(SignUpError
::InvalidPassword
, &form
, user
);
398 .sign_up_async(&form
.email
, &form
.password_1
)
401 Ok(db
::SignUpResult
::UserAlreadyExists
) => {
402 error_response(SignUpError
::UserAlreadyExists
, &form
, user
)
404 Ok(db
::SignUpResult
::UserCreatedWaitingForValidation(token
)) => {
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>() {
422 if port
.is_some() && port
.unwrap() != 443 {
431 let email
= form
.email
.clone();
433 match web
::block(move || {
434 email
::send_validation(
439 &config
.smtp_password
,
444 Ok(()) => Ok(HttpResponse
::Found()
445 .insert_header((header
::LOCATION
, "/signup_check_email"))
448 error!("Email validation error: {}", error
);
449 error_response(SignUpError
::UnableSendEmail
, &form
, user
)
454 error!("Signup database error: {}", error
);
455 error_response(SignUpError
::DatabaseError
, &form
, user
)
460 #[get("/signup_check_email")]
461 pub async
fn sign_up_check_email(
463 connection
: web
::Data
<db
::Connection
>,
464 ) -> impl Responder
{
465 let user
= get_current_user(&req
, connection
.clone()).await
;
468 message
: "An email has been sent, follow the link to validate your account.",
472 #[get("/validation")]
473 pub async
fn sign_up_validation(
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
;
481 match query
.get("token") {
486 Duration
::seconds(consts
::VALIDATION_TOKEN_DURATION
),
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
),
497 error!("Error retrieving user by id: {}", error
);
502 let mut response
= MessageTemplate
{
504 message
: "Email validation successful, your account has been created",
508 if let Err(error
) = response
.add_cookie(&cookie
) {
509 error!("Unable to set cookie after validation: {}", error
);
514 db
::ValidationResult
::ValidationExpired
=> Ok(MessageTemplate
{
516 message
: "The validation has expired. Try to sign up again.",
519 db
::ValidationResult
::UnknownUser
=> Ok(MessageTemplate
{
521 message
: "Validation error.",
526 None
=> Ok(MessageTemplate
{
528 message
: &format!("No token provided"),
537 #[template(path = "sign_in_form.html")]
538 struct SignInFormTemplate
{
539 user
: Option
<model
::User
>,
545 pub async
fn sign_in_get(
547 connection
: web
::Data
<db
::Connection
>,
548 ) -> impl Responder
{
549 let user
= get_current_user(&req
, connection
.clone()).await
;
552 email
: String
::new(),
553 message
: String
::new(),
557 #[derive(Deserialize)]
558 pub struct SignInFormData
{
565 AuthenticationFailed
,
569 pub async
fn sign_in_post(
571 form
: web
::Form
<SignInFormData
>,
572 connection
: web
::Data
<db
::Connection
>,
573 ) -> Result
<HttpResponse
> {
576 form
: &web
::Form
<SignInFormData
>,
577 user
: Option
<model
::User
>,
578 ) -> Result
<HttpResponse
> {
579 Ok(SignInFormTemplate
{
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",
591 let user
= get_current_user(&req
, connection
.clone()).await
;
592 let (client_ip
, client_user_agent
) = get_ip_and_user_agent(&req
);
595 .sign_in_async(&form
.email
, &form
.password
, &client_ip
, &client_user_agent
)
598 Ok(db
::SignInResult
::AccountNotValidated
) => {
599 error_response(SignInError
::AccountNotValidated
, &form
, user
)
601 Ok(db
::SignInResult
::UserNotFound
) | Ok(db
::SignInResult
::WrongPassword
) => {
602 error_response(SignInError
::AuthenticationFailed
, &form
, user
)
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
, "/"))
609 if let Err(error
) = response
.add_cookie(&cookie
) {
610 error!("Unable to set cookie after sign in: {}", error
);
615 error!("Signin error: {}", error
);
616 error_response(SignInError
::AuthenticationFailed
, &form
, user
)
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
, "/"))
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
);
635 response
.add_removal_cookie(&Cookie
::new(consts
::COOKIE_AUTH_TOKEN_NAME
, ""))
637 error!("Unable to set a removal cookie after sign out: {}", error
);
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
;
647 message
: "404: Not found",