Skip to content

Commit

Permalink
Merge 26d3d68 into 569bc62
Browse files Browse the repository at this point in the history
  • Loading branch information
flosse committed Jun 14, 2019
2 parents 569bc62 + 26d3d68 commit 126a989
Show file tree
Hide file tree
Showing 46 changed files with 1,273 additions and 100 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,3 +1,8 @@
## v0.5.5 (unreleased)

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

## v0.5.4 (2019-05-20)

- new(web): Added admin interface for archiving comments and ratings
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
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
@@ -0,0 +1 @@
DROP TABLE email_token_credentials;
10 changes: 10 additions & 0 deletions migrations/2019-03-10_email_token_credentials/up.sql
@@ -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
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
Expand Up @@ -14,6 +14,15 @@ pub fn user_registration_email(url: &str) -> EmailContent {
EmailContent { subject, body }
}

pub fn user_reset_password_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
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
@@ -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
8 changes: 7 additions & 1 deletion src/core/error.rs
Expand Up @@ -79,6 +79,12 @@ quick_error! {
Role{
description("Invalid role")
}
TokenExpired{
description("Token expired")
}
InvalidNonce{
description("Invalid nonce")
}
}
}

Expand All @@ -104,7 +110,7 @@ quick_error! {
cause(err)
description(err.description())
}
Other(err: Box<error::Error>){
Other(err: Box<dyn error::Error>){
description(err.description())
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/core/mod.rs
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
21 changes: 21 additions & 0 deletions src/core/repositories.rs
Expand Up @@ -53,3 +53,24 @@ 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>;

#[cfg(test)]
fn get_email_token_credentials_by_email_or_username(
&self,
email_or_username: &str,
) -> Result<EmailTokenCredentials>;
}
2 changes: 1 addition & 1 deletion src/core/usecases/confirm_email.rs
@@ -1,6 +1,6 @@
use crate::core::prelude::*;

pub fn confirm_email_address(db: &Db, u_id: &str) -> Result<()> {
pub fn confirm_email_address(db: &dyn Db, u_id: &str) -> Result<()> {
//TODO: use username instead of user ID
let mut u = db
.all_users()?
Expand Down
25 changes: 25 additions & 0 deletions src/core/usecases/confirm_email_and_reset_password.rs
@@ -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
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
@@ -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)?)
}
4 changes: 2 additions & 2 deletions src/core/usecases/indexing.rs
Expand Up @@ -3,7 +3,7 @@ use crate::core::{prelude::*, util::sort::Rated};
use failure::Fallible;

pub fn index_entry(
indexer: &mut EntryIndexer,
indexer: &mut dyn EntryIndexer,
entry: &Entry,
ratings: &[Rating],
) -> Fallible<AvgRatings> {
Expand All @@ -12,7 +12,7 @@ pub fn index_entry(
Ok(avg_ratings)
}

pub fn unindex_entry(indexer: &mut EntryIndexer, entry_id: &str) -> Fallible<()> {
pub fn unindex_entry(indexer: &mut dyn EntryIndexer, entry_id: &str) -> Fallible<()> {
indexer.remove_entry_by_id(entry_id)?;
Ok(())
}

0 comments on commit 126a989

Please sign in to comment.