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