From 49540693ab3f61adc745c7819206d582d5b2daf4 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Wed, 16 Jul 2025 09:16:06 -0400 Subject: [PATCH] Decouple (un)locking from (re/de)activation Unify the admin API, CLI, and GraphQL API in not having the unlock command also reactivate, or the deactivate command also lock. Still let the unlock command of the CLI and GraphQL API to also reactivate the target user, albeit as a non-default option. --- crates/cli/src/commands/manage.rs | 24 +++-- .../handlers/src/admin/v1/users/deactivate.rs | 96 +++++++------------ crates/handlers/src/admin/v1/users/lock.rs | 6 +- .../handlers/src/admin/v1/users/reactivate.rs | 3 +- crates/handlers/src/admin/v1/users/unlock.rs | 88 +++-------------- crates/handlers/src/graphql/mutations/user.rs | 18 +++- ...a18ded5613186104695524e85df9b6641ea4e.json | 14 --- crates/storage-pg/src/user/mod.rs | 36 +------ crates/storage/src/queue/tasks.rs | 4 +- crates/storage/src/user/mod.rs | 14 --- crates/tasks/src/user.rs | 15 +-- docs/api/spec.json | 45 +-------- frontend/schema.graphql | 4 + frontend/src/gql/graphql.ts | 2 + 14 files changed, 99 insertions(+), 270 deletions(-) delete mode 100644 crates/storage-pg/.sqlx/query-3e2d1ce1c7aba2952ed9c659972a18ded5613186104695524e85df9b6641ea4e.json 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']; };