mas_templates/
context.rs

1// Copyright 2024, 2025 New Vector Ltd.
2// Copyright 2021-2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7//! Contexts used in templates
8
9mod branding;
10mod captcha;
11mod ext;
12mod features;
13
14use std::{
15    fmt::Formatter,
16    net::{IpAddr, Ipv4Addr},
17};
18
19use chrono::{DateTime, Duration, Utc};
20use http::{Method, Uri, Version};
21use mas_data_model::{
22    AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
23    DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
24    UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode,
25    UpstreamOAuthProviderTokenAuthMethod, User, UserAgent, UserEmailAuthentication,
26    UserEmailAuthenticationCode, UserRecoverySession, UserRegistration,
27};
28use mas_i18n::DataLocale;
29use mas_iana::jose::JsonWebSignatureAlg;
30use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder};
31use oauth2_types::scope::{OPENID, Scope};
32use rand::{
33    Rng,
34    distributions::{Alphanumeric, DistString},
35};
36use serde::{Deserialize, Serialize, ser::SerializeStruct};
37use ulid::Ulid;
38use url::Url;
39
40pub use self::{
41    branding::SiteBranding, captcha::WithCaptcha, ext::SiteConfigExt, features::SiteFeatures,
42};
43use crate::{FieldError, FormField, FormState};
44
45/// Helper trait to construct context wrappers
46pub trait TemplateContext: Serialize {
47    /// Attach a user session to the template context
48    fn with_session(self, current_session: BrowserSession) -> WithSession<Self>
49    where
50        Self: Sized,
51    {
52        WithSession {
53            current_session,
54            inner: self,
55        }
56    }
57
58    /// Attach an optional user session to the template context
59    fn maybe_with_session(
60        self,
61        current_session: Option<BrowserSession>,
62    ) -> WithOptionalSession<Self>
63    where
64        Self: Sized,
65    {
66        WithOptionalSession {
67            current_session,
68            inner: self,
69        }
70    }
71
72    /// Attach a CSRF token to the template context
73    fn with_csrf<C>(self, csrf_token: C) -> WithCsrf<Self>
74    where
75        Self: Sized,
76        C: ToString,
77    {
78        // TODO: make this method use a CsrfToken again
79        WithCsrf {
80            csrf_token: csrf_token.to_string(),
81            inner: self,
82        }
83    }
84
85    /// Attach a language to the template context
86    fn with_language(self, lang: DataLocale) -> WithLanguage<Self>
87    where
88        Self: Sized,
89    {
90        WithLanguage {
91            lang: lang.to_string(),
92            inner: self,
93        }
94    }
95
96    /// Attach a CAPTCHA configuration to the template context
97    fn with_captcha(self, captcha: Option<mas_data_model::CaptchaConfig>) -> WithCaptcha<Self>
98    where
99        Self: Sized,
100    {
101        WithCaptcha::new(captcha, self)
102    }
103
104    /// Generate sample values for this context type
105    ///
106    /// This is then used to check for template validity in unit tests and in
107    /// the CLI (`cargo run -- templates check`)
108    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
109    where
110        Self: Sized;
111}
112
113impl TemplateContext for () {
114    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
115    where
116        Self: Sized,
117    {
118        Vec::new()
119    }
120}
121
122/// Context with a specified locale in it
123#[derive(Serialize, Debug)]
124pub struct WithLanguage<T> {
125    lang: String,
126
127    #[serde(flatten)]
128    inner: T,
129}
130
131impl<T> WithLanguage<T> {
132    /// Get the language of this context
133    pub fn language(&self) -> &str {
134        &self.lang
135    }
136}
137
138impl<T> std::ops::Deref for WithLanguage<T> {
139    type Target = T;
140
141    fn deref(&self) -> &Self::Target {
142        &self.inner
143    }
144}
145
146impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
147    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
148    where
149        Self: Sized,
150    {
151        T::sample(now, rng)
152            .into_iter()
153            .map(|inner| WithLanguage {
154                lang: "en".into(),
155                inner,
156            })
157            .collect()
158    }
159}
160
161/// Context with a CSRF token in it
162#[derive(Serialize, Debug)]
163pub struct WithCsrf<T> {
164    csrf_token: String,
165
166    #[serde(flatten)]
167    inner: T,
168}
169
170impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
171    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
172    where
173        Self: Sized,
174    {
175        T::sample(now, rng)
176            .into_iter()
177            .map(|inner| WithCsrf {
178                csrf_token: "fake_csrf_token".into(),
179                inner,
180            })
181            .collect()
182    }
183}
184
185/// Context with a user session in it
186#[derive(Serialize)]
187pub struct WithSession<T> {
188    current_session: BrowserSession,
189
190    #[serde(flatten)]
191    inner: T,
192}
193
194impl<T: TemplateContext> TemplateContext for WithSession<T> {
195    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
196    where
197        Self: Sized,
198    {
199        BrowserSession::samples(now, rng)
200            .into_iter()
201            .flat_map(|session| {
202                T::sample(now, rng)
203                    .into_iter()
204                    .map(move |inner| WithSession {
205                        current_session: session.clone(),
206                        inner,
207                    })
208            })
209            .collect()
210    }
211}
212
213/// Context with an optional user session in it
214#[derive(Serialize)]
215pub struct WithOptionalSession<T> {
216    current_session: Option<BrowserSession>,
217
218    #[serde(flatten)]
219    inner: T,
220}
221
222impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
223    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
224    where
225        Self: Sized,
226    {
227        BrowserSession::samples(now, rng)
228            .into_iter()
229            .map(Some) // Wrap all samples in an Option
230            .chain(std::iter::once(None)) // Add the "None" option
231            .flat_map(|session| {
232                T::sample(now, rng)
233                    .into_iter()
234                    .map(move |inner| WithOptionalSession {
235                        current_session: session.clone(),
236                        inner,
237                    })
238            })
239            .collect()
240    }
241}
242
243/// An empty context used for composition
244pub struct EmptyContext;
245
246impl Serialize for EmptyContext {
247    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
248    where
249        S: serde::Serializer,
250    {
251        let mut s = serializer.serialize_struct("EmptyContext", 0)?;
252        // FIXME: for some reason, serde seems to not like struct flattening with empty
253        // stuff
254        s.serialize_field("__UNUSED", &())?;
255        s.end()
256    }
257}
258
259impl TemplateContext for EmptyContext {
260    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
261    where
262        Self: Sized,
263    {
264        vec![EmptyContext]
265    }
266}
267
268/// Context used by the `index.html` template
269#[derive(Serialize)]
270pub struct IndexContext {
271    discovery_url: Url,
272}
273
274impl IndexContext {
275    /// Constructs the context for the index page from the OIDC discovery
276    /// document URL
277    #[must_use]
278    pub fn new(discovery_url: Url) -> Self {
279        Self { discovery_url }
280    }
281}
282
283impl TemplateContext for IndexContext {
284    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
285    where
286        Self: Sized,
287    {
288        vec![Self {
289            discovery_url: "https://example.com/.well-known/openid-configuration"
290                .parse()
291                .unwrap(),
292        }]
293    }
294}
295
296/// Config used by the frontend app
297#[derive(Serialize)]
298#[serde(rename_all = "camelCase")]
299pub struct AppConfig {
300    root: String,
301    graphql_endpoint: String,
302}
303
304/// Context used by the `app.html` template
305#[derive(Serialize)]
306pub struct AppContext {
307    app_config: AppConfig,
308}
309
310impl AppContext {
311    /// Constructs the context given the [`UrlBuilder`]
312    #[must_use]
313    pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
314        let root = url_builder.relative_url_for(&Account::default());
315        let graphql_endpoint = url_builder.relative_url_for(&GraphQL);
316        Self {
317            app_config: AppConfig {
318                root,
319                graphql_endpoint,
320            },
321        }
322    }
323}
324
325impl TemplateContext for AppContext {
326    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
327    where
328        Self: Sized,
329    {
330        let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
331        vec![Self::from_url_builder(&url_builder)]
332    }
333}
334
335/// Context used by the `swagger/doc.html` template
336#[derive(Serialize)]
337pub struct ApiDocContext {
338    openapi_url: Url,
339    callback_url: Url,
340}
341
342impl ApiDocContext {
343    /// Constructs a context for the API documentation page giben the
344    /// [`UrlBuilder`]
345    #[must_use]
346    pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
347        Self {
348            openapi_url: url_builder.absolute_url_for(&mas_router::ApiSpec),
349            callback_url: url_builder.absolute_url_for(&mas_router::ApiDocCallback),
350        }
351    }
352}
353
354impl TemplateContext for ApiDocContext {
355    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
356    where
357        Self: Sized,
358    {
359        let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
360        vec![Self::from_url_builder(&url_builder)]
361    }
362}
363
364/// Fields of the login form
365#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
366#[serde(rename_all = "snake_case")]
367pub enum LoginFormField {
368    /// The username field
369    Username,
370
371    /// The password field
372    Password,
373}
374
375impl FormField for LoginFormField {
376    fn keep(&self) -> bool {
377        match self {
378            Self::Username => true,
379            Self::Password => false,
380        }
381    }
382}
383
384/// Inner context used in login screen. See [`PostAuthContext`].
385#[derive(Serialize)]
386#[serde(tag = "kind", rename_all = "snake_case")]
387pub enum PostAuthContextInner {
388    /// Continue an authorization grant
389    ContinueAuthorizationGrant {
390        /// The authorization grant that will be continued after authentication
391        grant: Box<AuthorizationGrant>,
392    },
393
394    /// Continue a device code grant
395    ContinueDeviceCodeGrant {
396        /// The device code grant that will be continued after authentication
397        grant: Box<DeviceCodeGrant>,
398    },
399
400    /// Continue legacy login
401    /// TODO: add the login context in there
402    ContinueCompatSsoLogin {
403        /// The compat SSO login request
404        login: Box<CompatSsoLogin>,
405    },
406
407    /// Change the account password
408    ChangePassword,
409
410    /// Link an upstream account
411    LinkUpstream {
412        /// The upstream provider
413        provider: Box<UpstreamOAuthProvider>,
414
415        /// The link
416        link: Box<UpstreamOAuthLink>,
417    },
418
419    /// Go to the account management page
420    ManageAccount,
421}
422
423/// Context used in login screen, for the post-auth action to do
424#[derive(Serialize)]
425pub struct PostAuthContext {
426    /// The post auth action params from the URL
427    pub params: PostAuthAction,
428
429    /// The loaded post auth context
430    #[serde(flatten)]
431    pub ctx: PostAuthContextInner,
432}
433
434/// Context used by the `login.html` template
435#[derive(Serialize, Default)]
436pub struct LoginContext {
437    form: FormState<LoginFormField>,
438    next: Option<PostAuthContext>,
439    providers: Vec<UpstreamOAuthProvider>,
440}
441
442impl TemplateContext for LoginContext {
443    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
444    where
445        Self: Sized,
446    {
447        // TODO: samples with errors
448        vec![
449            LoginContext {
450                form: FormState::default(),
451                next: None,
452                providers: Vec::new(),
453            },
454            LoginContext {
455                form: FormState::default(),
456                next: None,
457                providers: Vec::new(),
458            },
459            LoginContext {
460                form: FormState::default()
461                    .with_error_on_field(LoginFormField::Username, FieldError::Required)
462                    .with_error_on_field(
463                        LoginFormField::Password,
464                        FieldError::Policy {
465                            code: None,
466                            message: "password too short".to_owned(),
467                        },
468                    ),
469                next: None,
470                providers: Vec::new(),
471            },
472            LoginContext {
473                form: FormState::default()
474                    .with_error_on_field(LoginFormField::Username, FieldError::Exists),
475                next: None,
476                providers: Vec::new(),
477            },
478        ]
479    }
480}
481
482impl LoginContext {
483    /// Set the form state
484    #[must_use]
485    pub fn with_form_state(self, form: FormState<LoginFormField>) -> Self {
486        Self { form, ..self }
487    }
488
489    /// Mutably borrow the form state
490    pub fn form_state_mut(&mut self) -> &mut FormState<LoginFormField> {
491        &mut self.form
492    }
493
494    /// Set the upstream OAuth 2.0 providers
495    #[must_use]
496    pub fn with_upstream_providers(self, providers: Vec<UpstreamOAuthProvider>) -> Self {
497        Self { providers, ..self }
498    }
499
500    /// Add a post authentication action to the context
501    #[must_use]
502    pub fn with_post_action(self, context: PostAuthContext) -> Self {
503        Self {
504            next: Some(context),
505            ..self
506        }
507    }
508}
509
510/// Fields of the registration form
511#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
512#[serde(rename_all = "snake_case")]
513pub enum RegisterFormField {
514    /// The username field
515    Username,
516
517    /// The email field
518    Email,
519
520    /// The password field
521    Password,
522
523    /// The password confirmation field
524    PasswordConfirm,
525
526    /// The terms of service agreement field
527    AcceptTerms,
528}
529
530impl FormField for RegisterFormField {
531    fn keep(&self) -> bool {
532        match self {
533            Self::Username | Self::Email | Self::AcceptTerms => true,
534            Self::Password | Self::PasswordConfirm => false,
535        }
536    }
537}
538
539/// Context used by the `register.html` template
540#[derive(Serialize, Default)]
541pub struct RegisterContext {
542    providers: Vec<UpstreamOAuthProvider>,
543    next: Option<PostAuthContext>,
544}
545
546impl TemplateContext for RegisterContext {
547    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
548    where
549        Self: Sized,
550    {
551        vec![RegisterContext {
552            providers: Vec::new(),
553            next: None,
554        }]
555    }
556}
557
558impl RegisterContext {
559    /// Create a new context with the given upstream providers
560    #[must_use]
561    pub fn new(providers: Vec<UpstreamOAuthProvider>) -> Self {
562        Self {
563            providers,
564            next: None,
565        }
566    }
567
568    /// Add a post authentication action to the context
569    #[must_use]
570    pub fn with_post_action(self, next: PostAuthContext) -> Self {
571        Self {
572            next: Some(next),
573            ..self
574        }
575    }
576}
577
578/// Context used by the `password_register.html` template
579#[derive(Serialize, Default)]
580pub struct PasswordRegisterContext {
581    form: FormState<RegisterFormField>,
582    next: Option<PostAuthContext>,
583}
584
585impl TemplateContext for PasswordRegisterContext {
586    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
587    where
588        Self: Sized,
589    {
590        // TODO: samples with errors
591        vec![PasswordRegisterContext {
592            form: FormState::default(),
593            next: None,
594        }]
595    }
596}
597
598impl PasswordRegisterContext {
599    /// Add an error on the registration form
600    #[must_use]
601    pub fn with_form_state(self, form: FormState<RegisterFormField>) -> Self {
602        Self { form, ..self }
603    }
604
605    /// Add a post authentication action to the context
606    #[must_use]
607    pub fn with_post_action(self, next: PostAuthContext) -> Self {
608        Self {
609            next: Some(next),
610            ..self
611        }
612    }
613}
614
615/// Context used by the `consent.html` template
616#[derive(Serialize)]
617pub struct ConsentContext {
618    grant: AuthorizationGrant,
619    client: Client,
620    action: PostAuthAction,
621}
622
623impl TemplateContext for ConsentContext {
624    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
625    where
626        Self: Sized,
627    {
628        Client::samples(now, rng)
629            .into_iter()
630            .map(|client| {
631                let mut grant = AuthorizationGrant::sample(now, rng);
632                let action = PostAuthAction::continue_grant(grant.id);
633                // XXX
634                grant.client_id = client.id;
635                Self {
636                    grant,
637                    client,
638                    action,
639                }
640            })
641            .collect()
642    }
643}
644
645impl ConsentContext {
646    /// Constructs a context for the client consent page
647    #[must_use]
648    pub fn new(grant: AuthorizationGrant, client: Client) -> Self {
649        let action = PostAuthAction::continue_grant(grant.id);
650        Self {
651            grant,
652            client,
653            action,
654        }
655    }
656}
657
658#[derive(Serialize)]
659#[serde(tag = "grant_type")]
660enum PolicyViolationGrant {
661    #[serde(rename = "authorization_code")]
662    Authorization(AuthorizationGrant),
663    #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
664    DeviceCode(DeviceCodeGrant),
665}
666
667/// Context used by the `policy_violation.html` template
668#[derive(Serialize)]
669pub struct PolicyViolationContext {
670    grant: PolicyViolationGrant,
671    client: Client,
672    action: PostAuthAction,
673}
674
675impl TemplateContext for PolicyViolationContext {
676    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
677    where
678        Self: Sized,
679    {
680        Client::samples(now, rng)
681            .into_iter()
682            .flat_map(|client| {
683                let mut grant = AuthorizationGrant::sample(now, rng);
684                // XXX
685                grant.client_id = client.id;
686
687                let authorization_grant =
688                    PolicyViolationContext::for_authorization_grant(grant, client.clone());
689                let device_code_grant = PolicyViolationContext::for_device_code_grant(
690                    DeviceCodeGrant {
691                        id: Ulid::from_datetime_with_source(now.into(), rng),
692                        state: mas_data_model::DeviceCodeGrantState::Pending,
693                        client_id: client.id,
694                        scope: [OPENID].into_iter().collect(),
695                        user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
696                        device_code: Alphanumeric.sample_string(rng, 32),
697                        created_at: now - Duration::try_minutes(5).unwrap(),
698                        expires_at: now + Duration::try_minutes(25).unwrap(),
699                        ip_address: None,
700                        user_agent: None,
701                    },
702                    client,
703                );
704
705                [authorization_grant, device_code_grant]
706            })
707            .collect()
708    }
709}
710
711impl PolicyViolationContext {
712    /// Constructs a context for the policy violation page for an authorization
713    /// grant
714    #[must_use]
715    pub const fn for_authorization_grant(grant: AuthorizationGrant, client: Client) -> Self {
716        let action = PostAuthAction::continue_grant(grant.id);
717        Self {
718            grant: PolicyViolationGrant::Authorization(grant),
719            client,
720            action,
721        }
722    }
723
724    /// Constructs a context for the policy violation page for a device code
725    /// grant
726    #[must_use]
727    pub const fn for_device_code_grant(grant: DeviceCodeGrant, client: Client) -> Self {
728        let action = PostAuthAction::continue_device_code_grant(grant.id);
729        Self {
730            grant: PolicyViolationGrant::DeviceCode(grant),
731            client,
732            action,
733        }
734    }
735}
736
737/// Context used by the `sso.html` template
738#[derive(Serialize)]
739pub struct CompatSsoContext {
740    login: CompatSsoLogin,
741    action: PostAuthAction,
742}
743
744impl TemplateContext for CompatSsoContext {
745    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
746    where
747        Self: Sized,
748    {
749        let id = Ulid::from_datetime_with_source(now.into(), rng);
750        vec![CompatSsoContext::new(CompatSsoLogin {
751            id,
752            redirect_uri: Url::parse("https://app.element.io/").unwrap(),
753            login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
754            created_at: now,
755            state: CompatSsoLoginState::Pending,
756        })]
757    }
758}
759
760impl CompatSsoContext {
761    /// Constructs a context for the legacy SSO login page
762    #[must_use]
763    pub fn new(login: CompatSsoLogin) -> Self
764where {
765        let action = PostAuthAction::continue_compat_sso_login(login.id);
766        Self { login, action }
767    }
768}
769
770/// Context used by the `emails/recovery.{txt,html,subject}` templates
771#[derive(Serialize)]
772pub struct EmailRecoveryContext {
773    user: User,
774    session: UserRecoverySession,
775    recovery_link: Url,
776}
777
778impl EmailRecoveryContext {
779    /// Constructs a context for the recovery email
780    #[must_use]
781    pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
782        Self {
783            user,
784            session,
785            recovery_link,
786        }
787    }
788
789    /// Returns the user associated with the recovery email
790    #[must_use]
791    pub fn user(&self) -> &User {
792        &self.user
793    }
794
795    /// Returns the recovery session associated with the recovery email
796    #[must_use]
797    pub fn session(&self) -> &UserRecoverySession {
798        &self.session
799    }
800}
801
802impl TemplateContext for EmailRecoveryContext {
803    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
804    where
805        Self: Sized,
806    {
807        User::samples(now, rng).into_iter().map(|user| {
808            let session = UserRecoverySession {
809                id: Ulid::from_datetime_with_source(now.into(), rng),
810                email: "hello@example.com".to_owned(),
811                user_agent: UserAgent::parse("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()),
812                ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
813                locale: "en".to_owned(),
814                created_at: now,
815                consumed_at: None,
816            };
817
818            let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
819
820            Self::new(user, session, link)
821        }).collect()
822    }
823}
824
825/// Context used by the `emails/verification.{txt,html,subject}` templates
826#[derive(Serialize)]
827pub struct EmailVerificationContext {
828    #[serde(skip_serializing_if = "Option::is_none")]
829    browser_session: Option<BrowserSession>,
830    #[serde(skip_serializing_if = "Option::is_none")]
831    user_registration: Option<UserRegistration>,
832    authentication_code: UserEmailAuthenticationCode,
833}
834
835impl EmailVerificationContext {
836    /// Constructs a context for the verification email
837    #[must_use]
838    pub fn new(
839        authentication_code: UserEmailAuthenticationCode,
840        browser_session: Option<BrowserSession>,
841        user_registration: Option<UserRegistration>,
842    ) -> Self {
843        Self {
844            browser_session,
845            user_registration,
846            authentication_code,
847        }
848    }
849
850    /// Get the user to which this email is being sent
851    #[must_use]
852    pub fn user(&self) -> Option<&User> {
853        self.browser_session.as_ref().map(|s| &s.user)
854    }
855
856    /// Get the verification code being sent
857    #[must_use]
858    pub fn code(&self) -> &str {
859        &self.authentication_code.code
860    }
861}
862
863impl TemplateContext for EmailVerificationContext {
864    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
865    where
866        Self: Sized,
867    {
868        BrowserSession::samples(now, rng)
869            .into_iter()
870            .map(|browser_session| {
871                let authentication_code = UserEmailAuthenticationCode {
872                    id: Ulid::from_datetime_with_source(now.into(), rng),
873                    user_email_authentication_id: Ulid::from_datetime_with_source(now.into(), rng),
874                    code: "123456".to_owned(),
875                    created_at: now - Duration::try_minutes(5).unwrap(),
876                    expires_at: now + Duration::try_minutes(25).unwrap(),
877                };
878
879                Self {
880                    browser_session: Some(browser_session),
881                    user_registration: None,
882                    authentication_code,
883                }
884            })
885            .collect()
886    }
887}
888
889/// Fields of the email verification form
890#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
891#[serde(rename_all = "snake_case")]
892pub enum RegisterStepsVerifyEmailFormField {
893    /// The code field
894    Code,
895}
896
897impl FormField for RegisterStepsVerifyEmailFormField {
898    fn keep(&self) -> bool {
899        match self {
900            Self::Code => true,
901        }
902    }
903}
904
905/// Context used by the `pages/register/steps/verify_email.html` templates
906#[derive(Serialize)]
907pub struct RegisterStepsVerifyEmailContext {
908    form: FormState<RegisterStepsVerifyEmailFormField>,
909    authentication: UserEmailAuthentication,
910}
911
912impl RegisterStepsVerifyEmailContext {
913    /// Constructs a context for the email verification page
914    #[must_use]
915    pub fn new(authentication: UserEmailAuthentication) -> Self {
916        Self {
917            form: FormState::default(),
918            authentication,
919        }
920    }
921
922    /// Set the form state
923    #[must_use]
924    pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
925        Self { form, ..self }
926    }
927}
928
929impl TemplateContext for RegisterStepsVerifyEmailContext {
930    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
931    where
932        Self: Sized,
933    {
934        let authentication = UserEmailAuthentication {
935            id: Ulid::from_datetime_with_source(now.into(), rng),
936            user_session_id: None,
937            user_registration_id: None,
938            email: "foobar@example.com".to_owned(),
939            created_at: now,
940            completed_at: None,
941        };
942
943        vec![Self {
944            form: FormState::default(),
945            authentication,
946        }]
947    }
948}
949
950/// Context used by the `pages/register/steps/email_in_use.html` template
951#[derive(Serialize)]
952pub struct RegisterStepsEmailInUseContext {
953    email: String,
954    action: Option<PostAuthAction>,
955}
956
957impl RegisterStepsEmailInUseContext {
958    /// Constructs a context for the email in use page
959    #[must_use]
960    pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
961        Self { email, action }
962    }
963}
964
965impl TemplateContext for RegisterStepsEmailInUseContext {
966    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
967    where
968        Self: Sized,
969    {
970        let email = "hello@example.com".to_owned();
971        let action = PostAuthAction::continue_grant(Ulid::nil());
972        vec![Self::new(email, Some(action))]
973    }
974}
975
976/// Fields for the display name form
977#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
978#[serde(rename_all = "snake_case")]
979pub enum RegisterStepsDisplayNameFormField {
980    /// The display name
981    DisplayName,
982}
983
984impl FormField for RegisterStepsDisplayNameFormField {
985    fn keep(&self) -> bool {
986        match self {
987            Self::DisplayName => true,
988        }
989    }
990}
991
992/// Context used by the `display_name.html` template
993#[derive(Serialize, Default)]
994pub struct RegisterStepsDisplayNameContext {
995    form: FormState<RegisterStepsDisplayNameFormField>,
996}
997
998impl RegisterStepsDisplayNameContext {
999    /// Constructs a context for the display name page
1000    #[must_use]
1001    pub fn new() -> Self {
1002        Self::default()
1003    }
1004
1005    /// Set the form state
1006    #[must_use]
1007    pub fn with_form_state(
1008        mut self,
1009        form_state: FormState<RegisterStepsDisplayNameFormField>,
1010    ) -> Self {
1011        self.form = form_state;
1012        self
1013    }
1014}
1015
1016impl TemplateContext for RegisterStepsDisplayNameContext {
1017    fn sample(_now: chrono::DateTime<chrono::Utc>, _rng: &mut impl Rng) -> Vec<Self>
1018    where
1019        Self: Sized,
1020    {
1021        vec![Self {
1022            form: FormState::default(),
1023        }]
1024    }
1025}
1026
1027/// Fields of the account recovery start form
1028#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1029#[serde(rename_all = "snake_case")]
1030pub enum RecoveryStartFormField {
1031    /// The email
1032    Email,
1033}
1034
1035impl FormField for RecoveryStartFormField {
1036    fn keep(&self) -> bool {
1037        match self {
1038            Self::Email => true,
1039        }
1040    }
1041}
1042
1043/// Context used by the `pages/recovery/start.html` template
1044#[derive(Serialize, Default)]
1045pub struct RecoveryStartContext {
1046    form: FormState<RecoveryStartFormField>,
1047}
1048
1049impl RecoveryStartContext {
1050    /// Constructs a context for the recovery start page
1051    #[must_use]
1052    pub fn new() -> Self {
1053        Self::default()
1054    }
1055
1056    /// Set the form state
1057    #[must_use]
1058    pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1059        Self { form }
1060    }
1061}
1062
1063impl TemplateContext for RecoveryStartContext {
1064    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1065    where
1066        Self: Sized,
1067    {
1068        vec![
1069            Self::new(),
1070            Self::new().with_form_state(
1071                FormState::default()
1072                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1073            ),
1074            Self::new().with_form_state(
1075                FormState::default()
1076                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1077            ),
1078        ]
1079    }
1080}
1081
1082/// Context used by the `pages/recovery/progress.html` template
1083#[derive(Serialize)]
1084pub struct RecoveryProgressContext {
1085    session: UserRecoverySession,
1086    /// Whether resending the e-mail was denied because of rate limits
1087    resend_failed_due_to_rate_limit: bool,
1088}
1089
1090impl RecoveryProgressContext {
1091    /// Constructs a context for the recovery progress page
1092    #[must_use]
1093    pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1094        Self {
1095            session,
1096            resend_failed_due_to_rate_limit,
1097        }
1098    }
1099}
1100
1101impl TemplateContext for RecoveryProgressContext {
1102    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1103    where
1104        Self: Sized,
1105    {
1106        let session = UserRecoverySession {
1107            id: Ulid::from_datetime_with_source(now.into(), rng),
1108            email: "name@mail.com".to_owned(),
1109            user_agent: UserAgent::parse("Mozilla/5.0".to_owned()),
1110            ip_address: None,
1111            locale: "en".to_owned(),
1112            created_at: now,
1113            consumed_at: None,
1114        };
1115
1116        vec![
1117            Self {
1118                session: session.clone(),
1119                resend_failed_due_to_rate_limit: false,
1120            },
1121            Self {
1122                session,
1123                resend_failed_due_to_rate_limit: true,
1124            },
1125        ]
1126    }
1127}
1128
1129/// Context used by the `pages/recovery/expired.html` template
1130#[derive(Serialize)]
1131pub struct RecoveryExpiredContext {
1132    session: UserRecoverySession,
1133}
1134
1135impl RecoveryExpiredContext {
1136    /// Constructs a context for the recovery expired page
1137    #[must_use]
1138    pub fn new(session: UserRecoverySession) -> Self {
1139        Self { session }
1140    }
1141}
1142
1143impl TemplateContext for RecoveryExpiredContext {
1144    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1145    where
1146        Self: Sized,
1147    {
1148        let session = UserRecoverySession {
1149            id: Ulid::from_datetime_with_source(now.into(), rng),
1150            email: "name@mail.com".to_owned(),
1151            user_agent: UserAgent::parse("Mozilla/5.0".to_owned()),
1152            ip_address: None,
1153            locale: "en".to_owned(),
1154            created_at: now,
1155            consumed_at: None,
1156        };
1157
1158        vec![Self { session }]
1159    }
1160}
1161
1162/// Fields of the account recovery finish form
1163#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1164#[serde(rename_all = "snake_case")]
1165pub enum RecoveryFinishFormField {
1166    /// The new password
1167    NewPassword,
1168
1169    /// The new password confirmation
1170    NewPasswordConfirm,
1171}
1172
1173impl FormField for RecoveryFinishFormField {
1174    fn keep(&self) -> bool {
1175        false
1176    }
1177}
1178
1179/// Context used by the `pages/recovery/finish.html` template
1180#[derive(Serialize)]
1181pub struct RecoveryFinishContext {
1182    user: User,
1183    form: FormState<RecoveryFinishFormField>,
1184}
1185
1186impl RecoveryFinishContext {
1187    /// Constructs a context for the recovery finish page
1188    #[must_use]
1189    pub fn new(user: User) -> Self {
1190        Self {
1191            user,
1192            form: FormState::default(),
1193        }
1194    }
1195
1196    /// Set the form state
1197    #[must_use]
1198    pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1199        self.form = form;
1200        self
1201    }
1202}
1203
1204impl TemplateContext for RecoveryFinishContext {
1205    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1206    where
1207        Self: Sized,
1208    {
1209        User::samples(now, rng)
1210            .into_iter()
1211            .flat_map(|user| {
1212                vec![
1213                    Self::new(user.clone()),
1214                    Self::new(user.clone()).with_form_state(
1215                        FormState::default().with_error_on_field(
1216                            RecoveryFinishFormField::NewPassword,
1217                            FieldError::Invalid,
1218                        ),
1219                    ),
1220                    Self::new(user.clone()).with_form_state(
1221                        FormState::default().with_error_on_field(
1222                            RecoveryFinishFormField::NewPasswordConfirm,
1223                            FieldError::Invalid,
1224                        ),
1225                    ),
1226                ]
1227            })
1228            .collect()
1229    }
1230}
1231
1232/// Context used by the `pages/upstream_oauth2/{link_mismatch,do_login}.html`
1233/// templates
1234#[derive(Serialize)]
1235pub struct UpstreamExistingLinkContext {
1236    linked_user: User,
1237}
1238
1239impl UpstreamExistingLinkContext {
1240    /// Constructs a new context with an existing linked user
1241    #[must_use]
1242    pub fn new(linked_user: User) -> Self {
1243        Self { linked_user }
1244    }
1245}
1246
1247impl TemplateContext for UpstreamExistingLinkContext {
1248    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1249    where
1250        Self: Sized,
1251    {
1252        User::samples(now, rng)
1253            .into_iter()
1254            .map(|linked_user| Self { linked_user })
1255            .collect()
1256    }
1257}
1258
1259/// Context used by the `pages/upstream_oauth2/suggest_link.html`
1260/// templates
1261#[derive(Serialize)]
1262pub struct UpstreamSuggestLink {
1263    post_logout_action: PostAuthAction,
1264}
1265
1266impl UpstreamSuggestLink {
1267    /// Constructs a new context with an existing linked user
1268    #[must_use]
1269    pub fn new(link: &UpstreamOAuthLink) -> Self {
1270        Self::for_link_id(link.id)
1271    }
1272
1273    fn for_link_id(id: Ulid) -> Self {
1274        let post_logout_action = PostAuthAction::link_upstream(id);
1275        Self { post_logout_action }
1276    }
1277}
1278
1279impl TemplateContext for UpstreamSuggestLink {
1280    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1281    where
1282        Self: Sized,
1283    {
1284        let id = Ulid::from_datetime_with_source(now.into(), rng);
1285        vec![Self::for_link_id(id)]
1286    }
1287}
1288
1289/// User-editeable fields of the upstream account link form
1290#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1291#[serde(rename_all = "snake_case")]
1292pub enum UpstreamRegisterFormField {
1293    /// The username field
1294    Username,
1295
1296    /// Accept the terms of service
1297    AcceptTerms,
1298}
1299
1300impl FormField for UpstreamRegisterFormField {
1301    fn keep(&self) -> bool {
1302        match self {
1303            Self::Username | Self::AcceptTerms => true,
1304        }
1305    }
1306}
1307
1308/// Context used by the `pages/upstream_oauth2/do_register.html`
1309/// templates
1310#[derive(Serialize)]
1311pub struct UpstreamRegister {
1312    upstream_oauth_link: UpstreamOAuthLink,
1313    upstream_oauth_provider: UpstreamOAuthProvider,
1314    imported_localpart: Option<String>,
1315    force_localpart: bool,
1316    imported_display_name: Option<String>,
1317    force_display_name: bool,
1318    imported_email: Option<String>,
1319    force_email: bool,
1320    form_state: FormState<UpstreamRegisterFormField>,
1321}
1322
1323impl UpstreamRegister {
1324    /// Constructs a new context for registering a new user from an upstream
1325    /// provider
1326    #[must_use]
1327    pub fn new(
1328        upstream_oauth_link: UpstreamOAuthLink,
1329        upstream_oauth_provider: UpstreamOAuthProvider,
1330    ) -> Self {
1331        Self {
1332            upstream_oauth_link,
1333            upstream_oauth_provider,
1334            imported_localpart: None,
1335            force_localpart: false,
1336            imported_display_name: None,
1337            force_display_name: false,
1338            imported_email: None,
1339            force_email: false,
1340            form_state: FormState::default(),
1341        }
1342    }
1343
1344    /// Set the imported localpart
1345    pub fn set_localpart(&mut self, localpart: String, force: bool) {
1346        self.imported_localpart = Some(localpart);
1347        self.force_localpart = force;
1348    }
1349
1350    /// Set the imported localpart
1351    #[must_use]
1352    pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1353        Self {
1354            imported_localpart: Some(localpart),
1355            force_localpart: force,
1356            ..self
1357        }
1358    }
1359
1360    /// Set the imported display name
1361    pub fn set_display_name(&mut self, display_name: String, force: bool) {
1362        self.imported_display_name = Some(display_name);
1363        self.force_display_name = force;
1364    }
1365
1366    /// Set the imported display name
1367    #[must_use]
1368    pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1369        Self {
1370            imported_display_name: Some(display_name),
1371            force_display_name: force,
1372            ..self
1373        }
1374    }
1375
1376    /// Set the imported email
1377    pub fn set_email(&mut self, email: String, force: bool) {
1378        self.imported_email = Some(email);
1379        self.force_email = force;
1380    }
1381
1382    /// Set the imported email
1383    #[must_use]
1384    pub fn with_email(self, email: String, force: bool) -> Self {
1385        Self {
1386            imported_email: Some(email),
1387            force_email: force,
1388            ..self
1389        }
1390    }
1391
1392    /// Set the form state
1393    pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1394        self.form_state = form_state;
1395    }
1396
1397    /// Set the form state
1398    #[must_use]
1399    pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1400        Self { form_state, ..self }
1401    }
1402}
1403
1404impl TemplateContext for UpstreamRegister {
1405    fn sample(now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1406    where
1407        Self: Sized,
1408    {
1409        vec![Self::new(
1410            UpstreamOAuthLink {
1411                id: Ulid::nil(),
1412                provider_id: Ulid::nil(),
1413                user_id: None,
1414                subject: "subject".to_owned(),
1415                human_account_name: Some("@john".to_owned()),
1416                created_at: now,
1417            },
1418            UpstreamOAuthProvider {
1419                id: Ulid::nil(),
1420                issuer: Some("https://example.com/".to_owned()),
1421                human_name: Some("Example Ltd.".to_owned()),
1422                brand_name: None,
1423                scope: Scope::from_iter([OPENID]),
1424                token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1425                token_endpoint_signing_alg: None,
1426                id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1427                client_id: "client-id".to_owned(),
1428                encrypted_client_secret: None,
1429                claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1430                authorization_endpoint_override: None,
1431                token_endpoint_override: None,
1432                jwks_uri_override: None,
1433                userinfo_endpoint_override: None,
1434                fetch_userinfo: false,
1435                userinfo_signed_response_alg: None,
1436                discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1437                pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1438                response_mode: None,
1439                additional_authorization_parameters: Vec::new(),
1440                created_at: now,
1441                disabled_at: None,
1442            },
1443        )]
1444    }
1445}
1446
1447/// Form fields on the device link page
1448#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1449#[serde(rename_all = "snake_case")]
1450pub enum DeviceLinkFormField {
1451    /// The device code field
1452    Code,
1453}
1454
1455impl FormField for DeviceLinkFormField {
1456    fn keep(&self) -> bool {
1457        match self {
1458            Self::Code => true,
1459        }
1460    }
1461}
1462
1463/// Context used by the `device_link.html` template
1464#[derive(Serialize, Default, Debug)]
1465pub struct DeviceLinkContext {
1466    form_state: FormState<DeviceLinkFormField>,
1467}
1468
1469impl DeviceLinkContext {
1470    /// Constructs a new context with an existing linked user
1471    #[must_use]
1472    pub fn new() -> Self {
1473        Self::default()
1474    }
1475
1476    /// Set the form state
1477    #[must_use]
1478    pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1479        self.form_state = form_state;
1480        self
1481    }
1482}
1483
1484impl TemplateContext for DeviceLinkContext {
1485    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1486    where
1487        Self: Sized,
1488    {
1489        vec![
1490            Self::new(),
1491            Self::new().with_form_state(
1492                FormState::default()
1493                    .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1494            ),
1495        ]
1496    }
1497}
1498
1499/// Context used by the `device_consent.html` template
1500#[derive(Serialize, Debug)]
1501pub struct DeviceConsentContext {
1502    grant: DeviceCodeGrant,
1503    client: Client,
1504}
1505
1506impl DeviceConsentContext {
1507    /// Constructs a new context with an existing linked user
1508    #[must_use]
1509    pub fn new(grant: DeviceCodeGrant, client: Client) -> Self {
1510        Self { grant, client }
1511    }
1512}
1513
1514impl TemplateContext for DeviceConsentContext {
1515    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1516    where
1517        Self: Sized,
1518    {
1519        Client::samples(now, rng)
1520            .into_iter()
1521            .map(|client| {
1522                let grant = DeviceCodeGrant {
1523                    id: Ulid::from_datetime_with_source(now.into(), rng),
1524                    state: mas_data_model::DeviceCodeGrantState::Pending,
1525                    client_id: client.id,
1526                    scope: [OPENID].into_iter().collect(),
1527                    user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
1528                    device_code: Alphanumeric.sample_string(rng, 32),
1529                    created_at: now - Duration::try_minutes(5).unwrap(),
1530                    expires_at: now + Duration::try_minutes(25).unwrap(),
1531                    ip_address: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1532                    user_agent: Some(UserAgent::parse("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())),
1533                };
1534                Self { grant, client }
1535            })
1536            .collect()
1537    }
1538}
1539
1540/// Context used by the `account/deactivated.html` and `account/locked.html`
1541/// templates
1542#[derive(Serialize)]
1543pub struct AccountInactiveContext {
1544    user: User,
1545}
1546
1547impl AccountInactiveContext {
1548    /// Constructs a new context with an existing linked user
1549    #[must_use]
1550    pub fn new(user: User) -> Self {
1551        Self { user }
1552    }
1553}
1554
1555impl TemplateContext for AccountInactiveContext {
1556    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1557    where
1558        Self: Sized,
1559    {
1560        User::samples(now, rng)
1561            .into_iter()
1562            .map(|user| AccountInactiveContext { user })
1563            .collect()
1564    }
1565}
1566
1567/// Context used by the `form_post.html` template
1568#[derive(Serialize)]
1569pub struct FormPostContext<T> {
1570    redirect_uri: Option<Url>,
1571    params: T,
1572}
1573
1574impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
1575    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1576    where
1577        Self: Sized,
1578    {
1579        let sample_params = T::sample(now, rng);
1580        sample_params
1581            .into_iter()
1582            .map(|params| FormPostContext {
1583                redirect_uri: "https://example.com/callback".parse().ok(),
1584                params,
1585            })
1586            .collect()
1587    }
1588}
1589
1590impl<T> FormPostContext<T> {
1591    /// Constructs a context for the `form_post` response mode form for a given
1592    /// URL
1593    pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
1594        Self {
1595            redirect_uri: Some(redirect_uri),
1596            params,
1597        }
1598    }
1599
1600    /// Constructs a context for the `form_post` response mode form for the
1601    /// current URL
1602    pub fn new_for_current_url(params: T) -> Self {
1603        Self {
1604            redirect_uri: None,
1605            params,
1606        }
1607    }
1608
1609    /// Add the language to the context
1610    ///
1611    /// This is usually implemented by the [`TemplateContext`] trait, but it is
1612    /// annoying to make it work because of the generic parameter
1613    pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
1614        WithLanguage {
1615            lang: lang.to_string(),
1616            inner: self,
1617        }
1618    }
1619}
1620
1621/// Context used by the `error.html` template
1622#[derive(Default, Serialize, Debug, Clone)]
1623pub struct ErrorContext {
1624    code: Option<&'static str>,
1625    description: Option<String>,
1626    details: Option<String>,
1627    lang: Option<String>,
1628}
1629
1630impl std::fmt::Display for ErrorContext {
1631    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1632        if let Some(code) = &self.code {
1633            writeln!(f, "code: {code}")?;
1634        }
1635        if let Some(description) = &self.description {
1636            writeln!(f, "{description}")?;
1637        }
1638
1639        if let Some(details) = &self.details {
1640            writeln!(f, "details: {details}")?;
1641        }
1642
1643        Ok(())
1644    }
1645}
1646
1647impl TemplateContext for ErrorContext {
1648    fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1649    where
1650        Self: Sized,
1651    {
1652        vec![
1653            Self::new()
1654                .with_code("sample_error")
1655                .with_description("A fancy description".into())
1656                .with_details("Something happened".into()),
1657            Self::new().with_code("another_error"),
1658            Self::new(),
1659        ]
1660    }
1661}
1662
1663impl ErrorContext {
1664    /// Constructs a context for the error page
1665    #[must_use]
1666    pub fn new() -> Self {
1667        Self::default()
1668    }
1669
1670    /// Add the error code to the context
1671    #[must_use]
1672    pub fn with_code(mut self, code: &'static str) -> Self {
1673        self.code = Some(code);
1674        self
1675    }
1676
1677    /// Add the error description to the context
1678    #[must_use]
1679    pub fn with_description(mut self, description: String) -> Self {
1680        self.description = Some(description);
1681        self
1682    }
1683
1684    /// Add the error details to the context
1685    #[must_use]
1686    pub fn with_details(mut self, details: String) -> Self {
1687        self.details = Some(details);
1688        self
1689    }
1690
1691    /// Add the language to the context
1692    #[must_use]
1693    pub fn with_language(mut self, lang: &DataLocale) -> Self {
1694        self.lang = Some(lang.to_string());
1695        self
1696    }
1697
1698    /// Get the error code, if any
1699    #[must_use]
1700    pub fn code(&self) -> Option<&'static str> {
1701        self.code
1702    }
1703
1704    /// Get the description, if any
1705    #[must_use]
1706    pub fn description(&self) -> Option<&str> {
1707        self.description.as_deref()
1708    }
1709
1710    /// Get the details, if any
1711    #[must_use]
1712    pub fn details(&self) -> Option<&str> {
1713        self.details.as_deref()
1714    }
1715}
1716
1717/// Context used by the not found (`404.html`) template
1718#[derive(Serialize)]
1719pub struct NotFoundContext {
1720    method: String,
1721    version: String,
1722    uri: String,
1723}
1724
1725impl NotFoundContext {
1726    /// Constructs a context for the not found page
1727    #[must_use]
1728    pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
1729        Self {
1730            method: method.to_string(),
1731            version: format!("{version:?}"),
1732            uri: uri.to_string(),
1733        }
1734    }
1735}
1736
1737impl TemplateContext for NotFoundContext {
1738    fn sample(_now: DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1739    where
1740        Self: Sized,
1741    {
1742        vec![
1743            Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
1744            Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
1745            Self::new(
1746                &Method::PUT,
1747                Version::HTTP_10,
1748                &"/foo?bar=baz".parse().unwrap(),
1749            ),
1750        ]
1751    }
1752}