mas_handlers/admin/v1/user_registration_tokens/
unrevoke.rs1use aide::{OperationIo, transform::TransformOperation};
8use axum::{Json, response::IntoResponse};
9use hyper::StatusCode;
10use mas_axum_utils::record_error;
11use ulid::Ulid;
12
13use crate::{
14 admin::{
15 call_context::CallContext,
16 model::{Resource, UserRegistrationToken},
17 params::UlidPathParam,
18 response::{ErrorResponse, SingleResponse},
19 },
20 impl_from_error_for_route,
21};
22
23#[derive(Debug, thiserror::Error, OperationIo)]
24#[aide(output_with = "Json<ErrorResponse>")]
25pub enum RouteError {
26 #[error(transparent)]
27 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
28
29 #[error("Registration token with ID {0} not found")]
30 NotFound(Ulid),
31
32 #[error("Registration token with ID {0} is not revoked")]
33 NotRevoked(Ulid),
34}
35
36impl_from_error_for_route!(mas_storage::RepositoryError);
37
38impl IntoResponse for RouteError {
39 fn into_response(self) -> axum::response::Response {
40 let error = ErrorResponse::from_error(&self);
41 let sentry_event_id = record_error!(self, Self::Internal(_));
42 let status = match self {
43 Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
44 Self::NotFound(_) => StatusCode::NOT_FOUND,
45 Self::NotRevoked(_) => StatusCode::BAD_REQUEST,
46 };
47 (status, sentry_event_id, Json(error)).into_response()
48 }
49}
50
51pub fn doc(operation: TransformOperation) -> TransformOperation {
52 operation
53 .id("unrevokeUserRegistrationToken")
54 .summary("Unrevoke a user registration token")
55 .description("Calling this endpoint will unrevoke a previously revoked user registration token, allowing it to be used for registrations again (subject to its usage limits and expiration).")
56 .tag("user-registration-token")
57 .response_with::<200, Json<SingleResponse<UserRegistrationToken>>, _>(|t| {
58 let [valid_token, _] = UserRegistrationToken::samples();
60 let id = valid_token.id();
61 let response = SingleResponse::new(valid_token, format!("/api/admin/v1/user-registration-tokens/{id}/unrevoke"));
62 t.description("Registration token was unrevoked").example(response)
63 })
64 .response_with::<400, RouteError, _>(|t| {
65 let response = ErrorResponse::from_error(&RouteError::NotRevoked(Ulid::nil()));
66 t.description("Token is not revoked").example(response)
67 })
68 .response_with::<404, RouteError, _>(|t| {
69 let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
70 t.description("Registration token was not found").example(response)
71 })
72}
73
74#[tracing::instrument(name = "handler.admin.v1.user_registration_tokens.unrevoke", skip_all)]
75pub async fn handler(
76 CallContext {
77 mut repo, clock, ..
78 }: CallContext,
79 id: UlidPathParam,
80) -> Result<Json<SingleResponse<UserRegistrationToken>>, RouteError> {
81 let id = *id;
82 let token = repo
83 .user_registration_token()
84 .lookup(id)
85 .await?
86 .ok_or(RouteError::NotFound(id))?;
87
88 if token.revoked_at.is_none() {
90 return Err(RouteError::NotRevoked(id));
91 }
92
93 let token = repo.user_registration_token().unrevoke(token).await?;
95
96 repo.save().await?;
97
98 Ok(Json(SingleResponse::new(
99 UserRegistrationToken::new(token, clock.now()),
100 format!("/api/admin/v1/user-registration-tokens/{id}/unrevoke"),
101 )))
102}
103
104#[cfg(test)]
105mod tests {
106 use hyper::{Request, StatusCode};
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_unrevoke_token(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
119 let registration_token = repo
121 .user_registration_token()
122 .add(
123 &mut state.rng(),
124 &state.clock,
125 "test_token_456".to_owned(),
126 Some(5),
127 None,
128 )
129 .await
130 .unwrap();
131
132 let registration_token = repo
134 .user_registration_token()
135 .revoke(&state.clock, registration_token)
136 .await
137 .unwrap();
138
139 repo.save().await.unwrap();
140
141 let request = Request::post(format!(
143 "/api/admin/v1/user-registration-tokens/{}/unrevoke",
144 registration_token.id
145 ))
146 .bearer(&token)
147 .empty();
148 let response = state.request(request).await;
149 response.assert_status(StatusCode::OK);
150 let body: serde_json::Value = response.json();
151
152 insta::assert_json_snapshot!(body, @r#"
154 {
155 "data": {
156 "type": "user-registration_token",
157 "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
158 "attributes": {
159 "token": "test_token_456",
160 "valid": true,
161 "usage_limit": 5,
162 "times_used": 0,
163 "created_at": "2022-01-16T14:40:00Z",
164 "last_used_at": null,
165 "expires_at": null,
166 "revoked_at": null
167 },
168 "links": {
169 "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
170 }
171 },
172 "links": {
173 "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E/unrevoke"
174 }
175 }
176 "#);
177 }
178
179 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
180 async fn test_unrevoke_not_revoked_token(pool: PgPool) {
181 setup();
182 let mut state = TestState::from_pool(pool).await.unwrap();
183 let token = state.token_with_scope("urn:mas:admin").await;
184
185 let mut repo = state.repository().await.unwrap();
186 let registration_token = repo
187 .user_registration_token()
188 .add(
189 &mut state.rng(),
190 &state.clock,
191 "test_token_789".to_owned(),
192 None,
193 None,
194 )
195 .await
196 .unwrap();
197
198 repo.save().await.unwrap();
199
200 let request = Request::post(format!(
202 "/api/admin/v1/user-registration-tokens/{}/unrevoke",
203 registration_token.id
204 ))
205 .bearer(&token)
206 .empty();
207 let response = state.request(request).await;
208 response.assert_status(StatusCode::BAD_REQUEST);
209 let body: serde_json::Value = response.json();
210 assert_eq!(
211 body["errors"][0]["title"],
212 format!(
213 "Registration token with ID {} is not revoked",
214 registration_token.id
215 )
216 );
217 }
218
219 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
220 async fn test_unrevoke_unknown_token(pool: PgPool) {
221 setup();
222 let mut state = TestState::from_pool(pool).await.unwrap();
223 let token = state.token_with_scope("urn:mas:admin").await;
224
225 let request = Request::post(
226 "/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081/unrevoke",
227 )
228 .bearer(&token)
229 .empty();
230 let response = state.request(request).await;
231 response.assert_status(StatusCode::NOT_FOUND);
232 let body: serde_json::Value = response.json();
233 assert_eq!(
234 body["errors"][0]["title"],
235 "Registration token with ID 01040G2081040G2081040G2081 not found"
236 );
237 }
238}