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