From 83995728349a2a589408d45078bbe9de474b3195 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:41:37 +0200 Subject: [PATCH 01/34] add is_active column to the database --- ...20240604104038_add_user_active_flag.down.sql | 1 + .../20240604104038_add_user_active_flag.up.sql | 5 +++++ src/db/models/user.rs | 17 +++++++---------- 3 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 migrations/20240604104038_add_user_active_flag.down.sql create mode 100644 migrations/20240604104038_add_user_active_flag.up.sql diff --git a/migrations/20240604104038_add_user_active_flag.down.sql b/migrations/20240604104038_add_user_active_flag.down.sql new file mode 100644 index 0000000000..0151cefaeb --- /dev/null +++ b/migrations/20240604104038_add_user_active_flag.down.sql @@ -0,0 +1 @@ +ALTER TABLE "user" DROP COLUMN is_active; \ No newline at end of file diff --git a/migrations/20240604104038_add_user_active_flag.up.sql b/migrations/20240604104038_add_user_active_flag.up.sql new file mode 100644 index 0000000000..a49a1c0b59 --- /dev/null +++ b/migrations/20240604104038_add_user_active_flag.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE "user" ADD COLUMN is_active boolean NOT NULL DEFAULT false; + +-- Update the user table to keep the old behaviour +-- Previously: active user = user with password set +UPDATE "user" SET is_active = TRUE WHERE password_hash IS NOT NULL; \ No newline at end of file diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 65cf433465..611ed5955d 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -71,6 +71,7 @@ pub struct User { pub email: String, pub phone: Option, pub mfa_enabled: bool, + pub is_active: bool, // secret has been verified and TOTP can be used pub(crate) totp_enabled: bool, pub(crate) email_mfa_enabled: bool, @@ -116,6 +117,7 @@ impl User { email_mfa_secret: None, mfa_method: MFAMethod::None, recovery_codes: Vec::new(), + is_active: false, } } @@ -136,11 +138,6 @@ impl User { } } - #[must_use] - pub fn has_password(&self) -> bool { - self.password_hash.is_some() - } - #[must_use] pub fn name(&self) -> String { format!("{} {}", self.first_name, self.last_name) @@ -447,7 +444,7 @@ impl User { ) -> Result, SqlxError> { let users = query!( "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, \ - mfa_method as \"mfa_method: MFAMethod\", password_hash \ + mfa_method as \"mfa_method: MFAMethod\", is_active \ FROM \"user\"" ) .fetch_all(pool) @@ -460,7 +457,7 @@ impl User { email_mfa_enabled: u.email_mfa_enabled, mfa_enabled: u.mfa_enabled, id: u.id, - is_active: u.password_hash.is_some(), + is_active: u.is_active, }) .collect(); Ok(res) @@ -476,7 +473,7 @@ impl User { "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, totp_secret, \ email_mfa_enabled, email_mfa_secret, \ - mfa_method \"mfa_method: _\", recovery_codes \ + mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" INNER JOIN \"group_user\" ON \"user\".id = \"group_user\".user_id INNER JOIN \"group\" ON \"group_user\".group_id = \"group\".id @@ -567,7 +564,7 @@ impl User { Self, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ - totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" WHERE username = $1", username ) @@ -583,7 +580,7 @@ impl User { Self, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ - totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" WHERE email = $1", email ) From 0c88ac439536992b9d42570bd48e7e4ef839a655 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Tue, 4 Jun 2024 13:43:06 +0200 Subject: [PATCH 02/34] use is_active field to check if the user is active --- src/db/models/enrollment.rs | 2 +- src/db/models/group.rs | 2 +- src/db/models/mod.rs | 2 +- src/grpc/enrollment.rs | 5 ++--- src/grpc/password_reset.rs | 4 ++-- src/handlers/group.rs | 2 +- src/handlers/user.rs | 2 +- 7 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 767af4fe9f..45c66b0fe2 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -376,7 +376,7 @@ impl User { "User {} starting enrollment for user {}, notification enabled: {send_user_notification}", admin.username, self.username ); - if self.has_password() { + if self.is_active { return Err(TokenError::AlreadyActive); } diff --git a/src/db/models/group.rs b/src/db/models/group.rs index ce8f00aad7..73f4476188 100644 --- a/src/db/models/group.rs +++ b/src/db/models/group.rs @@ -60,7 +60,7 @@ impl Group { User, "SELECT \"user\".id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, totp_secret, email_mfa_enabled, email_mfa_secret, \ - mfa_method \"mfa_method: _\", recovery_codes \ + mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" \ JOIN group_user ON \"user\".id = group_user.user_id \ WHERE group_user.group_id = $1", diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index e60dfdfe99..4f6e6b9e4e 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -96,7 +96,7 @@ impl UserInfo { groups, mfa_method: user.mfa_method.clone(), authorized_apps, - is_active: user.has_password(), + is_active: user.is_active, }) } diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 683677baa0..37514c13d3 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -212,7 +212,7 @@ impl EnrollmentServer { // fetch related users let mut user = enrollment.fetch_user(&self.pool).await?; info!("Activating user account for {}", user.username); - if user.has_password() { + if user.is_active { error!("User {} already activated", user.username); return Err(Status::invalid_argument("user already activated")); } @@ -465,7 +465,6 @@ impl From for AdminInfo { impl InitialUserInfo { async fn from_user(pool: &DbPool, user: User) -> Result { - let is_active = user.has_password(); let devices = user.devices(pool).await?; let device_names = devices.into_iter().map(|dev| dev.device.name).collect(); Ok(Self { @@ -474,7 +473,7 @@ impl InitialUserInfo { login: user.username, email: user.email, phone_number: user.phone, - is_active, + is_active: user.is_active, device_names, }) } diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs index c68a15c3c0..176f9e8298 100644 --- a/src/grpc/password_reset.rs +++ b/src/grpc/password_reset.rs @@ -89,7 +89,7 @@ impl PasswordResetServer { }; // Do not allow password change if user is not active - if !user.has_password() { + if !user.is_active { return Ok(()); } @@ -144,7 +144,7 @@ impl PasswordResetServer { let user = enrollment.fetch_user(&self.pool).await?; - if !user.has_password() { + if !user.is_active { return Err(Status::permission_denied("user inactive")); } diff --git a/src/handlers/group.rs b/src/handlers/group.rs index ba529874f3..748bdc5388 100644 --- a/src/handlers/group.rs +++ b/src/handlers/group.rs @@ -47,7 +47,7 @@ pub(crate) async fn bulk_assign_to_groups( User, "SELECT id \"id?\", username, password_hash, last_name, first_name, email, \ phone, mfa_enabled, totp_enabled, email_mfa_enabled, \ - totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes \ + totp_secret, email_mfa_secret, mfa_method \"mfa_method: _\", recovery_codes, is_active \ FROM \"user\" WHERE id = ANY($1)", &data.users ) diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 78803fe551..9f9f35e03f 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -165,7 +165,7 @@ pub async fn add_user( let user_info = UserInfo::from_user(&appstate.pool, &user).await?; appstate.trigger_action(AppEvent::UserCreated(user_info.clone())); info!("User {} added user {username}", session.user.username); - if !user.has_password() { + if !user.is_active { warn!("User {username} is not active yet. Please proceed with enrollment."); }; Ok(ApiResponse { From 04e71a84553453e8862c91fa477a753ddc43079b Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:28:09 +0200 Subject: [PATCH 03/34] add backend logic --- ...20240604104038_add_user_active_flag.up.sql | 6 +----- src/db/models/enrollment.rs | 9 ++++++++- src/db/models/mod.rs | 19 +++++++++++++++++++ src/db/models/user.rs | 9 ++++++++- src/db/models/wireguard.rs | 12 ++++++++++-- src/error.rs | 7 ++++--- src/grpc/enrollment.rs | 16 +++++++++++++++- src/grpc/gateway.rs | 2 ++ src/grpc/password_reset.rs | 10 ++++++---- src/handlers/auth.rs | 9 ++++++++- src/handlers/user.rs | 7 +++++-- src/handlers/wireguard.rs | 11 +++++++++++ 12 files changed, 97 insertions(+), 20 deletions(-) diff --git a/migrations/20240604104038_add_user_active_flag.up.sql b/migrations/20240604104038_add_user_active_flag.up.sql index a49a1c0b59..b426087f7b 100644 --- a/migrations/20240604104038_add_user_active_flag.up.sql +++ b/migrations/20240604104038_add_user_active_flag.up.sql @@ -1,5 +1 @@ -ALTER TABLE "user" ADD COLUMN is_active boolean NOT NULL DEFAULT false; - --- Update the user table to keep the old behaviour --- Previously: active user = user with password set -UPDATE "user" SET is_active = TRUE WHERE password_hash IS NOT NULL; \ No newline at end of file +ALTER TABLE "user" ADD COLUMN is_active boolean NOT NULL DEFAULT true; \ No newline at end of file diff --git a/src/db/models/enrollment.rs b/src/db/models/enrollment.rs index 45c66b0fe2..36a3658ce7 100644 --- a/src/db/models/enrollment.rs +++ b/src/db/models/enrollment.rs @@ -35,6 +35,8 @@ pub enum TokenError { TokenUsed, #[error("Enrollment user not found")] UserNotFound, + #[error("Enrollment user is disabled")] + UserDisabled, #[error("Enrollment admin not found")] AdminNotFound, #[error("User account is already activated")] @@ -58,6 +60,7 @@ impl From for Status { TokenError::DbError(_) | TokenError::AdminNotFound | TokenError::UserNotFound + | TokenError::UserDisabled | TokenError::NotificationError(_) | TokenError::WelcomeMsgNotConfigured | TokenError::WelcomeEmailNotConfigured @@ -376,10 +379,14 @@ impl User { "User {} starting enrollment for user {}, notification enabled: {send_user_notification}", admin.username, self.username ); - if self.is_active { + if self.has_password() { return Err(TokenError::AlreadyActive); } + if !self.is_active { + return Err(TokenError::UserDisabled); + } + let user_id = self.id.expect("User without ID"); let admin_id = admin.id.expect("Admin user without ID"); diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 4f6e6b9e4e..edac80e9c8 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -76,6 +76,7 @@ pub struct UserInfo { pub mfa_method: MFAMethod, pub authorized_apps: Vec, pub is_active: bool, + pub enrolled: bool, } impl UserInfo { @@ -97,9 +98,27 @@ impl UserInfo { mfa_method: user.mfa_method.clone(), authorized_apps, is_active: user.is_active, + enrolled: user.has_password(), }) } + /// Copy status to [`User`]. This function should be used by administrators. + /// + /// Return `true` if status was changed, `false` otherwise. + pub(crate) async fn handle_status_change( + &self, + transaction: &mut PgConnection, + user: &mut User, + ) -> Result { + if self.is_active != user.is_active { + user.is_active = self.is_active; + user.save(transaction).await?; + Ok(true) + } else { + Ok(false) + } + } + /// Copy groups to [`User`]. This function should be used by administrators. /// /// Return `true` if groups were changed, `false` otherwise. diff --git a/src/db/models/user.rs b/src/db/models/user.rs index 611ed5955d..6f1f3d3d40 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -59,6 +59,7 @@ pub struct UserDiagnostic { pub email_mfa_enabled: bool, pub mfa_method: MFAMethod, pub is_active: bool, + pub enrolled: bool, } #[derive(Model, PartialEq, Serialize, Clone, Debug)] @@ -138,6 +139,11 @@ impl User { } } + #[must_use] + pub fn has_password(&self) -> bool { + self.password_hash.is_some() + } + #[must_use] pub fn name(&self) -> String { format!("{} {}", self.first_name, self.last_name) @@ -444,7 +450,7 @@ impl User { ) -> Result, SqlxError> { let users = query!( "SELECT id, mfa_enabled, totp_enabled, email_mfa_enabled, \ - mfa_method as \"mfa_method: MFAMethod\", is_active \ + mfa_method as \"mfa_method: MFAMethod\", password_hash, is_active \ FROM \"user\"" ) .fetch_all(pool) @@ -458,6 +464,7 @@ impl User { mfa_enabled: u.mfa_enabled, id: u.id, is_active: u.is_active, + enrolled: u.password_hash.is_some(), }) .collect(); Ok(res) diff --git a/src/db/models/wireguard.rs b/src/db/models/wireguard.rs index 72a7b2e58b..d85c17f213 100644 --- a/src/db/models/wireguard.rs +++ b/src/db/models/wireguard.rs @@ -328,15 +328,23 @@ impl WireguardNetwork { JOIN group_user gu ON u.id = gu.user_id \ JOIN \"group\" g ON gu.group_id = g.id \ WHERE g.\"name\" IN (SELECT * FROM UNNEST($1::text[])) + AND u.is_active = true ORDER BY d.id ASC", &allowed_groups ) .fetch_all(&mut *transaction) .await? }, - // all devices are allowed + // all devices of enabled users are allowed None => { - Device::all(&mut *transaction).await? + query_as!( + Device, + "SELECT DISTINCT ON (d.id) d.id as \"id?\", d.name, d.wireguard_pubkey, d.user_id, d.created \ + FROM device d \ + JOIN \"user\" u ON d.user_id = u.id \ + WHERE u.is_active = true \ + ORDER BY d.id ASC" + ).fetch_all(&mut *transaction).await? } }; diff --git a/src/error.rs b/src/error.rs index 3c003b0f99..00db0dee6d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -134,9 +134,10 @@ impl From for WebError { TokenError::NotFound | TokenError::UserNotFound | TokenError::AdminNotFound => { WebError::ObjectNotFound(err.to_string()) } - TokenError::TokenExpired | TokenError::SessionExpired | TokenError::TokenUsed => { - WebError::Authorization(err.to_string()) - } + TokenError::TokenExpired + | TokenError::SessionExpired + | TokenError::TokenUsed + | TokenError::UserDisabled => WebError::Authorization(err.to_string()), TokenError::AlreadyActive => WebError::BadRequest(err.to_string()), TokenError::NotificationError(_) | TokenError::WelcomeMsgNotConfigured diff --git a/src/grpc/enrollment.rs b/src/grpc/enrollment.rs index 37514c13d3..f0304b11fd 100644 --- a/src/grpc/enrollment.rs +++ b/src/grpc/enrollment.rs @@ -212,11 +212,16 @@ impl EnrollmentServer { // fetch related users let mut user = enrollment.fetch_user(&self.pool).await?; info!("Activating user account for {}", user.username); - if user.is_active { + if user.has_password() { error!("User {} already activated", user.username); return Err(Status::invalid_argument("user already activated")); } + if !user.is_active { + error!("User {} is disabled", user.username); + return Err(Status::invalid_argument("user is disabled")); + } + let mut transaction = self.pool.begin().await.map_err(|_| { error!("Failed to begin transaction"); Status::internal("unexpected error") @@ -289,6 +294,13 @@ impl EnrollmentServer { // add device info!("Adding new device for user {}", user.username); + if !user.is_active { + error!("Can't create device for a disabled user {}", user.username); + return Err(Status::invalid_argument( + "can't add device to disabled user", + )); + } + let ip_address; let device_info; if let Some(info) = req_device_info { @@ -465,6 +477,7 @@ impl From for AdminInfo { impl InitialUserInfo { async fn from_user(pool: &DbPool, user: User) -> Result { + let is_enrolled = user.has_password(); let devices = user.devices(pool).await?; let device_names = devices.into_iter().map(|dev| dev.device.name).collect(); Ok(Self { @@ -475,6 +488,7 @@ impl InitialUserInfo { phone_number: user.phone, is_active: user.is_active, device_names, + enrolled: is_enrolled, }) } } diff --git a/src/grpc/gateway.rs b/src/grpc/gateway.rs index ea994fcb66..a22c800471 100644 --- a/src/grpc/gateway.rs +++ b/src/grpc/gateway.rs @@ -49,7 +49,9 @@ impl WireguardNetwork { array[host(wnd.wireguard_ip)] as \"allowed_ips!: Vec\" \ FROM wireguard_network_device wnd \ JOIN device d ON wnd.device_id = d.id \ + JOIN \"user\" u ON d.user_id = u.id \ WHERE wireguard_network_id = $1 AND (is_authorized = true OR NOT $2) \ + AND u.is_active = true \ ORDER BY d.id ASC", self.id, self.mfa_enabled diff --git a/src/grpc/password_reset.rs b/src/grpc/password_reset.rs index 176f9e8298..df8093f36a 100644 --- a/src/grpc/password_reset.rs +++ b/src/grpc/password_reset.rs @@ -88,8 +88,8 @@ impl PasswordResetServer { return Ok(()); }; - // Do not allow password change if user is not active - if !user.is_active { + // Do not allow password change if user is disabled or not enrolled + if !user.has_password() || !user.is_active { return Ok(()); } @@ -144,8 +144,10 @@ impl PasswordResetServer { let user = enrollment.fetch_user(&self.pool).await?; - if !user.is_active { - return Err(Status::permission_denied("user inactive")); + if !user.has_password() || !user.is_active { + return Err(Status::permission_denied( + "user disabled or not enrolled yet", + )); } let mut transaction = self.pool.begin().await.map_err(|_| { diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs index a4a1cbffb7..1da00d9094 100644 --- a/src/handlers/auth.rs +++ b/src/handlers/auth.rs @@ -59,7 +59,14 @@ pub async fn authenticate( let user = match User::find_by_username(&appstate.pool, &username).await { Ok(Some(user)) => match user.verify_password(&data.password) { - Ok(()) => user, + Ok(()) => { + if user.is_active { + user + } else { + info!("Failed to authenticate user {username}: user is inactive"); + return Err(WebError::Authorization("user not found".into())); + } + } Err(err) => { info!("Failed to authenticate user {username}: {err}"); log_failed_login_attempt(&appstate.failed_logins, &username); diff --git a/src/handlers/user.rs b/src/handlers/user.rs index 9f9f35e03f..fac38c41aa 100644 --- a/src/handlers/user.rs +++ b/src/handlers/user.rs @@ -165,8 +165,8 @@ pub async fn add_user( let user_info = UserInfo::from_user(&appstate.pool, &user).await?; appstate.trigger_action(AppEvent::UserCreated(user_info.clone())); info!("User {} added user {username}", session.user.username); - if !user.is_active { - warn!("User {username} is not active yet. Please proceed with enrollment."); + if !user_info.enrolled { + warn!("User {username} hasn't been enrolled yet. Please proceed with enrollment."); }; Ok(ApiResponse { json: json!(&user_info), @@ -326,6 +326,9 @@ pub async fn modify_user( if user_info .handle_user_groups(&mut transaction, &mut user) .await? + || user_info + .handle_status_change(&mut transaction, &mut user) + .await? { let networks = WireguardNetwork::all(&mut *transaction).await?; for network in networks { diff --git a/src/handlers/wireguard.rs b/src/handlers/wireguard.rs index 9350061aa6..7dc9aa146f 100644 --- a/src/handlers/wireguard.rs +++ b/src/handlers/wireguard.rs @@ -458,6 +458,17 @@ pub async fn add_device( ); let user = user_for_admin_or_self(&appstate.pool, &session, &username).await?; + + // Let admins manage devices for disabled users + if !user.is_active && !session.is_admin { + debug!( + "User {} tried to add a device for a disabled user {username}", + session.user.username + ); + + return Err(WebError::Forbidden("User is disabled.".into())); + } + let networks = WireguardNetwork::all(&appstate.pool).await?; if networks.is_empty() { error!("No network found, can't add device"); From eaffb43e1b9d5f53e6f048c0ea57fcd20766d753 Mon Sep 17 00:00:00 2001 From: Aleksander <170264518+t-aleksander@users.noreply.github.com> Date: Thu, 6 Jun 2024 12:30:58 +0200 Subject: [PATCH 04/34] handle user disabling on the frontend --- web/src/i18n/en/index.ts | 20 ++++ web/src/i18n/i18n-types.ts | 92 ++++++++++++++++++ web/src/i18n/pl/index.ts | 20 ++++ .../pages/users/UserProfile/UserProfile.tsx | 36 +++++++ .../UserEditButton/UserEditButton.tsx | 19 +++- web/src/pages/users/UsersSharedModals.tsx | 2 + .../ToggleUserModal/ToggleUserModal.tsx | 97 +++++++++++++++++++ web/src/shared/hooks/store/useModalStore.ts | 10 ++ web/src/shared/types.ts | 10 ++ 9 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 web/src/pages/users/shared/modals/ToggleUserModal/ToggleUserModal.tsx diff --git a/web/src/i18n/en/index.ts b/web/src/i18n/en/index.ts index b17b71b4e5..c54161b12f 100644 --- a/web/src/i18n/en/index.ts +++ b/web/src/i18n/en/index.ts @@ -273,6 +273,26 @@ const en: BaseTranslation = { success: '{username: string} deleted.', }, }, + disableUser: { + title: 'Disable account', + controls: { + submit: 'Disable account', + }, + message: 'Do you want to disable {username: string} account ?', + messages: { + success: '{username: string} disabled.', + }, + }, + enableUser: { + title: 'Enable account', + controls: { + submit: 'Enable account', + }, + message: 'Do you want to enable {username: string} account ?', + messages: { + success: '{username: string} enabled.', + }, + }, deleteProvisioner: { title: 'Delete provisioner', controls: { diff --git a/web/src/i18n/i18n-types.ts b/web/src/i18n/i18n-types.ts index 40142d1f62..cac929a9ad 100644 --- a/web/src/i18n/i18n-types.ts +++ b/web/src/i18n/i18n-types.ts @@ -648,6 +648,54 @@ type RootTranslation = { success: RequiredParams<'username'> } } + disableUser: { + /** + * D​i​s​a​b​l​e​ ​a​c​c​o​u​n​t + */ + title: string + controls: { + /** + * D​i​s​a​b​l​e​ ​a​c​c​o​u​n​t + */ + submit: string + } + /** + * D​o​ ​y​o​u​ ​w​a​n​t​ ​t​o​ ​d​i​s​a​b​l​e​ ​{​u​s​e​r​n​a​m​e​}​ ​a​c​c​o​u​n​t​ ​? + * @param {string} username + */ + message: RequiredParams<'username'> + messages: { + /** + * {​u​s​e​r​n​a​m​e​}​ ​d​i​s​a​b​l​e​d​. + * @param {string} username + */ + success: RequiredParams<'username'> + } + } + enableUser: { + /** + * E​n​a​b​l​e​ ​a​c​c​o​u​n​t + */ + title: string + controls: { + /** + * E​n​a​b​l​e​ ​a​c​c​o​u​n​t + */ + submit: string + } + /** + * D​o​ ​y​o​u​ ​w​a​n​t​ ​t​o​ ​e​n​a​b​l​e​ ​{​u​s​e​r​n​a​m​e​}​ ​a​c​c​o​u​n​t​ ​? + * @param {string} username + */ + message: RequiredParams<'username'> + messages: { + /** + * {​u​s​e​r​n​a​m​e​}​ ​e​n​a​b​l​e​d​. + * @param {string} username + */ + success: RequiredParams<'username'> + } + } deleteProvisioner: { /** * D​e​l​e​t​e​ ​p​r​o​v​i​s​i​o​n​e​r @@ -4488,6 +4536,50 @@ export type TranslationFunctions = { success: (arg: { username: string }) => LocalizedString } } + disableUser: { + /** + * Disable account + */ + title: () => LocalizedString + controls: { + /** + * Disable account + */ + submit: () => LocalizedString + } + /** + * Do you want to disable {username} account ? + */ + message: (arg: { username: string }) => LocalizedString + messages: { + /** + * {username} disabled. + */ + success: (arg: { username: string }) => LocalizedString + } + } + enableUser: { + /** + * Enable account + */ + title: () => LocalizedString + controls: { + /** + * Enable account + */ + submit: () => LocalizedString + } + /** + * Do you want to enable {username} account ? + */ + message: (arg: { username: string }) => LocalizedString + messages: { + /** + * {username} enabled. + */ + success: (arg: { username: string }) => LocalizedString + } + } deleteProvisioner: { /** * Delete provisioner diff --git a/web/src/i18n/pl/index.ts b/web/src/i18n/pl/index.ts index 698fa3f439..53680cca2d 100644 --- a/web/src/i18n/pl/index.ts +++ b/web/src/i18n/pl/index.ts @@ -275,6 +275,26 @@ const pl: Translation = { success: '{username} usunięte.', }, }, + disableUser: { + title: 'Wyłącz użytkownika', + controls: { + submit: 'Wyłącz użytkownika', + }, + message: 'Czy chcesz wyłączyć użytkownika {username}?', + messages: { + success: 'Użytkownik {username} został wyłączony.', + }, + }, + enableUser: { + title: 'Włącz użytkownika', + controls: { + submit: 'Włącz użytkownika', + }, + message: 'Czy chcesz włączyć użytkownika {username}?', + messages: { + success: 'Użytkownik {username} został włączony.', + }, + }, deleteProvisioner: { title: 'Usuń provisionera', controls: { diff --git a/web/src/pages/users/UserProfile/UserProfile.tsx b/web/src/pages/users/UserProfile/UserProfile.tsx index fc44b44b03..86211c3d12 100644 --- a/web/src/pages/users/UserProfile/UserProfile.tsx +++ b/web/src/pages/users/UserProfile/UserProfile.tsx @@ -133,6 +133,7 @@ const EditModeControls = () => { const isMe = useUserProfileStore((state) => state.isMe); const setUserProfileState = useUserProfileStore((state) => state.setState); const setDeleteUserModalState = useModalStore((state) => state.setDeleteUserModal); + const setToggleUserModalState = useModalStore((state) => state.setToggleUserModal); const loading = useUserProfileStore((state) => state.loading); const submitSubject = useUserProfileStore((state) => state.submitSubject); @@ -143,6 +144,12 @@ const EditModeControls = () => { } }; + const handleToggleUser = () => { + if (userProfile) { + setToggleUserModalState({ visible: true, user: userProfile.user }); + } + }; + return ( <> {isAdmin && !isMe && breakpoint === 'desktop' ? ( @@ -153,6 +160,20 @@ const EditModeControls = () => { styleVariant={ButtonStyleVariant.CONFIRM} onClick={handleDeleteUser} /> +