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