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