mas_handlers/admin/v1/user_registration_tokens/
list.rs

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