mas_handlers/admin/v1/user_registration_tokens/
list.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::{
9    Json,
10    extract::{Query, rejection::QueryRejection},
11    response::IntoResponse,
12};
13use axum_macros::FromRequestParts;
14use hyper::StatusCode;
15use mas_axum_utils::record_error;
16use mas_storage::{Page, user::UserRegistrationTokenFilter};
17use schemars::JsonSchema;
18use serde::Deserialize;
19
20use crate::{
21    admin::{
22        call_context::CallContext,
23        model::{Resource, UserRegistrationToken},
24        params::Pagination,
25        response::{ErrorResponse, PaginatedResponse},
26    },
27    impl_from_error_for_route,
28};
29
30#[derive(FromRequestParts, Deserialize, JsonSchema, OperationIo)]
31#[serde(rename = "RegistrationTokenFilter")]
32#[aide(input_with = "Query<FilterParams>")]
33#[from_request(via(Query), rejection(RouteError))]
34pub struct FilterParams {
35    /// Retrieve tokens that have (or have not) been used at least once
36    #[serde(rename = "filter[used]")]
37    used: Option<bool>,
38
39    /// Retrieve tokens that are (or are not) revoked
40    #[serde(rename = "filter[revoked]")]
41    revoked: Option<bool>,
42
43    /// Retrieve tokens that are (or are not) expired
44    #[serde(rename = "filter[expired]")]
45    expired: Option<bool>,
46
47    /// Retrieve tokens that are (or are not) valid
48    ///
49    /// Valid means that the token has not expired, is not revoked, and has not
50    /// reached its usage limit.
51    #[serde(rename = "filter[valid]")]
52    valid: Option<bool>,
53}
54
55impl std::fmt::Display for FilterParams {
56    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57        let mut sep = '?';
58
59        if let Some(used) = self.used {
60            write!(f, "{sep}filter[used]={used}")?;
61            sep = '&';
62        }
63        if let Some(revoked) = self.revoked {
64            write!(f, "{sep}filter[revoked]={revoked}")?;
65            sep = '&';
66        }
67        if let Some(expired) = self.expired {
68            write!(f, "{sep}filter[expired]={expired}")?;
69            sep = '&';
70        }
71        if let Some(valid) = self.valid {
72            write!(f, "{sep}filter[valid]={valid}")?;
73            sep = '&';
74        }
75
76        let _ = sep;
77        Ok(())
78    }
79}
80
81#[derive(Debug, thiserror::Error, OperationIo)]
82#[aide(output_with = "Json<ErrorResponse>")]
83pub enum RouteError {
84    #[error(transparent)]
85    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
86
87    #[error("Invalid filter parameters")]
88    InvalidFilter(#[from] QueryRejection),
89}
90
91impl_from_error_for_route!(mas_storage::RepositoryError);
92
93impl IntoResponse for RouteError {
94    fn into_response(self) -> axum::response::Response {
95        let error = ErrorResponse::from_error(&self);
96        let sentry_event_id = record_error!(self, Self::Internal(_));
97        let status = match self {
98            Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
99            Self::InvalidFilter(_) => StatusCode::BAD_REQUEST,
100        };
101
102        (status, sentry_event_id, Json(error)).into_response()
103    }
104}
105
106pub fn doc(operation: TransformOperation) -> TransformOperation {
107    operation
108        .id("listUserRegistrationTokens")
109        .summary("List user registration tokens")
110        .tag("user-registration-token")
111        .response_with::<200, Json<PaginatedResponse<UserRegistrationToken>>, _>(|t| {
112            let tokens = UserRegistrationToken::samples();
113            let pagination = mas_storage::Pagination::first(tokens.len());
114            let page = Page {
115                edges: tokens.into(),
116                has_next_page: true,
117                has_previous_page: false,
118            };
119
120            t.description("Paginated response of registration tokens")
121                .example(PaginatedResponse::new(
122                    page,
123                    pagination,
124                    42,
125                    UserRegistrationToken::PATH,
126                ))
127        })
128}
129
130#[tracing::instrument(name = "handler.admin.v1.registration_tokens.list", skip_all)]
131pub async fn handler(
132    CallContext {
133        mut repo, clock, ..
134    }: CallContext,
135    Pagination(pagination): Pagination,
136    params: FilterParams,
137) -> Result<Json<PaginatedResponse<UserRegistrationToken>>, RouteError> {
138    let base = format!("{path}{params}", path = UserRegistrationToken::PATH);
139    let now = clock.now();
140    let mut filter = UserRegistrationTokenFilter::new(now);
141
142    if let Some(used) = params.used {
143        filter = filter.with_been_used(used);
144    }
145
146    if let Some(revoked) = params.revoked {
147        filter = filter.with_revoked(revoked);
148    }
149
150    if let Some(expired) = params.expired {
151        filter = filter.with_expired(expired);
152    }
153
154    if let Some(valid) = params.valid {
155        filter = filter.with_valid(valid);
156    }
157
158    let page = repo
159        .user_registration_token()
160        .list(filter, pagination)
161        .await?;
162    let count = repo.user_registration_token().count(filter).await?;
163
164    Ok(Json(PaginatedResponse::new(
165        page.map(|token| UserRegistrationToken::new(token, now)),
166        pagination,
167        count,
168        &base,
169    )))
170}
171
172#[cfg(test)]
173mod tests {
174    use chrono::Duration;
175    use hyper::{Request, StatusCode};
176    use mas_storage::Clock as _;
177    use sqlx::PgPool;
178
179    use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
180
181    async fn create_test_tokens(state: &mut TestState) {
182        let mut repo = state.repository().await.unwrap();
183
184        // Token 1: Never used, not revoked
185        repo.user_registration_token()
186            .add(
187                &mut state.rng(),
188                &state.clock,
189                "token_unused".to_owned(),
190                Some(10),
191                None,
192            )
193            .await
194            .unwrap();
195
196        // Token 2: Used, not revoked
197        let token = repo
198            .user_registration_token()
199            .add(
200                &mut state.rng(),
201                &state.clock,
202                "token_used".to_owned(),
203                Some(10),
204                None,
205            )
206            .await
207            .unwrap();
208        repo.user_registration_token()
209            .use_token(&state.clock, token)
210            .await
211            .unwrap();
212
213        // Token 3: Never used, revoked
214        let token = repo
215            .user_registration_token()
216            .add(
217                &mut state.rng(),
218                &state.clock,
219                "token_revoked".to_owned(),
220                Some(10),
221                None,
222            )
223            .await
224            .unwrap();
225        repo.user_registration_token()
226            .revoke(&state.clock, token)
227            .await
228            .unwrap();
229
230        // Token 4: Used, revoked
231        let token = repo
232            .user_registration_token()
233            .add(
234                &mut state.rng(),
235                &state.clock,
236                "token_used_revoked".to_owned(),
237                Some(10),
238                None,
239            )
240            .await
241            .unwrap();
242        let token = repo
243            .user_registration_token()
244            .use_token(&state.clock, token)
245            .await
246            .unwrap();
247        repo.user_registration_token()
248            .revoke(&state.clock, token)
249            .await
250            .unwrap();
251
252        // Token 5: Expired token
253        let expires_at = state.clock.now() - Duration::try_days(1).unwrap();
254        repo.user_registration_token()
255            .add(
256                &mut state.rng(),
257                &state.clock,
258                "token_expired".to_owned(),
259                Some(5),
260                Some(expires_at),
261            )
262            .await
263            .unwrap();
264
265        repo.save().await.unwrap();
266    }
267
268    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
269    async fn test_list_all_tokens(pool: PgPool) {
270        setup();
271        let mut state = TestState::from_pool(pool).await.unwrap();
272        let admin_token = state.token_with_scope("urn:mas:admin").await;
273        create_test_tokens(&mut state).await;
274
275        let request = Request::get("/api/admin/v1/user-registration-tokens")
276            .bearer(&admin_token)
277            .empty();
278        let response = state.request(request).await;
279        response.assert_status(StatusCode::OK);
280
281        let body: serde_json::Value = response.json();
282        insta::assert_json_snapshot!(body, @r#"
283        {
284          "meta": {
285            "count": 5
286          },
287          "data": [
288            {
289              "type": "user-registration_token",
290              "id": "01FSHN9AG064K8BYZXSY5G511Z",
291              "attributes": {
292                "token": "token_expired",
293                "valid": false,
294                "usage_limit": 5,
295                "times_used": 0,
296                "created_at": "2022-01-16T14:40:00Z",
297                "last_used_at": null,
298                "expires_at": "2022-01-15T14:40:00Z",
299                "revoked_at": null
300              },
301              "links": {
302                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
303              }
304            },
305            {
306              "type": "user-registration_token",
307              "id": "01FSHN9AG07HNEZXNQM2KNBNF6",
308              "attributes": {
309                "token": "token_used",
310                "valid": true,
311                "usage_limit": 10,
312                "times_used": 1,
313                "created_at": "2022-01-16T14:40:00Z",
314                "last_used_at": "2022-01-16T14:40:00Z",
315                "expires_at": null,
316                "revoked_at": null
317              },
318              "links": {
319                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
320              }
321            },
322            {
323              "type": "user-registration_token",
324              "id": "01FSHN9AG09AVTNSQFMSR34AJC",
325              "attributes": {
326                "token": "token_revoked",
327                "valid": false,
328                "usage_limit": 10,
329                "times_used": 0,
330                "created_at": "2022-01-16T14:40:00Z",
331                "last_used_at": null,
332                "expires_at": null,
333                "revoked_at": "2022-01-16T14:40:00Z"
334              },
335              "links": {
336                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
337              }
338            },
339            {
340              "type": "user-registration_token",
341              "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
342              "attributes": {
343                "token": "token_unused",
344                "valid": true,
345                "usage_limit": 10,
346                "times_used": 0,
347                "created_at": "2022-01-16T14:40:00Z",
348                "last_used_at": null,
349                "expires_at": null,
350                "revoked_at": null
351              },
352              "links": {
353                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
354              }
355            },
356            {
357              "type": "user-registration_token",
358              "id": "01FSHN9AG0S3ZJD8CXQ7F11KXN",
359              "attributes": {
360                "token": "token_used_revoked",
361                "valid": false,
362                "usage_limit": 10,
363                "times_used": 1,
364                "created_at": "2022-01-16T14:40:00Z",
365                "last_used_at": "2022-01-16T14:40:00Z",
366                "expires_at": null,
367                "revoked_at": "2022-01-16T14:40:00Z"
368              },
369              "links": {
370                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
371              }
372            }
373          ],
374          "links": {
375            "self": "/api/admin/v1/user-registration-tokens?page[first]=10",
376            "first": "/api/admin/v1/user-registration-tokens?page[first]=10",
377            "last": "/api/admin/v1/user-registration-tokens?page[last]=10"
378          }
379        }
380        "#);
381    }
382
383    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
384    async fn test_filter_by_used(pool: PgPool) {
385        setup();
386        let mut state = TestState::from_pool(pool).await.unwrap();
387        let admin_token = state.token_with_scope("urn:mas:admin").await;
388        create_test_tokens(&mut state).await;
389
390        // Filter for used tokens
391        let request = Request::get("/api/admin/v1/user-registration-tokens?filter[used]=true")
392            .bearer(&admin_token)
393            .empty();
394        let response = state.request(request).await;
395        response.assert_status(StatusCode::OK);
396
397        let body: serde_json::Value = response.json();
398        insta::assert_json_snapshot!(body, @r#"
399        {
400          "meta": {
401            "count": 2
402          },
403          "data": [
404            {
405              "type": "user-registration_token",
406              "id": "01FSHN9AG07HNEZXNQM2KNBNF6",
407              "attributes": {
408                "token": "token_used",
409                "valid": true,
410                "usage_limit": 10,
411                "times_used": 1,
412                "created_at": "2022-01-16T14:40:00Z",
413                "last_used_at": "2022-01-16T14:40:00Z",
414                "expires_at": null,
415                "revoked_at": null
416              },
417              "links": {
418                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
419              }
420            },
421            {
422              "type": "user-registration_token",
423              "id": "01FSHN9AG0S3ZJD8CXQ7F11KXN",
424              "attributes": {
425                "token": "token_used_revoked",
426                "valid": false,
427                "usage_limit": 10,
428                "times_used": 1,
429                "created_at": "2022-01-16T14:40:00Z",
430                "last_used_at": "2022-01-16T14:40:00Z",
431                "expires_at": null,
432                "revoked_at": "2022-01-16T14:40:00Z"
433              },
434              "links": {
435                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
436              }
437            }
438          ],
439          "links": {
440            "self": "/api/admin/v1/user-registration-tokens?filter[used]=true&page[first]=10",
441            "first": "/api/admin/v1/user-registration-tokens?filter[used]=true&page[first]=10",
442            "last": "/api/admin/v1/user-registration-tokens?filter[used]=true&page[last]=10"
443          }
444        }
445        "#);
446
447        // Filter for unused tokens
448        let request = Request::get("/api/admin/v1/user-registration-tokens?filter[used]=false")
449            .bearer(&admin_token)
450            .empty();
451        let response = state.request(request).await;
452        response.assert_status(StatusCode::OK);
453
454        let body: serde_json::Value = response.json();
455        insta::assert_json_snapshot!(body, @r#"
456        {
457          "meta": {
458            "count": 3
459          },
460          "data": [
461            {
462              "type": "user-registration_token",
463              "id": "01FSHN9AG064K8BYZXSY5G511Z",
464              "attributes": {
465                "token": "token_expired",
466                "valid": false,
467                "usage_limit": 5,
468                "times_used": 0,
469                "created_at": "2022-01-16T14:40:00Z",
470                "last_used_at": null,
471                "expires_at": "2022-01-15T14:40:00Z",
472                "revoked_at": null
473              },
474              "links": {
475                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
476              }
477            },
478            {
479              "type": "user-registration_token",
480              "id": "01FSHN9AG09AVTNSQFMSR34AJC",
481              "attributes": {
482                "token": "token_revoked",
483                "valid": false,
484                "usage_limit": 10,
485                "times_used": 0,
486                "created_at": "2022-01-16T14:40:00Z",
487                "last_used_at": null,
488                "expires_at": null,
489                "revoked_at": "2022-01-16T14:40:00Z"
490              },
491              "links": {
492                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
493              }
494            },
495            {
496              "type": "user-registration_token",
497              "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
498              "attributes": {
499                "token": "token_unused",
500                "valid": true,
501                "usage_limit": 10,
502                "times_used": 0,
503                "created_at": "2022-01-16T14:40:00Z",
504                "last_used_at": null,
505                "expires_at": null,
506                "revoked_at": null
507              },
508              "links": {
509                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
510              }
511            }
512          ],
513          "links": {
514            "self": "/api/admin/v1/user-registration-tokens?filter[used]=false&page[first]=10",
515            "first": "/api/admin/v1/user-registration-tokens?filter[used]=false&page[first]=10",
516            "last": "/api/admin/v1/user-registration-tokens?filter[used]=false&page[last]=10"
517          }
518        }
519        "#);
520    }
521
522    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
523    async fn test_filter_by_revoked(pool: PgPool) {
524        setup();
525        let mut state = TestState::from_pool(pool).await.unwrap();
526        let admin_token = state.token_with_scope("urn:mas:admin").await;
527        create_test_tokens(&mut state).await;
528
529        // Filter for revoked tokens
530        let request = Request::get("/api/admin/v1/user-registration-tokens?filter[revoked]=true")
531            .bearer(&admin_token)
532            .empty();
533        let response = state.request(request).await;
534        response.assert_status(StatusCode::OK);
535
536        let body: serde_json::Value = response.json();
537        insta::assert_json_snapshot!(body, @r#"
538        {
539          "meta": {
540            "count": 2
541          },
542          "data": [
543            {
544              "type": "user-registration_token",
545              "id": "01FSHN9AG09AVTNSQFMSR34AJC",
546              "attributes": {
547                "token": "token_revoked",
548                "valid": false,
549                "usage_limit": 10,
550                "times_used": 0,
551                "created_at": "2022-01-16T14:40:00Z",
552                "last_used_at": null,
553                "expires_at": null,
554                "revoked_at": "2022-01-16T14:40:00Z"
555              },
556              "links": {
557                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
558              }
559            },
560            {
561              "type": "user-registration_token",
562              "id": "01FSHN9AG0S3ZJD8CXQ7F11KXN",
563              "attributes": {
564                "token": "token_used_revoked",
565                "valid": false,
566                "usage_limit": 10,
567                "times_used": 1,
568                "created_at": "2022-01-16T14:40:00Z",
569                "last_used_at": "2022-01-16T14:40:00Z",
570                "expires_at": null,
571                "revoked_at": "2022-01-16T14:40:00Z"
572              },
573              "links": {
574                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
575              }
576            }
577          ],
578          "links": {
579            "self": "/api/admin/v1/user-registration-tokens?filter[revoked]=true&page[first]=10",
580            "first": "/api/admin/v1/user-registration-tokens?filter[revoked]=true&page[first]=10",
581            "last": "/api/admin/v1/user-registration-tokens?filter[revoked]=true&page[last]=10"
582          }
583        }
584        "#);
585
586        // Filter for non-revoked tokens
587        let request = Request::get("/api/admin/v1/user-registration-tokens?filter[revoked]=false")
588            .bearer(&admin_token)
589            .empty();
590        let response = state.request(request).await;
591        response.assert_status(StatusCode::OK);
592
593        let body: serde_json::Value = response.json();
594        insta::assert_json_snapshot!(body, @r#"
595        {
596          "meta": {
597            "count": 3
598          },
599          "data": [
600            {
601              "type": "user-registration_token",
602              "id": "01FSHN9AG064K8BYZXSY5G511Z",
603              "attributes": {
604                "token": "token_expired",
605                "valid": false,
606                "usage_limit": 5,
607                "times_used": 0,
608                "created_at": "2022-01-16T14:40:00Z",
609                "last_used_at": null,
610                "expires_at": "2022-01-15T14:40:00Z",
611                "revoked_at": null
612              },
613              "links": {
614                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
615              }
616            },
617            {
618              "type": "user-registration_token",
619              "id": "01FSHN9AG07HNEZXNQM2KNBNF6",
620              "attributes": {
621                "token": "token_used",
622                "valid": true,
623                "usage_limit": 10,
624                "times_used": 1,
625                "created_at": "2022-01-16T14:40:00Z",
626                "last_used_at": "2022-01-16T14:40:00Z",
627                "expires_at": null,
628                "revoked_at": null
629              },
630              "links": {
631                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
632              }
633            },
634            {
635              "type": "user-registration_token",
636              "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
637              "attributes": {
638                "token": "token_unused",
639                "valid": true,
640                "usage_limit": 10,
641                "times_used": 0,
642                "created_at": "2022-01-16T14:40:00Z",
643                "last_used_at": null,
644                "expires_at": null,
645                "revoked_at": null
646              },
647              "links": {
648                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
649              }
650            }
651          ],
652          "links": {
653            "self": "/api/admin/v1/user-registration-tokens?filter[revoked]=false&page[first]=10",
654            "first": "/api/admin/v1/user-registration-tokens?filter[revoked]=false&page[first]=10",
655            "last": "/api/admin/v1/user-registration-tokens?filter[revoked]=false&page[last]=10"
656          }
657        }
658        "#);
659    }
660
661    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
662    async fn test_filter_by_expired(pool: PgPool) {
663        setup();
664        let mut state = TestState::from_pool(pool).await.unwrap();
665        let admin_token = state.token_with_scope("urn:mas:admin").await;
666        create_test_tokens(&mut state).await;
667
668        // Filter for expired tokens
669        let request = Request::get("/api/admin/v1/user-registration-tokens?filter[expired]=true")
670            .bearer(&admin_token)
671            .empty();
672        let response = state.request(request).await;
673        response.assert_status(StatusCode::OK);
674
675        let body: serde_json::Value = response.json();
676        insta::assert_json_snapshot!(body, @r#"
677        {
678          "meta": {
679            "count": 1
680          },
681          "data": [
682            {
683              "type": "user-registration_token",
684              "id": "01FSHN9AG064K8BYZXSY5G511Z",
685              "attributes": {
686                "token": "token_expired",
687                "valid": false,
688                "usage_limit": 5,
689                "times_used": 0,
690                "created_at": "2022-01-16T14:40:00Z",
691                "last_used_at": null,
692                "expires_at": "2022-01-15T14:40:00Z",
693                "revoked_at": null
694              },
695              "links": {
696                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
697              }
698            }
699          ],
700          "links": {
701            "self": "/api/admin/v1/user-registration-tokens?filter[expired]=true&page[first]=10",
702            "first": "/api/admin/v1/user-registration-tokens?filter[expired]=true&page[first]=10",
703            "last": "/api/admin/v1/user-registration-tokens?filter[expired]=true&page[last]=10"
704          }
705        }
706        "#);
707
708        // Filter for non-expired tokens
709        let request = Request::get("/api/admin/v1/user-registration-tokens?filter[expired]=false")
710            .bearer(&admin_token)
711            .empty();
712        let response = state.request(request).await;
713        response.assert_status(StatusCode::OK);
714
715        let body: serde_json::Value = response.json();
716        insta::assert_json_snapshot!(body, @r#"
717        {
718          "meta": {
719            "count": 4
720          },
721          "data": [
722            {
723              "type": "user-registration_token",
724              "id": "01FSHN9AG07HNEZXNQM2KNBNF6",
725              "attributes": {
726                "token": "token_used",
727                "valid": true,
728                "usage_limit": 10,
729                "times_used": 1,
730                "created_at": "2022-01-16T14:40:00Z",
731                "last_used_at": "2022-01-16T14:40:00Z",
732                "expires_at": null,
733                "revoked_at": null
734              },
735              "links": {
736                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
737              }
738            },
739            {
740              "type": "user-registration_token",
741              "id": "01FSHN9AG09AVTNSQFMSR34AJC",
742              "attributes": {
743                "token": "token_revoked",
744                "valid": false,
745                "usage_limit": 10,
746                "times_used": 0,
747                "created_at": "2022-01-16T14:40:00Z",
748                "last_used_at": null,
749                "expires_at": null,
750                "revoked_at": "2022-01-16T14:40:00Z"
751              },
752              "links": {
753                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
754              }
755            },
756            {
757              "type": "user-registration_token",
758              "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
759              "attributes": {
760                "token": "token_unused",
761                "valid": true,
762                "usage_limit": 10,
763                "times_used": 0,
764                "created_at": "2022-01-16T14:40:00Z",
765                "last_used_at": null,
766                "expires_at": null,
767                "revoked_at": null
768              },
769              "links": {
770                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
771              }
772            },
773            {
774              "type": "user-registration_token",
775              "id": "01FSHN9AG0S3ZJD8CXQ7F11KXN",
776              "attributes": {
777                "token": "token_used_revoked",
778                "valid": false,
779                "usage_limit": 10,
780                "times_used": 1,
781                "created_at": "2022-01-16T14:40:00Z",
782                "last_used_at": "2022-01-16T14:40:00Z",
783                "expires_at": null,
784                "revoked_at": "2022-01-16T14:40:00Z"
785              },
786              "links": {
787                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
788              }
789            }
790          ],
791          "links": {
792            "self": "/api/admin/v1/user-registration-tokens?filter[expired]=false&page[first]=10",
793            "first": "/api/admin/v1/user-registration-tokens?filter[expired]=false&page[first]=10",
794            "last": "/api/admin/v1/user-registration-tokens?filter[expired]=false&page[last]=10"
795          }
796        }
797        "#);
798    }
799
800    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
801    async fn test_filter_by_valid(pool: PgPool) {
802        setup();
803        let mut state = TestState::from_pool(pool).await.unwrap();
804        let admin_token = state.token_with_scope("urn:mas:admin").await;
805        create_test_tokens(&mut state).await;
806
807        // Filter for valid tokens
808        let request = Request::get("/api/admin/v1/user-registration-tokens?filter[valid]=true")
809            .bearer(&admin_token)
810            .empty();
811        let response = state.request(request).await;
812        response.assert_status(StatusCode::OK);
813
814        let body: serde_json::Value = response.json();
815        insta::assert_json_snapshot!(body, @r#"
816        {
817          "meta": {
818            "count": 2
819          },
820          "data": [
821            {
822              "type": "user-registration_token",
823              "id": "01FSHN9AG07HNEZXNQM2KNBNF6",
824              "attributes": {
825                "token": "token_used",
826                "valid": true,
827                "usage_limit": 10,
828                "times_used": 1,
829                "created_at": "2022-01-16T14:40:00Z",
830                "last_used_at": "2022-01-16T14:40:00Z",
831                "expires_at": null,
832                "revoked_at": null
833              },
834              "links": {
835                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
836              }
837            },
838            {
839              "type": "user-registration_token",
840              "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
841              "attributes": {
842                "token": "token_unused",
843                "valid": true,
844                "usage_limit": 10,
845                "times_used": 0,
846                "created_at": "2022-01-16T14:40:00Z",
847                "last_used_at": null,
848                "expires_at": null,
849                "revoked_at": null
850              },
851              "links": {
852                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
853              }
854            }
855          ],
856          "links": {
857            "self": "/api/admin/v1/user-registration-tokens?filter[valid]=true&page[first]=10",
858            "first": "/api/admin/v1/user-registration-tokens?filter[valid]=true&page[first]=10",
859            "last": "/api/admin/v1/user-registration-tokens?filter[valid]=true&page[last]=10"
860          }
861        }
862        "#);
863
864        // Filter for invalid tokens
865        let request = Request::get("/api/admin/v1/user-registration-tokens?filter[valid]=false")
866            .bearer(&admin_token)
867            .empty();
868        let response = state.request(request).await;
869        response.assert_status(StatusCode::OK);
870
871        let body: serde_json::Value = response.json();
872        insta::assert_json_snapshot!(body, @r#"
873        {
874          "meta": {
875            "count": 3
876          },
877          "data": [
878            {
879              "type": "user-registration_token",
880              "id": "01FSHN9AG064K8BYZXSY5G511Z",
881              "attributes": {
882                "token": "token_expired",
883                "valid": false,
884                "usage_limit": 5,
885                "times_used": 0,
886                "created_at": "2022-01-16T14:40:00Z",
887                "last_used_at": null,
888                "expires_at": "2022-01-15T14:40:00Z",
889                "revoked_at": null
890              },
891              "links": {
892                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
893              }
894            },
895            {
896              "type": "user-registration_token",
897              "id": "01FSHN9AG09AVTNSQFMSR34AJC",
898              "attributes": {
899                "token": "token_revoked",
900                "valid": false,
901                "usage_limit": 10,
902                "times_used": 0,
903                "created_at": "2022-01-16T14:40:00Z",
904                "last_used_at": null,
905                "expires_at": null,
906                "revoked_at": "2022-01-16T14:40:00Z"
907              },
908              "links": {
909                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
910              }
911            },
912            {
913              "type": "user-registration_token",
914              "id": "01FSHN9AG0S3ZJD8CXQ7F11KXN",
915              "attributes": {
916                "token": "token_used_revoked",
917                "valid": false,
918                "usage_limit": 10,
919                "times_used": 1,
920                "created_at": "2022-01-16T14:40:00Z",
921                "last_used_at": "2022-01-16T14:40:00Z",
922                "expires_at": null,
923                "revoked_at": "2022-01-16T14:40:00Z"
924              },
925              "links": {
926                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
927              }
928            }
929          ],
930          "links": {
931            "self": "/api/admin/v1/user-registration-tokens?filter[valid]=false&page[first]=10",
932            "first": "/api/admin/v1/user-registration-tokens?filter[valid]=false&page[first]=10",
933            "last": "/api/admin/v1/user-registration-tokens?filter[valid]=false&page[last]=10"
934          }
935        }
936        "#);
937    }
938
939    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
940    async fn test_combined_filters(pool: PgPool) {
941        setup();
942        let mut state = TestState::from_pool(pool).await.unwrap();
943        let admin_token = state.token_with_scope("urn:mas:admin").await;
944        create_test_tokens(&mut state).await;
945
946        // Filter for used AND revoked tokens
947        let request = Request::get(
948            "/api/admin/v1/user-registration-tokens?filter[used]=true&filter[revoked]=true",
949        )
950        .bearer(&admin_token)
951        .empty();
952        let response = state.request(request).await;
953        response.assert_status(StatusCode::OK);
954
955        let body: serde_json::Value = response.json();
956        insta::assert_json_snapshot!(body, @r#"
957        {
958          "meta": {
959            "count": 1
960          },
961          "data": [
962            {
963              "type": "user-registration_token",
964              "id": "01FSHN9AG0S3ZJD8CXQ7F11KXN",
965              "attributes": {
966                "token": "token_used_revoked",
967                "valid": false,
968                "usage_limit": 10,
969                "times_used": 1,
970                "created_at": "2022-01-16T14:40:00Z",
971                "last_used_at": "2022-01-16T14:40:00Z",
972                "expires_at": null,
973                "revoked_at": "2022-01-16T14:40:00Z"
974              },
975              "links": {
976                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
977              }
978            }
979          ],
980          "links": {
981            "self": "/api/admin/v1/user-registration-tokens?filter[used]=true&filter[revoked]=true&page[first]=10",
982            "first": "/api/admin/v1/user-registration-tokens?filter[used]=true&filter[revoked]=true&page[first]=10",
983            "last": "/api/admin/v1/user-registration-tokens?filter[used]=true&filter[revoked]=true&page[last]=10"
984          }
985        }
986        "#);
987    }
988
989    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
990    async fn test_pagination(pool: PgPool) {
991        setup();
992        let mut state = TestState::from_pool(pool).await.unwrap();
993        let admin_token = state.token_with_scope("urn:mas:admin").await;
994        create_test_tokens(&mut state).await;
995
996        // Request with pagination (2 per page)
997        let request = Request::get("/api/admin/v1/user-registration-tokens?page[first]=2")
998            .bearer(&admin_token)
999            .empty();
1000        let response = state.request(request).await;
1001        response.assert_status(StatusCode::OK);
1002
1003        let body: serde_json::Value = response.json();
1004        insta::assert_json_snapshot!(body, @r#"
1005        {
1006          "meta": {
1007            "count": 5
1008          },
1009          "data": [
1010            {
1011              "type": "user-registration_token",
1012              "id": "01FSHN9AG064K8BYZXSY5G511Z",
1013              "attributes": {
1014                "token": "token_expired",
1015                "valid": false,
1016                "usage_limit": 5,
1017                "times_used": 0,
1018                "created_at": "2022-01-16T14:40:00Z",
1019                "last_used_at": null,
1020                "expires_at": "2022-01-15T14:40:00Z",
1021                "revoked_at": null
1022              },
1023              "links": {
1024                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG064K8BYZXSY5G511Z"
1025              }
1026            },
1027            {
1028              "type": "user-registration_token",
1029              "id": "01FSHN9AG07HNEZXNQM2KNBNF6",
1030              "attributes": {
1031                "token": "token_used",
1032                "valid": true,
1033                "usage_limit": 10,
1034                "times_used": 1,
1035                "created_at": "2022-01-16T14:40:00Z",
1036                "last_used_at": "2022-01-16T14:40:00Z",
1037                "expires_at": null,
1038                "revoked_at": null
1039              },
1040              "links": {
1041                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG07HNEZXNQM2KNBNF6"
1042              }
1043            }
1044          ],
1045          "links": {
1046            "self": "/api/admin/v1/user-registration-tokens?page[first]=2",
1047            "first": "/api/admin/v1/user-registration-tokens?page[first]=2",
1048            "last": "/api/admin/v1/user-registration-tokens?page[last]=2",
1049            "next": "/api/admin/v1/user-registration-tokens?page[after]=01FSHN9AG07HNEZXNQM2KNBNF6&page[first]=2"
1050          }
1051        }
1052        "#);
1053
1054        // Request second page
1055        let request = Request::get("/api/admin/v1/user-registration-tokens?page[after]=01FSHN9AG07HNEZXNQM2KNBNF6&page[first]=2")
1056            .bearer(&admin_token)
1057            .empty();
1058        let response = state.request(request).await;
1059        response.assert_status(StatusCode::OK);
1060
1061        let body: serde_json::Value = response.json();
1062        insta::assert_json_snapshot!(body, @r#"
1063        {
1064          "meta": {
1065            "count": 5
1066          },
1067          "data": [
1068            {
1069              "type": "user-registration_token",
1070              "id": "01FSHN9AG09AVTNSQFMSR34AJC",
1071              "attributes": {
1072                "token": "token_revoked",
1073                "valid": false,
1074                "usage_limit": 10,
1075                "times_used": 0,
1076                "created_at": "2022-01-16T14:40:00Z",
1077                "last_used_at": null,
1078                "expires_at": null,
1079                "revoked_at": "2022-01-16T14:40:00Z"
1080              },
1081              "links": {
1082                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG09AVTNSQFMSR34AJC"
1083              }
1084            },
1085            {
1086              "type": "user-registration_token",
1087              "id": "01FSHN9AG0MZAA6S4AF7CTV32E",
1088              "attributes": {
1089                "token": "token_unused",
1090                "valid": true,
1091                "usage_limit": 10,
1092                "times_used": 0,
1093                "created_at": "2022-01-16T14:40:00Z",
1094                "last_used_at": null,
1095                "expires_at": null,
1096                "revoked_at": null
1097              },
1098              "links": {
1099                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0MZAA6S4AF7CTV32E"
1100              }
1101            }
1102          ],
1103          "links": {
1104            "self": "/api/admin/v1/user-registration-tokens?page[after]=01FSHN9AG07HNEZXNQM2KNBNF6&page[first]=2",
1105            "first": "/api/admin/v1/user-registration-tokens?page[first]=2",
1106            "last": "/api/admin/v1/user-registration-tokens?page[last]=2",
1107            "next": "/api/admin/v1/user-registration-tokens?page[after]=01FSHN9AG0MZAA6S4AF7CTV32E&page[first]=2"
1108          }
1109        }
1110        "#);
1111
1112        // Request last item
1113        let request = Request::get("/api/admin/v1/user-registration-tokens?page[last]=1")
1114            .bearer(&admin_token)
1115            .empty();
1116        let response = state.request(request).await;
1117        response.assert_status(StatusCode::OK);
1118
1119        let body: serde_json::Value = response.json();
1120        insta::assert_json_snapshot!(body, @r#"
1121        {
1122          "meta": {
1123            "count": 5
1124          },
1125          "data": [
1126            {
1127              "type": "user-registration_token",
1128              "id": "01FSHN9AG0S3ZJD8CXQ7F11KXN",
1129              "attributes": {
1130                "token": "token_used_revoked",
1131                "valid": false,
1132                "usage_limit": 10,
1133                "times_used": 1,
1134                "created_at": "2022-01-16T14:40:00Z",
1135                "last_used_at": "2022-01-16T14:40:00Z",
1136                "expires_at": null,
1137                "revoked_at": "2022-01-16T14:40:00Z"
1138              },
1139              "links": {
1140                "self": "/api/admin/v1/user-registration-tokens/01FSHN9AG0S3ZJD8CXQ7F11KXN"
1141              }
1142            }
1143          ],
1144          "links": {
1145            "self": "/api/admin/v1/user-registration-tokens?page[last]=1",
1146            "first": "/api/admin/v1/user-registration-tokens?page[first]=1",
1147            "last": "/api/admin/v1/user-registration-tokens?page[last]=1",
1148            "prev": "/api/admin/v1/user-registration-tokens?page[before]=01FSHN9AG0S3ZJD8CXQ7F11KXN&page[last]=1"
1149          }
1150        }
1151        "#);
1152    }
1153
1154    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
1155    async fn test_invalid_filter(pool: PgPool) {
1156        setup();
1157        let mut state = TestState::from_pool(pool).await.unwrap();
1158        let admin_token = state.token_with_scope("urn:mas:admin").await;
1159
1160        // Try with invalid filter value
1161        let request = Request::get("/api/admin/v1/user-registration-tokens?filter[used]=invalid")
1162            .bearer(&admin_token)
1163            .empty();
1164        let response = state.request(request).await;
1165        response.assert_status(StatusCode::BAD_REQUEST);
1166
1167        let body: serde_json::Value = response.json();
1168        assert!(
1169            body["errors"][0]["title"]
1170                .as_str()
1171                .unwrap()
1172                .contains("Invalid filter parameters")
1173        );
1174    }
1175}