@@ -12,6 +12,8 @@ use mas_storage::{
12
12
BoxRng ,
13
13
queue:: { DeactivateUserJob , QueueJobRepositoryExt as _} ,
14
14
} ;
15
+ use schemars:: JsonSchema ;
16
+ use serde:: Deserialize ;
15
17
use tracing:: info;
16
18
use ulid:: Ulid ;
17
19
@@ -49,18 +51,40 @@ impl IntoResponse for RouteError {
49
51
}
50
52
}
51
53
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
+
53
74
operation
54
75
. id ( "deactivateUser" )
55
76
. 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
+ )
58
81
. tag ( "user" )
59
82
. response_with :: < 200 , Json < SingleResponse < User > > , _ > ( |t| {
60
83
// In the samples, the third user is the one locked
61
84
let [ _alice, _bob, charlie, ..] = User :: samples ( ) ;
62
85
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" ) ) ;
64
88
t. description ( "User was deactivated" ) . example ( response)
65
89
} )
66
90
. response_with :: < 404 , RouteError , _ > ( |t| {
@@ -76,21 +100,25 @@ pub async fn handler(
76
100
} : CallContext ,
77
101
NoApi ( mut rng) : NoApi < BoxRng > ,
78
102
id : UlidPathParam ,
103
+ body : Option < Json < Request > > ,
79
104
) -> Result < Json < SingleResponse < User > > , RouteError > {
105
+ let Json ( params) = body. unwrap_or_default ( ) ;
80
106
let id = * id;
81
- let mut user = repo
107
+ let user = repo
82
108
. user ( )
83
109
. lookup ( id)
84
110
. await ?
85
111
. ok_or ( RouteError :: NotFound ( id) ) ?;
86
112
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 ?;
90
114
91
115
info ! ( %user. id, "Scheduling deactivation of user" ) ;
92
116
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
+ )
94
122
. await ?;
95
123
96
124
repo. save ( ) . await ?;
@@ -105,14 +133,13 @@ pub async fn handler(
105
133
mod tests {
106
134
use chrono:: Duration ;
107
135
use hyper:: { Request , StatusCode } ;
108
- use insta:: assert_json_snapshot;
136
+ use insta:: { allow_duplicates , assert_json_snapshot} ;
109
137
use mas_storage:: { Clock , RepositoryAccess , user:: UserRepository } ;
110
- use sqlx:: PgPool ;
138
+ use sqlx:: { PgPool , types :: Json } ;
111
139
112
140
use crate :: test_utils:: { RequestBuilderExt , ResponseExt , TestState , setup} ;
113
141
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 > ) {
116
143
setup ( ) ;
117
144
let mut state = TestState :: from_pool ( pool. clone ( ) ) . await . unwrap ( ) ;
118
145
let token = state. token_with_scope ( "urn:mas:admin" ) . await ;
@@ -125,19 +152,44 @@ mod tests {
125
152
. unwrap ( ) ;
126
153
repo. save ( ) . await . unwrap ( ) ;
127
154
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
+ } ;
131
163
let response = state. request ( request) . await ;
132
164
response. assert_status ( StatusCode :: OK ) ;
133
165
let body: serde_json:: Value = response. json ( ) ;
134
166
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
136
168
assert_eq ! (
137
- body[ "data" ] [ "attributes" ] [ "locked_at " ] ,
169
+ body[ "data" ] [ "attributes" ] [ "deactivated_at " ] ,
138
170
serde_json:: json!( state. clock. now( ) )
139
171
) ;
140
172
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
+
141
193
// Make sure to run the jobs in the queue
142
194
state. run_jobs_in_queue ( ) . await ;
143
195
@@ -148,15 +200,15 @@ mod tests {
148
200
response. assert_status ( StatusCode :: OK ) ;
149
201
let body: serde_json:: Value = response. json ( ) ;
150
202
151
- assert_json_snapshot ! ( body, @r#"
203
+ allow_duplicates ! ( assert_json_snapshot!( body, @r#"
152
204
{
153
205
"data": {
154
206
"type": "user",
155
207
"id": "01FSHN9AG0MZAA6S4AF7CTV32E",
156
208
"attributes": {
157
209
"username": "alice",
158
210
"created_at": "2022-01-16T14:40:00Z",
159
- "locked_at": "2022-01-16T14:40:00Z" ,
211
+ "locked_at": null ,
160
212
"deactivated_at": "2022-01-16T14:40:00Z",
161
213
"admin": false
162
214
},
@@ -168,7 +220,17 @@ mod tests {
168
220
"self": "/api/admin/v1/users/01FSHN9AG0MZAA6S4AF7CTV32E"
169
221
}
170
222
}
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 ;
172
234
}
173
235
174
236
#[ sqlx:: test( migrator = "mas_storage_pg::MIGRATOR" ) ]
@@ -196,10 +258,16 @@ mod tests {
196
258
response. assert_status ( StatusCode :: OK ) ;
197
259
let body: serde_json:: Value = response. json ( ) ;
198
260
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
200
268
assert_ne ! (
269
+ body[ "data" ] [ "attributes" ] [ "deactivated_at" ] ,
201
270
body[ "data" ] [ "attributes" ] [ "locked_at" ] ,
202
- serde_json:: json!( state. clock. now( ) )
203
271
) ;
204
272
205
273
// Make sure to run the jobs in the queue
0 commit comments