Skip to content

Commit

Permalink
Add email change support to the server
Browse files Browse the repository at this point in the history
  • Loading branch information
TobiasDeBruijn committed Jan 2, 2025
1 parent 8ded579 commit 85fabcc
Show file tree
Hide file tree
Showing 12 changed files with 202 additions and 1 deletion.
10 changes: 10 additions & 0 deletions server/database/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,16 @@ impl User {
Ok(())
}

pub async fn set_email(&self, driver: &Database, email: &str) -> Result<()> {
sqlx::query("UPDATE users SET email = ? WHERE user_id = ?")
.bind(email)
.bind(&self.user_id)
.execute(&**driver)
.await?;

Ok(())
}

#[instrument(skip(password))]
pub async fn set_password_hash<P: AsRef<str> + Debug>(
&self,
Expand Down
25 changes: 25 additions & 0 deletions server/mailer/src/email/email_changed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
use crate::email::Mailable;
use crate::locale::Locale;
use serde::Serialize;

pub struct EmailChangedMail;

#[derive(Serialize)]
pub struct EmailChangedData {
pub name: String,
}

impl Mailable for EmailChangedMail {
type Data = EmailChangedData;

fn template_name() -> &'static str {
"email_changed"
}

fn subject(locale: &Locale) -> &'static str {
match locale {
Locale::En => "Your email address was changed",
Locale::Nl => "Je email adres is gewijzigd",
}
}
}
2 changes: 2 additions & 0 deletions server/mailer/src/email/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod email_changed;
mod password_changed;
mod password_forgotten;

pub use email_changed::*;
pub use password_changed::*;
pub use password_forgotten::*;

Expand Down
13 changes: 13 additions & 0 deletions server/mailer/templates/email_changed.en.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<html lang="en">
{{> header }}
<body>
<div class="container">
{{> banner }}

<p>Hi {{ name }}, </p>
<p>
Your email address was changed
</p>
</div>
</body>
</html>
13 changes: 13 additions & 0 deletions server/mailer/templates/email_changed.nl.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<html lang="nl">
{{> header }}
<body>
<div class="container">
{{> banner }}

<p>Hoi {{ name }}, </p>
<p>
Je email address is gewijzigd.
</p>
</div>
</body>
</html>
24 changes: 24 additions & 0 deletions server/wilford/src/authorization/combined.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,28 @@ impl<'a> AuthorizationProvider for CombinedAuthorizationProvider<'a> {
.map_err(AuthorizationError::convert)?,
})
}

fn supports_email_change(&self) -> bool {
match self {
Self::Local(v) => v.supports_email_change(),
Self::EspoCrm(v) => v.supports_email_change(),
}
}

async fn set_email(
&self,
user_id: &str,
new_email: &str,
) -> Result<(), AuthorizationError<Self::Error>> {
match self {
Self::Local(v) => v
.set_email(user_id, new_email)
.await
.map_err(AuthorizationError::convert),
Self::EspoCrm(v) => v
.set_email(user_id, new_email)
.await
.map_err(AuthorizationError::convert),
}
}
}
9 changes: 9 additions & 0 deletions server/wilford/src/authorization/espo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,13 @@ impl<'a> AuthorizationProvider for EspoAuthorizationProvider<'a> {
) -> Result<UserInformation, AuthorizationError<Self::Error>> {
Err(AuthorizationError::UnsupportedOperation)
}

fn supports_email_change(&self) -> bool {
false
}

#[instrument(skip_all)]
async fn set_email(&self, _: &str, _: &str) -> Result<(), AuthorizationError<Self::Error>> {
Err(AuthorizationError::UnsupportedOperation)
}
}
22 changes: 22 additions & 0 deletions server/wilford/src/authorization/local_provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,28 @@ impl<'a> AuthorizationProvider for LocalAuthorizationProvider<'a> {
is_admin,
})
}

fn supports_email_change(&self) -> bool {
true
}

#[instrument(skip(self))]
async fn set_email(
&self,
user_id: &str,
new_email: &str,
) -> Result<(), AuthorizationError<Self::Error>> {
let user = User::get_by_id(&self.driver, user_id)
.await
.map_err(Self::Error::from)?
.ok_or(AuthorizationError::InvalidCredentials)?;

user.set_email(&self.driver, new_email)
.await
.map_err(Self::Error::from)?;

Ok(())
}
}

/// Hash the password.
Expand Down
17 changes: 16 additions & 1 deletion server/wilford/src/authorization/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ pub trait AuthorizationProvider {
/// Implementations do not have to support this operation, check this with [Self::supports_registration].
///
/// # Errors
/// - If the operation is not supports
/// - If the operation is not supported
/// - If the underlying operation fails
/// - If a user with the given e-mail address already exists
async fn register_user(
Expand All @@ -116,4 +116,19 @@ pub trait AuthorizationProvider {
password: &str,
is_admin: bool,
) -> Result<UserInformation, AuthorizationError<Self::Error>>;

/// Whether the provider supports changing the email address of the user.
fn supports_email_change(&self) -> bool;

/// Change the email address of the user.
/// Implementations do not have to support this operation, check this with [Self::supports_email_change]
///
/// # Errors
/// - If the operation is not supported
/// - IF the underlying operation fails
async fn set_email(
&self,
user_id: &str,
new_email: &str,
) -> Result<(), AuthorizationError<Self::Error>>;
}
64 changes: 64 additions & 0 deletions server/wilford/src/routes/v1/user/change_email.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use crate::authorization::combined::CombinedAuthorizationProvider;
use crate::authorization::AuthorizationProvider;
use crate::mail::WilfordMailer;
use crate::response_types::Empty;
use crate::routes::auth::Auth;
use crate::routes::error::{WebErrorKind, WebResult};
use crate::routes::{auth_error_to_web_error, WConfig, WDatabase};
use actix_web::web;
use mailer::{EmailChangedData, EmailChangedMail, Locale};
use serde::Deserialize;

#[derive(Deserialize)]
pub struct Request {
new_email: String,
password: String,
}

/// Change email address of the user
///
/// # Errors
/// - The operation is not supported
/// - The provided password is invalid
/// - The operation fails
pub async fn change_email(
auth: Auth,
payload: web::Json<Request>,
config: WConfig,
database: WDatabase,
) -> WebResult<Empty> {
// Check for support
let provider = CombinedAuthorizationProvider::new(&config, &database);
if !provider.supports_email_change() {
return Err(WebErrorKind::Unsupported.into());
}

// Validate password
auth_error_to_web_error(
provider
.validate_credentials(&auth.user.email, &payload.password, None)
.await,
)?;

// Change email
auth_error_to_web_error(provider.set_email(&auth.user_id, &payload.new_email).await)?;

// Send email
if let Some(email_cfg) = &config.email {
// Old email
WilfordMailer::new(email_cfg)
.send_email(
&auth.user.email,
EmailChangedMail,
&EmailChangedData {
name: auth.user.name,
},
Locale::En,
)
.await?;

// TODO: Verifaction email to new email
}

Ok(Empty)
}
2 changes: 2 additions & 0 deletions server/wilford/src/routes/v1/user/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use actix_route_config::Routable;
use actix_web::web;
use actix_web::web::ServiceConfig;

mod change_email;
mod change_password;
mod info;
mod list;
Expand Down Expand Up @@ -32,6 +33,7 @@ impl Routable for Router {
"/change-password",
web::post().to(change_password::change_password),
)
.route("/change-email", web::post().to(change_email::change_email))
.route(
"/supports-password-change",
web::get().to(supports_password_change::supports_password_change),
Expand Down
2 changes: 2 additions & 0 deletions server/wilford/src/routes/v1/user/register.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ pub async fn register(
)?
.unwrap_left();

// TODO registration email and email verification

Ok(web::Json(Response {
user_id: new_user.id,
}))
Expand Down

0 comments on commit 85fabcc

Please sign in to comment.