@@ -12,6 +12,8 @@ use mas_storage::{
1212 BoxRng ,
1313 queue:: { DeactivateUserJob , QueueJobRepositoryExt as _} ,
1414} ;
15+ use schemars:: JsonSchema ;
16+ use serde:: Deserialize ;
1517use tracing:: info;
1618use ulid:: Ulid ;
1719
@@ -49,18 +51,40 @@ impl IntoResponse for RouteError {
4951 }
5052}
5153
52- pub fn doc ( operation : TransformOperation ) -> TransformOperation {
54+ /// # JSON payload for the `POST /api/admin/v1/users/:id/deactivate` endpoint
55+ #[ derive( Default , Deserialize , JsonSchema ) ]
56+ #[ serde( rename = "DeactivateUserRequest" ) ]
57+ pub struct Request {
58+ /// Whether to skip requesting the homeserver to GDPR-erase the user upon
59+ /// deactivation.
60+ #[ serde( default ) ]
61+ skip_erase : bool ,
62+ }
63+
64+ pub fn doc ( mut operation : TransformOperation ) -> TransformOperation {
65+ operation
66+ . inner_mut ( )
67+ . request_body
68+ . as_mut ( )
69+ . unwrap ( )
70+ . as_item_mut ( )
71+ . unwrap ( )
72+ . required = false ;
73+
5374 operation
5475 . id ( "deactivateUser" )
5576 . summary ( "Deactivate a user" )
56- . description ( "Calling this endpoint will lock and deactivate the user, preventing them from doing any action.
57- This invalidates any existing session, and will ask the homeserver to make them leave all rooms." )
77+ . description (
78+ "Calling this endpoint will deactivate the user, preventing them from doing any action.
79+ This invalidates any existing session, and will ask the homeserver to make them leave all rooms." ,
80+ )
5881 . tag ( "user" )
5982 . response_with :: < 200 , Json < SingleResponse < User > > , _ > ( |t| {
6083 // In the samples, the third user is the one locked
6184 let [ _alice, _bob, charlie, ..] = User :: samples ( ) ;
6285 let id = charlie. id ( ) ;
63- let response = SingleResponse :: new ( charlie, format ! ( "/api/admin/v1/users/{id}/deactivate" ) ) ;
86+ let response =
87+ SingleResponse :: new ( charlie, format ! ( "/api/admin/v1/users/{id}/deactivate" ) ) ;
6488 t. description ( "User was deactivated" ) . example ( response)
6589 } )
6690 . response_with :: < 404 , RouteError , _ > ( |t| {
@@ -76,21 +100,25 @@ pub async fn handler(
76100 } : CallContext ,
77101 NoApi ( mut rng) : NoApi < BoxRng > ,
78102 id : UlidPathParam ,
103+ body : Option < Json < Request > > ,
79104) -> Result < Json < SingleResponse < User > > , RouteError > {
105+ let Json ( params) = body. unwrap_or_default ( ) ;
80106 let id = * id;
81- let mut user = repo
107+ let user = repo
82108 . user ( )
83109 . lookup ( id)
84110 . await ?
85111 . ok_or ( RouteError :: NotFound ( id) ) ?;
86112
87- if user. locked_at . is_none ( ) {
88- user = repo. user ( ) . lock ( & clock, user) . await ?;
89- }
113+ let user = repo. user ( ) . deactivate ( & clock, user) . await ?;
90114
91115 info ! ( %user. id, "Scheduling deactivation of user" ) ;
92116 repo. queue_job ( )
93- . schedule_job ( & mut rng, & clock, DeactivateUserJob :: new ( & user, true ) )
117+ . schedule_job (
118+ & mut rng,
119+ & clock,
120+ DeactivateUserJob :: new ( & user, !params. skip_erase ) ,
121+ )
94122 . await ?;
95123
96124 repo. save ( ) . await ?;
@@ -105,14 +133,13 @@ pub async fn handler(
105133mod tests {
106134 use chrono:: Duration ;
107135 use hyper:: { Request , StatusCode } ;
108- use insta:: assert_json_snapshot;
136+ use insta:: { allow_duplicates , assert_json_snapshot} ;
109137 use mas_storage:: { Clock , RepositoryAccess , user:: UserRepository } ;
110- use sqlx:: PgPool ;
138+ use sqlx:: { PgPool , types :: Json } ;
111139
112140 use crate :: test_utils:: { RequestBuilderExt , ResponseExt , TestState , setup} ;
113141
114- #[ sqlx:: test( migrator = "mas_storage_pg::MIGRATOR" ) ]
115- async fn test_deactivate_user ( pool : PgPool ) {
142+ async fn test_deactivate_user_helper ( pool : PgPool , skip_erase : Option < bool > ) {
116143 setup ( ) ;
117144 let mut state = TestState :: from_pool ( pool. clone ( ) ) . await . unwrap ( ) ;
118145 let token = state. token_with_scope ( "urn:mas:admin" ) . await ;
@@ -125,19 +152,44 @@ mod tests {
125152 . unwrap ( ) ;
126153 repo. save ( ) . await . unwrap ( ) ;
127154
128- let request = Request :: post ( format ! ( "/api/admin/v1/users/{}/deactivate" , user. id) )
129- . bearer ( & token)
130- . empty ( ) ;
155+ let request =
156+ Request :: post ( format ! ( "/api/admin/v1/users/{}/deactivate" , user. id) ) . bearer ( & token) ;
157+ let request = match skip_erase {
158+ None => request. empty ( ) ,
159+ Some ( skip_erase) => request. json ( serde_json:: json!( {
160+ "skip_erase" : skip_erase,
161+ } ) ) ,
162+ } ;
131163 let response = state. request ( request) . await ;
132164 response. assert_status ( StatusCode :: OK ) ;
133165 let body: serde_json:: Value = response. json ( ) ;
134166
135- // The locked_at timestamp should be the same as the current time
167+ // The deactivated_at timestamp should be the same as the current time
136168 assert_eq ! (
137- body[ "data" ] [ "attributes" ] [ "locked_at " ] ,
169+ body[ "data" ] [ "attributes" ] [ "deactivated_at " ] ,
138170 serde_json:: json!( state. clock. now( ) )
139171 ) ;
140172
173+ // Deactivating the user should not lock it
174+ assert_eq ! (
175+ body[ "data" ] [ "attributes" ] [ "locked_at" ] ,
176+ serde_json:: Value :: Null
177+ ) ;
178+
179+ // It should have scheduled a deactivation job for the user
180+ // XXX: we don't have a good way to look for the deactivation job
181+ let job: Json < serde_json:: Value > = sqlx:: query_scalar (
182+ "SELECT payload FROM queue_jobs WHERE queue_name = 'deactivate-user'" ,
183+ )
184+ . fetch_one ( & pool)
185+ . await
186+ . expect ( "Deactivation job to be scheduled" ) ;
187+ assert_eq ! ( job[ "user_id" ] , serde_json:: json!( user. id) ) ;
188+ assert_eq ! (
189+ job[ "hs_erase" ] ,
190+ serde_json:: json!( !skip_erase. unwrap_or( false ) )
191+ ) ;
192+
141193 // Make sure to run the jobs in the queue
142194 state. run_jobs_in_queue ( ) . await ;
143195
@@ -148,15 +200,15 @@ mod tests {
148200 response. assert_status ( StatusCode :: OK ) ;
149201 let body: serde_json:: Value = response. json ( ) ;
150202
151- assert_json_snapshot ! ( body, @r#"
203+ allow_duplicates ! ( assert_json_snapshot!( body, @r#"
152204 {
153205 "data": {
154206 "type": "user",
155207 "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
156208 "attributes": {
157209 "username": "alice",
158210 "created_at": "2022-01-16T14:40:00Z",
159- "locked_at": "2022-01-16T14:40:00Z" ,
211+ "locked_at": null ,
160212 "deactivated_at": "2022-01-16T14:40:00Z",
161213 "admin": false
162214 },
@@ -168,7 +220,17 @@ mod tests {
168220 "self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E"
169221 }
170222 }
171- "# ) ;
223+ "# ) ) ;
224+ }
225+
226+ #[ sqlx:: test( migrator = "mas_storage_pg::MIGRATOR" ) ]
227+ async fn test_deactivate_user ( pool : PgPool ) {
228+ test_deactivate_user_helper ( pool, Option :: None ) . await ;
229+ }
230+
231+ #[ sqlx:: test( migrator = "mas_storage_pg::MIGRATOR" ) ]
232+ async fn test_deactivate_user_skip_erase ( pool : PgPool ) {
233+ test_deactivate_user_helper ( pool, Option :: Some ( true ) ) . await ;
172234 }
173235
174236 #[ sqlx:: test( migrator = "mas_storage_pg::MIGRATOR" ) ]
@@ -196,10 +258,16 @@ mod tests {
196258 response. assert_status ( StatusCode :: OK ) ;
197259 let body: serde_json:: Value = response. json ( ) ;
198260
199- // The locked_at timestamp should be different from the current time
261+ // The deactivated_at timestamp should be the same as the current time
262+ assert_eq ! (
263+ body[ "data" ] [ "attributes" ] [ "deactivated_at" ] ,
264+ serde_json:: json!( state. clock. now( ) )
265+ ) ;
266+
267+ // The deactivated_at timestamp should be different from the locked_at timestamp
200268 assert_ne ! (
269+ body[ "data" ] [ "attributes" ] [ "deactivated_at" ] ,
201270 body[ "data" ] [ "attributes" ] [ "locked_at" ] ,
202- serde_json:: json!( state. clock. now( ) )
203271 ) ;
204272
205273 // Make sure to run the jobs in the queue
0 commit comments