Skip to content

Commit

Permalink
Reset password with token received by e-mail
Browse files Browse the repository at this point in the history
  • Loading branch information
uklotzde authored and flosse committed Jun 13, 2019
1 parent 77d1543 commit bda2703
Show file tree
Hide file tree
Showing 29 changed files with 812 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
- new(web): Make events queryable in the frontend
- fix(db): Fix incomplete visible search results

- new(web): Extended admin frontend
- new(web): Reset passwords by e-mail

## v0.5.2 (2019-03-18)

- fix(db): Retarget entry search and optimize tag lookup
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ edition = "2018"
geocoding = { git = "https://github.com/georust/geocoding" }

[dependencies]
bs58 = "*"
chrono = "*"
# clap 3 is supposed to introduce breaking changes
clap = "2"
Expand Down
1 change: 1 addition & 0 deletions migrations/2019-03-10_email_token_credentials/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE email_token_credentials;
10 changes: 10 additions & 0 deletions migrations/2019-03-10_email_token_credentials/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE email_token_credentials (
id INTEGER PRIMARY KEY,
expires_at INTEGER NOT NULL,
username TEXT NOT NULL,
email TEXT NOT NULL,
nonce TEXT NOT NULL,
UNIQUE (username),
UNIQUE (nonce),
FOREIGN KEY (username) REFERENCES users(username)
);
12 changes: 12 additions & 0 deletions src/adapters/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,15 @@ impl Entry {
}
}
}

#[derive(Deserialize)]
pub struct RequestPasswordReset {
pub email_or_username: String,
}

#[derive(Deserialize)]
pub struct ResetPassword {
pub email_or_username: String,
pub token: String,
pub new_password: String,
}
9 changes: 9 additions & 0 deletions src/adapters/user_communication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ pub fn user_registration_email(url: &str) -> EmailContent {
EmailContent { subject, body }
}

pub fn user_password_reset_email(url: &str) -> EmailContent {
let subject = "Karte von morgen: Passwort zurücksetzen".into();
let body = format!(
"Na du Weltverbesserer*,\nhast Du uns kürzlich gebeten Dein Passwort zurücksetzen?\n\nBitte folge zur Eingabe eines neuen Passworts diesem Link:\n{}\n\neuphorische Grüße\ndas Karte von morgen-Team",
url,
);
EmailContent { subject, body }
}

pub fn entry_added_email(e: &Entry, category_names: &[String]) -> EmailContent {
let subject = format!("Karte von morgen - neuer Eintrag: {}", e.title);
let intro_sentence = "ein neuer Eintrag auf der Karte von morgen wurde erstellt";
Expand Down
1 change: 1 addition & 0 deletions src/core/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ pub trait Db:
+ OrganizationGateway
+ CommentRepository
+ RatingRepository
+ EmailTokenCredentialsRepository
{
fn create_tag_if_it_does_not_exist(&self, _: &Tag) -> Result<()>;
fn create_category_if_it_does_not_exist(&mut self, _: &Category) -> Result<()>;
Expand Down
50 changes: 48 additions & 2 deletions src/core/entities.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
use chrono::prelude::*;

use crate::core::util::{
geo::{MapBbox, MapPoint},
nonce::Nonce,
password::Password,
time::Timestamp,
};

use chrono::prelude::*;
use failure::{bail, format_err, Fallible};

#[rustfmt::skip]
#[derive(Debug, Clone, PartialEq)]
pub struct Entry {
Expand Down Expand Up @@ -383,6 +385,50 @@ pub struct Organization {
pub api_token: String,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmailTokenCredentials {
pub expires_at: Timestamp,
pub username: String,
pub token: EmailToken,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EmailToken {
pub email: String,
pub nonce: Nonce,
}

impl EmailToken {
pub fn encode_to_string(&self) -> String {
let nonce = self.nonce.to_string();
debug_assert_eq!(Nonce::STR_LEN, nonce.len());
let mut concat = String::with_capacity(self.email.len() + nonce.len());
concat += &self.email;
concat += &nonce;
bs58::encode(concat).into_string()
}

pub fn decode_from_str(encoded: &str) -> Fallible<EmailToken> {
let decoded = bs58::decode(encoded).into_vec()?;
let mut concat = String::from_utf8(decoded)?;
if concat.len() <= Nonce::STR_LEN {
bail!(
"Invalid token - too short: {} <= {}",
concat.len(),
Nonce::STR_LEN
);
}
let email_len = concat.len() - Nonce::STR_LEN;
let nonce_slice: &str = &concat[email_len..];
let nonce = nonce_slice
.parse::<Nonce>()
.map_err(|err| format_err!("Failed to parse nonce from '{}': {}", nonce_slice, err))?;
concat.truncate(email_len);
let email = concat;
Ok(Self { email, nonce })
}
}

#[cfg(test)]
pub trait Builder {
type Build;
Expand Down
6 changes: 6 additions & 0 deletions src/core/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ quick_error! {
Role{
description("Invalid role")
}
TokenExpired{
description("Token expired")
}
InvalidNonce{
description("Invalid nonce")
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ pub mod prelude {
pub use super::repositories::*;
pub use super::util::{
geo::{Distance, LatCoord, LngCoord, MapPoint},
nonce::Nonce,
password::Password,
rowid::RowId,
time::Timestamp,
};

Expand Down
15 changes: 15 additions & 0 deletions src/core/repositories.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,18 @@ pub trait RatingRepository {

fn load_entry_ids_of_ratings(&self, ids: &[&str]) -> Result<Vec<String>>;
}

pub trait EmailTokenCredentialsRepository {
fn replace_email_token_credentials(
&self,
email_token_credentials: EmailTokenCredentials,
) -> Result<EmailTokenCredentials>;

fn consume_email_token_credentials(
&self,
email_or_username: &str,
token: &EmailToken,
) -> Result<EmailTokenCredentials>;

fn discard_expired_email_token_credentials(&self, expired_before: Timestamp) -> Result<usize>;
}
25 changes: 25 additions & 0 deletions src/core/usecases/confirm_email_and_reset_password.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use super::*;

use crate::core::error::{Error, ParameterError};

pub fn confirm_email_and_reset_password<D: Db>(
db: &D,
username: &str,
email: &str,
new_password: Password,
) -> Result<()> {
info!("Resetting password for user ({})", username);
let mut user = db.get_user(username)?;
debug_assert_eq!(user.username, username);
if user.email != email {
warn!(
"Invalid e-mail address for user ({}): expected = {}, actual = {}",
user.username, user.email, email,
);
return Err(Error::Parameter(ParameterError::Email));
}
user.email_confirmed = true;
user.password = new_password;
db.update_user(&user)?;
Ok(())
}
4 changes: 2 additions & 2 deletions src/core/usecases/create_new_user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub struct NewUser {
pub email: String,
}

pub fn create_new_user<D: UserGateway>(db: &mut D, u: NewUser) -> Result<()> {
pub fn create_new_user<D: UserGateway>(db: &D, u: NewUser) -> Result<()> {
validate::username(&u.username)?;
let password = u.password.parse::<Password>()?;
validate::email(&u.email)?;
Expand Down Expand Up @@ -51,7 +51,7 @@ pub fn generate_username_from_email(email: &str) -> String {
generated_username
}

pub fn create_user_from_email<D: Db>(db: &mut D, email: &str) -> Result<String> {
pub fn create_user_from_email<D: Db>(db: &D, email: &str) -> Result<String> {
let users: Vec<_> = db.all_users()?;
let username = match users.iter().find(|u| u.email == email) {
Some(u) => u.username.clone(),
Expand Down
37 changes: 37 additions & 0 deletions src/core/usecases/email_token_credentials.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::core::prelude::*;

use chrono::{Duration, Utc};

pub fn refresh_email_token_credentials<D: Db>(
db: &D,
username: String,
email: String,
) -> Result<EmailTokenCredentials> {
let token = EmailToken {
email,
nonce: Nonce::new(),
};
let credentials = EmailTokenCredentials {
expires_at: Timestamp::from(Utc::now() + Duration::days(1)),
username,
token,
};
Ok(db.replace_email_token_credentials(credentials)?)
}

pub fn consume_email_token_credentials<D: Db>(
db: &D,
email_or_username: &str,
token: &EmailToken,
) -> Result<EmailTokenCredentials> {
let credentials = db.consume_email_token_credentials(email_or_username, token)?;
if credentials.expires_at < Timestamp::now() {
return Err(Error::Parameter(ParameterError::TokenExpired));
}
Ok(credentials)
}

pub fn discard_expired_email_token_credentials<D: Db>(db: &D) -> Result<usize> {
let expired_before = Timestamp::now();
Ok(db.discard_expired_email_token_credentials(expired_before)?)
}
9 changes: 6 additions & 3 deletions src/core/usecases/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ mod archive_events;
mod archive_ratings;
mod change_user_role;
mod confirm_email;
mod confirm_email_and_reset_password;
mod create_new_entry;
mod create_new_event;
pub mod create_new_user;
mod delete_event;
mod email_token_credentials;
mod find_duplicates;
mod indexing;
mod login;
Expand All @@ -33,9 +35,10 @@ mod update_event;

pub use self::{
archive_comments::*, archive_entries::*, archive_events::*, archive_ratings::*,
change_user_role::*, confirm_email::*, create_new_entry::*, create_new_event::*,
create_new_user::*, delete_event::*, find_duplicates::*, indexing::*, login::*,
query_events::*, rate_entry::*, register::*, search::*, update_entry::*, update_event::*,
change_user_role::*, confirm_email::*, confirm_email_and_reset_password::*,
create_new_entry::*, create_new_event::*, create_new_user::*, delete_event::*,
email_token_credentials::*, find_duplicates::*, indexing::*, login::*, query_events::*,
rate_entry::*, register::*, search::*, update_entry::*, update_event::*,
};

pub fn load_ratings_with_comments<D: Db>(
Expand Down
62 changes: 62 additions & 0 deletions src/core/usecases/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,68 @@ pub struct MockDb {
pub comments: RefCell<Vec<Comment>>,
pub bbox_subscriptions: Vec<BboxSubscription>,
pub orgs: Vec<Organization>,
pub email_token_credentialss: RefCell<Vec<EmailTokenCredentials>>,
}

impl EmailTokenCredentialsRepository for MockDb {
fn replace_email_token_credentials(
&self,
email_token_credentials: EmailTokenCredentials,
) -> RepoResult<EmailTokenCredentials> {
for x in &mut self.email_token_credentialss.borrow_mut().iter_mut() {
if x.username == email_token_credentials.username {
*x = email_token_credentials.clone();
return Ok(email_token_credentials);
}
}
self.email_token_credentialss
.borrow_mut()
.push(email_token_credentials.clone());
Ok(email_token_credentials)
}

fn consume_email_token_credentials(
&self,
email_or_username: &str,
token: &EmailToken,
) -> RepoResult<EmailTokenCredentials> {
if let Some(index) = self
.email_token_credentialss
.borrow()
.iter()
.enumerate()
.find_map(|(i, x)| {
if (x.username == email_or_username || x.token.email == email_or_username)
&& x.token.email == token.email
&& x.token.nonce == token.nonce
{
Some(i)
} else {
None
}
})
{
Ok(self
.email_token_credentialss
.borrow_mut()
.swap_remove(index))
} else {
return Err(RepoError::NotFound);
}
}

fn discard_expired_email_token_credentials(
&self,
expired_before: Timestamp,
) -> RepoResult<usize> {
let len_before = self.email_token_credentialss.borrow().len();
self.email_token_credentialss
.borrow_mut()
.retain(|x| x.expires_at >= expired_before);
let len_after = self.email_token_credentialss.borrow().len();
debug_assert!(len_before >= len_after);
Ok(len_before - len_after)
}
}

impl EntryIndexer for MockDb {
Expand Down
2 changes: 2 additions & 0 deletions src/core/util/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
pub mod filter;
pub mod geo;
pub mod nonce;
pub mod parse;
pub mod password;
pub mod rowid;
pub mod sort;
pub mod time;
pub mod validate;
Expand Down
Loading

0 comments on commit bda2703

Please sign in to comment.