Add asynchronous call to database.
[recipes.git] / backend / src / services.rs
1 use std::collections::HashMap;
2
3 use actix_web::{http::{header, header::ContentType, StatusCode}, get, post, web, Responder, HttpRequest, HttpResponse, cookie::Cookie};
4 use askama_actix::{Template, TemplateToResponse};
5 use chrono::Duration;
6 use serde::Deserialize;
7 use log::{debug, error, log_enabled, info, Level};
8
9 use crate::utils;
10 use crate::email;
11 use crate::consts;
12 use crate::config::Config;
13 use crate::user::User;
14 use crate::model;
15 use crate::data::{db, asynchronous};
16
17 ///// UTILS /////
18
19 fn get_ip_and_user_agent(req: &HttpRequest) -> (String, String) {
20 let ip =
21 match req.headers().get(consts::REVERSE_PROXY_IP_HTTP_FIELD) {
22 Some(v) => v.to_str().unwrap_or_default().to_string(),
23 None => req.peer_addr().map(|addr| addr.ip().to_string()).unwrap_or_default()
24 };
25
26 let user_agent = req.headers().get(header::USER_AGENT).map(|v| v.to_str().unwrap_or_default()).unwrap_or_default().to_string();
27
28 (ip, user_agent)
29 }
30
31 async fn get_current_user(req: &HttpRequest, connection: web::Data<db::Connection>) -> Option<User> {
32 let (client_ip, client_user_agent) = get_ip_and_user_agent(req);
33
34 match req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) {
35 Some(token_cookie) =>
36 match connection.authentication_async(token_cookie.value(), &client_ip, &client_user_agent).await {
37 Ok(db::AuthenticationResult::NotValidToken) =>
38 // TODO: remove cookie?
39 None,
40 Ok(db::AuthenticationResult::Ok(user_id)) =>
41 match connection.load_user_async(user_id).await {
42 Ok(user) =>
43 Some(user),
44 Err(error) => {
45 error!("Error during authentication: {}", error);
46 None
47 }
48 },
49 Err(error) => {
50 error!("Error during authentication: {}", error);
51 None
52 },
53 },
54 None => None
55 }
56 }
57
58 type Result<T> = std::result::Result<T, ServiceError>;
59
60 ///// ERROR /////
61
62 #[derive(Debug)]
63 pub struct ServiceError {
64 status_code: StatusCode,
65 message: Option<String>,
66 }
67
68 impl From<asynchronous::DBAsyncError> for ServiceError {
69 fn from(error: asynchronous::DBAsyncError) -> Self {
70 ServiceError {
71 status_code: StatusCode::INTERNAL_SERVER_ERROR,
72 message: Some(format!("{:?}", error)),
73 }
74 }
75 }
76
77 impl From<email::Error> for ServiceError {
78 fn from(error: email::Error) -> Self {
79 ServiceError {
80 status_code: StatusCode::INTERNAL_SERVER_ERROR,
81 message: Some(format!("{:?}", error)),
82 }
83 }
84 }
85
86 impl From<actix_web::error::BlockingError> for ServiceError {
87 fn from(error: actix_web::error::BlockingError) -> Self {
88 ServiceError {
89 status_code: StatusCode::INTERNAL_SERVER_ERROR,
90 message: Some(format!("{:?}", error)),
91 }
92 }
93 }
94
95 impl std::fmt::Display for ServiceError {
96 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
97 if let Some(ref m) = self.message {
98 write!(f, "**{}**\n\n", m)?;
99 }
100 write!(f, "Code: {}", self.status_code)
101 }
102 }
103
104 impl actix_web::error::ResponseError for ServiceError {
105 fn error_response(&self) -> HttpResponse {
106 MessageBaseTemplate {
107 message: &self.to_string(),
108 }.to_response()
109 }
110
111 fn status_code(&self) -> StatusCode {
112 self.status_code
113 }
114 }
115
116 ///// HOME /////
117
118 #[derive(Template)]
119 #[template(path = "home.html")]
120 struct HomeTemplate {
121 user: Option<User>,
122 recipes: Vec<(i32, String)>,
123 current_recipe_id: Option<i32>,
124 }
125
126 #[get("/")]
127 pub async fn home_page(req: HttpRequest, connection: web::Data<db::Connection>) -> Result<HttpResponse> {
128 let user = get_current_user(&req, connection.clone()).await;
129 let recipes = connection.get_all_recipe_titles_async().await?;
130
131 Ok(HomeTemplate { user, current_recipe_id: None, recipes }.to_response())
132 }
133
134 ///// VIEW RECIPE /////
135
136 #[derive(Template)]
137 #[template(path = "view_recipe.html")]
138 struct ViewRecipeTemplate {
139 user: Option<User>,
140 recipes: Vec<(i32, String)>,
141 current_recipe_id: Option<i32>,
142 current_recipe: model::Recipe,
143 }
144
145 #[get("/recipe/view/{id}")]
146 pub async fn view_recipe(req: HttpRequest, path: web::Path<(i32,)>, connection: web::Data<db::Connection>) -> Result<HttpResponse> {
147 let (id,)= path.into_inner();
148 let user = get_current_user(&req, connection.clone()).await;
149 let recipes = connection.get_all_recipe_titles_async().await?;
150 let recipe = connection.get_recipe_async(id).await?;
151
152 Ok(ViewRecipeTemplate {
153 user,
154 current_recipe_id: Some(recipe.id),
155 recipes,
156 current_recipe: recipe,
157 }.to_response())
158 }
159
160 ///// MESSAGE /////
161
162 #[derive(Template)]
163 #[template(path = "message_base.html")]
164 struct MessageBaseTemplate<'a> {
165 message: &'a str,
166 }
167
168 #[derive(Template)]
169 #[template(path = "message.html")]
170 struct MessageTemplate<'a> {
171 user: Option<User>,
172 message: &'a str,
173 }
174
175 //// SIGN UP /////
176
177 #[derive(Template)]
178 #[template(path = "sign_up_form.html")]
179 struct SignUpFormTemplate {
180 user: Option<User>,
181 email: String,
182 message: String,
183 message_email: String,
184 message_password: String,
185 }
186
187 #[get("/signup")]
188 pub async fn sign_up_get(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
189 let user = get_current_user(&req, connection.clone()).await;
190 SignUpFormTemplate { user, email: String::new(), message: String::new(), message_email: String::new(), message_password: String::new() }
191 }
192
193 #[derive(Deserialize)]
194 pub struct SignUpFormData {
195 email: String,
196 password_1: String,
197 password_2: String,
198 }
199
200 enum SignUpError {
201 InvalidEmail,
202 PasswordsNotEqual,
203 InvalidPassword,
204 UserAlreadyExists,
205 DatabaseError,
206 UnableSendEmail,
207 }
208
209 #[post("/signup")]
210 pub async fn sign_up_post(req: HttpRequest, form: web::Form<SignUpFormData>, connection: web::Data<db::Connection>, config: web::Data<Config>) -> Result<HttpResponse> {
211 fn error_response(error: SignUpError, form: &web::Form<SignUpFormData>, user: Option<User>) -> Result<HttpResponse> {
212 Ok(SignUpFormTemplate {
213 user,
214 email: form.email.clone(),
215 message_email:
216 match error {
217 SignUpError::InvalidEmail => "Invalid email",
218 _ => "",
219 }.to_string(),
220 message_password:
221 match error {
222 SignUpError::PasswordsNotEqual => "Passwords don't match",
223 SignUpError::InvalidPassword => "Password must have at least eight characters",
224 _ => "",
225 }.to_string(),
226 message:
227 match error {
228 SignUpError::UserAlreadyExists => "This email is already taken",
229 SignUpError::DatabaseError => "Database error",
230 SignUpError::UnableSendEmail => "Unable to send the validation email",
231 _ => "",
232 }.to_string(),
233 }.to_response())
234 }
235
236 let user = get_current_user(&req, connection.clone()).await;
237
238 // Validation of email and password.
239 if let common::utils::EmailValidation::NotValid = common::utils::validate_email(&form.email) {
240 return error_response(SignUpError::InvalidEmail, &form, user);
241 }
242
243 if form.password_1 != form.password_2 {
244 return error_response(SignUpError::PasswordsNotEqual, &form, user);
245 }
246
247 if let common::utils::PasswordValidation::TooShort = common::utils::validate_password(&form.password_1) {
248 return error_response(SignUpError::InvalidPassword, &form, user);
249 }
250
251 match connection.sign_up_async(&form.email, &form.password_1).await {
252 Ok(db::SignUpResult::UserAlreadyExists) => {
253 error_response(SignUpError::UserAlreadyExists, &form, user)
254 },
255 Ok(db::SignUpResult::UserCreatedWaitingForValidation(token)) => {
256 let url = {
257 let host = req.headers().get(header::HOST).map(|v| v.to_str().unwrap_or_default()).unwrap_or_default();
258 let port: Option<i32> = 'p: {
259 let split_port: Vec<&str> = host.split(':').collect();
260 if split_port.len() == 2 {
261 if let Ok(p) = split_port[1].parse::<i32>() {
262 break 'p Some(p)
263 }
264 }
265 None
266 };
267 format!("http{}://{}", if port.is_some() && port.unwrap() != 443 { "" } else { "s" }, host)
268 };
269
270 let email = form.email.clone();
271
272 match web::block(move || { email::send_validation(&url, &email, &token, &config.smtp_login, &config.smtp_password) }).await? {
273 Ok(()) =>
274 Ok(HttpResponse::Found()
275 .insert_header((header::LOCATION, "/signup_check_email"))
276 .finish()),
277 Err(error) => {
278 error!("Email validation error: {}", error);
279 error_response(SignUpError::UnableSendEmail, &form, user)
280 },
281 }
282 },
283 Err(error) => {
284 error!("Signup database error: {}", error);
285 error_response(SignUpError::DatabaseError, &form, user)
286 },
287 }
288 }
289
290 #[get("/signup_check_email")]
291 pub async fn sign_up_check_email(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
292 let user = get_current_user(&req, connection.clone()).await;
293 MessageTemplate {
294 user,
295 message: "An email has been sent, follow the link to validate your account.",
296 }
297 }
298
299 #[get("/validation")]
300 pub async fn sign_up_validation(req: HttpRequest, query: web::Query<HashMap<String, String>>, connection: web::Data<db::Connection>) -> Result<HttpResponse> {
301 let (client_ip, client_user_agent) = get_ip_and_user_agent(&req);
302 let user = get_current_user(&req, connection.clone()).await;
303
304 match query.get("token") {
305 Some(token) => {
306 match connection.validation_async(token, Duration::seconds(consts::VALIDATION_TOKEN_DURATION), &client_ip, &client_user_agent).await? {
307 db::ValidationResult::Ok(token, user_id) => {
308 let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
309 let user =
310 match connection.load_user(user_id) {
311 Ok(user) =>
312 Some(user),
313 Err(error) => {
314 error!("Error retrieving user by id: {}", error);
315 None
316 }
317 };
318
319 let mut response =
320 MessageTemplate {
321 user,
322 message: "Email validation successful, your account has been created",
323 }.to_response();
324
325 if let Err(error) = response.add_cookie(&cookie) {
326 error!("Unable to set cookie after validation: {}", error);
327 };
328
329 Ok(response)
330 },
331 db::ValidationResult::ValidationExpired =>
332 Ok(MessageTemplate {
333 user,
334 message: "The validation has expired. Try to sign up again.",
335 }.to_response()),
336 db::ValidationResult::UnknownUser =>
337 Ok(MessageTemplate {
338 user,
339 message: "Validation error.",
340 }.to_response()),
341 }
342 },
343 None => {
344 Ok(MessageTemplate {
345 user,
346 message: &format!("No token provided"),
347 }.to_response())
348 },
349 }
350 }
351
352 ///// SIGN IN /////
353
354 #[derive(Template)]
355 #[template(path = "sign_in_form.html")]
356 struct SignInFormTemplate {
357 user: Option<User>,
358 email: String,
359 message: String,
360 }
361
362 #[get("/signin")]
363 pub async fn sign_in_get(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
364 let user = get_current_user(&req, connection.clone()).await;
365 SignInFormTemplate {
366 user,
367 email: String::new(),
368 message: String::new(),
369 }
370 }
371
372 #[derive(Deserialize)]
373 pub struct SignInFormData {
374 email: String,
375 password: String,
376 }
377
378 enum SignInError {
379 AccountNotValidated,
380 AuthenticationFailed,
381 }
382
383 #[post("/signin")]
384 pub async fn sign_in_post(req: HttpRequest, form: web::Form<SignInFormData>, connection: web::Data<db::Connection>) -> Result<HttpResponse> {
385 fn error_response(error: SignInError, form: &web::Form<SignInFormData>, user: Option<User>) -> Result<HttpResponse> {
386 Ok(SignInFormTemplate {
387 user,
388 email: form.email.clone(),
389 message:
390 match error {
391 SignInError::AccountNotValidated => "This account must be validated first",
392 SignInError::AuthenticationFailed => "Wrong email or password",
393 }.to_string(),
394 }.to_response())
395 }
396
397 let user = get_current_user(&req, connection.clone()).await;
398 let (client_ip, client_user_agent) = get_ip_and_user_agent(&req);
399
400 match connection.sign_in_async(&form.email, &form.password, &client_ip, &client_user_agent).await {
401 Ok(db::SignInResult::AccountNotValidated) =>
402 error_response(SignInError::AccountNotValidated, &form, user),
403 Ok(db::SignInResult::UserNotFound) | Ok(db::SignInResult::WrongPassword) => {
404 error_response(SignInError::AuthenticationFailed, &form, user)
405 },
406 Ok(db::SignInResult::Ok(token, user_id)) => {
407 let cookie = Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, token);
408 let mut response =
409 HttpResponse::Found()
410 .insert_header((header::LOCATION, "/"))
411 .finish();
412 if let Err(error) = response.add_cookie(&cookie) {
413 error!("Unable to set cookie after sign in: {}", error);
414 };
415 Ok(response)
416 },
417 Err(error) => {
418 error!("Signin error: {}", error);
419 error_response(SignInError::AuthenticationFailed, &form, user)
420 },
421 }
422 }
423
424
425 ///// SIGN OUT /////
426
427 #[get("/signout")]
428 pub async fn sign_out(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
429 let mut response =
430 HttpResponse::Found()
431 .insert_header((header::LOCATION, "/"))
432 .finish();
433
434 if let Some(token_cookie) = req.cookie(consts::COOKIE_AUTH_TOKEN_NAME) {
435 if let Err(error) = connection.sign_out_async(token_cookie.value()).await {
436 error!("Unable to sign out: {}", error);
437 };
438
439 if let Err(error) = response.add_removal_cookie(&Cookie::new(consts::COOKIE_AUTH_TOKEN_NAME, "")) {
440 error!("Unable to set a removal cookie after sign out: {}", error);
441 };
442 };
443 response
444 }
445
446 pub async fn not_found(req: HttpRequest, connection: web::Data<db::Connection>) -> impl Responder {
447 let user = get_current_user(&req, connection.clone()).await;
448 MessageTemplate {
449 user,
450 message: "404: Not found",
451 }
452 }