diff --git a/crates/handlers/src/admin/v1/users/deactivate.rs b/crates/handlers/src/admin/v1/users/deactivate.rs index 87c2361a2..076c06914 100644 --- a/crates/handlers/src/admin/v1/users/deactivate.rs +++ b/crates/handlers/src/admin/v1/users/deactivate.rs @@ -12,6 +12,8 @@ use mas_storage::{ BoxRng, queue::{DeactivateUserJob, QueueJobRepositoryExt as _}, }; +use schemars::JsonSchema; +use serde::Deserialize; use tracing::info; use ulid::Ulid; @@ -49,7 +51,26 @@ impl IntoResponse for RouteError { } } -pub fn doc(operation: TransformOperation) -> TransformOperation { +/// # 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 requesting the homeserver to GDPR-erase the user upon + /// deactivation. + #[serde(default)] + skip_erase: bool, +} + +pub fn doc(mut operation: TransformOperation) -> TransformOperation { + operation + .inner_mut() + .request_body + .as_mut() + .unwrap() + .as_item_mut() + .unwrap() + .required = false; + operation .id("deactivateUser") .summary("Deactivate a user") @@ -76,7 +97,9 @@ 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 .user() @@ -90,7 +113,11 @@ pub async fn handler( info!(%user.id, "Scheduling deactivation of user"); repo.queue_job() - .schedule_job(&mut rng, &clock, DeactivateUserJob::new(&user, true)) + .schedule_job( + &mut rng, + &clock, + DeactivateUserJob::new(&user, !params.skip_erase), + ) .await?; repo.save().await?; @@ -105,14 +132,13 @@ pub async fn handler( mod tests { use chrono::Duration; use hyper::{Request, StatusCode}; - use insta::assert_json_snapshot; + use insta::{allow_duplicates, assert_json_snapshot}; use mas_storage::{Clock, RepositoryAccess, user::UserRepository}; - use sqlx::PgPool; + use sqlx::{PgPool, types::Json}; use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; - #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] - async fn test_deactivate_user(pool: PgPool) { + async fn test_deactivate_user_helper(pool: PgPool, skip_erase: Option) { setup(); let mut state = TestState::from_pool(pool.clone()).await.unwrap(); let token = state.token_with_scope("urn:mas:admin").await; @@ -125,9 +151,14 @@ mod tests { .unwrap(); repo.save().await.unwrap(); - let request = Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id)) - .bearer(&token) - .empty(); + let request = + Request::post(format!("/api/admin/v1/users/{}/deactivate", user.id)).bearer(&token); + let request = match skip_erase { + None => request.empty(), + Some(skip_erase) => request.json(serde_json::json!({ + "skip_erase": skip_erase, + })), + }; let response = state.request(request).await; response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); @@ -138,6 +169,20 @@ mod tests { serde_json::json!(state.clock.now()) ); + // It should have scheduled a deactivation job for the user + // XXX: we don't have a good way to look for the deactivation job + let job: Json = sqlx::query_scalar( + "SELECT payload FROM queue_jobs WHERE queue_name = 'deactivate-user'", + ) + .fetch_one(&pool) + .await + .expect("Deactivation job to be scheduled"); + assert_eq!(job["user_id"], serde_json::json!(user.id)); + assert_eq!( + job["hs_erase"], + serde_json::json!(!skip_erase.unwrap_or(false)) + ); + // Make sure to run the jobs in the queue state.run_jobs_in_queue().await; @@ -148,7 +193,7 @@ mod tests { response.assert_status(StatusCode::OK); let body: serde_json::Value = response.json(); - assert_json_snapshot!(body, @r#" + allow_duplicates!(assert_json_snapshot!(body, @r#" { "data": { "type": "user", @@ -168,7 +213,17 @@ 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_erase(pool: PgPool) { + test_deactivate_user_helper(pool, Option::Some(true)).await; } #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] diff --git a/docs/api/spec.json b/docs/api/spec.json index 0082ea37c..5cb1beba1 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -1359,6 +1359,15 @@ "style": "simple" } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeactivateUserRequest" + } + } + } + }, "responses": { "200": { "description": "User was deactivated", @@ -3872,6 +3881,17 @@ } } }, + "DeactivateUserRequest": { + "title": "JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint", + "type": "object", + "properties": { + "skip_erase": { + "description": "Whether to skip requesting the homeserver to GDPR-erase the user upon deactivation.", + "default": false, + "type": "boolean" + } + } + }, "UserEmailFilter": { "type": "object", "properties": {