use lettre::Transport; use rocket::{ State, http::{Cookie, Cookies, SameSite, uri::Uri}, response::Redirect, request::{LenientForm, FlashMessage, Form} }; use rocket::http::ext::IntoOwned; use rocket_i18n::I18n; use std::{borrow::Cow, sync::{Arc, Mutex}, time::Instant}; use validator::{Validate, ValidationError, ValidationErrors}; use template_utils::Ructe; use plume_models::{ BASE_URL, Error, db_conn::DbConn, users::{User, AUTH_COOKIE} }; use mail::{build_mail, Mailer}; use routes::errors::ErrorPage; #[get("/login?")] pub fn new(user: Option, conn: DbConn, m: Option, intl: I18n) -> Ructe { render!(session::login( &(&*conn, &intl.catalog, user), m, &LoginForm::default(), ValidationErrors::default() )) } #[derive(Default, FromForm, Validate, Serialize)] pub struct LoginForm { #[validate(length(min = "1", message = "We need an email or a username to identify you"))] pub email_or_name: String, #[validate(length(min = "1", message = "Your password can't be empty"))] pub password: String } #[post("/login", data = "
")] pub fn create(conn: DbConn, form: LenientForm, flash: Option, mut cookies: Cookies, intl: I18n) -> Result { let user = User::find_by_email(&*conn, &form.email_or_name) .or_else(|_| User::find_local(&*conn, &form.email_or_name)); let mut errors = match form.validate() { Ok(_) => ValidationErrors::new(), Err(e) => e }; let user_id = if let Ok(user) = user { if !user.auth(&form.password) { let mut err = ValidationError::new("invalid_login"); err.message = Some(Cow::from("Invalid username or password")); errors.add("email_or_name", err); String::new() } else { user.id.to_string() } } else { // Fake password verification, only to avoid different login times // that could be used to see if an email adress is registered or not User::get(&*conn, 1).map(|u| u.auth(&form.password)).expect("No user is registered"); let mut err = ValidationError::new("invalid_login"); err.message = Some(Cow::from("Invalid username or password")); errors.add("email_or_name", err); String::new() }; if errors.is_empty() { cookies.add_private(Cookie::build(AUTH_COOKIE, user_id) .same_site(SameSite::Lax) .finish()); let destination = flash .and_then(|f| if f.name() == "callback" { Some(f.msg().to_owned()) } else { None }) .unwrap_or_else(|| "/".to_owned()); let uri = Uri::parse(&destination) .map(|x| x.into_owned()) .map_err(|_| render!(session::login( &(&*conn, &intl.catalog, None), None, &*form, errors )))?; Ok(Redirect::to(uri)) } else { Err(render!(session::login( &(&*conn, &intl.catalog, None), None, &*form, errors ))) } } #[get("/logout")] pub fn delete(mut cookies: Cookies) -> Redirect { if let Some(cookie) = cookies.get_private(AUTH_COOKIE) { cookies.remove_private(cookie); } Redirect::to("/") } #[derive(Clone)] pub struct ResetRequest { pub mail: String, pub id: String, pub creation_date: Instant, } impl PartialEq for ResetRequest { fn eq(&self, other: &Self) -> bool { self.id == other.id } } #[get("/password-reset")] pub fn password_reset_request_form(conn: DbConn, intl: I18n) -> Ructe { render!(session::password_reset_request( &(&*conn, &intl.catalog, None), &ResetForm::default(), ValidationErrors::default() )) } #[derive(FromForm, Validate, Default)] pub struct ResetForm { #[validate(email)] pub email: String, } #[post("/password-reset", data = "")] pub fn password_reset_request( conn: DbConn, intl: I18n, mail: State>>, form: Form, requests: State>>> ) -> Ructe { let mut requests = requests.lock().unwrap(); // Remove outdated requests (more than 1 day old) to avoid the list to grow too much requests.retain(|r| r.creation_date.elapsed().as_secs() < 24 * 60 * 60); if User::find_by_email(&*conn, &form.email).is_ok() && !requests.iter().any(|x| x.mail == form.email.clone()) { let id = plume_common::utils::random_hex(); requests.push(ResetRequest { mail: form.email.clone(), id: id.clone(), creation_date: Instant::now(), }); let link = format!("https://{}/password-reset/{}", *BASE_URL, id); if let Some(message) = build_mail( form.email.clone(), i18n!(intl.catalog, "Password reset"), i18n!(intl.catalog, "Here is the link to reset your password: {0}"; link) ) { match *mail.lock().unwrap() { Some(ref mut mail) => { mail.send(message.into()).map_err(|_| eprintln!("Couldn't send password reset mail")).ok(); } None => {} } } } render!(session::password_reset_request_ok( &(&*conn, &intl.catalog, None) )) } #[get("/password-reset/")] pub fn password_reset_form(conn: DbConn, intl: I18n, token: String, requests: State>>>) -> Result { requests.lock().unwrap().iter().find(|x| x.id == token.clone()).ok_or(Error::NotFound)?; Ok(render!(session::password_reset( &(&*conn, &intl.catalog, None), &NewPasswordForm::default(), ValidationErrors::default() ))) } #[derive(FromForm, Default, Validate)] #[validate( schema( function = "passwords_match", skip_on_field_errors = "false", message = "Passwords are not matching" ) )] pub struct NewPasswordForm { pub password: String, pub password_confirmation: String, } fn passwords_match(form: &NewPasswordForm) -> Result<(), ValidationError> { if form.password != form.password_confirmation { Err(ValidationError::new("password_match")) } else { Ok(()) } } #[post("/password-reset/", data = "")] pub fn password_reset( conn: DbConn, intl: I18n, token: String, requests: State>>>, form: Form ) -> Result { form.validate() .and_then(|_| { let mut requests = requests.lock().unwrap(); let req = requests.iter().find(|x| x.id == token.clone()).ok_or(to_validation(0))?.clone(); if req.creation_date.elapsed().as_secs() < 60 * 60 * 2 { // Reset link is only valid for 2 hours requests.retain(|r| *r != req); let user = User::find_by_email(&*conn, &req.mail).map_err(to_validation)?; user.reset_password(&*conn, &form.password).ok(); Ok(Redirect::to(uri!(new: m = i18n!(intl.catalog, "Your password was successfully reset.")))) } else { Ok(Redirect::to(uri!(new: m = i18n!(intl.catalog, "Sorry, but the link expired. Try again")))) } }) .map_err(|err| { render!(session::password_reset( &(&*conn, &intl.catalog, None), &form, err )) }) } fn to_validation(_: T) -> ValidationErrors { let mut errors = ValidationErrors::new(); errors.add("", ValidationError { code: Cow::from("server_error"), message: Some(Cow::from("An unknown error occured")), params: std::collections::HashMap::new() }); errors }