Feature: Reset password

This commit is contained in:
Keanu D?lle 2020-12-30 12:20:53 +01:00
parent 0b99d8d50f
commit 23595f6d3b
16 changed files with 342 additions and 52 deletions

View File

@ -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
login_lock_duration = 1800
[mail]
from = "No Reply <noreply@localhost>"
reply_to = "support@localhost"

View File

@ -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}}

View File

@ -30,11 +30,11 @@
</form>
</div>
<div class="col">
<form>
<form method="post" action="/password_reset">
<h3>Passwort vergessen?</h3>
<div class="form-group">
<label for="forgot_password_email">E-Mail Adresse</label>
<input type="email" class="form-control" name="forgot_password_email" id="forgot_password_email" required>
<label for="email">E-Mail Adresse</label>
<input type="email" class="form-control" name="email" id="email" required>
</div>
<button type="submit" class="login_submit btn btn-secondary">Passwort zurücksetzen</button>
</form>

View File

@ -0,0 +1,30 @@
{{> header }}
<div class="jumbotron">
<div class="row">
<div class="col-lq">
<h1>Willkommen bei einsatz.online!</h1>
</div>
{{#if logo_path}}
<div class="col">
<img class="welcome_logo" src="{{logo_path}}">
</div>
{{/if}}
</div>
</div>
<div class="row">
<div class="col">
<form method="post">
{{#if alert}}
{{> alert}}
{{/if}}
<h3>Neues Passwort festlegen</h3>
<p>Ihr Passwort muss mindestens 10 Zeichen lang sein und sollte Buchstaben, Zahlen und Sonderzeichen enthalten.</p>
<div class="form-group">
<label for="password">Passwort</label>
<input type="password" class="form-control" name="password" id="password" required>
</div>
<button type="submit" class="login_submit btn btn-primary">Passwort ändern</button>
</form>
</div>
</div>
{{> footer }}

View File

@ -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<Settings>, user_id: uuid::Uuid) -> Result<String, diesel::result::Error>{
@ -25,4 +30,71 @@ pub fn add_token(settings: &State<Settings>, user_id: uuid::Uuid) -> Result<Stri
Err(e)
}
}
}
pub fn remove_token(settings: &State<Settings>, 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<Settings>, token2: String) -> Result<uuid::Uuid, diesel::result::Error>{
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<Settings>, 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
}

View File

@ -84,40 +84,19 @@ impl MailQueue{
}
}
pub trait CreateMail<T>{
fn new(args : T) -> Mail;
}
impl CreateMail<(String, Vec<String>, String, Vec<String>, Vec<String>, Option<String>, String, Option<NaiveDateTime>)> for Mail{
impl Mail{
/// Create Mail with from, to, subject, cc, bcc, reply_to, deliver_until
fn new(args: (String, Vec<String>, String, Vec<String>, Vec<String>, Option<String>, String, Option<NaiveDateTime>)) -> Mail {
pub(crate) fn new(from: String, to: Vec<String>, subject: String, cc: Vec<String>, bcc: Vec<String>, reply_to: Option<String>, body: String, deliver_until: Option<NaiveDateTime>) -> 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>, String, Option<String>, String)> for Mail{
/// Create Mail with from, to, subject, reply_to
fn new(args: (String, Vec<String>, String, Option<String>, 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
}
}
}

View File

@ -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(())
}
},

View File

@ -0,0 +1,5 @@
use rocket_contrib::templates::handlebars::Handlebars;
pub struct MailTemplates{
pub registry: Handlebars,
}

View File

@ -11,4 +11,5 @@ pub mod settings;
pub mod sitebuilder;
pub mod translate_diesel_error;
pub mod user_request_guard;
pub mod mail_queue;
pub mod mail_queue;
pub mod mail_templates;

View File

@ -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 {

View File

@ -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,

View File

@ -1,2 +1,3 @@
pub mod login;
pub mod render;
pub mod password_reset;

View File

@ -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<Settings>, mt: &State<MailTemplates>, mq: State<Arc<MailQueue>>, 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(())
}
}

View File

@ -4,8 +4,8 @@ use crate::modules::welcome::model::welcome_module::WelcomeModule;
pub fn get_context(alert: Option<Alert>) -> 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(),
}],

View File

@ -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("/?<error>")]
pub fn welcome_get(error: Option<String>, mail_queue: State<Arc<MailQueue>>) -> Result<Template, Status> {
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<String>) -> Result<Template, Status> {
let alert = match error {
Some(error) => match error.as_str() {
"unauthorized" => Some(Alert::new(
@ -19,6 +17,11 @@ pub fn welcome_get(error: Option<String>, mail_queue: State<Arc<MailQueue>>) ->
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(),

View File

@ -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 = "<login_form>")]
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 = "<password_reset_form>")]
pub fn password_reset_post(
password_reset_form: Form<PasswordResetForm>,
settings: State<Settings>,
mt: State<MailTemplates>,
mq: State<Arc<MailQueue>>,
) -> Template {
let mut alert : Option<Alert> = 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<Alert>,
}
#[get("/password_reset?<token>")]
pub fn password_change_get(
settings: State<Settings>,
token: String,
) -> Result<Template, Redirect> {
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<token>", data = "<password_change_form>")]
pub fn password_change_post(
settings: State<Settings>,
password_change_form: Form<PasswordChangeForm>,
token: String,
) -> Result<Redirect, Template> {
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))
}