Skip to main content

mas_router/
endpoints.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
4//
5// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
6// Please see LICENSE files in the repository root for full details.
7
8use serde::{Deserialize, Serialize};
9use ulid::Ulid;
10
11use crate::UrlBuilder;
12pub use crate::traits::*;
13
14#[derive(Deserialize, Serialize, Clone, Debug)]
15#[serde(rename_all = "snake_case", tag = "kind")]
16pub enum PostAuthAction {
17    ContinueAuthorizationGrant {
18        id: Ulid,
19    },
20    ContinueDeviceCodeGrant {
21        id: Ulid,
22    },
23    ContinueCompatSsoLogin {
24        id: Ulid,
25    },
26    ChangePassword,
27    LinkUpstream {
28        id: Ulid,
29    },
30    ManageAccount {
31        #[serde(flatten)]
32        action: Option<AccountAction>,
33    },
34    ContinueRoomInvite {
35        id: Ulid,
36    },
37}
38
39impl PostAuthAction {
40    #[must_use]
41    pub const fn continue_grant(id: Ulid) -> Self {
42        PostAuthAction::ContinueAuthorizationGrant { id }
43    }
44
45    #[must_use]
46    pub const fn continue_device_code_grant(id: Ulid) -> Self {
47        PostAuthAction::ContinueDeviceCodeGrant { id }
48    }
49
50    #[must_use]
51    pub const fn continue_compat_sso_login(id: Ulid) -> Self {
52        PostAuthAction::ContinueCompatSsoLogin { id }
53    }
54
55    #[must_use]
56    pub const fn link_upstream(id: Ulid) -> Self {
57        PostAuthAction::LinkUpstream { id }
58    }
59
60    #[must_use]
61    pub const fn manage_account(action: Option<AccountAction>) -> Self {
62        PostAuthAction::ManageAccount { action }
63    }
64
65    pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
66        match self {
67            Self::ContinueAuthorizationGrant { id } => url_builder.redirect(&Consent(*id)),
68            Self::ContinueDeviceCodeGrant { id } => {
69                url_builder.redirect(&DeviceCodeConsent::new(*id))
70            }
71            Self::ContinueCompatSsoLogin { id } => {
72                url_builder.redirect(&CompatLoginSsoComplete::new(*id, None))
73            }
74            Self::ChangePassword => url_builder.redirect(&AccountPasswordChange),
75            Self::LinkUpstream { id } => url_builder.redirect(&UpstreamOAuth2Link::new(*id)),
76            Self::ManageAccount { action } => url_builder.redirect(&Account {
77                action: action.clone(),
78            }),
79            Self::ContinueRoomInvite { id } => url_builder.redirect(&OpenInviteRoom(*id)),
80        }
81    }
82}
83
84/// `GET /.well-known/openid-configuration`
85#[derive(Default, Debug, Clone)]
86pub struct OidcConfiguration;
87
88impl SimpleRoute for OidcConfiguration {
89    const PATH: &'static str = "/.well-known/openid-configuration";
90}
91
92/// `GET /.well-known/webfinger`
93#[derive(Default, Debug, Clone)]
94pub struct Webfinger;
95
96impl SimpleRoute for Webfinger {
97    const PATH: &'static str = "/.well-known/webfinger";
98}
99
100/// `GET /.well-known/change-password`
101pub struct ChangePasswordDiscovery;
102
103impl SimpleRoute for ChangePasswordDiscovery {
104    const PATH: &'static str = "/.well-known/change-password";
105}
106
107/// `GET /oauth2/keys.json`
108#[derive(Default, Debug, Clone)]
109pub struct OAuth2Keys;
110
111impl SimpleRoute for OAuth2Keys {
112    const PATH: &'static str = "/oauth2/keys.json";
113}
114
115/// `GET /oauth2/userinfo`
116#[derive(Default, Debug, Clone)]
117pub struct OidcUserinfo;
118
119impl SimpleRoute for OidcUserinfo {
120    const PATH: &'static str = "/oauth2/userinfo";
121}
122
123/// `POST /oauth2/introspect`
124#[derive(Default, Debug, Clone)]
125pub struct OAuth2Introspection;
126
127impl SimpleRoute for OAuth2Introspection {
128    const PATH: &'static str = "/oauth2/introspect";
129}
130
131/// `POST /oauth2/revoke`
132#[derive(Default, Debug, Clone)]
133pub struct OAuth2Revocation;
134
135impl SimpleRoute for OAuth2Revocation {
136    const PATH: &'static str = "/oauth2/revoke";
137}
138
139/// `POST /oauth2/token`
140#[derive(Default, Debug, Clone)]
141pub struct OAuth2TokenEndpoint;
142
143impl SimpleRoute for OAuth2TokenEndpoint {
144    const PATH: &'static str = "/oauth2/token";
145}
146
147/// `POST /oauth2/registration`
148#[derive(Default, Debug, Clone)]
149pub struct OAuth2RegistrationEndpoint;
150
151impl SimpleRoute for OAuth2RegistrationEndpoint {
152    const PATH: &'static str = "/oauth2/registration";
153}
154
155/// `GET /authorize`
156#[derive(Default, Debug, Clone)]
157pub struct OAuth2AuthorizationEndpoint;
158
159impl SimpleRoute for OAuth2AuthorizationEndpoint {
160    const PATH: &'static str = "/authorize";
161}
162
163/// `GET /`
164#[derive(Default, Debug, Clone)]
165pub struct Index;
166
167impl SimpleRoute for Index {
168    const PATH: &'static str = "/";
169}
170
171/// `GET /health`
172#[derive(Default, Debug, Clone)]
173pub struct Healthcheck;
174
175impl SimpleRoute for Healthcheck {
176    const PATH: &'static str = "/health";
177}
178
179/// `GET|POST /login`
180#[derive(Default, Debug, Clone, Serialize, Deserialize)]
181pub struct Login {
182    #[serde(flatten)]
183    post_auth_action: Option<PostAuthAction>,
184
185    login_hint: Option<String>,
186}
187
188impl Route for Login {
189    type Query = Self;
190
191    fn route() -> &'static str {
192        "/login"
193    }
194
195    fn query(&self) -> Option<&Self::Query> {
196        Some(self)
197    }
198}
199
200impl Login {
201    #[must_use]
202    pub const fn and_then(action: PostAuthAction) -> Self {
203        Self {
204            post_auth_action: Some(action),
205            login_hint: None,
206        }
207    }
208
209    #[must_use]
210    pub const fn and_continue_grant(id: Ulid) -> Self {
211        Self {
212            post_auth_action: Some(PostAuthAction::continue_grant(id)),
213            login_hint: None,
214        }
215    }
216
217    #[must_use]
218    pub const fn and_continue_device_code_grant(id: Ulid) -> Self {
219        Self {
220            post_auth_action: Some(PostAuthAction::continue_device_code_grant(id)),
221            login_hint: None,
222        }
223    }
224
225    #[must_use]
226    pub const fn and_continue_compat_sso_login(id: Ulid) -> Self {
227        Self {
228            post_auth_action: Some(PostAuthAction::continue_compat_sso_login(id)),
229            login_hint: None,
230        }
231    }
232
233    #[must_use]
234    pub const fn and_link_upstream(id: Ulid) -> Self {
235        Self {
236            post_auth_action: Some(PostAuthAction::link_upstream(id)),
237            login_hint: None,
238        }
239    }
240
241    #[must_use]
242    pub fn with_login_hint(mut self, login_hint: String) -> Self {
243        self.login_hint = Some(login_hint);
244        self
245    }
246
247    /// Get a reference to the login's post auth action.
248    #[must_use]
249    pub fn post_auth_action(&self) -> Option<&PostAuthAction> {
250        self.post_auth_action.as_ref()
251    }
252
253    pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
254        match &self.post_auth_action {
255            Some(action) => action.go_next(url_builder),
256            None => url_builder.redirect(&Index),
257        }
258    }
259}
260
261impl From<Option<PostAuthAction>> for Login {
262    fn from(post_auth_action: Option<PostAuthAction>) -> Self {
263        Self {
264            post_auth_action,
265            login_hint: None,
266        }
267    }
268}
269
270/// `POST /logout`
271#[derive(Default, Debug, Clone)]
272pub struct Logout;
273
274impl SimpleRoute for Logout {
275    const PATH: &'static str = "/logout";
276}
277
278/// `GET|POST /register`
279#[derive(Default, Debug, Clone)]
280pub struct Register {
281    query: RegisterQuery,
282}
283
284/// The query parameters of the registration page
285#[derive(Default, Debug, Clone, Serialize)]
286pub struct RegisterQuery {
287    /// The registration token an invite link deep-links with
288    #[serde(skip_serializing_if = "Option::is_none")]
289    token: Option<String>,
290
291    #[serde(flatten)]
292    post_auth_action: Option<PostAuthAction>,
293}
294
295impl Register {
296    #[must_use]
297    pub fn and_then(action: PostAuthAction) -> Self {
298        Self {
299            query: RegisterQuery {
300                token: None,
301                post_auth_action: Some(action),
302            },
303        }
304    }
305
306    #[must_use]
307    pub fn and_continue_grant(data: Ulid) -> Self {
308        Self::and_then(PostAuthAction::continue_grant(data))
309    }
310
311    #[must_use]
312    pub fn and_continue_compat_sso_login(data: Ulid) -> Self {
313        Self::and_then(PostAuthAction::continue_compat_sso_login(data))
314    }
315
316    /// Deep-link to the registration page with a registration token
317    #[must_use]
318    pub fn with_token(mut self, token: String) -> Self {
319        self.query.token = Some(token);
320        self
321    }
322
323    /// Get a reference to the reauth's post auth action.
324    #[must_use]
325    pub fn post_auth_action(&self) -> Option<&PostAuthAction> {
326        self.query.post_auth_action.as_ref()
327    }
328
329    pub fn go_next(&self, url_builder: &UrlBuilder) -> axum::response::Redirect {
330        match &self.query.post_auth_action {
331            Some(action) => action.go_next(url_builder),
332            None => url_builder.redirect(&Index),
333        }
334    }
335}
336
337impl Route for Register {
338    type Query = RegisterQuery;
339
340    fn route() -> &'static str {
341        "/register"
342    }
343
344    fn query(&self) -> Option<&Self::Query> {
345        Some(&self.query)
346    }
347}
348
349impl From<Option<PostAuthAction>> for Register {
350    fn from(post_auth_action: Option<PostAuthAction>) -> Self {
351        Self {
352            query: RegisterQuery {
353                token: None,
354                post_auth_action,
355            },
356        }
357    }
358}
359
360/// `GET /open-room/{id}`
361#[derive(Debug, Clone)]
362pub struct OpenInviteRoom(pub Ulid);
363
364impl Route for OpenInviteRoom {
365    type Query = ();
366
367    fn route() -> &'static str {
368        "/open-room/{id}"
369    }
370
371    fn path(&self) -> std::borrow::Cow<'static, str> {
372        format!("/open-room/{}", self.0).into()
373    }
374}
375
376/// `GET|POST /register/steps/{id}/token`
377#[derive(Debug, Clone)]
378pub struct RegisterToken {
379    id: Ulid,
380}
381
382impl RegisterToken {
383    #[must_use]
384    pub fn new(id: Ulid) -> Self {
385        Self { id }
386    }
387}
388
389impl Route for RegisterToken {
390    type Query = ();
391    fn route() -> &'static str {
392        "/register/steps/{id}/token"
393    }
394
395    fn path(&self) -> std::borrow::Cow<'static, str> {
396        format!("/register/steps/{}/token", self.id).into()
397    }
398}
399
400/// `GET|POST /register/steps/{id}/display-name`
401#[derive(Debug, Clone)]
402pub struct RegisterDisplayName {
403    id: Ulid,
404}
405
406impl RegisterDisplayName {
407    #[must_use]
408    pub fn new(id: Ulid) -> Self {
409        Self { id }
410    }
411}
412
413impl Route for RegisterDisplayName {
414    type Query = ();
415    fn route() -> &'static str {
416        "/register/steps/{id}/display-name"
417    }
418
419    fn path(&self) -> std::borrow::Cow<'static, str> {
420        format!("/register/steps/{}/display-name", self.id).into()
421    }
422}
423
424/// `GET|POST /register/steps/{id}/verify-email`
425#[derive(Debug, Clone)]
426pub struct RegisterVerifyEmail {
427    id: Ulid,
428}
429
430impl RegisterVerifyEmail {
431    #[must_use]
432    pub fn new(id: Ulid) -> Self {
433        Self { id }
434    }
435}
436
437impl Route for RegisterVerifyEmail {
438    type Query = ();
439    fn route() -> &'static str {
440        "/register/steps/{id}/verify-email"
441    }
442
443    fn path(&self) -> std::borrow::Cow<'static, str> {
444        format!("/register/steps/{}/verify-email", self.id).into()
445    }
446}
447
448/// `GET /register/steps/{id}/finish`
449#[derive(Debug, Clone)]
450pub struct RegisterFinish {
451    id: Ulid,
452}
453
454impl RegisterFinish {
455    #[must_use]
456    pub const fn new(id: Ulid) -> Self {
457        Self { id }
458    }
459}
460
461impl Route for RegisterFinish {
462    type Query = ();
463    fn route() -> &'static str {
464        "/register/steps/{id}/finish"
465    }
466
467    fn path(&self) -> std::borrow::Cow<'static, str> {
468        format!("/register/steps/{}/finish", self.id).into()
469    }
470}
471
472/// Actions parameters as defined by MSC4191
473#[derive(Debug, Clone, Serialize, Deserialize)]
474#[serde(tag = "action")]
475pub enum AccountAction {
476    #[serde(rename = "org.matrix.profile")]
477    OrgMatrixProfile,
478    /// DEPRECATED: Use `OrgMatrixProfile` instead
479    #[serde(rename = "profile")]
480    Profile,
481
482    #[serde(rename = "org.matrix.devices_list")]
483    OrgMatrixDevicesList,
484    /// DEPRECATED: Use `OrgMatrixDevicesList` instead
485    #[serde(rename = "org.matrix.sessions_list")]
486    OrgMatrixSessionsList,
487    /// DEPRECATED: Use `OrgMatrixDevicesList` instead
488    #[serde(rename = "sessions_list")]
489    SessionsList,
490
491    #[serde(rename = "org.matrix.device_view")]
492    OrgMatrixDeviceView { device_id: String },
493    /// DEPRECATED: Use `OrgMatrixDeviceView` instead
494    #[serde(rename = "org.matrix.session_view")]
495    OrgMatrixSessionView { device_id: String },
496    /// DEPRECATED: Use `OrgMatrixDeviceView` instead
497    #[serde(rename = "session_view")]
498    SessionView { device_id: String },
499
500    #[serde(rename = "org.matrix.device_delete")]
501    OrgMatrixDeviceDelete { device_id: String },
502    /// DEPRECATED: Use `OrgMatrixDeviceDelete` instead
503    #[serde(rename = "org.matrix.session_end")]
504    OrgMatrixSessionEnd { device_id: String },
505    /// DEPRECATED: Use `OrgMatrixDeviceDelete` instead
506    #[serde(rename = "session_end")]
507    SessionEnd { device_id: String },
508
509    #[serde(rename = "org.matrix.cross_signing_reset")]
510    OrgMatrixCrossSigningReset,
511}
512
513/// `GET /account/`
514#[derive(Default, Debug, Clone)]
515pub struct Account {
516    action: Option<AccountAction>,
517}
518
519impl Route for Account {
520    type Query = AccountAction;
521
522    fn route() -> &'static str {
523        "/account/"
524    }
525
526    fn query(&self) -> Option<&Self::Query> {
527        self.action.as_ref()
528    }
529}
530
531/// `GET /account/*`
532#[derive(Default, Debug, Clone)]
533pub struct AccountWildcard;
534
535impl SimpleRoute for AccountWildcard {
536    const PATH: &'static str = "/account/{*rest}";
537}
538
539/// `GET /account/password/change`
540///
541/// Handled by the React frontend; this struct definition is purely for
542/// redirects.
543#[derive(Default, Debug, Clone)]
544pub struct AccountPasswordChange;
545
546impl SimpleRoute for AccountPasswordChange {
547    const PATH: &'static str = "/account/password/change";
548}
549
550/// `GET /consent/{grant_id}`
551#[derive(Debug, Clone)]
552pub struct Consent(pub Ulid);
553
554impl Route for Consent {
555    type Query = ();
556    fn route() -> &'static str {
557        "/consent/{grant_id}"
558    }
559
560    fn path(&self) -> std::borrow::Cow<'static, str> {
561        format!("/consent/{}", self.0).into()
562    }
563}
564
565/// `GET|POST /_matrix/client/v3/login`
566pub struct CompatLogin;
567
568impl SimpleRoute for CompatLogin {
569    const PATH: &'static str = "/_matrix/client/{version}/login";
570}
571
572/// `POST /_matrix/client/v3/logout`
573pub struct CompatLogout;
574
575impl SimpleRoute for CompatLogout {
576    const PATH: &'static str = "/_matrix/client/{version}/logout";
577}
578
579/// `POST /_matrix/client/v3/logout/all`
580pub struct CompatLogoutAll;
581
582impl SimpleRoute for CompatLogoutAll {
583    const PATH: &'static str = "/_matrix/client/{version}/logout/all";
584}
585
586/// `POST /_matrix/client/v3/refresh`
587pub struct CompatRefresh;
588
589impl SimpleRoute for CompatRefresh {
590    const PATH: &'static str = "/_matrix/client/{version}/refresh";
591}
592
593/// `GET /_matrix/client/v3/login/sso/redirect`
594pub struct CompatLoginSsoRedirect;
595
596impl SimpleRoute for CompatLoginSsoRedirect {
597    const PATH: &'static str = "/_matrix/client/{version}/login/sso/redirect";
598}
599
600/// `GET /_matrix/client/v3/login/sso/redirect/`
601///
602/// This is a workaround for the fact some clients (Element iOS) sends a
603/// trailing slash, even though it's not in the spec.
604pub struct CompatLoginSsoRedirectSlash;
605
606impl SimpleRoute for CompatLoginSsoRedirectSlash {
607    const PATH: &'static str = "/_matrix/client/{version}/login/sso/redirect/";
608}
609
610/// `GET /_matrix/client/v3/login/sso/redirect/{idp}`
611pub struct CompatLoginSsoRedirectIdp;
612
613impl SimpleRoute for CompatLoginSsoRedirectIdp {
614    const PATH: &'static str = "/_matrix/client/{version}/login/sso/redirect/{idp}";
615}
616
617#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
618#[serde(rename_all = "lowercase")]
619pub enum CompatLoginSsoAction {
620    Login,
621    Register,
622    #[serde(other)]
623    Unknown,
624}
625
626impl CompatLoginSsoAction {
627    /// Returns true if the action is a known action.
628    #[must_use]
629    pub fn is_known(&self) -> bool {
630        !matches!(self, Self::Unknown)
631    }
632}
633
634#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
635pub struct CompatLoginSsoActionParams {
636    action: CompatLoginSsoAction,
637    /// DEPRECATED: Use `action` instead. We will remove this once enough
638    /// clients support the stable name.
639    #[serde(rename = "org.matrix.msc3824.action")]
640    unstable_action: CompatLoginSsoAction,
641}
642
643/// `GET|POST /complete-compat-sso/{id}`
644pub struct CompatLoginSsoComplete {
645    id: Ulid,
646    query: Option<CompatLoginSsoActionParams>,
647}
648
649impl CompatLoginSsoComplete {
650    #[must_use]
651    pub fn new(id: Ulid, action: Option<CompatLoginSsoAction>) -> Self {
652        Self {
653            id,
654            query: action.map(|action| CompatLoginSsoActionParams {
655                action,
656                unstable_action: action,
657            }),
658        }
659    }
660}
661
662impl Route for CompatLoginSsoComplete {
663    type Query = CompatLoginSsoActionParams;
664
665    fn query(&self) -> Option<&Self::Query> {
666        self.query.as_ref()
667    }
668
669    fn route() -> &'static str {
670        "/complete-compat-sso/{grant_id}"
671    }
672
673    fn path(&self) -> std::borrow::Cow<'static, str> {
674        format!("/complete-compat-sso/{}", self.id).into()
675    }
676}
677
678/// `GET /upstream/authorize/{id}`
679pub struct UpstreamOAuth2Authorize {
680    id: Ulid,
681    post_auth_action: Option<PostAuthAction>,
682}
683
684impl UpstreamOAuth2Authorize {
685    #[must_use]
686    pub const fn new(id: Ulid) -> Self {
687        Self {
688            id,
689            post_auth_action: None,
690        }
691    }
692
693    #[must_use]
694    pub fn and_then(mut self, action: PostAuthAction) -> Self {
695        self.post_auth_action = Some(action);
696        self
697    }
698}
699
700impl Route for UpstreamOAuth2Authorize {
701    type Query = PostAuthAction;
702    fn route() -> &'static str {
703        "/upstream/authorize/{provider_id}"
704    }
705
706    fn path(&self) -> std::borrow::Cow<'static, str> {
707        format!("/upstream/authorize/{}", self.id).into()
708    }
709
710    fn query(&self) -> Option<&Self::Query> {
711        self.post_auth_action.as_ref()
712    }
713}
714
715/// `GET /upstream/callback/{id}`
716pub struct UpstreamOAuth2Callback {
717    id: Ulid,
718}
719
720impl UpstreamOAuth2Callback {
721    #[must_use]
722    pub const fn new(id: Ulid) -> Self {
723        Self { id }
724    }
725}
726
727impl Route for UpstreamOAuth2Callback {
728    type Query = ();
729    fn route() -> &'static str {
730        "/upstream/callback/{provider_id}"
731    }
732
733    fn path(&self) -> std::borrow::Cow<'static, str> {
734        format!("/upstream/callback/{}", self.id).into()
735    }
736}
737
738/// `GET /upstream/link/{id}`
739pub struct UpstreamOAuth2Link {
740    id: Ulid,
741}
742
743impl UpstreamOAuth2Link {
744    #[must_use]
745    pub const fn new(id: Ulid) -> Self {
746        Self { id }
747    }
748}
749
750impl Route for UpstreamOAuth2Link {
751    type Query = ();
752    fn route() -> &'static str {
753        "/upstream/link/{link_id}"
754    }
755
756    fn path(&self) -> std::borrow::Cow<'static, str> {
757        format!("/upstream/link/{}", self.id).into()
758    }
759}
760
761/// `POST /upstream/backchannel-logout/{id}`
762pub struct UpstreamOAuth2BackchannelLogout {
763    id: Ulid,
764}
765
766impl UpstreamOAuth2BackchannelLogout {
767    #[must_use]
768    pub const fn new(id: Ulid) -> Self {
769        Self { id }
770    }
771}
772
773impl Route for UpstreamOAuth2BackchannelLogout {
774    type Query = ();
775    fn route() -> &'static str {
776        "/upstream/backchannel-logout/{provider_id}"
777    }
778
779    fn path(&self) -> std::borrow::Cow<'static, str> {
780        format!("/upstream/backchannel-logout/{}", self.id).into()
781    }
782}
783
784/// `GET|POST /link`
785#[derive(Default, Serialize, Deserialize, Debug, Clone)]
786pub struct DeviceCodeLink {
787    code: Option<String>,
788}
789
790impl DeviceCodeLink {
791    #[must_use]
792    pub fn with_code(code: String) -> Self {
793        Self { code: Some(code) }
794    }
795}
796
797impl Route for DeviceCodeLink {
798    type Query = DeviceCodeLink;
799    fn route() -> &'static str {
800        "/link"
801    }
802
803    fn query(&self) -> Option<&Self::Query> {
804        Some(self)
805    }
806}
807
808/// `GET|POST /device/{device_code_id}`
809#[derive(Default, Serialize, Deserialize, Debug, Clone)]
810pub struct DeviceCodeConsent {
811    id: Ulid,
812}
813
814impl Route for DeviceCodeConsent {
815    type Query = ();
816    fn route() -> &'static str {
817        "/device/{device_code_id}"
818    }
819
820    fn path(&self) -> std::borrow::Cow<'static, str> {
821        format!("/device/{}", self.id).into()
822    }
823}
824
825impl DeviceCodeConsent {
826    #[must_use]
827    pub fn new(id: Ulid) -> Self {
828        Self { id }
829    }
830}
831
832/// `POST /oauth2/device`
833#[derive(Default, Serialize, Deserialize, Debug, Clone)]
834pub struct OAuth2DeviceAuthorizationEndpoint;
835
836impl SimpleRoute for OAuth2DeviceAuthorizationEndpoint {
837    const PATH: &'static str = "/oauth2/device";
838}
839
840/// `GET|POST /recover`
841#[derive(Default, Serialize, Deserialize, Debug, Clone)]
842pub struct AccountRecoveryStart;
843
844impl SimpleRoute for AccountRecoveryStart {
845    const PATH: &'static str = "/recover";
846}
847
848/// `GET|POST /recover/progress/{session_id}`
849#[derive(Default, Serialize, Deserialize, Debug, Clone)]
850pub struct AccountRecoveryProgress {
851    session_id: Ulid,
852}
853
854impl AccountRecoveryProgress {
855    #[must_use]
856    pub fn new(session_id: Ulid) -> Self {
857        Self { session_id }
858    }
859}
860
861impl Route for AccountRecoveryProgress {
862    type Query = ();
863    fn route() -> &'static str {
864        "/recover/progress/{session_id}"
865    }
866
867    fn path(&self) -> std::borrow::Cow<'static, str> {
868        format!("/recover/progress/{}", self.session_id).into()
869    }
870}
871
872/// `GET /account/password/recovery?ticket=:ticket`
873/// Rendered by the React frontend
874#[derive(Default, Serialize, Deserialize, Debug, Clone)]
875pub struct AccountRecoveryFinish {
876    ticket: String,
877}
878
879impl AccountRecoveryFinish {
880    #[must_use]
881    pub fn new(ticket: String) -> Self {
882        Self { ticket }
883    }
884}
885
886impl Route for AccountRecoveryFinish {
887    type Query = AccountRecoveryFinish;
888
889    fn route() -> &'static str {
890        "/account/password/recovery"
891    }
892
893    fn query(&self) -> Option<&Self::Query> {
894        Some(self)
895    }
896}
897
898/// `GET /assets`
899pub struct StaticAsset {
900    path: String,
901}
902
903impl StaticAsset {
904    #[must_use]
905    pub fn new(path: String) -> Self {
906        Self { path }
907    }
908}
909
910impl Route for StaticAsset {
911    type Query = ();
912    fn route() -> &'static str {
913        "/assets/"
914    }
915
916    fn path(&self) -> std::borrow::Cow<'static, str> {
917        format!("/assets/{}", self.path).into()
918    }
919}
920
921/// `GET|POST /graphql`
922pub struct GraphQL;
923
924impl SimpleRoute for GraphQL {
925    const PATH: &'static str = "/graphql";
926}
927
928/// `GET /api/spec.json`
929pub struct ApiSpec;
930
931impl SimpleRoute for ApiSpec {
932    const PATH: &'static str = "/api/spec.json";
933}
934
935/// `GET /api/doc/`
936pub struct ApiDoc;
937
938impl SimpleRoute for ApiDoc {
939    const PATH: &'static str = "/api/doc/";
940}
941
942/// `GET /api/doc/oauth2-callback`
943pub struct ApiDocCallback;
944
945impl SimpleRoute for ApiDocCallback {
946    const PATH: &'static str = "/api/doc/oauth2-callback";
947}