diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index 97c019175..12560a9ac 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -149,6 +149,10 @@ enum Subcommand { UnlockUser { /// User to unlock username: String, + + /// Whether to reactivate the user if it had been deactivated + #[arg(long)] + reactivate: bool, }, /// Register a user @@ -527,8 +531,12 @@ impl Options { Ok(ExitCode::SUCCESS) } - SC::UnlockUser { username } => { - let _span = info_span!("cli.manage.lock_user", user.username = username).entered(); + SC::UnlockUser { + username, + reactivate, + } => { + let _span = + info_span!("cli.manage.unlock_user", user.username = username).entered(); let config = DatabaseConfig::extract_or_default(figment)?; let mut conn = database_connection_from_config(&config).await?; let txn = conn.begin().await?; @@ -540,10 +548,14 @@ impl Options { .await? .context("User not found")?; - warn!(%user.id, "User scheduling user reactivation"); - repo.queue_job() - .schedule_job(&mut rng, &clock, ReactivateUserJob::new(&user)) - .await?; + if reactivate { + warn!(%user.id, "Scheduling user reactivation"); + repo.queue_job() + .schedule_job(&mut rng, &clock, ReactivateUserJob::new(&user)) + .await?; + } else { + repo.user().unlock(user).await?; + } repo.into_inner().commit().await?; diff --git a/crates/handlers/src/admin/v1/users/deactivate.rs b/crates/handlers/src/admin/v1/users/deactivate.rs index ddefbf44c..ac4943f93 100644 --- a/crates/handlers/src/admin/v1/users/deactivate.rs +++ b/crates/handlers/src/admin/v1/users/deactivate.rs @@ -12,8 +12,6 @@ use mas_storage::{ BoxRng, queue::{DeactivateUserJob, QueueJobRepositoryExt as _}, }; -use schemars::JsonSchema; -use serde::Deserialize; use tracing::info; use ulid::Ulid; @@ -51,36 +49,21 @@ impl IntoResponse for RouteError { } } -/// # JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint -#[derive(Default, Deserialize, JsonSchema)] -#[serde(rename = "DeactivateUserRequest")] -pub struct Request { - /// Whether to skip locking the user before deactivation. - #[serde(default)] - skip_lock: bool, -} - -pub fn doc(mut operation: TransformOperation) -> TransformOperation { - operation - .inner_mut() - .request_body - .as_mut() - .unwrap() - .as_item_mut() - .unwrap() - .required = false; - +pub fn doc(operation: TransformOperation) -> TransformOperation { operation .id("deactivateUser") .summary("Deactivate a user") - .description("Calling this endpoint will lock and deactivate the user, preventing them from doing any action. -This invalidates any existing session, and will ask the homeserver to make them leave all rooms.") + .description( + "Calling this endpoint will deactivate the user, preventing them from doing any action. +This invalidates any existing session, and will ask the homeserver to make them leave all rooms.", + ) .tag("user") .response_with::<200, Json>, _>(|t| { // In the samples, the third user is the one locked let [_alice, _bob, charlie, ..] = User::samples(); let id = charlie.id(); - let response = SingleResponse::new(charlie, format!("/api/admin/v1/users/{id}/deactivate")); + let response = + SingleResponse::new(charlie, format!("/api/admin/v1/users/{id}/deactivate")); t.description("User was deactivated").example(response) }) .response_with::<404, RouteError, _>(|t| { @@ -96,19 +79,15 @@ pub async fn handler( }: CallContext, NoApi(mut rng): NoApi, id: UlidPathParam, - body: Option>, ) -> Result>, RouteError> { - let Json(params) = body.unwrap_or_default(); let id = *id; - let mut user = repo + let user = repo .user() .lookup(id) .await? .ok_or(RouteError::NotFound(id))?; - if !params.skip_lock && user.locked_at.is_none() { - user = repo.user().lock(&clock, user).await?; - } + let user = repo.user().deactivate(&clock, user).await?; info!(%user.id, "Scheduling deactivation of user"); repo.queue_job() @@ -127,13 +106,14 @@ pub async fn handler( mod tests { use chrono::Duration; use hyper::{Request, StatusCode}; - use insta::{allow_duplicates, assert_json_snapshot}; + use insta::assert_json_snapshot; use mas_storage::{Clock, RepositoryAccess, user::UserRepository}; use sqlx::PgPool; use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; - async fn test_deactivate_user_helper(pool: PgPool, skip_lock: Option) { + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_deactivate_user(pool: PgPool) { setup(); let mut state = TestState::from_pool(pool.clone()).await.unwrap(); let token = state.token_with_scope("urn:mas:admin").await; @@ -146,27 +126,23 @@ mod tests { .unwrap(); repo.save().await.unwrap(); - let request = - Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id)).bearer(&token); - let request = match skip_lock { - None => request.empty(), - Some(skip_lock) => request.json(serde_json::json!({ - "skip_lock": skip_lock, - })), - }; + let request = Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id)) + .bearer(&token) + .empty(); let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - // The locked_at timestamp should be the same as the current time, or null if - // not locked + // The deactivated_at timestamp should be the same as the current time + assert_eq!( + body["data"]["attributes"]["deactivated_at"], + serde_json::json!(state.clock.now()) + ); + + // Deactivating the user should not lock it assert_eq!( body["data"]["attributes"]["locked_at"], - if skip_lock.unwrap_or(false) { - serde_json::Value::Null - } else { - serde_json::json!(state.clock.now()) - } + serde_json::Value::Null ); // Make sure to run the jobs in the queue @@ -179,7 +155,7 @@ mod tests { response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - allow_duplicates!(assert_json_snapshot!(body, @r#" + assert_json_snapshot!(body, @r#" { "data": { "type": "user", @@ -187,7 +163,7 @@ mod tests { "attributes": { "username": "alice", "created_at": "2022-01-16T14:40:00Z", - "locked_at": "2022-01-16T14:40:00Z", + "locked_at": null, "deactivated_at": "2022-01-16T14:40:00Z", "admin": false }, @@ -199,17 +175,7 @@ mod tests { "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E" } } - "#)); - } - - #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] - async fn test_deactivate_user(pool: PgPool) { - test_deactivate_user_helper(pool, Option::None).await; - } - - #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] - async fn test_deactivate_user_skip_lock(pool: PgPool) { - test_deactivate_user_helper(pool, Option::Some(true)).await; + "#); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] @@ -237,14 +203,16 @@ mod tests { response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - // The locked_at timestamp should be different from the current time - assert_ne!( - body["data"]["attributes"]["locked_at"], + // The deactivated_at timestamp should be the same as the current time + assert_eq!( + body["data"]["attributes"]["deactivated_at"], serde_json::json!(state.clock.now()) ); + + // The deactivated_at timestamp should be different from the locked_at timestamp assert_ne!( + body["data"]["attributes"]["deactivated_at"], body["data"]["attributes"]["locked_at"], - serde_json::Value::Null ); // Make sure to run the jobs in the queue diff --git a/crates/handlers/src/admin/v1/users/lock.rs b/crates/handlers/src/admin/v1/users/lock.rs index 9db2a065a..ec8159532 100644 --- a/crates/handlers/src/admin/v1/users/lock.rs +++ b/crates/handlers/src/admin/v1/users/lock.rs @@ -72,15 +72,13 @@ pub async fn handler( id: UlidPathParam, ) -> Result>, RouteError> { let id = *id; - let mut user = repo + let user = repo .user() .lookup(id) .await? .ok_or(RouteError::NotFound(id))?; - if user.locked_at.is_none() { - user = repo.user().lock(&clock, user).await?; - } + let user = repo.user().lock(&clock, user).await?; repo.save().await?; diff --git a/crates/handlers/src/admin/v1/users/reactivate.rs b/crates/handlers/src/admin/v1/users/reactivate.rs index ad73c4dba..37b38c6b6 100644 --- a/crates/handlers/src/admin/v1/users/reactivate.rs +++ b/crates/handlers/src/admin/v1/users/reactivate.rs @@ -53,7 +53,8 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { operation .id("reactivateUser") .summary("Reactivate a user") - .description("Calling this endpoint will reactivate a deactivated user, both locally and on the Matrix homeserver.") + .description("Calling this endpoint will reactivate a deactivated user. +This DOES NOT unlock a locked user, which is still prevented from doing any action until it is explicitly unlocked.") .tag("user") .response_with::<200, Json>, _>(|t| { // In the samples, the third user is the one locked diff --git a/crates/handlers/src/admin/v1/users/unlock.rs b/crates/handlers/src/admin/v1/users/unlock.rs index 78c4f8cc2..5584f4a69 100644 --- a/crates/handlers/src/admin/v1/users/unlock.rs +++ b/crates/handlers/src/admin/v1/users/unlock.rs @@ -4,15 +4,10 @@ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial // Please see LICENSE files in the repository root for full details. -use std::sync::Arc; - use aide::{OperationIo, transform::TransformOperation}; -use axum::{Json, extract::State, response::IntoResponse}; +use axum::{Json, response::IntoResponse}; use hyper::StatusCode; use mas_axum_utils::record_error; -use mas_matrix::HomeserverConnection; -use schemars::JsonSchema; -use serde::Deserialize; use ulid::Ulid; use crate::{ @@ -31,9 +26,6 @@ pub enum RouteError { #[error(transparent)] Internal(Box), - #[error(transparent)] - Homeserver(anyhow::Error), - #[error("User ID {0} not found")] NotFound(Ulid), } @@ -43,37 +35,21 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let error = ErrorResponse::from_error(&self); - let sentry_event_id = record_error!(self, Self::Internal(_) | Self::Homeserver(_)); + let sentry_event_id = record_error!(self, Self::Internal(_)); let status = match self { - Self::Internal(_) | Self::Homeserver(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::NotFound(_) => StatusCode::NOT_FOUND, }; (status, sentry_event_id, Json(error)).into_response() } } -/// # JSON payload for the `POST /api/admin/v1/users/:id/unlock` endpoint -#[derive(Default, Deserialize, JsonSchema)] -#[serde(rename = "UnlockUserRequest")] -pub struct Request { - /// Whether to skip ensuring the user is active upon being unlocked. - #[serde(default)] - skip_reactivate: bool, -} - -pub fn doc(mut operation: TransformOperation) -> TransformOperation { - operation - .inner_mut() - .request_body - .as_mut() - .unwrap() - .as_item_mut() - .unwrap() - .required = false; - +pub fn doc(operation: TransformOperation) -> TransformOperation { operation .id("unlockUser") .summary("Unlock a user") + .description("Calling this endpoint will lift restrictions on user actions that had imposed by locking. +This DOES NOT reactivate a deactivated user, which will remain unavailable until it is explicitly reactivated.") .tag("user") .response_with::<200, Json>, _>(|t| { // In the samples, the third user is the one locked @@ -91,11 +67,8 @@ pub fn doc(mut operation: TransformOperation) -> TransformOperation { #[tracing::instrument(name = "handler.admin.v1.users.unlock", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - State(homeserver): State>, id: UlidPathParam, - body: Option>, ) -> Result>, RouteError> { - let Json(params) = body.unwrap_or_default(); let id = *id; let user = repo .user() @@ -103,17 +76,7 @@ pub async fn handler( .await? .ok_or(RouteError::NotFound(id))?; - let user = if params.skip_reactivate { - repo.user().unlock(user).await? - } else { - // Call the homeserver synchronously to reactivate the user - let mxid = homeserver.mxid(&user.username); - homeserver - .reactivate_user(&mxid) - .await - .map_err(RouteError::Homeserver)?; - repo.user().reactivate_and_unlock(user).await? - }; + let user = repo.user().unlock(user).await?; repo.save().await?; @@ -169,7 +132,8 @@ mod tests { ); } - async fn test_unlock_deactivated_user_helper(pool: PgPool, skip_reactivate: Option) { + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_unlock_deactivated_user(pool: PgPool) { setup(); let mut state = TestState::from_pool(pool).await.unwrap(); let token = state.token_with_scope("urn:mas:admin").await; @@ -202,14 +166,9 @@ mod tests { let mx_user = state.homeserver_connection.query_user(&mxid).await.unwrap(); assert!(mx_user.deactivated); - let request = - Request::post(format!("/api/admin/v1/users/{}/unlock", user.id)).bearer(&token); - let request = match skip_reactivate { - None => request.empty(), - Some(skip_reactivate) => request.json(serde_json::json!({ - "skip_reactivate": skip_reactivate, - })), - }; + let request = Request::post(format!("/api/admin/v1/users/{}/unlock", user.id)) + .bearer(&token) + .empty(); let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); @@ -218,30 +177,13 @@ mod tests { body["data"]["attributes"]["locked_at"], serde_json::Value::Null ); - - let skip_reactivate = skip_reactivate.unwrap_or(false); + // The user should remain deactivated assert_eq!( body["data"]["attributes"]["deactivated_at"], - if skip_reactivate { - serde_json::json!(state.clock.now()) - } else { - serde_json::Value::Null - } + serde_json::json!(state.clock.now()) ); - - // Check whether the user should be reactivated on the homeserver let mx_user = state.homeserver_connection.query_user(&mxid).await.unwrap(); - assert_eq!(mx_user.deactivated, skip_reactivate); - } - - #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] - async fn test_unlock_deactivated_user(pool: PgPool) { - test_unlock_deactivated_user_helper(pool, Option::None).await; - } - - #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] - async fn test_unlock_deactivated_user_skip_reactivate(pool: PgPool) { - test_unlock_deactivated_user_helper(pool, Option::Some(true)).await; + assert!(mx_user.deactivated); } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/crates/handlers/src/graphql/mutations/user.rs b/crates/handlers/src/graphql/mutations/user.rs index a5d7e0fc2..80be76b24 100644 --- a/crates/handlers/src/graphql/mutations/user.rs +++ b/crates/handlers/src/graphql/mutations/user.rs @@ -144,6 +144,9 @@ impl LockUserPayload { struct UnlockUserInput { /// The ID of the user to unlock user_id: ID, + + /// Reactivate the user if it had been deactivated + reactivate: Option, } /// The status of the `unlockUser` mutation. @@ -585,12 +588,19 @@ impl UserMutations { return Ok(UnlockUserPayload::NotFound); }; - // Call the homeserver synchronously to unlock the user - let mxid = matrix.mxid(&user.username); - matrix.reactivate_user(&mxid).await?; + let user = if input.reactivate.unwrap_or(false) { + // Call the homeserver synchronously to reactivate the user + let mxid = matrix.mxid(&user.username); + matrix.reactivate_user(&mxid).await?; + + // Now reactivate the user in our database + repo.user().reactivate(user).await? + } else { + user + }; // Now unlock the user in our database - let user = repo.user().reactivate_and_unlock(user).await?; + let user = repo.user().unlock(user).await?; repo.save().await?; diff --git a/crates/storage-pg/.sqlx/query-3e2d1ce1c7aba2952ed9c659972a18ded5613186104695524e85df9b6641ea4e.json b/crates/storage-pg/.sqlx/query-3e2d1ce1c7aba2952ed9c659972a18ded5613186104695524e85df9b6641ea4e.json deleted file mode 100644 index 738adae1c..000000000 --- a/crates/storage-pg/.sqlx/query-3e2d1ce1c7aba2952ed9c659972a18ded5613186104695524e85df9b6641ea4e.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n UPDATE users\n SET deactivated_at = NULL, locked_at = NULL\n WHERE user_id = $1\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid" - ] - }, - "nullable": [] - }, - "hash": "3e2d1ce1c7aba2952ed9c659972a18ded5613186104695524e85df9b6641ea4e" -} diff --git a/crates/storage-pg/src/user/mod.rs b/crates/storage-pg/src/user/mod.rs index a1f321e7c..6d03e9bf7 100644 --- a/crates/storage-pg/src/user/mod.rs +++ b/crates/storage-pg/src/user/mod.rs @@ -379,7 +379,7 @@ impl UserRepository for PgUserRepository<'_> { DatabaseError::ensure_affected_rows(&res, 1)?; - user.deactivated_at = Some(user.created_at); + user.deactivated_at = Some(deactivated_at); Ok(user) } @@ -417,40 +417,6 @@ impl UserRepository for PgUserRepository<'_> { Ok(user) } - #[tracing::instrument( - name = "db.user.reactivate_and_unlock", - skip_all, - fields( - db.query.text, - %user.id, - ), - err, - )] - async fn reactivate_and_unlock(&mut self, mut user: User) -> Result { - if user.deactivated_at.is_none() && user.locked_at.is_none() { - return Ok(user); - } - - let res = sqlx::query!( - r#" - UPDATE users - SET deactivated_at = NULL, locked_at = NULL - WHERE user_id = $1 - "#, - Uuid::from(user.id), - ) - .traced() - .execute(&mut *self.conn) - .await?; - - DatabaseError::ensure_affected_rows(&res, 1)?; - - user.deactivated_at = None; - user.locked_at = None; - - Ok(user) - } - #[tracing::instrument( name = "db.user.set_can_request_admin", skip_all, diff --git a/crates/storage/src/queue/tasks.rs b/crates/storage/src/queue/tasks.rs index f59971ba4..eb16f6e29 100644 --- a/crates/storage/src/queue/tasks.rs +++ b/crates/storage/src/queue/tasks.rs @@ -257,14 +257,14 @@ impl InsertableJob for DeactivateUserJob { const QUEUE_NAME: &'static str = "deactivate-user"; } -/// A job to reactivate and unlock a user +/// A job to reactivate a user #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ReactivateUserJob { user_id: Ulid, } impl ReactivateUserJob { - /// Create a new job to reactivate and unlock a user + /// Create a new job to reactivate a user /// /// # Parameters /// diff --git a/crates/storage/src/user/mod.rs b/crates/storage/src/user/mod.rs index f990af3e2..f864157b1 100644 --- a/crates/storage/src/user/mod.rs +++ b/crates/storage/src/user/mod.rs @@ -257,19 +257,6 @@ pub trait UserRepository: Send + Sync { /// Returns [`Self::Error`] if the underlying repository fails async fn reactivate(&mut self, user: User) -> Result; - /// Reactivate and unlock a [`User`] - /// - /// Returns the reactivated and unlocked [`User`] - /// - /// # Parameters - /// - /// * `user`: The [`User`] to reactivate and unlock - /// - /// # Errors - /// - /// Returns [`Self::Error`] if the underlying repository fails - async fn reactivate_and_unlock(&mut self, user: User) -> Result; - /// Set whether a [`User`] can request admin /// /// Returns the [`User`] with the new `can_request_admin` value @@ -342,7 +329,6 @@ repository_impl!(UserRepository: async fn unlock(&mut self, user: User) -> Result; async fn deactivate(&mut self, clock: &dyn Clock, user: User) -> Result; async fn reactivate(&mut self, user: User) -> Result; - async fn reactivate_and_unlock(&mut self, user: User) -> Result; async fn set_can_request_admin( &mut self, user: User, diff --git a/crates/tasks/src/user.rs b/crates/tasks/src/user.rs index f7ce2f9ff..245733aa5 100644 --- a/crates/tasks/src/user.rs +++ b/crates/tasks/src/user.rs @@ -41,14 +41,7 @@ impl RunnableJob for DeactivateUserJob { .context("User not found") .map_err(JobError::fail)?; - // Let's first lock & deactivate the user - let user = repo - .user() - .lock(clock, user) - .await - .context("Failed to lock user") - .map_err(JobError::retry)?; - + // Let's first deactivate the user let user = repo .user() .deactivate(clock, user) @@ -137,11 +130,11 @@ impl RunnableJob for ReactivateUserJob { .await .map_err(JobError::retry)?; - // We want to unlock the user from our side only once it has been reactivated on - // the homeserver + // We want to reactivate the user from our side only once it has been + // reactivated on the homeserver let _user = repo .user() - .reactivate_and_unlock(user) + .reactivate(user) .await .map_err(JobError::retry)?; repo.save().await.map_err(JobError::retry)?; diff --git a/docs/api/spec.json b/docs/api/spec.json index 28d394254..b30155b96 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -1345,7 +1345,7 @@ "user" ], "summary": "Deactivate a user", - "description": "Calling this endpoint will lock and deactivate the user, preventing them from doing any action.\nThis invalidates any existing session, and will ask the homeserver to make them leave all rooms.", + "description": "Calling this endpoint will deactivate the user, preventing them from doing any action.\nThis invalidates any existing session, and will ask the homeserver to make them leave all rooms.", "operationId": "deactivateUser", "parameters": [ { @@ -1359,15 +1359,6 @@ "style": "simple" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/DeactivateUserRequest" - } - } - } - }, "responses": { "200": { "description": "User was deactivated", @@ -1424,7 +1415,7 @@ "user" ], "summary": "Reactivate a user", - "description": "Calling this endpoint will reactivate a deactivated user, both locally and on the Matrix homeserver.", + "description": "Calling this endpoint will reactivate a deactivated user.\nThis DOES NOT unlock a locked user, which is still prevented from doing any action until it is explicitly unlocked.", "operationId": "reactivateUser", "parameters": [ { @@ -1564,6 +1555,7 @@ "user" ], "summary": "Unlock a user", + "description": "Calling this endpoint will lift restrictions on user actions that had imposed by locking.\nThis DOES NOT reactivate a deactivated user, which will remain unavailable until it is explicitly reactivated.", "operationId": "unlockUser", "parameters": [ { @@ -1577,15 +1569,6 @@ "style": "simple" } ], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UnlockUserRequest" - } - } - } - }, "responses": { "200": { "description": "User was unlocked", @@ -3960,28 +3943,6 @@ } } }, - "DeactivateUserRequest": { - "title": "JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint", - "type": "object", - "properties": { - "skip_lock": { - "description": "Whether to skip locking the user before deactivation.", - "default": false, - "type": "boolean" - } - } - }, - "UnlockUserRequest": { - "title": "JSON payload for the `POST /api/admin/v1/users/:id/unlock` endpoint", - "type": "object", - "properties": { - "skip_reactivate": { - "description": "Whether to skip ensuring the user is active upon being unlocked.", - "default": false, - "type": "boolean" - } - } - }, "UserEmailFilter": { "type": "object", "properties": { diff --git a/frontend/schema.graphql b/frontend/schema.graphql index 0e71a519d..993a554a3 100644 --- a/frontend/schema.graphql +++ b/frontend/schema.graphql @@ -1842,6 +1842,10 @@ input UnlockUserInput { The ID of the user to unlock """ userId: ID! + """ + Reactivate the user if it had been deactivated + """ + reactivate: Boolean } """ diff --git a/frontend/src/gql/graphql.ts b/frontend/src/gql/graphql.ts index ff482af6c..b07b89cf5 100644 --- a/frontend/src/gql/graphql.ts +++ b/frontend/src/gql/graphql.ts @@ -1347,6 +1347,8 @@ export type StartEmailAuthenticationStatus = /** The input for the `unlockUser` mutation. */ export type UnlockUserInput = { + /** Reactivate the user if it had been deactivated */ + reactivate?: InputMaybe; /** The ID of the user to unlock */ userId: Scalars['ID']['input']; };