Skip to content

Commit

Permalink
feat: [#654] remove config option email_on_signup
Browse files Browse the repository at this point in the history
  • Loading branch information
josecelano committed Jul 9, 2024
1 parent 1c5e862 commit 29cc9dc
Show file tree
Hide file tree
Showing 10 changed files with 208 additions and 130 deletions.
86 changes: 80 additions & 6 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
pub mod v2;
pub mod validator;

use std::env;
use std::str::FromStr;
use std::sync::Arc;
use std::{env, fmt};

use camino::Utf8PathBuf;
use derive_more::Display;
Expand All @@ -22,8 +23,10 @@ pub type Settings = v2::Settings;

pub type Api = v2::api::Api;

pub type Registration = v2::registration::Registration;
pub type Email = v2::registration::Email;

pub type Auth = v2::auth::Auth;
pub type EmailOnSignup = v2::auth::EmailOnSignup;
pub type SecretKey = v2::auth::SecretKey;
pub type PasswordConstraints = v2::auth::PasswordConstraints;

Expand Down Expand Up @@ -301,12 +304,26 @@ impl Configuration {
pub async fn get_public(&self) -> ConfigurationPublic {
let settings_lock = self.settings.read().await;

let email_on_signup = match &settings_lock.registration {
Some(registration) => match &registration.email {
Some(email) => {
if email.required {
EmailOnSignup::Required
} else {
EmailOnSignup::Optional
}
}
None => EmailOnSignup::NotIncluded,
},
None => EmailOnSignup::NotIncluded,
};

ConfigurationPublic {
website_name: settings_lock.website.name.clone(),
tracker_url: settings_lock.tracker.url.clone(),
tracker_listed: settings_lock.tracker.listed,
tracker_private: settings_lock.tracker.private,
email_on_signup: settings_lock.auth.email_on_signup.clone(),
email_on_signup,
}
}

Expand All @@ -333,12 +350,56 @@ pub struct ConfigurationPublic {
email_on_signup: EmailOnSignup,
}

/// Whether the email is required on signup or not.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum EmailOnSignup {
/// The email is required on signup.
Required,
/// The email is optional on signup.
Optional,
/// The email is not allowed on signup. It will only be ignored if provided.
NotIncluded,
}

impl Default for EmailOnSignup {
fn default() -> Self {
Self::Optional
}
}

impl fmt::Display for EmailOnSignup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let display_str = match self {
EmailOnSignup::Required => "required",
EmailOnSignup::Optional => "optional",
EmailOnSignup::NotIncluded => "ignored",
};
write!(f, "{display_str}")
}
}

impl FromStr for EmailOnSignup {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"required" => Ok(EmailOnSignup::Required),
"optional" => Ok(EmailOnSignup::Optional),
"none" => Ok(EmailOnSignup::NotIncluded),
_ => Err(format!(
"Unknown config 'email_on_signup' option (required, optional, none): {s}"
)),
}
}
}

#[cfg(test)]
mod tests {

use url::Url;

use crate::config::{ApiToken, Configuration, ConfigurationPublic, Info, SecretKey, Settings};
use crate::config::{ApiToken, Configuration, ConfigurationPublic, EmailOnSignup, Info, SecretKey, Settings};

#[cfg(test)]
fn default_config_toml() -> String {
Expand All @@ -362,7 +423,6 @@ mod tests {
bind_address = "0.0.0.0:3001"
[auth]
email_on_signup = "optional"
secret_key = "MaxVerstappenWC2021"
[auth.password_constraints]
Expand Down Expand Up @@ -430,14 +490,28 @@ mod tests {
let configuration = Configuration::default();
let all_settings = configuration.get_all().await;

let email_on_signup = match &all_settings.registration {
Some(registration) => match &registration.email {
Some(email) => {
if email.required {
EmailOnSignup::Required
} else {
EmailOnSignup::Optional
}
}
None => EmailOnSignup::NotIncluded,
},
None => EmailOnSignup::NotIncluded,
};

assert_eq!(
configuration.get_public().await,
ConfigurationPublic {
website_name: all_settings.website.name,
tracker_url: all_settings.tracker.url,
tracker_listed: all_settings.tracker.listed,
tracker_private: all_settings.tracker.private,
email_on_signup: all_settings.auth.email_on_signup,
email_on_signup,
}
);
}
Expand Down
54 changes: 0 additions & 54 deletions src/config/v2/auth.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
use std::fmt;
use std::str::FromStr;

use serde::{Deserialize, Serialize};

/// Authentication options.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct Auth {
/// Whether or not to require an email on signup.
#[serde(default = "Auth::default_email_on_signup")]
pub email_on_signup: EmailOnSignup,

/// The secret key used to sign JWT tokens.
#[serde(default = "Auth::default_secret_key")]
pub secret_key: SecretKey,
Expand All @@ -22,7 +17,6 @@ pub struct Auth {
impl Default for Auth {
fn default() -> Self {
Self {
email_on_signup: EmailOnSignup::default(),
password_constraints: Self::default_password_constraints(),
secret_key: Self::default_secret_key(),
}
Expand All @@ -34,10 +28,6 @@ impl Auth {
self.secret_key = SecretKey::new(secret_key);
}

fn default_email_on_signup() -> EmailOnSignup {
EmailOnSignup::default()
}

fn default_secret_key() -> SecretKey {
SecretKey::new("MaxVerstappenWC2021")
}
Expand All @@ -47,50 +37,6 @@ impl Auth {
}
}

/// Whether the email is required on signup or not.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum EmailOnSignup {
/// The email is required on signup.
Required,
/// The email is optional on signup.
Optional,
/// The email is not allowed on signup. It will only be ignored if provided.
Ignored,
}

impl Default for EmailOnSignup {
fn default() -> Self {
Self::Optional
}
}

impl fmt::Display for EmailOnSignup {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let display_str = match self {
EmailOnSignup::Required => "required",
EmailOnSignup::Optional => "optional",
EmailOnSignup::Ignored => "ignored",
};
write!(f, "{display_str}")
}
}

impl FromStr for EmailOnSignup {
type Err = String;

fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"required" => Ok(EmailOnSignup::Required),
"optional" => Ok(EmailOnSignup::Optional),
"none" => Ok(EmailOnSignup::Ignored),
_ => Err(format!(
"Unknown config 'email_on_signup' option (required, optional, ignored): {s}"
)),
}
}
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct SecretKey(String);

Expand Down
1 change: 0 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@
//! bind_address = "0.0.0.0:3001"
//!
//! [auth]
//! email_on_signup = "optional"
//! secret_key = "MaxVerstappenWC2021"
//!
//! [auth.password_constraints]
Expand Down
120 changes: 62 additions & 58 deletions src/services/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use tracing::{debug, info};

use super::authentication::DbUserAuthenticationRepository;
use super::authorization::{self, ACTION};
use crate::config::{Configuration, EmailOnSignup, PasswordConstraints};
use crate::config::{Configuration, PasswordConstraints};
use crate::databases::database::{Database, Error};
use crate::errors::ServiceError;
use crate::mailer;
Expand Down Expand Up @@ -74,71 +74,75 @@ impl RegistrationService {
pub async fn register_user(&self, registration_form: &RegistrationForm, api_base_url: &str) -> Result<UserId, ServiceError> {
info!("registering user: {}", registration_form.username);

let Ok(username) = registration_form.username.parse::<Username>() else {
return Err(ServiceError::UsernameInvalid);
};

let settings = self.configuration.settings.read().await;

let opt_email = match settings.auth.email_on_signup {
EmailOnSignup::Required => {
if registration_form.email.is_none() {
return Err(ServiceError::EmailMissing);
match &settings.registration {
Some(registration) => {
let Ok(username) = registration_form.username.parse::<Username>() else {
return Err(ServiceError::UsernameInvalid);
};

let opt_email = match &registration.email {
Some(email) => {
if email.required && registration_form.email.is_none() {
return Err(ServiceError::EmailMissing);
}
registration_form.email.clone()
}
None => None,
};

if let Some(email) = &registration_form.email {
if !validate_email_address(email) {
return Err(ServiceError::EmailInvalid);
}
}
registration_form.email.clone()
}
EmailOnSignup::Ignored => None,
EmailOnSignup::Optional => registration_form.email.clone(),
};

if let Some(email) = &registration_form.email {
if !validate_email_address(email) {
return Err(ServiceError::EmailInvalid);
}
}

let password_constraints = PasswordConstraints {
min_password_length: settings.auth.password_constraints.min_password_length,
max_password_length: settings.auth.password_constraints.max_password_length,
};

validate_password_constraints(
&registration_form.password,
&registration_form.confirm_password,
&password_constraints,
)?;

let password_hash = hash_password(&registration_form.password)?;

let user_id = self
.user_repository
.add(
&username.to_string(),
&opt_email.clone().unwrap_or(no_email()),
&password_hash,
)
.await?;

// If this is the first created account, give administrator rights
if user_id == 1 {
drop(self.user_repository.grant_admin_role(&user_id).await);
}

if settings.mail.email_verification_enabled {
if let Some(email) = opt_email {
let mail_res = self
.mailer
.send_verification_mail(&email, &registration_form.username, user_id, api_base_url)
.await;
let password_constraints = PasswordConstraints {
min_password_length: settings.auth.password_constraints.min_password_length,
max_password_length: settings.auth.password_constraints.max_password_length,
};

validate_password_constraints(
&registration_form.password,
&registration_form.confirm_password,
&password_constraints,
)?;

let password_hash = hash_password(&registration_form.password)?;

let user_id = self
.user_repository
.add(
&username.to_string(),
&opt_email.clone().unwrap_or(no_email()),
&password_hash,
)
.await?;

// If this is the first created account, give administrator rights
if user_id == 1 {
drop(self.user_repository.grant_admin_role(&user_id).await);
}

if mail_res.is_err() {
drop(self.user_repository.delete(&user_id).await);
return Err(ServiceError::FailedToSendVerificationEmail);
if settings.mail.email_verification_enabled {
if let Some(email) = opt_email {
let mail_res = self
.mailer
.send_verification_mail(&email, &registration_form.username, user_id, api_base_url)
.await;

if mail_res.is_err() {
drop(self.user_repository.delete(&user_id).await);
return Err(ServiceError::FailedToSendVerificationEmail);
}
}
}

Ok(user_id)
}
None => Err(ServiceError::ClosedForRegistration),
}

Ok(user_id)
}

/// It verifies the email address of a user via the token sent to the
Expand Down
2 changes: 0 additions & 2 deletions src/web/api/client/v1/contexts/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ pub struct Network {

#[derive(Deserialize, Serialize, PartialEq, Debug, Clone)]
pub struct Auth {
pub email_on_signup: String,
pub secret_key: String,
pub password_constraints: PasswordConstraints,
}
Expand Down Expand Up @@ -154,7 +153,6 @@ impl From<DomainNetwork> for Network {
impl From<DomainAuth> for Auth {
fn from(auth: DomainAuth) -> Self {
Self {
email_on_signup: format!("{:?}", auth.email_on_signup),
secret_key: auth.secret_key.to_string(),
password_constraints: auth.password_constraints.into(),
}
Expand Down
Loading

0 comments on commit 29cc9dc

Please sign in to comment.