mas_handlers/admin/v1/users/
unlock.rs1use std::sync::Arc;
8
9use aide::{OperationIo, transform::TransformOperation};
10use axum::{Json, extract::State, response::IntoResponse};
11use hyper::StatusCode;
12use mas_axum_utils::record_error;
13use mas_matrix::HomeserverConnection;
14use ulid::Ulid;
15
16use crate::{
17 admin::{
18 call_context::CallContext,
19 model::{Resource, User},
20 params::UlidPathParam,
21 response::{ErrorResponse, SingleResponse},
22 },
23 impl_from_error_for_route,
24};
25
26#[derive(Debug, thiserror::Error, OperationIo)]
27#[aide(output_with = "Json<ErrorResponse>")]
28pub enum RouteError {
29 #[error(transparent)]
30 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
31
32 #[error(transparent)]
33 Homeserver(anyhow::Error),
34
35 #[error("User ID {0} not found")]
36 NotFound(Ulid),
37}
38
39impl_from_error_for_route!(mas_storage::RepositoryError);
40
41impl IntoResponse for RouteError {
42 fn into_response(self) -> axum::response::Response {
43 let error = ErrorResponse::from_error(&self);
44 let sentry_event_id = record_error!(self, Self::Internal(_) | Self::Homeserver(_));
45 let status = match self {
46 Self::Internal(_) | Self::Homeserver(_) => StatusCode::INTERNAL_SERVER_ERROR,
47 Self::NotFound(_) => StatusCode::NOT_FOUND,
48 };
49 (status, sentry_event_id, Json(error)).into_response()
50 }
51}
52
53pub fn doc(operation: TransformOperation) -> TransformOperation {
54 operation
55 .id("unlockUser")
56 .summary("Unlock a user")
57 .tag("user")
58 .response_with::<200, Json<SingleResponse<User>>, _>(|t| {
59 let [sample, ..] = User::samples();
61 let id = sample.id();
62 let response = SingleResponse::new(sample, format!("/api/admin/v1/users/{id}/unlock"));
63 t.description("User was unlocked").example(response)
64 })
65 .response_with::<404, RouteError, _>(|t| {
66 let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
67 t.description("User ID not found").example(response)
68 })
69}
70
71#[tracing::instrument(name = "handler.admin.v1.users.unlock", skip_all)]
72pub async fn handler(
73 CallContext { mut repo, .. }: CallContext,
74 State(homeserver): State<Arc<dyn HomeserverConnection>>,
75 id: UlidPathParam,
76) -> Result<Json<SingleResponse<User>>, RouteError> {
77 let id = *id;
78 let user = repo
79 .user()
80 .lookup(id)
81 .await?
82 .ok_or(RouteError::NotFound(id))?;
83
84 let mxid = homeserver.mxid(&user.username);
86 homeserver
87 .reactivate_user(&mxid)
88 .await
89 .map_err(RouteError::Homeserver)?;
90
91 let user = repo.user().unlock(user).await?;
93
94 repo.save().await?;
95
96 Ok(Json(SingleResponse::new(
97 User::from(user),
98 format!("/api/admin/v1/users/{id}/unlock"),
99 )))
100}
101
102#[cfg(test)]
103mod tests {
104 use hyper::{Request, StatusCode};
105 use mas_matrix::{HomeserverConnection, ProvisionRequest};
106 use mas_storage::{RepositoryAccess, user::UserRepository};
107 use sqlx::PgPool;
108
109 use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
110
111 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
112 async fn test_unlock_user(pool: PgPool) {
113 setup();
114 let mut state = TestState::from_pool(pool).await.unwrap();
115 let token = state.token_with_scope("urn:mas:admin").await;
116
117 let mut repo = state.repository().await.unwrap();
118 let user = repo
119 .user()
120 .add(&mut state.rng(), &state.clock, "alice".to_owned())
121 .await
122 .unwrap();
123 let user = repo.user().lock(&state.clock, user).await.unwrap();
124 repo.save().await.unwrap();
125
126 let mxid = state.homeserver_connection.mxid(&user.username);
129 state
130 .homeserver_connection
131 .provision_user(&ProvisionRequest::new(&mxid, &user.sub))
132 .await
133 .unwrap();
134
135 let request = Request::post(format!("/api/admin/v1/users/{}/unlock", user.id))
136 .bearer(&token)
137 .empty();
138 let response = state.request(request).await;
139 response.assert_status(StatusCode::OK);
140 let body: serde_json::Value = response.json();
141
142 assert_eq!(
143 body["data"]["attributes"]["locked_at"],
144 serde_json::json!(null)
145 );
146 }
147
148 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
149 async fn test_unlock_deactivated_user(pool: PgPool) {
150 setup();
151 let mut state = TestState::from_pool(pool).await.unwrap();
152 let token = state.token_with_scope("urn:mas:admin").await;
153
154 let mut repo = state.repository().await.unwrap();
155 let user = repo
156 .user()
157 .add(&mut state.rng(), &state.clock, "alice".to_owned())
158 .await
159 .unwrap();
160 let user = repo.user().lock(&state.clock, user).await.unwrap();
161 repo.save().await.unwrap();
162
163 let mxid = state.homeserver_connection.mxid(&user.username);
165 state
166 .homeserver_connection
167 .provision_user(&ProvisionRequest::new(&mxid, &user.sub))
168 .await
169 .unwrap();
170 state
172 .homeserver_connection
173 .delete_user(&mxid, true)
174 .await
175 .unwrap();
176
177 let mx_user = state.homeserver_connection.query_user(&mxid).await.unwrap();
179 assert!(mx_user.deactivated);
180
181 let request = Request::post(format!("/api/admin/v1/users/{}/unlock", user.id))
182 .bearer(&token)
183 .empty();
184 let response = state.request(request).await;
185 response.assert_status(StatusCode::OK);
186 let body: serde_json::Value = response.json();
187
188 assert_eq!(
189 body["data"]["attributes"]["locked_at"],
190 serde_json::json!(null)
191 );
192 let mx_user = state.homeserver_connection.query_user(&mxid).await.unwrap();
194 assert!(!mx_user.deactivated);
195 }
196
197 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
198 async fn test_lock_unknown_user(pool: PgPool) {
199 setup();
200 let mut state = TestState::from_pool(pool).await.unwrap();
201 let token = state.token_with_scope("urn:mas:admin").await;
202
203 let request = Request::post("/api/admin/v1/users/01040G2081040G2081040G2081/unlock")
204 .bearer(&token)
205 .empty();
206 let response = state.request(request).await;
207 response.assert_status(StatusCode::NOT_FOUND);
208 let body: serde_json::Value = response.json();
209 assert_eq!(
210 body["errors"][0]["title"],
211 "User ID 01040G2081040G2081040G2081 not found"
212 );
213 }
214}