diff --git a/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql b/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql index bb6e644..1dfe1c9 100644 --- a/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql +++ b/geneit_backend/migrations/2023-05-24-102711_create_users/up.sql @@ -4,9 +4,11 @@ CREATE TABLE users ( name VARCHAR(30) NOT NULL, email VARCHAR(255) NOT NULL, password VARCHAR NULL, - reset_password_token VARCHAR(150) NULL, time_create BIGINT NOT NULL, + reset_password_token VARCHAR(150) NULL, time_gen_reset_token BIGINT NOT NULL DEFAULT 0, + delete_account_token VARCHAR(150) NULL, + time_gen_delete_account_token BIGINT NOT NULL DEFAULT 0, time_activate BIGINT NOT NULL DEFAULT 0, active BOOLEAN NOT NULL DEFAULT TRUE, admin BOOLEAN NOT NULL DEFAULT FALSE diff --git a/geneit_backend/src/app_config.rs b/geneit_backend/src/app_config.rs index cc8e8f7..49b1f90 100644 --- a/geneit_backend/src/app_config.rs +++ b/geneit_backend/src/app_config.rs @@ -88,6 +88,14 @@ pub struct AppConfig { )] pub reset_password_url: String, + /// Delete account URL + #[clap( + long, + env, + default_value = "http://localhost:3000/delete_account#TOKEN" + )] + pub delete_account_url: String, + /// URL where the OpenID configuration can be found #[arg( long, @@ -154,6 +162,11 @@ impl AppConfig { self.reset_password_url.replace("TOKEN", token) } + /// Get account delete URL + pub fn get_account_delete_url(&self, token: &str) -> String { + self.delete_account_url.replace("TOKEN", token) + } + /// Get OpenID providers configuration pub fn openid_providers(&self) -> Vec> { if self.disable_oidc { diff --git a/geneit_backend/src/controllers/user_controller.rs b/geneit_backend/src/controllers/user_controller.rs index 3d57511..a18f59c 100644 --- a/geneit_backend/src/controllers/user_controller.rs +++ b/geneit_backend/src/controllers/user_controller.rs @@ -97,3 +97,19 @@ pub async fn replace_password( Ok(HttpResponse::Accepted().finish()) } + +/// Request delete account +pub async fn request_delete_account(remote_ip: RemoteIP, token: LoginToken) -> HttpResult { + // Rate limiting + if rate_limiter_service::should_block_action(remote_ip.0, RatedAction::RequestDeleteAccount) + .await? + { + return Ok(HttpResponse::TooManyRequests().finish()); + } + rate_limiter_service::record_action(remote_ip.0, RatedAction::RequestDeleteAccount).await?; + + let mut user = users_service::get_by_id(token.user_id).await?; + users_service::request_delete_account(&mut user).await?; + + Ok(HttpResponse::Accepted().finish()) +} diff --git a/geneit_backend/src/main.rs b/geneit_backend/src/main.rs index 6f1deca..07af848 100644 --- a/geneit_backend/src/main.rs +++ b/geneit_backend/src/main.rs @@ -62,6 +62,10 @@ async fn main() -> std::io::Result<()> { "/user/replace_password", web::post().to(user_controller::replace_password), ) + .route( + "/user/request_delete", + web::get().to(user_controller::request_delete_account), + ) }) .bind(AppConfig::get().listen_address.as_str())? .run() diff --git a/geneit_backend/src/models.rs b/geneit_backend/src/models.rs index 27866a0..f393cd3 100644 --- a/geneit_backend/src/models.rs +++ b/geneit_backend/src/models.rs @@ -12,11 +12,15 @@ pub struct User { pub email: String, #[serde(skip_serializing)] pub password: Option, - #[serde(skip_serializing)] - pub reset_password_token: Option, pub time_create: i64, #[serde(skip_serializing)] + pub reset_password_token: Option, + #[serde(skip_serializing)] pub time_gen_reset_token: i64, + #[serde(skip_serializing)] + pub delete_account_token: Option, + #[serde(skip_serializing)] + pub time_gen_delete_account_token: i64, pub time_activate: i64, pub active: bool, pub admin: bool, diff --git a/geneit_backend/src/schema.rs b/geneit_backend/src/schema.rs index 2c5be74..589622d 100644 --- a/geneit_backend/src/schema.rs +++ b/geneit_backend/src/schema.rs @@ -6,9 +6,11 @@ diesel::table! { name -> Varchar, email -> Varchar, password -> Nullable, - reset_password_token -> Nullable, time_create -> Int8, + reset_password_token -> Nullable, time_gen_reset_token -> Int8, + delete_account_token -> Nullable, + time_gen_delete_account_token -> Int8, time_activate -> Int8, active -> Bool, admin -> Bool, diff --git a/geneit_backend/src/services/rate_limiter_service.rs b/geneit_backend/src/services/rate_limiter_service.rs index 560be1a..3541d8d 100644 --- a/geneit_backend/src/services/rate_limiter_service.rs +++ b/geneit_backend/src/services/rate_limiter_service.rs @@ -11,6 +11,7 @@ pub enum RatedAction { FailedPasswordLogin, StartOpenIDLogin, RequestReplacePasswordSignedIn, + RequestDeleteAccount, } impl RatedAction { @@ -21,7 +22,8 @@ impl RatedAction { RatedAction::RequestNewPasswordResetLink => "req-pwd-reset-lnk", RatedAction::FailedPasswordLogin => "failed-login", RatedAction::StartOpenIDLogin => "start-oidc-login", - RatedAction::RequestReplacePasswordSignedIn => "rep-pwd-signed-in", + RatedAction::RequestReplacePasswordSignedIn => "req-pwd-signed-in", + RatedAction::RequestDeleteAccount => "req-del-acct", } } @@ -33,6 +35,7 @@ impl RatedAction { RatedAction::FailedPasswordLogin => 15, RatedAction::StartOpenIDLogin => 30, RatedAction::RequestReplacePasswordSignedIn => 5, + RatedAction::RequestDeleteAccount => 5, } } diff --git a/geneit_backend/src/services/users_service.rs b/geneit_backend/src/services/users_service.rs index cb90830..abfacfa 100644 --- a/geneit_backend/src/services/users_service.rs +++ b/geneit_backend/src/services/users_service.rs @@ -103,6 +103,37 @@ pub async fn request_reset_password(user: &mut User) -> anyhow::Result<()> { Ok(()) } +/// Request delete account +pub async fn request_delete_account(user: &mut User) -> anyhow::Result<()> { + // If required, regenerate token + if user.delete_account_token.is_none() + || user.time_gen_delete_account_token as u64 + 3600 * 2 < time() + { + user.delete_account_token = Some(rand_str(149)); + user.time_gen_delete_account_token = time() as i64; + + update_account(user).await?; + } + + // Send mail + mail_service::send_mail( + &user.email, + "Suppression de votre compte", + format!( + "Bonjour, \n\n\ + Vous avez demandé la suppression de votre compte GeneIT. Cette opération peut être effectuée via le lien suivant : {} \n\n\ + Ce lien est valide durant 24 heures.\n\n\ + Cordialement,\n\n\ + L'équipe de GeneIT", + AppConfig::get() + .get_account_delete_url(user.delete_account_token.as_deref().unwrap_or("")) + ), + ) + .await?; + + Ok(()) +} + /// Delete not validated accounts whose reset token has expired pub async fn delete_not_validated_accounts() -> anyhow::Result<()> { db_connection::execute(|conn| { @@ -158,6 +189,9 @@ pub async fn update_account(user: &User) -> anyhow::Result<()> { users::dsl::email.eq(user.email.clone()), users::dsl::time_gen_reset_token.eq(user.time_gen_reset_token), users::dsl::reset_password_token.eq(user.reset_password_token.clone()), + users::dsl::time_gen_delete_account_token + .eq(user.time_gen_delete_account_token), + users::dsl::delete_account_token.eq(user.delete_account_token.clone()), users::dsl::time_activate.eq(time() as i64), users::dsl::password.eq(user.password.clone()), ))