Skip to main content

mas_templates/
context.rs

1// Copyright 2025, 2026 Element Creations Ltd.
2// Copyright 2024, 2025 New Vector Ltd.
3// Copyright 2021-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
8//! Contexts used in templates
9
10mod branding;
11mod captcha;
12mod ext;
13mod features;
14
15use std::{
16    collections::BTreeMap,
17    fmt::Formatter,
18    net::{IpAddr, Ipv4Addr},
19};
20
21use chrono::{DateTime, Duration, Utc};
22use http::{Method, Uri, Version};
23use mas_data_model::{
24    AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
25    DeviceCodeGrant, MatrixUser, UlidExt as _, UpstreamOAuthLink, UpstreamOAuthProvider,
26    UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode,
27    UpstreamOAuthProviderOnBackchannelLogout, UpstreamOAuthProviderPkceMode,
28    UpstreamOAuthProviderTokenAuthMethod, User, UserEmailAuthentication,
29    UserEmailAuthenticationCode, UserRecoverySession, UserRegistration, UserRegistrationToken,
30};
31use mas_i18n::DataLocale;
32use mas_iana::jose::JsonWebSignatureAlg;
33use mas_policy::{Violation, ViolationVariant};
34use mas_router::{Account, GraphQL, Login, PostAuthAction, UrlBuilder};
35use oauth2_types::{
36    requests::ResponseMode,
37    scope::{OPENID, Scope},
38};
39use rand::{
40    RngExt, SeedableRng,
41    distr::{Alphanumeric, SampleString},
42};
43use rand_chacha::ChaCha8Rng;
44use serde::{Deserialize, Serialize, ser::SerializeStruct};
45use ulid::Ulid;
46use url::Url;
47
48pub use self::{
49    branding::SiteBranding, captcha::WithCaptcha, ext::SiteConfigExt, features::SiteFeatures,
50};
51use crate::{FieldError, FormField, FormState};
52
53/// Helper trait to construct context wrappers
54pub trait TemplateContext: Serialize {
55    /// Attach a user session to the template context
56    fn with_session(self, current_session: BrowserSession) -> WithSession<Self>
57    where
58        Self: Sized,
59    {
60        WithSession {
61            current_session,
62            inner: self,
63        }
64    }
65
66    /// Attach an optional user session to the template context
67    fn maybe_with_session(
68        self,
69        current_session: Option<BrowserSession>,
70    ) -> WithOptionalSession<Self>
71    where
72        Self: Sized,
73    {
74        WithOptionalSession {
75            current_session,
76            inner: self,
77        }
78    }
79
80    /// Attach a CSRF token to the template context
81    fn with_csrf<C>(self, csrf_token: C) -> WithCsrf<Self>
82    where
83        Self: Sized,
84        C: ToString,
85    {
86        // TODO: make this method use a CsrfToken again
87        WithCsrf {
88            csrf_token: csrf_token.to_string(),
89            inner: self,
90        }
91    }
92
93    /// Attach a language to the template context
94    fn with_language(self, lang: DataLocale) -> WithLanguage<Self>
95    where
96        Self: Sized,
97    {
98        WithLanguage {
99            lang: lang.to_string(),
100            inner: self,
101        }
102    }
103
104    /// Attach a CAPTCHA configuration to the template context
105    fn with_captcha(self, captcha: Option<mas_data_model::CaptchaConfig>) -> WithCaptcha<Self>
106    where
107        Self: Sized,
108    {
109        WithCaptcha::new(captcha, self)
110    }
111
112    /// Generate sample values for this context type
113    ///
114    /// This is then used to check for template validity in unit tests and in
115    /// the CLI (`cargo run -- templates check`)
116    fn sample<R: RngExt>(
117        now: chrono::DateTime<Utc>,
118        rng: &mut R,
119        locales: &[DataLocale],
120    ) -> BTreeMap<SampleIdentifier, Self>
121    where
122        Self: Sized;
123}
124
125#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
126pub struct SampleIdentifier {
127    pub components: Vec<(&'static str, String)>,
128}
129
130impl SampleIdentifier {
131    pub fn from_index(index: usize) -> Self {
132        Self {
133            components: Vec::default(),
134        }
135        .with_appended("index", format!("{index}"))
136    }
137
138    pub fn with_appended(&self, kind: &'static str, locale: String) -> Self {
139        let mut new = self.clone();
140        new.components.push((kind, locale));
141        new
142    }
143}
144
145pub(crate) fn sample_list<T: TemplateContext>(samples: Vec<T>) -> BTreeMap<SampleIdentifier, T> {
146    samples
147        .into_iter()
148        .enumerate()
149        .map(|(index, sample)| (SampleIdentifier::from_index(index), sample))
150        .collect()
151}
152
153impl TemplateContext for () {
154    fn sample<R: RngExt>(
155        _now: chrono::DateTime<Utc>,
156        _rng: &mut R,
157        _locales: &[DataLocale],
158    ) -> BTreeMap<SampleIdentifier, Self>
159    where
160        Self: Sized,
161    {
162        BTreeMap::new()
163    }
164}
165
166/// Context with a specified locale in it
167#[derive(Serialize, Debug)]
168pub struct WithLanguage<T> {
169    lang: String,
170
171    #[serde(flatten)]
172    inner: T,
173}
174
175impl<T> WithLanguage<T> {
176    /// Get the language of this context
177    pub fn language(&self) -> &str {
178        &self.lang
179    }
180}
181
182impl<T> std::ops::Deref for WithLanguage<T> {
183    type Target = T;
184
185    fn deref(&self) -> &Self::Target {
186        &self.inner
187    }
188}
189
190impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
191    fn sample<R: RngExt>(
192        now: chrono::DateTime<Utc>,
193        rng: &mut R,
194        locales: &[DataLocale],
195    ) -> BTreeMap<SampleIdentifier, Self>
196    where
197        Self: Sized,
198    {
199        // Create a forked RNG so we make samples deterministic between locales
200        let rng = ChaCha8Rng::from_rng(&mut *rng);
201        locales
202            .iter()
203            .flat_map(|locale| {
204                T::sample(now, &mut rng.clone(), locales)
205                    .into_iter()
206                    .map(|(sample_id, sample)| {
207                        (
208                            sample_id.with_appended("locale", locale.to_string()),
209                            WithLanguage {
210                                lang: locale.to_string(),
211                                inner: sample,
212                            },
213                        )
214                    })
215            })
216            .collect()
217    }
218}
219
220/// Context with a CSRF token in it
221#[derive(Serialize, Debug)]
222pub struct WithCsrf<T> {
223    csrf_token: String,
224
225    #[serde(flatten)]
226    inner: T,
227}
228
229impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
230    fn sample<R: RngExt>(
231        now: chrono::DateTime<Utc>,
232        rng: &mut R,
233        locales: &[DataLocale],
234    ) -> BTreeMap<SampleIdentifier, Self>
235    where
236        Self: Sized,
237    {
238        T::sample(now, rng, locales)
239            .into_iter()
240            .map(|(k, inner)| {
241                (
242                    k,
243                    WithCsrf {
244                        csrf_token: "fake_csrf_token".into(),
245                        inner,
246                    },
247                )
248            })
249            .collect()
250    }
251}
252
253/// Context with a user session in it
254#[derive(Serialize, Debug)]
255pub struct WithSession<T> {
256    current_session: BrowserSession,
257
258    #[serde(flatten)]
259    inner: T,
260}
261
262impl<T: TemplateContext> TemplateContext for WithSession<T> {
263    fn sample<R: RngExt>(
264        now: chrono::DateTime<Utc>,
265        rng: &mut R,
266        locales: &[DataLocale],
267    ) -> BTreeMap<SampleIdentifier, Self>
268    where
269        Self: Sized,
270    {
271        BrowserSession::samples(now, rng)
272            .into_iter()
273            .enumerate()
274            .flat_map(|(session_index, session)| {
275                T::sample(now, rng, locales)
276                    .into_iter()
277                    .map(move |(k, inner)| {
278                        (
279                            k.with_appended("browser-session", session_index.to_string()),
280                            WithSession {
281                                current_session: session.clone(),
282                                inner,
283                            },
284                        )
285                    })
286            })
287            .collect()
288    }
289}
290
291/// Context with an optional user session in it
292#[derive(Serialize)]
293pub struct WithOptionalSession<T> {
294    current_session: Option<BrowserSession>,
295
296    #[serde(flatten)]
297    inner: T,
298}
299
300impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
301    fn sample<R: RngExt>(
302        now: chrono::DateTime<Utc>,
303        rng: &mut R,
304        locales: &[DataLocale],
305    ) -> BTreeMap<SampleIdentifier, Self>
306    where
307        Self: Sized,
308    {
309        BrowserSession::samples(now, rng)
310            .into_iter()
311            .map(Some) // Wrap all samples in an Option
312            .chain(std::iter::once(None)) // Add the "None" option
313            .enumerate()
314            .flat_map(|(session_index, session)| {
315                T::sample(now, rng, locales)
316                    .into_iter()
317                    .map(move |(k, inner)| {
318                        (
319                            if session.is_some() {
320                                k.with_appended("browser-session", session_index.to_string())
321                            } else {
322                                k
323                            },
324                            WithOptionalSession {
325                                current_session: session.clone(),
326                                inner,
327                            },
328                        )
329                    })
330            })
331            .collect()
332    }
333}
334
335/// An empty context used for composition
336pub struct EmptyContext;
337
338impl Serialize for EmptyContext {
339    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
340    where
341        S: serde::Serializer,
342    {
343        let mut s = serializer.serialize_struct("EmptyContext", 0)?;
344        // FIXME: for some reason, serde seems to not like struct flattening with empty
345        // stuff
346        s.serialize_field("__UNUSED", &())?;
347        s.end()
348    }
349}
350
351impl TemplateContext for EmptyContext {
352    fn sample<R: RngExt>(
353        _now: chrono::DateTime<Utc>,
354        _rng: &mut R,
355        _locales: &[DataLocale],
356    ) -> BTreeMap<SampleIdentifier, Self>
357    where
358        Self: Sized,
359    {
360        sample_list(vec![EmptyContext])
361    }
362}
363
364/// Context used by the `index.html` template
365#[derive(Serialize)]
366pub struct IndexContext {
367    discovery_url: Url,
368}
369
370impl IndexContext {
371    /// Constructs the context for the index page from the OIDC discovery
372    /// document URL
373    #[must_use]
374    pub fn new(discovery_url: Url) -> Self {
375        Self { discovery_url }
376    }
377}
378
379impl TemplateContext for IndexContext {
380    fn sample<R: RngExt>(
381        _now: chrono::DateTime<Utc>,
382        _rng: &mut R,
383        _locales: &[DataLocale],
384    ) -> BTreeMap<SampleIdentifier, Self>
385    where
386        Self: Sized,
387    {
388        sample_list(vec![Self {
389            discovery_url: "https://example.com/.well-known/openid-configuration"
390                .parse()
391                .unwrap(),
392        }])
393    }
394}
395
396/// Config used by the frontend app
397#[derive(Serialize)]
398#[serde(rename_all = "camelCase")]
399pub struct AppConfig {
400    root: String,
401    graphql_endpoint: String,
402}
403
404/// Context used by the `app.html` template
405#[derive(Serialize)]
406pub struct AppContext {
407    app_config: AppConfig,
408}
409
410impl AppContext {
411    /// Constructs the context given the [`UrlBuilder`]
412    #[must_use]
413    pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
414        let root = url_builder.relative_url_for(&Account::default());
415        let graphql_endpoint = url_builder.relative_url_for(&GraphQL);
416        Self {
417            app_config: AppConfig {
418                root,
419                graphql_endpoint,
420            },
421        }
422    }
423}
424
425impl TemplateContext for AppContext {
426    fn sample<R: RngExt>(
427        _now: chrono::DateTime<Utc>,
428        _rng: &mut R,
429        _locales: &[DataLocale],
430    ) -> BTreeMap<SampleIdentifier, Self>
431    where
432        Self: Sized,
433    {
434        let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
435        sample_list(vec![Self::from_url_builder(&url_builder)])
436    }
437}
438
439/// Context used by the `swagger/doc.html` template
440#[derive(Serialize)]
441pub struct ApiDocContext {
442    openapi_url: Url,
443    callback_url: Url,
444}
445
446impl ApiDocContext {
447    /// Constructs a context for the API documentation page giben the
448    /// [`UrlBuilder`]
449    #[must_use]
450    pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
451        Self {
452            openapi_url: url_builder.absolute_url_for(&mas_router::ApiSpec),
453            callback_url: url_builder.absolute_url_for(&mas_router::ApiDocCallback),
454        }
455    }
456}
457
458impl TemplateContext for ApiDocContext {
459    fn sample<R: RngExt>(
460        _now: chrono::DateTime<Utc>,
461        _rng: &mut R,
462        _locales: &[DataLocale],
463    ) -> BTreeMap<SampleIdentifier, Self>
464    where
465        Self: Sized,
466    {
467        let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
468        sample_list(vec![Self::from_url_builder(&url_builder)])
469    }
470}
471
472/// Fields of the login form
473#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
474#[serde(rename_all = "snake_case")]
475pub enum LoginFormField {
476    /// The username field
477    Username,
478
479    /// The password field
480    Password,
481}
482
483impl FormField for LoginFormField {
484    fn keep(&self) -> bool {
485        match self {
486            Self::Username => true,
487            Self::Password => false,
488        }
489    }
490}
491
492/// Inner context used in login screen. See [`PostAuthContext`].
493#[derive(Serialize)]
494#[serde(tag = "kind", rename_all = "snake_case")]
495pub enum PostAuthContextInner {
496    /// Continue an authorization grant
497    ContinueAuthorizationGrant {
498        /// The authorization grant that will be continued after authentication
499        grant: Box<AuthorizationGrant>,
500    },
501
502    /// Continue a device code grant
503    ContinueDeviceCodeGrant {
504        /// The device code grant that will be continued after authentication
505        grant: Box<DeviceCodeGrant>,
506    },
507
508    /// Continue legacy login
509    /// TODO: add the login context in there
510    ContinueCompatSsoLogin {
511        /// The compat SSO login request
512        login: Box<CompatSsoLogin>,
513    },
514
515    /// Change the account password
516    ChangePassword,
517
518    /// Link an upstream account
519    LinkUpstream {
520        /// The upstream provider
521        provider: Box<UpstreamOAuthProvider>,
522
523        /// The link
524        link: Box<UpstreamOAuthLink>,
525    },
526
527    /// Go to the account management page
528    ManageAccount,
529
530    /// Continue a room invite, by opening its room in the client
531    ContinueRoomInvite,
532}
533
534/// Context used in login screen, for the post-auth action to do
535#[derive(Serialize)]
536pub struct PostAuthContext {
537    /// The post auth action params from the URL
538    pub params: PostAuthAction,
539
540    /// The loaded post auth context
541    #[serde(flatten)]
542    pub ctx: PostAuthContextInner,
543}
544
545/// Context used by the `login.html` template
546#[derive(Serialize, Default)]
547pub struct LoginContext {
548    form: FormState<LoginFormField>,
549    next: Option<PostAuthContext>,
550    providers: Vec<UpstreamOAuthProvider>,
551}
552
553impl TemplateContext for LoginContext {
554    fn sample<R: RngExt>(
555        _now: chrono::DateTime<Utc>,
556        _rng: &mut R,
557        _locales: &[DataLocale],
558    ) -> BTreeMap<SampleIdentifier, Self>
559    where
560        Self: Sized,
561    {
562        // TODO: samples with errors
563        sample_list(vec![
564            LoginContext {
565                form: FormState::default(),
566                next: None,
567                providers: Vec::new(),
568            },
569            LoginContext {
570                form: FormState::default(),
571                next: None,
572                providers: Vec::new(),
573            },
574            LoginContext {
575                form: FormState::default()
576                    .with_error_on_field(LoginFormField::Username, FieldError::Required)
577                    .with_error_on_field(
578                        LoginFormField::Password,
579                        FieldError::Policy {
580                            code: None,
581                            message: "password too short".to_owned(),
582                        },
583                    ),
584                next: None,
585                providers: Vec::new(),
586            },
587            LoginContext {
588                form: FormState::default()
589                    .with_error_on_field(LoginFormField::Username, FieldError::Exists),
590                next: None,
591                providers: Vec::new(),
592            },
593        ])
594    }
595}
596
597impl LoginContext {
598    /// Set the form state
599    #[must_use]
600    pub fn with_form_state(self, form: FormState<LoginFormField>) -> Self {
601        Self { form, ..self }
602    }
603
604    /// Mutably borrow the form state
605    pub fn form_state_mut(&mut self) -> &mut FormState<LoginFormField> {
606        &mut self.form
607    }
608
609    /// Set the upstream OAuth 2.0 providers
610    #[must_use]
611    pub fn with_upstream_providers(self, providers: Vec<UpstreamOAuthProvider>) -> Self {
612        Self { providers, ..self }
613    }
614
615    /// Add a post authentication action to the context
616    #[must_use]
617    pub fn with_post_action(self, context: PostAuthContext) -> Self {
618        Self {
619            next: Some(context),
620            ..self
621        }
622    }
623}
624
625/// Fields of the registration form
626#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
627#[serde(rename_all = "snake_case")]
628pub enum RegisterFormField {
629    /// The username field
630    Username,
631
632    /// The email field
633    Email,
634
635    /// The password field
636    Password,
637
638    /// The password confirmation field
639    PasswordConfirm,
640
641    /// The terms of service agreement field
642    AcceptTerms,
643
644    /// The registration token (a.k.a. invite code) field
645    Token,
646}
647
648impl FormField for RegisterFormField {
649    fn keep(&self) -> bool {
650        match self {
651            Self::Username | Self::Email | Self::AcceptTerms | Self::Token => true,
652            Self::Password | Self::PasswordConfirm => false,
653        }
654    }
655}
656
657/// An upstream OAuth 2.0 provider, as rendered by the registration page island
658#[derive(Serialize)]
659struct RegisterPageProvider {
660    name: String,
661    brand: Option<String>,
662    /// Submitted back as the `provider` field of the registration form
663    id: String,
664}
665
666impl RegisterPageProvider {
667    fn new(provider: UpstreamOAuthProvider) -> Self {
668        let name = provider
669            .human_name
670            .or_else(|| {
671                provider
672                    .issuer
673                    .as_deref()
674                    .map(|issuer| crate::functions::simplify_url(issuer, true))
675            })
676            .filter(|name| !name.is_empty())
677            .unwrap_or_else(|| provider.id.to_string());
678
679        Self {
680            name,
681            brand: provider.brand_name,
682            id: provider.id.to_string(),
683        }
684    }
685}
686
687/// The identity an invite code was issued for
688#[derive(Serialize)]
689struct InvitePinnedIdentity {
690    username: Option<String>,
691    email: Option<String>,
692    passwordless: bool,
693}
694
695/// The invite code the registration page was opened with, as the island sees it
696#[derive(Serialize)]
697pub struct InviteContext {
698    valid: bool,
699
700    #[serde(flatten)]
701    pinned: Option<InvitePinnedIdentity>,
702}
703
704impl InviteContext {
705    /// Describe the invite code a link carried, `None` standing for a code
706    /// which is unknown, expired, revoked or exhausted
707    #[must_use]
708    pub fn new(token: Option<&UserRegistrationToken>) -> Self {
709        Self {
710            valid: token.is_some(),
711            pinned: token.map(|token| InvitePinnedIdentity {
712                username: token.username.clone(),
713                email: token.email.clone(),
714                passwordless: token.passwordless,
715            }),
716        }
717    }
718}
719
720/// Context used by the `register/index.html` template
721#[derive(Serialize)]
722pub struct RegisterContext {
723    providers: Vec<RegisterPageProvider>,
724    login_link: String,
725    form: FormState<RegisterFormField>,
726    graphql_endpoint: String,
727    invite: Option<InviteContext>,
728}
729
730impl TemplateContext for RegisterContext {
731    fn sample<R: RngExt>(
732        _now: chrono::DateTime<Utc>,
733        _rng: &mut R,
734        _locales: &[DataLocale],
735    ) -> BTreeMap<SampleIdentifier, Self>
736    where
737        Self: Sized,
738    {
739        let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
740        // TODO: samples with errors and with upstream providers
741        sample_list(vec![RegisterContext::new(&url_builder, Vec::new(), None)])
742    }
743}
744
745impl RegisterContext {
746    /// Create a new context with the given upstream providers, resolving the
747    /// URLs used by the client-side island from the given [`UrlBuilder`]
748    #[must_use]
749    pub fn new(
750        url_builder: &UrlBuilder,
751        providers: Vec<UpstreamOAuthProvider>,
752        post_auth_action: Option<&PostAuthAction>,
753    ) -> Self {
754        Self {
755            providers: providers
756                .into_iter()
757                .map(RegisterPageProvider::new)
758                .collect(),
759            login_link: url_builder.relative_url_for(&Login::from(post_auth_action.cloned())),
760            form: FormState::default(),
761            graphql_endpoint: url_builder.relative_url_for(&GraphQL),
762            invite: None,
763        }
764    }
765
766    /// Set the state of the registration form
767    #[must_use]
768    pub fn with_form_state(self, form: FormState<RegisterFormField>) -> Self {
769        Self { form, ..self }
770    }
771
772    /// Set the invite code the page was opened with
773    #[must_use]
774    pub fn with_invite(self, invite: InviteContext) -> Self {
775        Self {
776            invite: Some(invite),
777            ..self
778        }
779    }
780}
781
782/// Context used by the `consent.html` template
783#[derive(Serialize)]
784pub struct ConsentContext {
785    grant: AuthorizationGrant,
786    client: Client,
787    action: PostAuthAction,
788    matrix_user: MatrixUser,
789}
790
791impl TemplateContext for ConsentContext {
792    fn sample<R: RngExt>(
793        now: chrono::DateTime<Utc>,
794        rng: &mut R,
795        _locales: &[DataLocale],
796    ) -> BTreeMap<SampleIdentifier, Self>
797    where
798        Self: Sized,
799    {
800        sample_list(
801            Client::samples(now, rng)
802                .into_iter()
803                .flat_map(|client| {
804                    [
805                        (None, ResponseMode::Query),
806                        (None, ResponseMode::Fragment),
807                        (None, ResponseMode::FormPost),
808                        (Some("some-state".to_owned()), ResponseMode::Query),
809                        (Some("some-state".to_owned()), ResponseMode::Fragment),
810                        (Some("some-state".to_owned()), ResponseMode::FormPost),
811                    ]
812                    .map(|(state, response_mode)| {
813                        let mut grant = AuthorizationGrant::sample(now, rng);
814                        let action = PostAuthAction::continue_grant(grant.id);
815                        // XXX
816                        grant.client_id = client.id;
817                        grant.state = state;
818                        grant.response_mode = response_mode;
819                        Self {
820                            grant,
821                            client: client.clone(),
822                            action,
823                            matrix_user: MatrixUser {
824                                mxid: "@alice:example.com".to_owned(),
825                                display_name: Some("Alice".to_owned()),
826                            },
827                        }
828                    })
829                })
830                .collect(),
831        )
832    }
833}
834
835impl ConsentContext {
836    /// Constructs a context for the client consent page
837    #[must_use]
838    pub fn new(grant: AuthorizationGrant, client: Client, matrix_user: MatrixUser) -> Self {
839        let action = PostAuthAction::continue_grant(grant.id);
840        Self {
841            grant,
842            client,
843            action,
844            matrix_user,
845        }
846    }
847}
848
849#[derive(Serialize, Debug)]
850#[serde(tag = "grant_type")]
851enum PolicyViolationGrant {
852    #[serde(rename = "authorization_code")]
853    Authorization(AuthorizationGrant),
854    #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
855    DeviceCode(DeviceCodeGrant),
856}
857
858/// Context used by the `policy_violation.html` template
859#[derive(Serialize, Debug)]
860pub struct PolicyViolationContext {
861    grant: PolicyViolationGrant,
862    client: Client,
863    action: PostAuthAction,
864    violations: Vec<Violation>,
865}
866
867impl TemplateContext for PolicyViolationContext {
868    fn sample<R: RngExt>(
869        now: chrono::DateTime<Utc>,
870        rng: &mut R,
871        _locales: &[DataLocale],
872    ) -> BTreeMap<SampleIdentifier, Self>
873    where
874        Self: Sized,
875    {
876        sample_list(
877            Client::samples(now, rng)
878                .into_iter()
879                .flat_map(|client| {
880                    let mut grant = AuthorizationGrant::sample(now, rng);
881                    // XXX
882                    grant.client_id = client.id;
883
884                    let authorization_grant = PolicyViolationContext::for_authorization_grant(
885                        grant.clone(),
886                        client.clone(),
887                        Vec::new(),
888                    );
889
890                    let authorization_grant_invalid_scope =
891                        PolicyViolationContext::for_authorization_grant(
892                            grant.clone(),
893                            client.clone(),
894                            vec![Violation {
895                                msg: "scope 'foo' not allowed".to_owned(),
896                                redirect_uri: None,
897                                field: None,
898                                variant: None,
899                            }],
900                        );
901                    let authorization_grant_admin_scope =
902                        PolicyViolationContext::for_authorization_grant(
903                            grant.clone(),
904                            client.clone(),
905                            vec![Violation {
906                                msg: "scope 'urn:mas:admin' requires admin privileges".to_owned(),
907                                redirect_uri: None,
908                                field: None,
909                                variant: Some(ViolationVariant::AdminScopeNotAllowed),
910                            }],
911                        );
912
913                    let authorization_grant_client_not_allowed =
914                        PolicyViolationContext::for_authorization_grant(
915                            grant,
916                            client.clone(),
917                            vec![Violation {
918                                msg: "This client is not allowed to login".to_owned(),
919                                redirect_uri: None,
920                                field: None,
921                                variant: Some(ViolationVariant::ClientNotAllowed),
922                            }],
923                        );
924
925                    let device_code_grant = PolicyViolationContext::for_device_code_grant(
926                        DeviceCodeGrant {
927                            id: Ulid::from_datetime_with_rng(now, rng),
928                            state: mas_data_model::DeviceCodeGrantState::Pending,
929                            client_id: client.id,
930                            scope: [OPENID].into_iter().collect(),
931                            user_code: mas_data_model::generate_user_code(rng),
932                            device_code: Alphanumeric.sample_string(rng, 32),
933                            created_at: now - Duration::try_minutes(5).unwrap(),
934                            expires_at: now + Duration::try_minutes(25).unwrap(),
935                            ip_address: None,
936                            user_agent: None,
937                            locale: None,
938                        },
939                        client.clone(),
940                        Vec::new(),
941                    );
942
943                    let device_code_grant_invalid_scope =
944                        PolicyViolationContext::for_device_code_grant(
945                            DeviceCodeGrant {
946                                id: Ulid::from_datetime_with_rng(now, rng),
947                                state: mas_data_model::DeviceCodeGrantState::Pending,
948                                client_id: client.id,
949                                scope: [OPENID].into_iter().collect(),
950                                user_code: mas_data_model::generate_user_code(rng),
951                                device_code: Alphanumeric.sample_string(rng, 32),
952                                created_at: now - Duration::try_minutes(5).unwrap(),
953                                expires_at: now + Duration::try_minutes(25).unwrap(),
954                                ip_address: None,
955                                user_agent: None,
956                                locale: None,
957                            },
958                            client,
959                            vec![Violation {
960                                msg: "user has too many active sessions".to_owned(),
961                                redirect_uri: None,
962                                field: None,
963                                variant: Some(ViolationVariant::TooManySessions {
964                                    need_to_remove: 1,
965                                }),
966                            }],
967                        );
968
969                    [
970                        authorization_grant,
971                        authorization_grant_invalid_scope,
972                        authorization_grant_admin_scope,
973                        authorization_grant_client_not_allowed,
974                        device_code_grant,
975                        device_code_grant_invalid_scope,
976                    ]
977                })
978                .collect(),
979        )
980    }
981}
982
983impl PolicyViolationContext {
984    /// Constructs a context for the policy violation page for an authorization
985    /// grant
986    #[must_use]
987    pub const fn for_authorization_grant(
988        grant: AuthorizationGrant,
989        client: Client,
990        violations: Vec<Violation>,
991    ) -> Self {
992        let action = PostAuthAction::continue_grant(grant.id);
993        Self {
994            grant: PolicyViolationGrant::Authorization(grant),
995            client,
996            action,
997            violations,
998        }
999    }
1000
1001    /// Constructs a context for the policy violation page for a device code
1002    /// grant
1003    #[must_use]
1004    pub const fn for_device_code_grant(
1005        grant: DeviceCodeGrant,
1006        client: Client,
1007        violations: Vec<Violation>,
1008    ) -> Self {
1009        let action = PostAuthAction::continue_device_code_grant(grant.id);
1010        Self {
1011            grant: PolicyViolationGrant::DeviceCode(grant),
1012            client,
1013            action,
1014            violations,
1015        }
1016    }
1017}
1018
1019/// Context used by the `compat_login_policy_violation.html` template
1020#[derive(Serialize)]
1021pub struct CompatLoginPolicyViolationContext {
1022    violations: Vec<Violation>,
1023}
1024
1025impl TemplateContext for CompatLoginPolicyViolationContext {
1026    fn sample<R: RngExt>(
1027        _now: chrono::DateTime<Utc>,
1028        _rng: &mut R,
1029        _locales: &[DataLocale],
1030    ) -> BTreeMap<SampleIdentifier, Self>
1031    where
1032        Self: Sized,
1033    {
1034        sample_list(vec![
1035            CompatLoginPolicyViolationContext { violations: vec![] },
1036            CompatLoginPolicyViolationContext {
1037                violations: vec![Violation {
1038                    msg: "scope 'foo' not allowed".to_owned(),
1039                    redirect_uri: None,
1040                    field: None,
1041                    variant: None,
1042                }],
1043            },
1044            CompatLoginPolicyViolationContext {
1045                violations: vec![Violation {
1046                    msg: "user has too many active sessions".to_owned(),
1047                    redirect_uri: None,
1048                    field: None,
1049                    variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1050                }],
1051            },
1052            CompatLoginPolicyViolationContext {
1053                violations: vec![Violation {
1054                    msg: "This client is not allowed to login".to_owned(),
1055                    redirect_uri: None,
1056                    field: None,
1057                    variant: Some(ViolationVariant::ClientNotAllowed),
1058                }],
1059            },
1060            CompatLoginPolicyViolationContext {
1061                violations: vec![
1062                    Violation {
1063                        msg: "scope 'foo' not allowed".to_owned(),
1064                        redirect_uri: None,
1065                        field: None,
1066                        variant: None,
1067                    },
1068                    Violation {
1069                        msg: "user has too many active sessions".to_owned(),
1070                        redirect_uri: None,
1071                        field: None,
1072                        variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1073                    },
1074                ],
1075            },
1076        ])
1077    }
1078}
1079
1080impl CompatLoginPolicyViolationContext {
1081    /// Constructs a context for the compatibility login policy violation page
1082    /// given the list of violations
1083    #[must_use]
1084    pub const fn for_violations(violations: Vec<Violation>) -> Self {
1085        Self { violations }
1086    }
1087}
1088
1089/// Context used by the `sso.html` template
1090#[derive(Serialize)]
1091pub struct CompatSsoContext {
1092    login: CompatSsoLogin,
1093    action: PostAuthAction,
1094    matrix_user: MatrixUser,
1095}
1096
1097impl TemplateContext for CompatSsoContext {
1098    fn sample<R: RngExt>(
1099        now: chrono::DateTime<Utc>,
1100        rng: &mut R,
1101        _locales: &[DataLocale],
1102    ) -> BTreeMap<SampleIdentifier, Self>
1103    where
1104        Self: Sized,
1105    {
1106        let id = Ulid::from_datetime_with_rng(now, rng);
1107        sample_list(vec![CompatSsoContext::new(
1108            CompatSsoLogin {
1109                id,
1110                redirect_uri: Url::parse("https://app.element.io/").unwrap(),
1111                login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
1112                created_at: now,
1113                state: CompatSsoLoginState::Pending,
1114            },
1115            MatrixUser {
1116                mxid: "@alice:example.com".to_owned(),
1117                display_name: Some("Alice".to_owned()),
1118            },
1119        )])
1120    }
1121}
1122
1123impl CompatSsoContext {
1124    /// Constructs a context for the legacy SSO login page
1125    #[must_use]
1126    pub fn new(login: CompatSsoLogin, matrix_user: MatrixUser) -> Self
1127where {
1128        let action = PostAuthAction::continue_compat_sso_login(login.id);
1129        Self {
1130            login,
1131            action,
1132            matrix_user,
1133        }
1134    }
1135}
1136
1137/// Context used by the `emails/recovery.{txt,html,subject}` templates
1138#[derive(Serialize)]
1139pub struct EmailRecoveryContext {
1140    user: User,
1141    session: UserRecoverySession,
1142    recovery_link: Url,
1143}
1144
1145impl EmailRecoveryContext {
1146    /// Constructs a context for the recovery email
1147    #[must_use]
1148    pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
1149        Self {
1150            user,
1151            session,
1152            recovery_link,
1153        }
1154    }
1155
1156    /// Returns the user associated with the recovery email
1157    #[must_use]
1158    pub fn user(&self) -> &User {
1159        &self.user
1160    }
1161
1162    /// Returns the recovery session associated with the recovery email
1163    #[must_use]
1164    pub fn session(&self) -> &UserRecoverySession {
1165        &self.session
1166    }
1167}
1168
1169impl TemplateContext for EmailRecoveryContext {
1170    fn sample<R: RngExt>(
1171        now: chrono::DateTime<Utc>,
1172        rng: &mut R,
1173        _locales: &[DataLocale],
1174    ) -> BTreeMap<SampleIdentifier, Self>
1175    where
1176        Self: Sized,
1177    {
1178        sample_list(User::samples(now, rng).into_iter().map(|user| {
1179            let session = UserRecoverySession {
1180                id: Ulid::from_datetime_with_rng(now, rng),
1181                email: "hello@example.com".to_owned(),
1182                user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/536.30.1 (KHTML, like Gecko) Version/6.0.5 Safari/536.30.1".to_owned(),
1183                ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
1184                locale: "en".to_owned(),
1185                created_at: now,
1186                consumed_at: None,
1187            };
1188
1189            let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
1190
1191            Self::new(user, session, link)
1192        }).collect())
1193    }
1194}
1195
1196/// Context used by the `emails/verification.{txt,html,subject}` templates
1197#[derive(Serialize)]
1198pub struct EmailVerificationContext {
1199    #[serde(skip_serializing_if = "Option::is_none")]
1200    browser_session: Option<BrowserSession>,
1201    #[serde(skip_serializing_if = "Option::is_none")]
1202    user_registration: Option<UserRegistration>,
1203    authentication_code: UserEmailAuthenticationCode,
1204}
1205
1206impl EmailVerificationContext {
1207    /// Constructs a context for the verification email
1208    #[must_use]
1209    pub fn new(
1210        authentication_code: UserEmailAuthenticationCode,
1211        browser_session: Option<BrowserSession>,
1212        user_registration: Option<UserRegistration>,
1213    ) -> Self {
1214        Self {
1215            browser_session,
1216            user_registration,
1217            authentication_code,
1218        }
1219    }
1220
1221    /// Get the user to which this email is being sent
1222    #[must_use]
1223    pub fn user(&self) -> Option<&User> {
1224        self.browser_session.as_ref().map(|s| &s.user)
1225    }
1226
1227    /// Get the verification code being sent
1228    #[must_use]
1229    pub fn code(&self) -> &str {
1230        &self.authentication_code.code
1231    }
1232}
1233
1234impl TemplateContext for EmailVerificationContext {
1235    fn sample<R: RngExt>(
1236        now: chrono::DateTime<Utc>,
1237        rng: &mut R,
1238        _locales: &[DataLocale],
1239    ) -> BTreeMap<SampleIdentifier, Self>
1240    where
1241        Self: Sized,
1242    {
1243        sample_list(
1244            BrowserSession::samples(now, rng)
1245                .into_iter()
1246                .map(|browser_session| {
1247                    let authentication_code = UserEmailAuthenticationCode {
1248                        id: Ulid::from_datetime_with_rng(now, rng),
1249                        user_email_authentication_id: Ulid::from_datetime_with_rng(now, rng),
1250                        code: "123456".to_owned(),
1251                        created_at: now - Duration::try_minutes(5).unwrap(),
1252                        expires_at: now + Duration::try_minutes(25).unwrap(),
1253                    };
1254
1255                    Self {
1256                        browser_session: Some(browser_session),
1257                        user_registration: None,
1258                        authentication_code,
1259                    }
1260                })
1261                .collect(),
1262        )
1263    }
1264}
1265
1266/// Context used by the `emails/invite.{txt,html,subject}` templates
1267#[derive(Serialize)]
1268pub struct EmailInviteContext {
1269    opens_room: bool,
1270    room_name: Option<String>,
1271    inviter: Option<String>,
1272    inviter_name: Option<String>,
1273    invite_link: Url,
1274}
1275
1276impl EmailInviteContext {
1277    /// Constructs a context for the invite email
1278    #[must_use]
1279    pub fn new(
1280        opens_room: bool,
1281        room_name: Option<String>,
1282        inviter: Option<String>,
1283        inviter_name: Option<String>,
1284        invite_link: Url,
1285    ) -> Self {
1286        Self {
1287            opens_room,
1288            room_name,
1289            inviter,
1290            inviter_name,
1291            invite_link,
1292        }
1293    }
1294}
1295
1296impl TemplateContext for EmailInviteContext {
1297    fn sample<R: RngExt>(
1298        _now: chrono::DateTime<Utc>,
1299        _rng: &mut R,
1300        _locales: &[DataLocale],
1301    ) -> BTreeMap<SampleIdentifier, Self>
1302    where
1303        Self: Sized,
1304    {
1305        let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
1306        let invite_link = url_builder.absolute_url_for(
1307            &mas_router::Register::default().with_token("abcdefghijkl".to_owned()),
1308        );
1309
1310        sample_list(vec![
1311            Self::new(
1312                true,
1313                Some("Project X".to_owned()),
1314                Some("@alice:example.com".to_owned()),
1315                Some("Alice O'Brien & Co".to_owned()),
1316                invite_link.clone(),
1317            ),
1318            Self::new(
1319                true,
1320                None,
1321                Some("@alice:example.com".to_owned()),
1322                Some("Alice O'Brien & Co".to_owned()),
1323                invite_link.clone(),
1324            ),
1325            Self::new(
1326                true,
1327                Some("Project X".to_owned()),
1328                None,
1329                Some("Alice O'Brien & Co".to_owned()),
1330                invite_link.clone(),
1331            ),
1332            Self::new(
1333                true,
1334                None,
1335                Some("@alice:example.com".to_owned()),
1336                None,
1337                invite_link.clone(),
1338            ),
1339            Self::new(
1340                true,
1341                Some("Project X".to_owned()),
1342                None,
1343                None,
1344                invite_link.clone(),
1345            ),
1346            Self::new(true, None, None, None, invite_link.clone()),
1347            Self::new(
1348                false,
1349                None,
1350                Some("@alice:example.com".to_owned()),
1351                Some("Alice O'Brien & Co".to_owned()),
1352                invite_link.clone(),
1353            ),
1354            Self::new(
1355                false,
1356                None,
1357                Some("@alice:example.com".to_owned()),
1358                None,
1359                invite_link.clone(),
1360            ),
1361            Self::new(false, None, None, None, invite_link),
1362        ])
1363    }
1364}
1365
1366/// Fields of the email verification form
1367#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1368#[serde(rename_all = "snake_case")]
1369pub enum RegisterStepsVerifyEmailFormField {
1370    /// The code field
1371    Code,
1372}
1373
1374impl FormField for RegisterStepsVerifyEmailFormField {
1375    fn keep(&self) -> bool {
1376        match self {
1377            Self::Code => true,
1378        }
1379    }
1380}
1381
1382/// Context used by the `pages/register/steps/verify_email.html` templates
1383#[derive(Serialize)]
1384pub struct RegisterStepsVerifyEmailContext {
1385    form: FormState<RegisterStepsVerifyEmailFormField>,
1386    authentication: UserEmailAuthentication,
1387}
1388
1389impl RegisterStepsVerifyEmailContext {
1390    /// Constructs a context for the email verification page
1391    #[must_use]
1392    pub fn new(authentication: UserEmailAuthentication) -> Self {
1393        Self {
1394            form: FormState::default(),
1395            authentication,
1396        }
1397    }
1398
1399    /// Set the form state
1400    #[must_use]
1401    pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
1402        Self { form, ..self }
1403    }
1404}
1405
1406impl TemplateContext for RegisterStepsVerifyEmailContext {
1407    fn sample<R: RngExt>(
1408        now: chrono::DateTime<Utc>,
1409        rng: &mut R,
1410        _locales: &[DataLocale],
1411    ) -> BTreeMap<SampleIdentifier, Self>
1412    where
1413        Self: Sized,
1414    {
1415        let authentication = UserEmailAuthentication {
1416            id: Ulid::from_datetime_with_rng(now, rng),
1417            user_session_id: None,
1418            user_registration_id: None,
1419            email: "foobar@example.com".to_owned(),
1420            created_at: now,
1421            completed_at: None,
1422        };
1423
1424        sample_list(vec![Self {
1425            form: FormState::default(),
1426            authentication,
1427        }])
1428    }
1429}
1430
1431/// Context used by the `pages/register/steps/email_in_use.html` template
1432#[derive(Serialize)]
1433pub struct RegisterStepsEmailInUseContext {
1434    email: String,
1435    action: Option<PostAuthAction>,
1436}
1437
1438impl RegisterStepsEmailInUseContext {
1439    /// Constructs a context for the email in use page
1440    #[must_use]
1441    pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
1442        Self { email, action }
1443    }
1444}
1445
1446impl TemplateContext for RegisterStepsEmailInUseContext {
1447    fn sample<R: RngExt>(
1448        _now: chrono::DateTime<Utc>,
1449        _rng: &mut R,
1450        _locales: &[DataLocale],
1451    ) -> BTreeMap<SampleIdentifier, Self>
1452    where
1453        Self: Sized,
1454    {
1455        let email = "hello@example.com".to_owned();
1456        let action = PostAuthAction::continue_grant(Ulid::nil());
1457        sample_list(vec![Self::new(email, Some(action))])
1458    }
1459}
1460
1461/// Fields for the display name form
1462#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1463#[serde(rename_all = "snake_case")]
1464pub enum RegisterStepsDisplayNameFormField {
1465    /// The display name
1466    DisplayName,
1467}
1468
1469impl FormField for RegisterStepsDisplayNameFormField {
1470    fn keep(&self) -> bool {
1471        match self {
1472            Self::DisplayName => true,
1473        }
1474    }
1475}
1476
1477/// Context used by the `display_name.html` template
1478#[derive(Serialize, Default)]
1479pub struct RegisterStepsDisplayNameContext {
1480    form: FormState<RegisterStepsDisplayNameFormField>,
1481}
1482
1483impl RegisterStepsDisplayNameContext {
1484    /// Constructs a context for the display name page
1485    #[must_use]
1486    pub fn new() -> Self {
1487        Self::default()
1488    }
1489
1490    /// Set the form state
1491    #[must_use]
1492    pub fn with_form_state(
1493        mut self,
1494        form_state: FormState<RegisterStepsDisplayNameFormField>,
1495    ) -> Self {
1496        self.form = form_state;
1497        self
1498    }
1499}
1500
1501impl TemplateContext for RegisterStepsDisplayNameContext {
1502    fn sample<R: RngExt>(
1503        _now: chrono::DateTime<chrono::Utc>,
1504        _rng: &mut R,
1505        _locales: &[DataLocale],
1506    ) -> BTreeMap<SampleIdentifier, Self>
1507    where
1508        Self: Sized,
1509    {
1510        sample_list(vec![Self {
1511            form: FormState::default(),
1512        }])
1513    }
1514}
1515
1516/// Fields of the registration token form
1517#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1518#[serde(rename_all = "snake_case")]
1519pub enum RegisterStepsRegistrationTokenFormField {
1520    /// The registration token
1521    Token,
1522}
1523
1524impl FormField for RegisterStepsRegistrationTokenFormField {
1525    fn keep(&self) -> bool {
1526        match self {
1527            Self::Token => true,
1528        }
1529    }
1530}
1531
1532/// The registration token page context
1533#[derive(Serialize, Default)]
1534pub struct RegisterStepsRegistrationTokenContext {
1535    form: FormState<RegisterStepsRegistrationTokenFormField>,
1536}
1537
1538impl RegisterStepsRegistrationTokenContext {
1539    /// Constructs a context for the registration token page
1540    #[must_use]
1541    pub fn new() -> Self {
1542        Self::default()
1543    }
1544
1545    /// Set the form state
1546    #[must_use]
1547    pub fn with_form_state(
1548        mut self,
1549        form_state: FormState<RegisterStepsRegistrationTokenFormField>,
1550    ) -> Self {
1551        self.form = form_state;
1552        self
1553    }
1554}
1555
1556impl TemplateContext for RegisterStepsRegistrationTokenContext {
1557    fn sample<R: RngExt>(
1558        _now: chrono::DateTime<chrono::Utc>,
1559        _rng: &mut R,
1560        _locales: &[DataLocale],
1561    ) -> BTreeMap<SampleIdentifier, Self>
1562    where
1563        Self: Sized,
1564    {
1565        sample_list(vec![Self {
1566            form: FormState::default(),
1567        }])
1568    }
1569}
1570
1571/// Fields of the account recovery start form
1572#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1573#[serde(rename_all = "snake_case")]
1574pub enum RecoveryStartFormField {
1575    /// The email
1576    Email,
1577}
1578
1579impl FormField for RecoveryStartFormField {
1580    fn keep(&self) -> bool {
1581        match self {
1582            Self::Email => true,
1583        }
1584    }
1585}
1586
1587/// Context used by the `pages/recovery/start.html` template
1588#[derive(Serialize, Default)]
1589pub struct RecoveryStartContext {
1590    form: FormState<RecoveryStartFormField>,
1591}
1592
1593impl RecoveryStartContext {
1594    /// Constructs a context for the recovery start page
1595    #[must_use]
1596    pub fn new() -> Self {
1597        Self::default()
1598    }
1599
1600    /// Set the form state
1601    #[must_use]
1602    pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1603        Self { form }
1604    }
1605}
1606
1607impl TemplateContext for RecoveryStartContext {
1608    fn sample<R: RngExt>(
1609        _now: chrono::DateTime<Utc>,
1610        _rng: &mut R,
1611        _locales: &[DataLocale],
1612    ) -> BTreeMap<SampleIdentifier, Self>
1613    where
1614        Self: Sized,
1615    {
1616        sample_list(vec![
1617            Self::new(),
1618            Self::new().with_form_state(
1619                FormState::default()
1620                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1621            ),
1622            Self::new().with_form_state(
1623                FormState::default()
1624                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1625            ),
1626        ])
1627    }
1628}
1629
1630/// Context used by the `pages/recovery/progress.html` template
1631#[derive(Serialize)]
1632pub struct RecoveryProgressContext {
1633    session: UserRecoverySession,
1634    /// Whether resending the e-mail was denied because of rate limits
1635    resend_failed_due_to_rate_limit: bool,
1636}
1637
1638impl RecoveryProgressContext {
1639    /// Constructs a context for the recovery progress page
1640    #[must_use]
1641    pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1642        Self {
1643            session,
1644            resend_failed_due_to_rate_limit,
1645        }
1646    }
1647}
1648
1649impl TemplateContext for RecoveryProgressContext {
1650    fn sample<R: RngExt>(
1651        now: chrono::DateTime<Utc>,
1652        rng: &mut R,
1653        _locales: &[DataLocale],
1654    ) -> BTreeMap<SampleIdentifier, Self>
1655    where
1656        Self: Sized,
1657    {
1658        let session = UserRecoverySession {
1659            id: Ulid::from_datetime_with_rng(now, rng),
1660            email: "name@mail.com".to_owned(),
1661            user_agent: "Mozilla/5.0".to_owned(),
1662            ip_address: None,
1663            locale: "en".to_owned(),
1664            created_at: now,
1665            consumed_at: None,
1666        };
1667
1668        sample_list(vec![
1669            Self {
1670                session: session.clone(),
1671                resend_failed_due_to_rate_limit: false,
1672            },
1673            Self {
1674                session,
1675                resend_failed_due_to_rate_limit: true,
1676            },
1677        ])
1678    }
1679}
1680
1681/// Context used by the `pages/recovery/expired.html` template
1682#[derive(Serialize)]
1683pub struct RecoveryExpiredContext {
1684    session: UserRecoverySession,
1685}
1686
1687impl RecoveryExpiredContext {
1688    /// Constructs a context for the recovery expired page
1689    #[must_use]
1690    pub fn new(session: UserRecoverySession) -> Self {
1691        Self { session }
1692    }
1693}
1694
1695impl TemplateContext for RecoveryExpiredContext {
1696    fn sample<R: RngExt>(
1697        now: chrono::DateTime<Utc>,
1698        rng: &mut R,
1699        _locales: &[DataLocale],
1700    ) -> BTreeMap<SampleIdentifier, Self>
1701    where
1702        Self: Sized,
1703    {
1704        let session = UserRecoverySession {
1705            id: Ulid::from_datetime_with_rng(now, rng),
1706            email: "name@mail.com".to_owned(),
1707            user_agent: "Mozilla/5.0".to_owned(),
1708            ip_address: None,
1709            locale: "en".to_owned(),
1710            created_at: now,
1711            consumed_at: None,
1712        };
1713
1714        sample_list(vec![Self { session }])
1715    }
1716}
1717/// Fields of the account recovery finish form
1718#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1719#[serde(rename_all = "snake_case")]
1720pub enum RecoveryFinishFormField {
1721    /// The new password
1722    NewPassword,
1723
1724    /// The new password confirmation
1725    NewPasswordConfirm,
1726}
1727
1728impl FormField for RecoveryFinishFormField {
1729    fn keep(&self) -> bool {
1730        false
1731    }
1732}
1733
1734/// Context used by the `pages/recovery/finish.html` template
1735#[derive(Serialize)]
1736pub struct RecoveryFinishContext {
1737    user: User,
1738    form: FormState<RecoveryFinishFormField>,
1739}
1740
1741impl RecoveryFinishContext {
1742    /// Constructs a context for the recovery finish page
1743    #[must_use]
1744    pub fn new(user: User) -> Self {
1745        Self {
1746            user,
1747            form: FormState::default(),
1748        }
1749    }
1750
1751    /// Set the form state
1752    #[must_use]
1753    pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1754        self.form = form;
1755        self
1756    }
1757}
1758
1759impl TemplateContext for RecoveryFinishContext {
1760    fn sample<R: RngExt>(
1761        now: chrono::DateTime<Utc>,
1762        rng: &mut R,
1763        _locales: &[DataLocale],
1764    ) -> BTreeMap<SampleIdentifier, Self>
1765    where
1766        Self: Sized,
1767    {
1768        sample_list(
1769            User::samples(now, rng)
1770                .into_iter()
1771                .flat_map(|user| {
1772                    vec![
1773                        Self::new(user.clone()),
1774                        Self::new(user.clone()).with_form_state(
1775                            FormState::default().with_error_on_field(
1776                                RecoveryFinishFormField::NewPassword,
1777                                FieldError::Invalid,
1778                            ),
1779                        ),
1780                        Self::new(user.clone()).with_form_state(
1781                            FormState::default().with_error_on_field(
1782                                RecoveryFinishFormField::NewPasswordConfirm,
1783                                FieldError::Invalid,
1784                            ),
1785                        ),
1786                    ]
1787                })
1788                .collect(),
1789        )
1790    }
1791}
1792
1793/// Context used by the `pages/upstream_oauth2/link_mismatch.html`
1794/// templates
1795#[derive(Serialize)]
1796pub struct UpstreamExistingLinkContext {
1797    linked_user: User,
1798}
1799
1800impl UpstreamExistingLinkContext {
1801    /// Constructs a new context with an existing linked user
1802    #[must_use]
1803    pub fn new(linked_user: User) -> Self {
1804        Self { linked_user }
1805    }
1806}
1807
1808impl TemplateContext for UpstreamExistingLinkContext {
1809    fn sample<R: RngExt>(
1810        now: chrono::DateTime<Utc>,
1811        rng: &mut R,
1812        _locales: &[DataLocale],
1813    ) -> BTreeMap<SampleIdentifier, Self>
1814    where
1815        Self: Sized,
1816    {
1817        sample_list(
1818            User::samples(now, rng)
1819                .into_iter()
1820                .map(|linked_user| Self { linked_user })
1821                .collect(),
1822        )
1823    }
1824}
1825
1826/// Context used by the `pages/upstream_oauth2/suggest_link.html`
1827/// templates
1828#[derive(Serialize)]
1829pub struct UpstreamSuggestLink {
1830    post_logout_action: PostAuthAction,
1831}
1832
1833impl UpstreamSuggestLink {
1834    /// Constructs a new context with an existing linked user
1835    #[must_use]
1836    pub fn new(link: &UpstreamOAuthLink) -> Self {
1837        Self::for_link_id(link.id)
1838    }
1839
1840    fn for_link_id(id: Ulid) -> Self {
1841        let post_logout_action = PostAuthAction::link_upstream(id);
1842        Self { post_logout_action }
1843    }
1844}
1845
1846impl TemplateContext for UpstreamSuggestLink {
1847    fn sample<R: RngExt>(
1848        now: chrono::DateTime<Utc>,
1849        rng: &mut R,
1850        _locales: &[DataLocale],
1851    ) -> BTreeMap<SampleIdentifier, Self>
1852    where
1853        Self: Sized,
1854    {
1855        let id = Ulid::from_datetime_with_rng(now, rng);
1856        sample_list(vec![Self::for_link_id(id)])
1857    }
1858}
1859
1860/// User-editeable fields of the upstream account link form
1861#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1862#[serde(rename_all = "snake_case")]
1863pub enum UpstreamRegisterFormField {
1864    /// The username field
1865    Username,
1866
1867    /// Accept the terms of service
1868    AcceptTerms,
1869}
1870
1871impl FormField for UpstreamRegisterFormField {
1872    fn keep(&self) -> bool {
1873        match self {
1874            Self::Username | Self::AcceptTerms => true,
1875        }
1876    }
1877}
1878
1879/// Context used by the `pages/upstream_oauth2/do_register.html`
1880/// templates
1881#[derive(Serialize)]
1882pub struct UpstreamRegister {
1883    upstream_oauth_link: UpstreamOAuthLink,
1884    upstream_oauth_provider: UpstreamOAuthProvider,
1885    imported_localpart: Option<String>,
1886    force_localpart: bool,
1887    imported_display_name: Option<String>,
1888    force_display_name: bool,
1889    imported_email: Option<String>,
1890    force_email: bool,
1891    form_state: FormState<UpstreamRegisterFormField>,
1892}
1893
1894impl UpstreamRegister {
1895    /// Constructs a new context for registering a new user from an upstream
1896    /// provider
1897    #[must_use]
1898    pub fn new(
1899        upstream_oauth_link: UpstreamOAuthLink,
1900        upstream_oauth_provider: UpstreamOAuthProvider,
1901    ) -> Self {
1902        Self {
1903            upstream_oauth_link,
1904            upstream_oauth_provider,
1905            imported_localpart: None,
1906            force_localpart: false,
1907            imported_display_name: None,
1908            force_display_name: false,
1909            imported_email: None,
1910            force_email: false,
1911            form_state: FormState::default(),
1912        }
1913    }
1914
1915    /// Set the imported localpart
1916    pub fn set_localpart(&mut self, localpart: String, force: bool) {
1917        self.imported_localpart = Some(localpart);
1918        self.force_localpart = force;
1919    }
1920
1921    /// Set the imported localpart
1922    #[must_use]
1923    pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1924        Self {
1925            imported_localpart: Some(localpart),
1926            force_localpart: force,
1927            ..self
1928        }
1929    }
1930
1931    /// Set the imported display name
1932    pub fn set_display_name(&mut self, display_name: String, force: bool) {
1933        self.imported_display_name = Some(display_name);
1934        self.force_display_name = force;
1935    }
1936
1937    /// Set the imported display name
1938    #[must_use]
1939    pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1940        Self {
1941            imported_display_name: Some(display_name),
1942            force_display_name: force,
1943            ..self
1944        }
1945    }
1946
1947    /// Set the imported email
1948    pub fn set_email(&mut self, email: String, force: bool) {
1949        self.imported_email = Some(email);
1950        self.force_email = force;
1951    }
1952
1953    /// Set the imported email
1954    #[must_use]
1955    pub fn with_email(self, email: String, force: bool) -> Self {
1956        Self {
1957            imported_email: Some(email),
1958            force_email: force,
1959            ..self
1960        }
1961    }
1962
1963    /// Set the form state
1964    pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1965        self.form_state = form_state;
1966    }
1967
1968    /// Set the form state
1969    #[must_use]
1970    pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1971        Self { form_state, ..self }
1972    }
1973}
1974
1975impl TemplateContext for UpstreamRegister {
1976    fn sample<R: RngExt>(
1977        now: chrono::DateTime<Utc>,
1978        _rng: &mut R,
1979        _locales: &[DataLocale],
1980    ) -> BTreeMap<SampleIdentifier, Self>
1981    where
1982        Self: Sized,
1983    {
1984        sample_list(vec![Self::new(
1985            UpstreamOAuthLink {
1986                id: Ulid::nil(),
1987                provider_id: Ulid::nil(),
1988                user_id: None,
1989                subject: "subject".to_owned(),
1990                human_account_name: Some("@john".to_owned()),
1991                created_at: now,
1992            },
1993            UpstreamOAuthProvider {
1994                id: Ulid::nil(),
1995                issuer: Some("https://example.com/".to_owned()),
1996                human_name: Some("Example Ltd.".to_owned()),
1997                brand_name: None,
1998                scope: Scope::from_iter([OPENID]),
1999                token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
2000                token_endpoint_signing_alg: None,
2001                id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
2002                client_id: "client-id".to_owned(),
2003                encrypted_client_secret: None,
2004                claims_imports: UpstreamOAuthProviderClaimsImports::default(),
2005                authorization_endpoint_override: None,
2006                token_endpoint_override: None,
2007                jwks_uri_override: None,
2008                userinfo_endpoint_override: None,
2009                fetch_userinfo: false,
2010                userinfo_signed_response_alg: None,
2011                discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
2012                pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
2013                response_mode: None,
2014                additional_authorization_parameters: Vec::new(),
2015                forward_login_hint: false,
2016                created_at: now,
2017                disabled_at: None,
2018                on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
2019                registration_token_required: false,
2020            },
2021        )])
2022    }
2023}
2024
2025/// Form fields on the device link page
2026#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
2027#[serde(rename_all = "snake_case")]
2028pub enum DeviceLinkFormField {
2029    /// The device code field
2030    Code,
2031}
2032
2033impl FormField for DeviceLinkFormField {
2034    fn keep(&self) -> bool {
2035        match self {
2036            Self::Code => true,
2037        }
2038    }
2039}
2040
2041/// Context used by the `device_link.html` template
2042#[derive(Serialize, Default, Debug)]
2043pub struct DeviceLinkContext {
2044    form_state: FormState<DeviceLinkFormField>,
2045}
2046
2047impl DeviceLinkContext {
2048    /// Constructs a new context with an existing linked user
2049    #[must_use]
2050    pub fn new() -> Self {
2051        Self::default()
2052    }
2053
2054    /// Set the form state
2055    #[must_use]
2056    pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
2057        self.form_state = form_state;
2058        self
2059    }
2060}
2061
2062impl TemplateContext for DeviceLinkContext {
2063    fn sample<R: RngExt>(
2064        _now: chrono::DateTime<Utc>,
2065        _rng: &mut R,
2066        _locales: &[DataLocale],
2067    ) -> BTreeMap<SampleIdentifier, Self>
2068    where
2069        Self: Sized,
2070    {
2071        sample_list(vec![
2072            Self::new(),
2073            Self::new().with_form_state(
2074                FormState::default()
2075                    .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
2076            ),
2077        ])
2078    }
2079}
2080
2081/// Context used by the `device_consent.html` template
2082#[derive(Serialize, Debug)]
2083pub struct DeviceConsentContext {
2084    grant: DeviceCodeGrant,
2085    client: Client,
2086    matrix_user: MatrixUser,
2087}
2088
2089impl DeviceConsentContext {
2090    /// Constructs a new context with an existing linked user
2091    #[must_use]
2092    pub fn new(grant: DeviceCodeGrant, client: Client, matrix_user: MatrixUser) -> Self {
2093        Self {
2094            grant,
2095            client,
2096            matrix_user,
2097        }
2098    }
2099}
2100
2101impl TemplateContext for DeviceConsentContext {
2102    fn sample<R: RngExt>(
2103        now: chrono::DateTime<Utc>,
2104        rng: &mut R,
2105        _locales: &[DataLocale],
2106    ) -> BTreeMap<SampleIdentifier, Self>
2107    where
2108        Self: Sized,
2109    {
2110        sample_list(Client::samples(now, rng)
2111            .into_iter()
2112            .map(|client|  {
2113                let grant = DeviceCodeGrant {
2114                    id: Ulid::from_datetime_with_rng(now, rng),
2115                    state: mas_data_model::DeviceCodeGrantState::Pending,
2116                    client_id: client.id,
2117                    scope: [OPENID].into_iter().collect(),
2118                    user_code: mas_data_model::generate_user_code(rng),
2119                    device_code: Alphanumeric.sample_string(rng, 32),
2120                    created_at: now - Duration::try_minutes(5).unwrap(),
2121                    expires_at: now + Duration::try_minutes(25).unwrap(),
2122                    ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
2123                    user_agent: Some("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned()),
2124                    locale: None,
2125                };
2126                Self {
2127                    grant,
2128                    client,
2129                    matrix_user: MatrixUser {
2130                        mxid: "@alice:example.com".to_owned(),
2131                        display_name: Some("Alice".to_owned()),
2132                    }
2133                }
2134            })
2135            .collect())
2136    }
2137}
2138
2139/// Context used by the `account/deactivated.html`, `account/locked.html` and
2140/// `account/logged_out.html` templates
2141#[derive(Serialize)]
2142pub struct AccountInactiveContext {
2143    user: User,
2144
2145    /// The action to continue after signing out and back in from the
2146    /// interstitial. Absent when there is no continuation to preserve.
2147    #[serde(skip_serializing_if = "Option::is_none")]
2148    post_logout_action: Option<PostAuthAction>,
2149}
2150
2151impl AccountInactiveContext {
2152    /// Constructs a new context with an existing linked user
2153    #[must_use]
2154    pub fn new(user: User) -> Self {
2155        Self {
2156            user,
2157            post_logout_action: None,
2158        }
2159    }
2160
2161    /// Set the action to continue once the user has signed out and back in
2162    #[must_use]
2163    pub fn with_post_auth_action(mut self, action: Option<PostAuthAction>) -> Self {
2164        self.post_logout_action = action;
2165        self
2166    }
2167}
2168
2169impl TemplateContext for AccountInactiveContext {
2170    fn sample<R: RngExt>(
2171        now: chrono::DateTime<Utc>,
2172        rng: &mut R,
2173        _locales: &[DataLocale],
2174    ) -> BTreeMap<SampleIdentifier, Self>
2175    where
2176        Self: Sized,
2177    {
2178        let action = PostAuthAction::continue_grant(Ulid::from_datetime_with_rng(now, rng));
2179        sample_list(
2180            User::samples(now, rng)
2181                .into_iter()
2182                .flat_map(|user| {
2183                    // Cover both the "no continuation" and "with continuation" render
2184                    // paths so the template gallery exercises the hidden inputs.
2185                    [
2186                        AccountInactiveContext::new(user.clone()),
2187                        AccountInactiveContext::new(user)
2188                            .with_post_auth_action(Some(action.clone())),
2189                    ]
2190                })
2191                .collect(),
2192        )
2193    }
2194}
2195
2196/// Context used by the `device_name.txt` template
2197#[derive(Serialize)]
2198pub struct DeviceNameContext {
2199    client: Client,
2200    raw_user_agent: String,
2201}
2202
2203impl DeviceNameContext {
2204    /// Constructs a new context with a client and user agent
2205    #[must_use]
2206    pub fn new(client: Client, user_agent: Option<String>) -> Self {
2207        Self {
2208            client,
2209            raw_user_agent: user_agent.unwrap_or_default(),
2210        }
2211    }
2212}
2213
2214impl TemplateContext for DeviceNameContext {
2215    fn sample<R: RngExt>(
2216        now: chrono::DateTime<Utc>,
2217        rng: &mut R,
2218        _locales: &[DataLocale],
2219    ) -> BTreeMap<SampleIdentifier, Self>
2220    where
2221        Self: Sized,
2222    {
2223        sample_list(Client::samples(now, rng)
2224            .into_iter()
2225            .map(|client| DeviceNameContext {
2226                client,
2227                raw_user_agent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.0.0 Safari/537.36".to_owned(),
2228            })
2229            .collect())
2230    }
2231}
2232
2233/// Context used by the `form_post.html` template
2234#[derive(Serialize)]
2235pub struct FormPostContext<T> {
2236    redirect_uri: Option<Url>,
2237    params: T,
2238}
2239
2240impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
2241    fn sample<R: RngExt>(
2242        now: chrono::DateTime<Utc>,
2243        rng: &mut R,
2244        locales: &[DataLocale],
2245    ) -> BTreeMap<SampleIdentifier, Self>
2246    where
2247        Self: Sized,
2248    {
2249        let sample_params = T::sample(now, rng, locales);
2250        sample_params
2251            .into_iter()
2252            .map(|(k, params)| {
2253                (
2254                    k,
2255                    FormPostContext {
2256                        redirect_uri: "https://example.com/callback".parse().ok(),
2257                        params,
2258                    },
2259                )
2260            })
2261            .collect()
2262    }
2263}
2264
2265impl<T> FormPostContext<T> {
2266    /// Constructs a context for the `form_post` response mode form for a given
2267    /// URL
2268    pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
2269        Self {
2270            redirect_uri: Some(redirect_uri),
2271            params,
2272        }
2273    }
2274
2275    /// Constructs a context for the `form_post` response mode form for the
2276    /// current URL
2277    pub fn new_for_current_url(params: T) -> Self {
2278        Self {
2279            redirect_uri: None,
2280            params,
2281        }
2282    }
2283
2284    /// Add the language to the context
2285    ///
2286    /// This is usually implemented by the [`TemplateContext`] trait, but it is
2287    /// annoying to make it work because of the generic parameter
2288    pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
2289        WithLanguage {
2290            lang: lang.to_string(),
2291            inner: self,
2292        }
2293    }
2294}
2295
2296/// Context used by the `error.html` template
2297#[derive(Default, Serialize, Debug, Clone)]
2298pub struct ErrorContext {
2299    code: Option<&'static str>,
2300    description: Option<String>,
2301    details: Option<String>,
2302    lang: Option<String>,
2303}
2304
2305impl std::fmt::Display for ErrorContext {
2306    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2307        if let Some(code) = &self.code {
2308            writeln!(f, "code: {code}")?;
2309        }
2310        if let Some(description) = &self.description {
2311            writeln!(f, "{description}")?;
2312        }
2313
2314        if let Some(details) = &self.details {
2315            writeln!(f, "details: {details}")?;
2316        }
2317
2318        Ok(())
2319    }
2320}
2321
2322impl TemplateContext for ErrorContext {
2323    fn sample<R: RngExt>(
2324        _now: chrono::DateTime<Utc>,
2325        _rng: &mut R,
2326        _locales: &[DataLocale],
2327    ) -> BTreeMap<SampleIdentifier, Self>
2328    where
2329        Self: Sized,
2330    {
2331        sample_list(vec![
2332            Self::new()
2333                .with_code("sample_error")
2334                .with_description("A fancy description".into())
2335                .with_details("Something happened".into()),
2336            Self::new().with_code("another_error"),
2337            Self::new(),
2338        ])
2339    }
2340}
2341
2342impl ErrorContext {
2343    /// Constructs a context for the error page
2344    #[must_use]
2345    pub fn new() -> Self {
2346        Self::default()
2347    }
2348
2349    /// Add the error code to the context
2350    #[must_use]
2351    pub fn with_code(mut self, code: &'static str) -> Self {
2352        self.code = Some(code);
2353        self
2354    }
2355
2356    /// Add the error description to the context
2357    #[must_use]
2358    pub fn with_description(mut self, description: String) -> Self {
2359        self.description = Some(description);
2360        self
2361    }
2362
2363    /// Add the error details to the context
2364    #[must_use]
2365    pub fn with_details(mut self, details: String) -> Self {
2366        self.details = Some(details);
2367        self
2368    }
2369
2370    /// Add the language to the context
2371    #[must_use]
2372    pub fn with_language(mut self, lang: &DataLocale) -> Self {
2373        self.lang = Some(lang.to_string());
2374        self
2375    }
2376
2377    /// Get the error code, if any
2378    #[must_use]
2379    pub fn code(&self) -> Option<&'static str> {
2380        self.code
2381    }
2382
2383    /// Get the description, if any
2384    #[must_use]
2385    pub fn description(&self) -> Option<&str> {
2386        self.description.as_deref()
2387    }
2388
2389    /// Get the details, if any
2390    #[must_use]
2391    pub fn details(&self) -> Option<&str> {
2392        self.details.as_deref()
2393    }
2394}
2395
2396/// Context used by the not found (`404.html`) template
2397#[derive(Serialize)]
2398pub struct NotFoundContext {
2399    method: String,
2400    version: String,
2401    uri: String,
2402}
2403
2404impl NotFoundContext {
2405    /// Constructs a context for the not found page
2406    #[must_use]
2407    pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
2408        Self {
2409            method: method.to_string(),
2410            version: format!("{version:?}"),
2411            uri: uri.to_string(),
2412        }
2413    }
2414}
2415
2416impl TemplateContext for NotFoundContext {
2417    fn sample<R: RngExt>(
2418        _now: DateTime<Utc>,
2419        _rng: &mut R,
2420        _locales: &[DataLocale],
2421    ) -> BTreeMap<SampleIdentifier, Self>
2422    where
2423        Self: Sized,
2424    {
2425        sample_list(vec![
2426            Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
2427            Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
2428            Self::new(
2429                &Method::PUT,
2430                Version::HTTP_10,
2431                &"/foo?bar=baz".parse().unwrap(),
2432            ),
2433        ])
2434    }
2435}