mas_handlers/admin/v1/user_registration_tokens/
get.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::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
33impl_from_error_for_route!(mas_storage::RepositoryError);
34
35impl IntoResponse for RouteError {
36    fn into_response(self) -> axum::response::Response {
37        let error = ErrorResponse::from_error(&self);
38        let sentry_event_id = record_error!(self, Self::Internal(_));
39        let status = match self {
40            Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
41            Self::NotFound(_) => StatusCode::NOT_FOUND,
42        };
43        (status, sentry_event_id, Json(error)).into_response()
44    }
45}
46
47pub fn doc(operation: TransformOperation) -> TransformOperation {
48    operation
49        .id("getUserRegistrationToken")
50        .summary("Get a user registration token")
51        .tag("user-registration-token")
52        .response_with::<200, Json<SingleResponse<UserRegistrationToken>>, _>(|t| {
53            let [sample, ..] = UserRegistrationToken::samples();
54            let response = SingleResponse::new_canonical(sample);
55            t.description("Registration token was found")
56                .example(response)
57        })
58        .response_with::<404, RouteError, _>(|t| {
59            let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
60            t.description("Registration token was not found")
61                .example(response)
62        })
63}
64
65#[tracing::instrument(name = "handler.admin.v1.user_registration_tokens.get", skip_all)]
66pub async fn handler(
67    CallContext {
68        mut repo, clock, ..
69    }: CallContext,
70    id: UlidPathParam,
71) -> Result<Json<SingleResponse<UserRegistrationToken>>, RouteError> {
72    let token = repo
73        .user_registration_token()
74        .lookup(*id)
75        .await?
76        .ok_or(RouteError::NotFound(*id))?;
77
78    Ok(Json(SingleResponse::new_canonical(
79        UserRegistrationToken::new(token, clock.now()),
80    )))
81}
82
83#[cfg(test)]
84mod tests {
85    use hyper::{Request, StatusCode};
86    use insta::assert_json_snapshot;
87    use sqlx::PgPool;
88    use ulid::Ulid;
89
90    use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
91
92    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
93    async fn test_get_token(pool: PgPool) {
94        setup();
95        let mut state = TestState::from_pool(pool).await.unwrap();
96        let token = state.token_with_scope("urn:mas:admin").await;
97
98        let mut repo = state.repository().await.unwrap();
99        let registration_token = repo
100            .user_registration_token()
101            .add(
102                &mut state.rng(),
103                &state.clock,
104                "test_token_123".to_owned(),
105                Some(5),
106                None,
107            )
108            .await
109            .unwrap();
110        repo.save().await.unwrap();
111
112        let request = Request::get(format!(
113            "/api/admin/v1/user-registration-tokens/{}",
114            registration_token.id
115        ))
116        .bearer(&token)
117        .empty();
118        let response = state.request(request).await;
119        response.assert_status(StatusCode::OK);
120        let body: serde_json::Value = response.json();
121
122        assert_json_snapshot!(body, @r#"
123        {
124          "data": {
125            "type": "user-registration_token",
126            "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
127            "attributes": {
128              "token": "test_token_123",
129              "valid": true,
130              "usage_limit": 5,
131              "times_used": 0,
132              "created_at": "2022-01-16T14:40:00Z",
133              "last_used_at": null,
134              "expires_at": null,
135              "revoked_at": null
136            },
137            "links": {
138              "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
139            }
140          },
141          "links": {
142            "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
143          }
144        }
145        "#);
146    }
147
148    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
149    async fn test_get_nonexistent_token(pool: PgPool) {
150        setup();
151        let mut state = TestState::from_pool(pool).await.unwrap();
152        let token = state.token_with_scope("urn:mas:admin").await;
153
154        // Use a fixed ID for the test to ensure consistent snapshots
155        let nonexistent_id = Ulid::from_string("00000000000000000000000000").unwrap();
156        let request = Request::get(format!(
157            "/api/admin/v1/user-registration-tokens/{nonexistent_id}"
158        ))
159        .bearer(&token)
160        .empty();
161        let response = state.request(request).await;
162        response.assert_status(StatusCode::NOT_FOUND);
163        let body: serde_json::Value = response.json();
164
165        assert_json_snapshot!(body, @r###"
166        {
167          "errors": [
168            {
169              "title": "Registration token with ID 00000000000000000000000000 not found"
170            }
171          ]
172        }
173        "###);
174    }
175}