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
;
14 use crate::config
::Config
;
16 use crate::data
::{asynchronous
, db
};
19 use crate::user
::User
;
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(),
31 .map(|addr
| addr
.ip().to_string())
37 .get(header
::USER_AGENT
)
38 .map(|v
| v
.to_str().unwrap_or_default())
45 async
fn get_current_user(
47 connection
: web
::Data
<db
::Connection
>,
49 let (client_ip
, client_user_agent
) = get_ip_and_user_agent(req
);
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
)
56 Ok(db
::AuthenticationResult
::NotValidToken
) =>
57 // TODO: remove cookie?
61 Ok(db
::AuthenticationResult
::Ok(user_id
)) => {
62 match connection
.load_user_async(user_id
).await
{
63 Ok(user
) => Some(user
),
65 error!("Error during authentication: {}", error
);
71 error!("Error during authentication: {}", error
);
79 type Result
<T
> = std
::result
::Result
<T
, ServiceError
>;
84 pub struct ServiceError
{
85 status_code
: StatusCode
,
86 message
: Option
<String
>,
89 impl From
<asynchronous
::DBAsyncError
> for ServiceError
{
90 fn from(error
: asynchronous
::DBAsyncError
) -> Self {
92 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
93 message
: Some(format!("{:?}", error
)),
98 impl From
<email
::Error
> for ServiceError
{
99 fn from(error
: email
::Error
) -> Self {
101 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
102 message
: Some(format!("{:?}", error
)),
107 impl From
<actix_web
::error
::BlockingError
> for ServiceError
{
108 fn from(error
: actix_web
::error
::BlockingError
) -> Self {
110 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
111 message
: Some(format!("{:?}", error
)),
116 impl From
<ron
::error
::SpannedError
> for ServiceError
{
117 fn from(error
: ron
::error
::SpannedError
) -> Self {
119 status_code
: StatusCode
::INTERNAL_SERVER_ERROR
,
120 message
: Some(format!("{:?}", error
)),
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
)?
;
130 write!(f
, "Code: {}", self.status_code
)
134 impl actix_web
::error
::ResponseError
for ServiceError
{
135 fn error_response(&self) -> HttpResponse
{
136 MessageBaseTemplate
{
137 message
: &self.to_string(),
142 fn status_code(&self) -> StatusCode
{
150 #[template(path = "home.html")]
151 struct HomeTemplate
{
153 recipes
: Vec
<(i64, String
)>,
154 current_recipe_id
: Option
<i64>,
158 pub async
fn home_page(
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?
;
167 current_recipe_id
: None
,
173 ///// VIEW RECIPE /////
176 #[template(path = "view_recipe.html")]
177 struct ViewRecipeTemplate
{
179 recipes
: Vec
<(i64, String
)>,
180 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 RECIPE /////
207 #[template(path = "edit_recipe.html")]
208 struct EditRecipeTemplate
{
210 recipes
: Vec
<(i64, String
)>,
211 current_recipe_id
: Option
<i64>,
212 current_recipe
: model
::Recipe
,
215 #[get("/recipe/edit/{id}")]
216 pub async
fn edit_recipe(
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?
;
226 Ok(EditRecipeTemplate
{
228 current_recipe_id
: Some(recipe
.id
),
230 current_recipe
: recipe
,
238 #[template(path = "message_base.html")]
239 struct MessageBaseTemplate
<'a
> {
244 #[template(path = "message.html")]
245 struct MessageTemplate
<'a
> {
253 #[template(path = "sign_up_form.html")]
254 struct SignUpFormTemplate
{
258 message_email
: String
,
259 message_password
: String
,
263 pub async
fn sign_up_get(
265 connection
: web
::Data
<db
::Connection
>,
266 ) -> impl Responder
{
267 let user
= get_current_user(&req
, connection
.clone()).await
;
270 email
: String
::new(),
271 message
: String
::new(),
272 message_email
: String
::new(),
273 message_password
: String
::new(),
277 #[derive(Deserialize)]
278 pub struct SignUpFormData
{
294 pub async
fn sign_up_post(
296 form
: web
::Form
<SignUpFormData
>,
297 connection
: web
::Data
<db
::Connection
>,
298 config
: web
::Data
<Config
>,
299 ) -> Result
<HttpResponse
> {
302 form
: &web
::Form
<SignUpFormData
>,
304 ) -> Result
<HttpResponse
> {
305 Ok(SignUpFormTemplate
{
307 email
: form
.email
.clone(),
308 message_email
: match error
{
309 SignUpError
::InvalidEmail
=> "Invalid email",
313 message_password
: match error
{
314 SignUpError
::PasswordsNotEqual
=> "Passwords don't match",
315 SignUpError
::InvalidPassword
=> "Password must have at least eight characters",
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",
330 let user
= get_current_user(&req
, connection
.clone()).await
;
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
);
337 if form
.password_1
!= form
.password_2
{
338 return error_response(SignUpError
::PasswordsNotEqual
, &form
, user
);
341 if let common
::utils
::PasswordValidation
::TooShort
=
342 common
::utils
::validate_password(&form
.password_1
)
344 return error_response(SignUpError
::InvalidPassword
, &form
, user
);
348 .sign_up_async(&form
.email
, &form
.password_1
)
351 Ok(db
::SignUpResult
::UserAlreadyExists
) => {
352 error_response(SignUpError
::UserAlreadyExists
, &form
, user
)
354 Ok(db
::SignUpResult
::UserCreatedWaitingForValidation(token
)) => {
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>() {
372 if port
.is_some() && port
.unwrap() != 443 {
381 let email
= form
.email
.clone();
383 match web
::block(move || {
384 email
::send_validation(
389 &config
.smtp_password
,
394 Ok(()) => Ok(HttpResponse
::Found()
395 .insert_header((header
::LOCATION
, "/signup_check_email"))
398 error!("Email validation error: {}", error
);
399 error_response(SignUpError
::UnableSendEmail
, &form
, user
)
404 error!("Signup database error: {}", error
);
405 error_response(SignUpError
::DatabaseError
, &form
, user
)
410 #[get("/signup_check_email")]
411 pub async
fn sign_up_check_email(
413 connection
: web
::Data
<db
::Connection
>,
414 ) -> impl Responder
{
415 let user
= get_current_user(&req
, connection
.clone()).await
;
418 message
: "An email has been sent, follow the link to validate your account.",
422 #[get("/validation")]
423 pub async
fn sign_up_validation(
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
;
431 match query
.get("token") {
436 Duration
::seconds(consts
::VALIDATION_TOKEN_DURATION
),
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
),
447 error!("Error retrieving user by id: {}", error
);
452 let mut response
= MessageTemplate
{
454 message
: "Email validation successful, your account has been created",
458 if let Err(error
) = response
.add_cookie(&cookie
) {
459 error!("Unable to set cookie after validation: {}", error
);
464 db
::ValidationResult
::ValidationExpired
=> Ok(MessageTemplate
{
466 message
: "The validation has expired. Try to sign up again.",
469 db
::ValidationResult
::UnknownUser
=> Ok(MessageTemplate
{
471 message
: "Validation error.",
476 None
=> Ok(MessageTemplate
{
478 message
: &format!("No token provided"),
487 #[template(path = "sign_in_form.html")]
488 struct SignInFormTemplate
{
495 pub async
fn sign_in_get(
497 connection
: web
::Data
<db
::Connection
>,
498 ) -> impl Responder
{
499 let user
= get_current_user(&req
, connection
.clone()).await
;
502 email
: String
::new(),
503 message
: String
::new(),
507 #[derive(Deserialize)]
508 pub struct SignInFormData
{
515 AuthenticationFailed
,
519 pub async
fn sign_in_post(
521 form
: web
::Form
<SignInFormData
>,
522 connection
: web
::Data
<db
::Connection
>,
523 ) -> Result
<HttpResponse
> {
526 form
: &web
::Form
<SignInFormData
>,
528 ) -> Result
<HttpResponse
> {
529 Ok(SignInFormTemplate
{
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",
541 let user
= get_current_user(&req
, connection
.clone()).await
;
542 let (client_ip
, client_user_agent
) = get_ip_and_user_agent(&req
);
545 .sign_in_async(&form
.email
, &form
.password
, &client_ip
, &client_user_agent
)
548 Ok(db
::SignInResult
::AccountNotValidated
) => {
549 error_response(SignInError
::AccountNotValidated
, &form
, user
)
551 Ok(db
::SignInResult
::UserNotFound
) | Ok(db
::SignInResult
::WrongPassword
) => {
552 error_response(SignInError
::AuthenticationFailed
, &form
, user
)
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
, "/"))
559 if let Err(error
) = response
.add_cookie(&cookie
) {
560 error!("Unable to set cookie after sign in: {}", error
);
565 error!("Signin error: {}", error
);
566 error_response(SignInError
::AuthenticationFailed
, &form
, user
)
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
, "/"))
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
);
585 response
.add_removal_cookie(&Cookie
::new(consts
::COOKIE_AUTH_TOKEN_NAME
, ""))
587 error!("Unable to set a removal cookie after sign out: {}", error
);
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
;
597 message
: "404: Not found",