mas_handlers/admin/v1/user_registration_tokens/
revoke.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 already revoked")]
33 AlreadyRevoked(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::AlreadyRevoked(_) => 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("revokeUserRegistrationToken")
54 .summary("Revoke a user registration token")
55 .description("Calling this endpoint will revoke the user registration token, preventing it from being used for new registrations.")
56 .tag("user-registration-token")
57 .response_with::<200, Json<SingleResponse<UserRegistrationToken>>, _>(|t| {
58 let [_, revoked_token] = UserRegistrationToken::samples();
60 let id = revoked_token.id();
61 let response = SingleResponse::new(revoked_token, format!("/api/admin/v1/user-registration-tokens/{id}/revoke"));
62 t.description("Registration token was revoked").example(response)
63 })
64 .response_with::<400, RouteError, _>(|t| {
65 let response = ErrorResponse::from_error(&RouteError::AlreadyRevoked(Ulid::nil()));
66 t.description("Token is already 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.revoke", 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_some() {
90 return Err(RouteError::AlreadyRevoked(id));
91 }
92
93 let token = repo.user_registration_token().revoke(&clock, 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}/revoke"),
101 )))
102}
103
104#[cfg(test)]
105mod tests {
106 use chrono::Duration;
107 use hyper::{Request, StatusCode};
108 use mas_storage::Clock as _;
109 use sqlx::PgPool;
110
111 use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
112
113 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
114 async fn test_revoke_token(pool: PgPool) {
115 setup();
116 let mut state = TestState::from_pool(pool).await.unwrap();
117 let token = state.token_with_scope("urn:mas:admin").await;
118
119 let mut repo = state.repository().await.unwrap();
120 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 repo.save().await.unwrap();
132
133 let request = Request::post(format!(
134 "/api/admin/v1/user-registration-tokens/{}/revoke",
135 registration_token.id
136 ))
137 .bearer(&token)
138 .empty();
139 let response = state.request(request).await;
140 response.assert_status(StatusCode::OK);
141 let body: serde_json::Value = response.json();
142
143 assert_eq!(
145 body["data"]["attributes"]["revoked_at"],
146 serde_json::json!(state.clock.now())
147 );
148 }
149
150 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
151 async fn test_revoke_already_revoked_token(pool: PgPool) {
152 setup();
153 let mut state = TestState::from_pool(pool).await.unwrap();
154 let token = state.token_with_scope("urn:mas:admin").await;
155
156 let mut repo = state.repository().await.unwrap();
157 let registration_token = repo
158 .user_registration_token()
159 .add(
160 &mut state.rng(),
161 &state.clock,
162 "test_token_789".to_owned(),
163 None,
164 None,
165 )
166 .await
167 .unwrap();
168
169 let registration_token = repo
171 .user_registration_token()
172 .revoke(&state.clock, registration_token)
173 .await
174 .unwrap();
175
176 repo.save().await.unwrap();
177
178 state.clock.advance(Duration::try_minutes(1).unwrap());
180
181 let request = Request::post(format!(
182 "/api/admin/v1/user-registration-tokens/{}/revoke",
183 registration_token.id
184 ))
185 .bearer(&token)
186 .empty();
187 let response = state.request(request).await;
188 response.assert_status(StatusCode::BAD_REQUEST);
189 let body: serde_json::Value = response.json();
190 assert_eq!(
191 body["errors"][0]["title"],
192 format!(
193 "Registration token with ID {} is already revoked",
194 registration_token.id
195 )
196 );
197 }
198
199 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
200 async fn test_revoke_unknown_token(pool: PgPool) {
201 setup();
202 let mut state = TestState::from_pool(pool).await.unwrap();
203 let token = state.token_with_scope("urn:mas:admin").await;
204
205 let request = Request::post(
206 "/api/admin/v1/user-registration-tokens/01040G2081040G2081040G2081/revoke",
207 )
208 .bearer(&token)
209 .empty();
210 let response = state.request(request).await;
211 response.assert_status(StatusCode::NOT_FOUND);
212 let body: serde_json::Value = response.json();
213 assert_eq!(
214 body["errors"][0]["title"],
215 "Registration token with ID 01040G2081040G2081040G2081 not found"
216 );
217 }
218}