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
},
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(),
32 .map(|addr
| addr
.ip().to_string())
38 .get(header
::USER_AGENT
)
39 .map(|v
| v
.to_str().unwrap_or_default())
46 async
fn get_current_user(
48 connection
: web
::Data
<db
::Connection
>,
49 ) -> Option
<model
::User
> {
50 let (client_ip
, client_user_agent
) = get_ip_and_user_agent(req
);
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
)
57 Ok(db
::AuthenticationResult
::NotValidToken
) =>
58 // TODO: remove cookie?
62 Ok(db
::AuthenticationResult
::Ok(user_id
)) => {
63 match connection
.load_user_async(user_id
).await
{
64 Ok(user
) => Some(user
),
66 error!("Error during authentication: {}", error
);
72 error!("Error during authentication: {}", error
);
80 type Result
<T
> = std
::result
::Result
<T
, ServiceError
>;
85 pub struct ServiceError
{
86 status_code
: StatusCode
,
87 message
: Option
<String
>,
90 impl From
<asynchronous
::DBAsyncError
> for ServiceError
{
91 fn from(error
: asynchronous
::DBAsyncError
) -> Self {
93 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
94 message
: Some(format!("{:?}", error
)),
99 impl From
<email
::Error
> for ServiceError
{
100 fn from(error
: email
::Error
) -> Self {
102 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
103 message
: Some(format!("{:?}", error
)),
108 impl From
<actix_web
::error
::BlockingError
> for ServiceError
{
109 fn from(error
: actix_web
::error
::BlockingError
) -> Self {
111 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
112 message
: Some(format!("{:?}", error
)),
117 impl From
<ron
::error
::SpannedError
> for ServiceError
{
118 fn from(error
: ron
::error
::SpannedError
) -> Self {
120 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
121 message
: Some(format!("{:?}", error
)),
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
)?
;
131 write!(f
, "Code: {}", self.status_code
)
135 impl actix_web
::error
::ResponseError
for ServiceError
{
136 fn error_response(&self) -> HttpResponse
{
137 MessageBaseTemplate
{
138 message
: &self.to_string(),
143 fn status_code(&self) -> StatusCode
{
151 #[template(path = "home.html")]
152 struct HomeTemplate
{
153 user
: Option
<model
::User
>,
154 recipes
: Vec
<(i64, String
)>,
155 current_recipe_id
: Option
<i64>,
159 pub async
fn home_page(
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?
;
168 current_recipe_id
: None
,
174 ///// VIEW RECIPE /////
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>,
183 current_recipe
: model
::Recipe
,
186 #[get("/recipe/view/{id}")]
187 pub async
fn view_recipe(
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?
;
197 Ok(ViewRecipeTemplate
{
199 current_recipe_id
: Some(recipe
.id
),
201 current_recipe
: recipe
,
206 ///// EDIT/NEW RECIPE /////
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>,
215 current_recipe
: model
::Recipe
,
218 #[get("/recipe/edit/{id}")]
219 pub async
fn edit_recipe(
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
{
228 return Ok(MessageTemplate
{
230 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",
243 let recipes
= connection
.get_all_recipe_titles_async().await?
;
245 Ok(EditRecipeTemplate
{
247 current_recipe_id
: Some(recipe
.id
),
249 current_recipe
: recipe
,
254 #[get("/recipe/new")]
255 pub async
fn new_recipe(
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
{
263 return Ok(MessageTemplate
{
264 message
: "Cannot create a recipe without being logged in",
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
;
273 Ok(EditRecipeTemplate
{
275 current_recipe_id
: Some(recipe_id
),
277 current_recipe
: model
::Recipe
::empty(recipe_id
, user_id
),
285 #[template(path = "message_base.html")]
286 struct MessageBaseTemplate
<'a
> {
291 #[template(path = "message.html")]
292 struct MessageTemplate
<'a
> {
293 user
: Option
<model
::User
>,
300 #[template(path = "sign_up_form.html")]
301 struct SignUpFormTemplate
{
302 user
: Option
<model
::User
>,
305 message_email
: String
,
306 message_password
: String
,
310 pub async
fn sign_up_get(
312 connection
: web
::Data
<db
::Connection
>,
313 ) -> impl Responder
{
314 let user
= get_current_user(&req
, connection
.clone()).await
;
317 email
: String
::new(),
318 message
: String
::new(),
319 message_email
: String
::new(),
320 message_password
: String
::new(),
324 #[derive(Deserialize)]
325 pub struct SignUpFormData
{
341 pub async
fn sign_up_post(
343 form
: web
::Form
<SignUpFormData
>,
344 connection
: web
::Data
<db
::Connection
>,
345 config
: web
::Data
<Config
>,
346 ) -> Result
<HttpResponse
> {
349 form
: &web
::Form
<SignUpFormData
>,
350 user
: Option
<model
::User
>,
351 ) -> Result
<HttpResponse
> {
352 Ok(SignUpFormTemplate
{
354 email
: form
.email
.clone(),
355 message_email
: match error
{
356 SignUpError
::InvalidEmail
=> "Invalid email",
360 message_password
: match error
{
361 SignUpError
::PasswordsNotEqual
=> "Passwords don't match",
362 SignUpError
::InvalidPassword
=> "Password must have at least eight characters",
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",
377 let user
= get_current_user(&req
, connection
.clone()).await
;
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
);
384 if form
.password_1
!= form
.password_2
{
385 return error_response(SignUpError
::PasswordsNotEqual
, &form
, user
);
388 if let common
::utils
::PasswordValidation
::TooShort
=
389 common
::utils
::validate_password(&form
.password_1
)
391 return error_response(SignUpError
::InvalidPassword
, &form
, user
);
395 .sign_up_async(&form
.email
, &form
.password_1
)
398 Ok(db
::SignUpResult
::UserAlreadyExists
) => {
399 error_response(SignUpError
::UserAlreadyExists
, &form
, user
)
401 Ok(db
::SignUpResult
::UserCreatedWaitingForValidation(token
)) => {
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>() {
419 if port
.is_some() && port
.unwrap() != 443 {
428 let email
= form
.email
.clone();
430 match web
::block(move || {
431 email
::send_validation(
436 &config
.smtp_password
,
441 Ok(()) => Ok(HttpResponse
::Found()
442 .insert_header((header
::LOCATION
, "/signup_check_email"))
445 error!("Email validation error: {}", error
);
446 error_response(SignUpError
::UnableSendEmail
, &form
, user
)
451 error!("Signup database error: {}", error
);
452 error_response(SignUpError
::DatabaseError
, &form
, user
)
457 #[get("/signup_check_email")]
458 pub async
fn sign_up_check_email(
460 connection
: web
::Data
<db
::Connection
>,
461 ) -> impl Responder
{
462 let user
= get_current_user(&req
, connection
.clone()).await
;
465 message
: "An email has been sent, follow the link to validate your account.",
469 #[get("/validation")]
470 pub async
fn sign_up_validation(
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
;
478 match query
.get("token") {
483 Duration
::seconds(consts
::VALIDATION_TOKEN_DURATION
),
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
),
494 error!("Error retrieving user by id: {}", error
);
499 let mut response
= MessageTemplate
{
501 message
: "Email validation successful, your account has been created",
505 if let Err(error
) = response
.add_cookie(&cookie
) {
506 error!("Unable to set cookie after validation: {}", error
);
511 db
::ValidationResult
::ValidationExpired
=> Ok(MessageTemplate
{
513 message
: "The validation has expired. Try to sign up again.",
516 db
::ValidationResult
::UnknownUser
=> Ok(MessageTemplate
{
518 message
: "Validation error.",
523 None
=> Ok(MessageTemplate
{
525 message
: &format!("No token provided"),
534 #[template(path = "sign_in_form.html")]
535 struct SignInFormTemplate
{
536 user
: Option
<model
::User
>,
542 pub async
fn sign_in_get(
544 connection
: web
::Data
<db
::Connection
>,
545 ) -> impl Responder
{
546 let user
= get_current_user(&req
, connection
.clone()).await
;
549 email
: String
::new(),
550 message
: String
::new(),
554 #[derive(Deserialize)]
555 pub struct SignInFormData
{
562 AuthenticationFailed
,
566 pub async
fn sign_in_post(
568 form
: web
::Form
<SignInFormData
>,
569 connection
: web
::Data
<db
::Connection
>,
570 ) -> Result
<HttpResponse
> {
573 form
: &web
::Form
<SignInFormData
>,
574 user
: Option
<model
::User
>,
575 ) -> Result
<HttpResponse
> {
576 Ok(SignInFormTemplate
{
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",
588 let user
= get_current_user(&req
, connection
.clone()).await
;
589 let (client_ip
, client_user_agent
) = get_ip_and_user_agent(&req
);
592 .sign_in_async(&form
.email
, &form
.password
, &client_ip
, &client_user_agent
)
595 Ok(db
::SignInResult
::AccountNotValidated
) => {
596 error_response(SignInError
::AccountNotValidated
, &form
, user
)
598 Ok(db
::SignInResult
::UserNotFound
) | Ok(db
::SignInResult
::WrongPassword
) => {
599 error_response(SignInError
::AuthenticationFailed
, &form
, user
)
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
, "/"))
606 if let Err(error
) = response
.add_cookie(&cookie
) {
607 error!("Unable to set cookie after sign in: {}", error
);
612 error!("Signin error: {}", error
);
613 error_response(SignInError
::AuthenticationFailed
, &form
, user
)
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
, "/"))
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
);
632 response
.add_removal_cookie(&Cookie
::new(consts
::COOKIE_AUTH_TOKEN_NAME
, ""))
634 error!("Unable to set a removal cookie after sign out: {}", error
);
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
;
644 message
: "404: Not found",