diff --git a/config/default.toml b/config/default.toml index f98f569..b085bd9 100644 --- a/config/default.toml +++ b/config/default.toml @@ -2,6 +2,8 @@ connection_string = "postgresql://postgres:qwertz@localhost:5432/postgres" [application] +url = "http://localhost:8000/" +name = "einsatz.online" default_language = "de-DE" fallback_language = "en-US" loglevel = "debug" @@ -11,4 +13,8 @@ upload_path = "uploads/" #Maximum login attempts until email is locked for login max_login_attempts = 6 #Duration of email lock after max_login_attempts in seconds. Default is 30 minutes -login_lock_duration = 1800 \ No newline at end of file +login_lock_duration = 1800 + +[mail] +from = "No Reply " +reply_to = "support@localhost" \ No newline at end of file diff --git a/resources/mail_templates/password-reset-de.hbs b/resources/mail_templates/password-reset-de.hbs new file mode 100644 index 0000000..d183e2e --- /dev/null +++ b/resources/mail_templates/password-reset-de.hbs @@ -0,0 +1,7 @@ +Jemand hat das Zurücksetzen des Passworts auf {{frontpage}} für die Email +{{email}} +angefordert. +Falls dies nicht beabsichtigt war, ignoriere einfach diese E-Mail. Dein altes Passwort bleibt wirksam. + +Um dein Passwort zurückzusetzen, besuche folgende Adresse: +{{reset_url}} \ No newline at end of file diff --git a/resources/templates/module_welcome.hbs b/resources/templates/module_welcome.hbs index e5118d2..4b754d9 100644 --- a/resources/templates/module_welcome.hbs +++ b/resources/templates/module_welcome.hbs @@ -30,11 +30,11 @@
-
+

Passwort vergessen?

- - + +
diff --git a/resources/templates/password_reset.hbs b/resources/templates/password_reset.hbs new file mode 100644 index 0000000..383d636 --- /dev/null +++ b/resources/templates/password_reset.hbs @@ -0,0 +1,30 @@ +{{> header }} +
+
+
+

Willkommen bei einsatz.online!

+
+ {{#if logo_path}} +
+ +
+ {{/if}} +
+
+
+
+
+ {{#if alert}} + {{> alert}} + {{/if}} +

Neues Passwort festlegen

+

Ihr Passwort muss mindestens 10 Zeichen lang sein und sollte Buchstaben, Zahlen und Sonderzeichen enthalten.

+
+ + +
+ +
+
+
+{{> footer }} \ No newline at end of file diff --git a/src/database/controller/password_resets.rs b/src/database/controller/password_resets.rs index dce08ad..2246e9f 100644 --- a/src/database/controller/password_resets.rs +++ b/src/database/controller/password_resets.rs @@ -1,11 +1,16 @@ use crate::helper::settings::Settings; use rocket::State; use crate::database::controller::connector::establish_connection; -use crate::schema::password_resets::dsl::password_resets; +use crate::schema::password_resets::dsl::{password_resets}; use rand::{thread_rng, Rng}; use rand::distributions::Alphanumeric; use diesel::{ExpressionMethods, RunQueryDsl}; use chrono::NaiveDateTime; +use diesel::query_dsl::filter_dsl::FilterDsl; +use diesel::query_dsl::select_dsl::SelectDsl; +use argon2::Config; +use crate::schema::users::dsl::users; +use diesel::result::Error; /// Adds password reset token to database and returns it pub fn add_token(settings: &State, user_id: uuid::Uuid) -> Result{ @@ -25,4 +30,71 @@ pub fn add_token(settings: &State, user_id: uuid::Uuid) -> Result, token2: String) -> Result<(), diesel::result::Error>{ + let connection = establish_connection(settings); + + match diesel::delete(password_resets).filter(crate::schema::password_resets::dsl::token.eq(token2)).execute(&connection){ + Ok(_) => Ok(()), + Err(e) => { + error!("Couldn't delete token: {}", e); + Err(e) + } + } +} + +pub fn validate_token(settings: &State, token2: String) -> Result{ + let connection = establish_connection(settings); + + match password_resets.select(crate::schema::password_resets::dsl::user_id).filter(crate::schema::password_resets::dsl::token.eq(token2)).get_result(&connection){ + Ok(user_id) => Ok(user_id), + Err(e) => match e{ + diesel::result::Error::NotFound => { + return Err(e) + }, + _ => { + error!("Couldn't validate token: {}", e); + return Err(e) + } + } + } +} + +pub fn change_password_with_token(settings: &State, token2: String, password: String) -> Result<(), ()>{ + let user_id = match validate_token(settings, token2.clone()){ + Ok(user_id) => user_id, + Err(e) => return Err(()) + }; + + let connection =establish_connection(settings); + let salt = rand::thread_rng().gen::<[u8; 32]>(); + let hashed_password = match argon2::hash_encoded(password.as_bytes(), &salt, &Config::default()){ + Ok(pw) => pw, + Err(e) => { + error!("Couldn't hash password: {}", e); + return Err(()) + } + }; + + match diesel::update(users).filter(crate::schema::users::dsl::id.eq(user_id)).set(crate::schema::users::dsl::password.eq(hashed_password)).execute(&connection){ + Ok(_) => (), + Err(e) => { + error!("Couldn't set new password: {}", e); + return Err(()) + } + }; + + match remove_token(settings, token2){ + Ok(_) => Ok(()), + Err(_) => Err(()) + } +} + +pub fn validate_password(password: String) -> bool{ + if password.len() < 10{ + return false + } + + true } \ No newline at end of file diff --git a/src/helper/mail_queue/queue.rs b/src/helper/mail_queue/queue.rs index 24d560e..8d0f74c 100644 --- a/src/helper/mail_queue/queue.rs +++ b/src/helper/mail_queue/queue.rs @@ -84,40 +84,19 @@ impl MailQueue{ } } -pub trait CreateMail{ - fn new(args : T) -> Mail; -} - -impl CreateMail<(String, Vec, String, Vec, Vec, Option, String, Option)> for Mail{ +impl Mail{ /// Create Mail with from, to, subject, cc, bcc, reply_to, deliver_until - fn new(args: (String, Vec, String, Vec, Vec, Option, String, Option)) -> Mail { + pub(crate) fn new(from: String, to: Vec, subject: String, cc: Vec, bcc: Vec, reply_to: Option, body: String, deliver_until: Option) -> Mail { Mail{ uuid: uuid::Uuid::new_v4(), - from: args.0, - to: args.1, - subject: args.2, - cc: args.3, - bcc: args.4, - reply_to: args.5, - body: args.6, - deliver_until: args.7 - } - } -} - -impl CreateMail<(String, Vec, String, Option, String)> for Mail{ - /// Create Mail with from, to, subject, reply_to - fn new(args: (String, Vec, String, Option, String)) -> Mail { - Mail{ - uuid: uuid::Uuid::new_v4(), - from: args.0, - to: args.1, - subject: args.2, - cc: vec![], - bcc: vec![], - reply_to: args.3, - body: args.4, - deliver_until: None + from, + to, + subject, + cc, + bcc, + reply_to, + body, + deliver_until } } } diff --git a/src/helper/mail_queue/worker.rs b/src/helper/mail_queue/worker.rs index f715782..9715775 100644 --- a/src/helper/mail_queue/worker.rs +++ b/src/helper/mail_queue/worker.rs @@ -31,6 +31,12 @@ pub fn send_mail(mail: Mail) -> Result<(), ()> { } arg.push_str("' ") } + match mail.reply_to{ + Some(reply_to) => { + arg.push_str(&format!("--append='Reply-To: {} ' ", reply_to)) + }, + None => () + } arg.push_str("-s \""); arg.push_str(&subject); arg.push_str("\" "); @@ -38,15 +44,12 @@ pub fn send_mail(mail: Mail) -> Result<(), ()> { arg.push_str(&(receiver+" ")); } - debug!("Trying to send mail: {}", arg); - match Command::new("sh").arg("-c").arg(arg).output(){ Ok(output) => { if !output.status.success(){ error!("Couldn't send mail: {} {} {}", output.status, String::from_utf8_lossy(&output.stderr), String::from_utf8_lossy(&output.stdout)); Err(()) }else { - debug!("Mail sent: {}", String::from_utf8_lossy(&output.stdout)); Ok(()) } }, diff --git a/src/helper/mail_templates.rs b/src/helper/mail_templates.rs new file mode 100644 index 0000000..0bba9cf --- /dev/null +++ b/src/helper/mail_templates.rs @@ -0,0 +1,5 @@ +use rocket_contrib::templates::handlebars::Handlebars; + +pub struct MailTemplates{ + pub registry: Handlebars, +} \ No newline at end of file diff --git a/src/helper/mod.rs b/src/helper/mod.rs index 7298955..e39a719 100644 --- a/src/helper/mod.rs +++ b/src/helper/mod.rs @@ -11,4 +11,5 @@ pub mod settings; pub mod sitebuilder; pub mod translate_diesel_error; pub mod user_request_guard; -pub mod mail_queue; \ No newline at end of file +pub mod mail_queue; +pub mod mail_templates; \ No newline at end of file diff --git a/src/helper/settings.rs b/src/helper/settings.rs index 9a1bc93..9eb8536 100644 --- a/src/helper/settings.rs +++ b/src/helper/settings.rs @@ -8,6 +8,7 @@ pub struct Database { #[derive(Debug, Deserialize, Default)] pub struct Application { + pub url: String, pub default_language: String, pub fallback_language: String, pub loglevel: String, @@ -15,12 +16,20 @@ pub struct Application { pub upload_path: String, pub max_login_attempts: i32, pub login_lock_duration: i32, + pub name: String, +} + +#[derive(Debug, Deserialize, Default)] +pub struct Mail{ + pub from : String, + pub reply_to : String, } #[derive(Debug, Deserialize, Default)] pub struct Settings { pub database: Database, pub application: Application, + pub mail: Mail } impl Settings { diff --git a/src/main.rs b/src/main.rs index 16298f1..a27064f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,6 +25,8 @@ use chrono::{DateTime, Utc}; use crate::helper::mail_queue::queue::{MailQueue, Mail}; use std::sync::{PoisonError, RwLockWriteGuard, Arc}; use std::collections::VecDeque; +use rocket_contrib::templates::handlebars::{Handlebars, TemplateFileError}; +use crate::helper::mail_templates::MailTemplates; pub mod database; pub mod helper; @@ -43,27 +45,38 @@ fn main() { } }; + // Initialize storage for session cookies let cookie_storage = SessionCookieStorage::new(); - debug!( - "Hello, world! Default Language: {}", - settings.application.default_language - ); - + // Initialize mail queue for second thread handling outgoing mails + // We are using Arc to access mail queue in all threads let mail_queue = Arc::new(MailQueue::load_or_create_new()); let c_lock = Arc::clone(&mail_queue); thread::spawn(move ||{ loop { - c_lock.process_next(); - thread::sleep(time::Duration::from_millis(500)) + match c_lock.process_next(){ + Ok(_) => {} + Err(e) => {error!("MailQueue poisoned: {}", e)} + } + thread::sleep(time::Duration::from_millis(500)) // Only check for new mails ever 500 ms } }); + let mut mail_templates = MailTemplates{ registry: Handlebars::new() }; + match mail_templates.registry.register_templates_directory(".hbs", "resources/mail_templates"){ + Ok(_) => {} + Err(e) => { + error!("Couldn't register mail templates: {}",e); + std::process::exit(1); + } + } + rocket::ignite() .manage(settings) .manage(cookie_storage) .manage(mail_queue) + .manage(mail_templates) .register(catchers![helper::server_errors::unauthorized, helper::server_errors::forbidden, helper::server_errors::notfound, helper::server_errors::notimplemented]) .mount( "/", @@ -71,6 +84,9 @@ fn main() { modules::dashboard::view::dashboard, modules::welcome::view::welcome_get::welcome_get, modules::welcome::view::welcome_post::welcome_post, + modules::welcome::view::welcome_post::password_reset_post, + modules::welcome::view::welcome_post::password_change_post, + modules::welcome::view::welcome_post::password_change_get, modules::welcome::view::login_select_get::login_select_get, modules::welcome::view::logout::logout_get, modules::member_management::view::selection_get::member_management_selection_get, diff --git a/src/modules/welcome/controller/mod.rs b/src/modules/welcome/controller/mod.rs index 24d4eee..7715a01 100644 --- a/src/modules/welcome/controller/mod.rs +++ b/src/modules/welcome/controller/mod.rs @@ -1,2 +1,3 @@ pub mod login; pub mod render; +pub mod password_reset; \ No newline at end of file diff --git a/src/modules/welcome/controller/password_reset.rs b/src/modules/welcome/controller/password_reset.rs new file mode 100644 index 0000000..cff8708 --- /dev/null +++ b/src/modules/welcome/controller/password_reset.rs @@ -0,0 +1,42 @@ +use crate::helper::settings::Settings; +use rocket::State; +use crate::database::controller::users::get_user_by_email; +use crate::database::controller::password_resets::add_token; +use crate::helper::mail_templates::MailTemplates; +use crate::helper::mail_queue::queue::{Mail, MailQueue}; +use std::sync::Arc; + +#[derive(Serialize)] +pub struct PasswortResetMail{ + frontpage: String, + email: String, + reset_url: String, +} + +/// Checks if email belongs to user, if so resets password +pub fn request_password_reset(settings: &State, mt: &State, mq: State>, email: String) -> Result<(), ()>{ + let user = match get_user_by_email(email.clone(), settings){ //Check if email belongs to user, if not return + Some(user) => user, + None => return Ok(()), + }; + + let token = match add_token(settings, user.id){ + Ok(token) => token, + Err(e) => return Err(()), + }; + let pwrm = PasswortResetMail{ + frontpage: settings.application.url.clone(), + email: email.clone(), + reset_url: format!("{}password_reset?token={}", settings.application.url.clone(), token) + }; + let body = match mt.registry.render("password-reset-de", &pwrm){ + Ok(body) => body, + Err(e) => return Err(()), + }; + let mail = Mail::new(settings.mail.from.clone(), vec![email], format!("[{}] - Passwort Zurücksetzen", settings.application.name.clone()), vec![], vec![], Some(settings.mail.reply_to.clone()), body, None); //TODO: Add deliver_until + + match mq.add_mail(mail){ + Ok(_) => Ok(()), + Err(_) => Err(()) + } +} \ No newline at end of file diff --git a/src/modules/welcome/controller/render.rs b/src/modules/welcome/controller/render.rs index 8a172d2..6557357 100644 --- a/src/modules/welcome/controller/render.rs +++ b/src/modules/welcome/controller/render.rs @@ -4,8 +4,8 @@ use crate::modules::welcome::model::welcome_module::WelcomeModule; pub fn get_context(alert: Option) -> WelcomeModule { let header = Header { - html_language: "en".to_string(), - site_title: "ERRMS".to_string(), + html_language: "de".to_string(), + site_title: "einsatz.online".to_string(), stylesheets: vec![Stylesheet { path: "/css/errms.css".to_string(), }], diff --git a/src/modules/welcome/view/welcome_get.rs b/src/modules/welcome/view/welcome_get.rs index 827998f..cea720d 100644 --- a/src/modules/welcome/view/welcome_get.rs +++ b/src/modules/welcome/view/welcome_get.rs @@ -2,13 +2,11 @@ use crate::helper::sitebuilder::model::alerts::{Alert, AlertClass}; use crate::modules::welcome::controller::render::get_context; use rocket::http::Status; use rocket_contrib::templates::Template; -use crate::helper::mail_queue::queue::{MailQueue, Mail, CreateMail}; use rocket::State; use std::sync::Arc; #[get("/?")] -pub fn welcome_get(error: Option, mail_queue: State>) -> Result { - mail_queue.add_mail(Mail::new(("noreply@mgs.einsatz.online".to_string(), vec!["ares@anghenfil.de".to_string()], "Test.".to_string(), None, "Das ist eine Testnachricht! \"Zitat\"".to_string()))); +pub fn welcome_get(error: Option) -> Result { let alert = match error { Some(error) => match error.as_str() { "unauthorized" => Some(Alert::new( @@ -19,6 +17,11 @@ pub fn welcome_get(error: Option, mail_queue: State>) -> AlertClass::Success, "Sie wurden erfolgreich abgemeldet!".to_string(), )), + "password_reset_success" => Some(Alert::new( + AlertClass::Success, + "Ihr Passwort wurde erfolgreich zurückgesetzt!".to_string(), + )), + "password_reset_token_invalid" => Some(Alert::new(AlertClass::Danger, "Der Passwort zurücksetzen Token ist ungültig! Bitte einen neuen Anfordern!".to_string())), "notimplemented" => Some(Alert::new( AlertClass::Danger, "Fehler: Diese Funktion wurde noch nicht implementiert!".to_string(), diff --git a/src/modules/welcome/view/welcome_post.rs b/src/modules/welcome/view/welcome_post.rs index a55db9b..9427ae6 100644 --- a/src/modules/welcome/view/welcome_post.rs +++ b/src/modules/welcome/view/welcome_post.rs @@ -10,6 +10,12 @@ use rocket::response::Redirect; use rocket::State; use rocket_contrib::templates::Template; use crate::modules::welcome::model::login_error_type::LoginError; +use crate::modules::welcome::controller::password_reset::request_password_reset; +use crate::helper::mail_templates::MailTemplates; +use crate::helper::mail_queue::queue::MailQueue; +use std::sync::Arc; +use crate::helper::sitebuilder::model::general::{Footer, Header, Stylesheet}; +use crate::database::controller::password_resets::{validate_token, change_password_with_token, validate_password}; #[post("/", data = "")] pub fn welcome_post( @@ -56,3 +62,113 @@ pub fn welcome_post( Err(Template::render("module_welcome", &get_context(alert))) } + +#[derive(Serialize, Deserialize, FromForm)] +pub struct PasswordResetForm{ + pub(crate) email: String, +} + +#[post("/password_reset", data = "")] +pub fn password_reset_post( + password_reset_form: Form, + settings: State, + mt: State, + mq: State>, +) -> Template { + let mut alert : Option = None; + + match request_password_reset(&settings, &mt, mq, password_reset_form.email.clone()){ + Ok(_) => { + alert = Some(Alert::new( + AlertClass::Success, + "Falls ein Benutzer mit der angegebenen Email existiert wurde ein Link zum Zurücksetzen des Passwortes per Email verschickt!".to_string())) + } + Err(_) => { + alert = Some(Alert::new( + AlertClass::Danger, + "Das Passwort konnte nicht zurückgesetzt werden. Bitte versuchen Sie es später erneut.".to_string())) + + } + } + Template::render("module_welcome", &get_context(alert)) +} + +#[derive(Serialize)] +pub struct PasswordResetPage{ + header: Header, + footer: Footer, + logo_path: String, + alert: Option, +} + +#[get("/password_reset?")] +pub fn password_change_get( + settings: State, + token: String, +) -> Result { + match validate_token(&settings, token){ + Ok(_) => (), + Err(_) => { + return Err(Redirect::to("/?error=password_reset_token_invalid")) + } + } + let header = Header { + html_language: "de".to_string(), + site_title: "einsatz.online".to_string(), + stylesheets: vec![Stylesheet { + path: "/css/errms.css".to_string(), + }], + }; + let footer = Footer { scripts: vec![] }; + let render = PasswordResetPage{ + header, + footer, + logo_path: "/img/logo.jpg".to_string(), + alert: None + }; + + Ok(Template::render("password_reset", &render)) +} + +#[derive(Serialize, Deserialize, FromForm)] +pub struct PasswordChangeForm{ + pub(crate) password: String, +} + +#[post("/password_reset?1", data = "")] +pub fn password_change_post( + settings: State, + password_change_form: Form, + token: String, +) -> Result { + let mut alert = None; + let password_change_form = password_change_form.into_inner(); + if !validate_password(password_change_form.password.clone()){ + alert = Some(Alert::new(AlertClass::Danger, "Das Passwort muss mindestens 10 Zeichen lang sein!".to_string())); + }else { + match change_password_with_token(&settings, token, password_change_form.password) { + Ok(_) => { + return Ok(Redirect::to("/?error=password_reset_success")) + }, + Err(_) => { + alert = Some(Alert::new(AlertClass::Danger, "Es ist ein interner Fehler aufgetreten, das Passwort konnte nicht geändert werden.".to_string())); + } + } + } + let header = Header { + html_language: "de".to_string(), + site_title: "einsatz.online".to_string(), + stylesheets: vec![Stylesheet { + path: "/css/errms.css".to_string(), + }], + }; + let footer = Footer { scripts: vec![] }; + let render = PasswordResetPage{ + header, + footer, + logo_path: "/img/logo.jpg".to_string(), + alert, + }; + + Err(Template::render("password_reset", &render)) +} \ No newline at end of file