mas_handlers/admin/v1/user_registration_tokens/
unrevoke.rs

1// Copyright 2025 New Vector Ltd.
2// Copyright 2025 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
5// Please see LICENSE files in the repository root for full details.
6
7use 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            // Get the valid token sample
59            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    // Check if the token is not revoked
89    if token.revoked_at.is_none() {
90        return Err(RouteError::NotRevoked(id));
91    }
92
93    // Unrevoke the token using the repository method
94    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        // Create a token
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
132        // Revoke it
133        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        // Now unrevoke it
142        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        // The revoked_at timestamp should be null
153        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        // Try to unrevoke a token that's not revoked
201        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}