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, 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 account recovery start form
1072#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1073#[serde(rename_all = "snake_case")]
1074pub enum RecoveryStartFormField {
1075    /// The email
1076    Email,
1077}
1078
1079impl FormField for RecoveryStartFormField {
1080    fn keep(&self) -> bool {
1081        match self {
1082            Self::Email => true,
1083        }
1084    }
1085}
1086
1087/// Context used by the `pages/recovery/start.html` template
1088#[derive(Serialize, Default)]
1089pub struct RecoveryStartContext {
1090    form: FormState<RecoveryStartFormField>,
1091}
1092
1093impl RecoveryStartContext {
1094    /// Constructs a context for the recovery start 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(self, form: FormState<RecoveryStartFormField>) -> Self {
1103        Self { form }
1104    }
1105}
1106
1107impl TemplateContext for RecoveryStartContext {
1108    fn sample(
1109        _now: chrono::DateTime<Utc>,
1110        _rng: &mut impl Rng,
1111        _locales: &[DataLocale],
1112    ) -> Vec<Self>
1113    where
1114        Self: Sized,
1115    {
1116        vec![
1117            Self::new(),
1118            Self::new().with_form_state(
1119                FormState::default()
1120                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1121            ),
1122            Self::new().with_form_state(
1123                FormState::default()
1124                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1125            ),
1126        ]
1127    }
1128}
1129
1130/// Context used by the `pages/recovery/progress.html` template
1131#[derive(Serialize)]
1132pub struct RecoveryProgressContext {
1133    session: UserRecoverySession,
1134    /// Whether resending the e-mail was denied because of rate limits
1135    resend_failed_due_to_rate_limit: bool,
1136}
1137
1138impl RecoveryProgressContext {
1139    /// Constructs a context for the recovery progress page
1140    #[must_use]
1141    pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1142        Self {
1143            session,
1144            resend_failed_due_to_rate_limit,
1145        }
1146    }
1147}
1148
1149impl TemplateContext for RecoveryProgressContext {
1150    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1151    where
1152        Self: Sized,
1153    {
1154        let session = UserRecoverySession {
1155            id: Ulid::from_datetime_with_source(now.into(), rng),
1156            email: "name@mail.com".to_owned(),
1157            user_agent: "Mozilla/5.0".to_owned(),
1158            ip_address: None,
1159            locale: "en".to_owned(),
1160            created_at: now,
1161            consumed_at: None,
1162        };
1163
1164        vec![
1165            Self {
1166                session: session.clone(),
1167                resend_failed_due_to_rate_limit: false,
1168            },
1169            Self {
1170                session,
1171                resend_failed_due_to_rate_limit: true,
1172            },
1173        ]
1174    }
1175}
1176
1177/// Context used by the `pages/recovery/expired.html` template
1178#[derive(Serialize)]
1179pub struct RecoveryExpiredContext {
1180    session: UserRecoverySession,
1181}
1182
1183impl RecoveryExpiredContext {
1184    /// Constructs a context for the recovery expired page
1185    #[must_use]
1186    pub fn new(session: UserRecoverySession) -> Self {
1187        Self { session }
1188    }
1189}
1190
1191impl TemplateContext for RecoveryExpiredContext {
1192    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1193    where
1194        Self: Sized,
1195    {
1196        let session = UserRecoverySession {
1197            id: Ulid::from_datetime_with_source(now.into(), rng),
1198            email: "name@mail.com".to_owned(),
1199            user_agent: "Mozilla/5.0".to_owned(),
1200            ip_address: None,
1201            locale: "en".to_owned(),
1202            created_at: now,
1203            consumed_at: None,
1204        };
1205
1206        vec![Self { session }]
1207    }
1208}
1209
1210/// Fields of the account recovery finish form
1211#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1212#[serde(rename_all = "snake_case")]
1213pub enum RecoveryFinishFormField {
1214    /// The new password
1215    NewPassword,
1216
1217    /// The new password confirmation
1218    NewPasswordConfirm,
1219}
1220
1221impl FormField for RecoveryFinishFormField {
1222    fn keep(&self) -> bool {
1223        false
1224    }
1225}
1226
1227/// Context used by the `pages/recovery/finish.html` template
1228#[derive(Serialize)]
1229pub struct RecoveryFinishContext {
1230    user: User,
1231    form: FormState<RecoveryFinishFormField>,
1232}
1233
1234impl RecoveryFinishContext {
1235    /// Constructs a context for the recovery finish page
1236    #[must_use]
1237    pub fn new(user: User) -> Self {
1238        Self {
1239            user,
1240            form: FormState::default(),
1241        }
1242    }
1243
1244    /// Set the form state
1245    #[must_use]
1246    pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1247        self.form = form;
1248        self
1249    }
1250}
1251
1252impl TemplateContext for RecoveryFinishContext {
1253    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1254    where
1255        Self: Sized,
1256    {
1257        User::samples(now, rng)
1258            .into_iter()
1259            .flat_map(|user| {
1260                vec![
1261                    Self::new(user.clone()),
1262                    Self::new(user.clone()).with_form_state(
1263                        FormState::default().with_error_on_field(
1264                            RecoveryFinishFormField::NewPassword,
1265                            FieldError::Invalid,
1266                        ),
1267                    ),
1268                    Self::new(user.clone()).with_form_state(
1269                        FormState::default().with_error_on_field(
1270                            RecoveryFinishFormField::NewPasswordConfirm,
1271                            FieldError::Invalid,
1272                        ),
1273                    ),
1274                ]
1275            })
1276            .collect()
1277    }
1278}
1279
1280/// Context used by the `pages/upstream_oauth2/{link_mismatch,do_login}.html`
1281/// templates
1282#[derive(Serialize)]
1283pub struct UpstreamExistingLinkContext {
1284    linked_user: User,
1285}
1286
1287impl UpstreamExistingLinkContext {
1288    /// Constructs a new context with an existing linked user
1289    #[must_use]
1290    pub fn new(linked_user: User) -> Self {
1291        Self { linked_user }
1292    }
1293}
1294
1295impl TemplateContext for UpstreamExistingLinkContext {
1296    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1297    where
1298        Self: Sized,
1299    {
1300        User::samples(now, rng)
1301            .into_iter()
1302            .map(|linked_user| Self { linked_user })
1303            .collect()
1304    }
1305}
1306
1307/// Context used by the `pages/upstream_oauth2/suggest_link.html`
1308/// templates
1309#[derive(Serialize)]
1310pub struct UpstreamSuggestLink {
1311    post_logout_action: PostAuthAction,
1312}
1313
1314impl UpstreamSuggestLink {
1315    /// Constructs a new context with an existing linked user
1316    #[must_use]
1317    pub fn new(link: &UpstreamOAuthLink) -> Self {
1318        Self::for_link_id(link.id)
1319    }
1320
1321    fn for_link_id(id: Ulid) -> Self {
1322        let post_logout_action = PostAuthAction::link_upstream(id);
1323        Self { post_logout_action }
1324    }
1325}
1326
1327impl TemplateContext for UpstreamSuggestLink {
1328    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1329    where
1330        Self: Sized,
1331    {
1332        let id = Ulid::from_datetime_with_source(now.into(), rng);
1333        vec![Self::for_link_id(id)]
1334    }
1335}
1336
1337/// User-editeable fields of the upstream account link form
1338#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1339#[serde(rename_all = "snake_case")]
1340pub enum UpstreamRegisterFormField {
1341    /// The username field
1342    Username,
1343
1344    /// Accept the terms of service
1345    AcceptTerms,
1346}
1347
1348impl FormField for UpstreamRegisterFormField {
1349    fn keep(&self) -> bool {
1350        match self {
1351            Self::Username | Self::AcceptTerms => true,
1352        }
1353    }
1354}
1355
1356/// Context used by the `pages/upstream_oauth2/do_register.html`
1357/// templates
1358#[derive(Serialize)]
1359pub struct UpstreamRegister {
1360    upstream_oauth_link: UpstreamOAuthLink,
1361    upstream_oauth_provider: UpstreamOAuthProvider,
1362    imported_localpart: Option<String>,
1363    force_localpart: bool,
1364    imported_display_name: Option<String>,
1365    force_display_name: bool,
1366    imported_email: Option<String>,
1367    force_email: bool,
1368    form_state: FormState<UpstreamRegisterFormField>,
1369}
1370
1371impl UpstreamRegister {
1372    /// Constructs a new context for registering a new user from an upstream
1373    /// provider
1374    #[must_use]
1375    pub fn new(
1376        upstream_oauth_link: UpstreamOAuthLink,
1377        upstream_oauth_provider: UpstreamOAuthProvider,
1378    ) -> Self {
1379        Self {
1380            upstream_oauth_link,
1381            upstream_oauth_provider,
1382            imported_localpart: None,
1383            force_localpart: false,
1384            imported_display_name: None,
1385            force_display_name: false,
1386            imported_email: None,
1387            force_email: false,
1388            form_state: FormState::default(),
1389        }
1390    }
1391
1392    /// Set the imported localpart
1393    pub fn set_localpart(&mut self, localpart: String, force: bool) {
1394        self.imported_localpart = Some(localpart);
1395        self.force_localpart = force;
1396    }
1397
1398    /// Set the imported localpart
1399    #[must_use]
1400    pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1401        Self {
1402            imported_localpart: Some(localpart),
1403            force_localpart: force,
1404            ..self
1405        }
1406    }
1407
1408    /// Set the imported display name
1409    pub fn set_display_name(&mut self, display_name: String, force: bool) {
1410        self.imported_display_name = Some(display_name);
1411        self.force_display_name = force;
1412    }
1413
1414    /// Set the imported display name
1415    #[must_use]
1416    pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1417        Self {
1418            imported_display_name: Some(display_name),
1419            force_display_name: force,
1420            ..self
1421        }
1422    }
1423
1424    /// Set the imported email
1425    pub fn set_email(&mut self, email: String, force: bool) {
1426        self.imported_email = Some(email);
1427        self.force_email = force;
1428    }
1429
1430    /// Set the imported email
1431    #[must_use]
1432    pub fn with_email(self, email: String, force: bool) -> Self {
1433        Self {
1434            imported_email: Some(email),
1435            force_email: force,
1436            ..self
1437        }
1438    }
1439
1440    /// Set the form state
1441    pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1442        self.form_state = form_state;
1443    }
1444
1445    /// Set the form state
1446    #[must_use]
1447    pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1448        Self { form_state, ..self }
1449    }
1450}
1451
1452impl TemplateContext for UpstreamRegister {
1453    fn sample(now: chrono::DateTime<Utc>, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1454    where
1455        Self: Sized,
1456    {
1457        vec![Self::new(
1458            UpstreamOAuthLink {
1459                id: Ulid::nil(),
1460                provider_id: Ulid::nil(),
1461                user_id: None,
1462                subject: "subject".to_owned(),
1463                human_account_name: Some("@john".to_owned()),
1464                created_at: now,
1465            },
1466            UpstreamOAuthProvider {
1467                id: Ulid::nil(),
1468                issuer: Some("https://example.com/".to_owned()),
1469                human_name: Some("Example Ltd.".to_owned()),
1470                brand_name: None,
1471                scope: Scope::from_iter([OPENID]),
1472                token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1473                token_endpoint_signing_alg: None,
1474                id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1475                client_id: "client-id".to_owned(),
1476                encrypted_client_secret: None,
1477                claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1478                authorization_endpoint_override: None,
1479                token_endpoint_override: None,
1480                jwks_uri_override: None,
1481                userinfo_endpoint_override: None,
1482                fetch_userinfo: false,
1483                userinfo_signed_response_alg: None,
1484                discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1485                pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1486                response_mode: None,
1487                additional_authorization_parameters: Vec::new(),
1488                forward_login_hint: false,
1489                created_at: now,
1490                disabled_at: None,
1491            },
1492        )]
1493    }
1494}
1495
1496/// Form fields on the device link page
1497#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1498#[serde(rename_all = "snake_case")]
1499pub enum DeviceLinkFormField {
1500    /// The device code field
1501    Code,
1502}
1503
1504impl FormField for DeviceLinkFormField {
1505    fn keep(&self) -> bool {
1506        match self {
1507            Self::Code => true,
1508        }
1509    }
1510}
1511
1512/// Context used by the `device_link.html` template
1513#[derive(Serialize, Default, Debug)]
1514pub struct DeviceLinkContext {
1515    form_state: FormState<DeviceLinkFormField>,
1516}
1517
1518impl DeviceLinkContext {
1519    /// Constructs a new context with an existing linked user
1520    #[must_use]
1521    pub fn new() -> Self {
1522        Self::default()
1523    }
1524
1525    /// Set the form state
1526    #[must_use]
1527    pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1528        self.form_state = form_state;
1529        self
1530    }
1531}
1532
1533impl TemplateContext for DeviceLinkContext {
1534    fn sample(
1535        _now: chrono::DateTime<Utc>,
1536        _rng: &mut impl Rng,
1537        _locales: &[DataLocale],
1538    ) -> Vec<Self>
1539    where
1540        Self: Sized,
1541    {
1542        vec![
1543            Self::new(),
1544            Self::new().with_form_state(
1545                FormState::default()
1546                    .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1547            ),
1548        ]
1549    }
1550}
1551
1552/// Context used by the `device_consent.html` template
1553#[derive(Serialize, Debug)]
1554pub struct DeviceConsentContext {
1555    grant: DeviceCodeGrant,
1556    client: Client,
1557}
1558
1559impl DeviceConsentContext {
1560    /// Constructs a new context with an existing linked user
1561    #[must_use]
1562    pub fn new(grant: DeviceCodeGrant, client: Client) -> Self {
1563        Self { grant, client }
1564    }
1565}
1566
1567impl TemplateContext for DeviceConsentContext {
1568    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1569    where
1570        Self: Sized,
1571    {
1572        Client::samples(now, rng)
1573            .into_iter()
1574            .map(|client| {
1575                let grant = DeviceCodeGrant {
1576                    id: Ulid::from_datetime_with_source(now.into(), rng),
1577                    state: mas_data_model::DeviceCodeGrantState::Pending,
1578                    client_id: client.id,
1579                    scope: [OPENID].into_iter().collect(),
1580                    user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
1581                    device_code: Alphanumeric.sample_string(rng, 32),
1582                    created_at: now - Duration::try_minutes(5).unwrap(),
1583                    expires_at: now + Duration::try_minutes(25).unwrap(),
1584                    ip_address: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1585                    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()),
1586                };
1587                Self { grant, client }
1588            })
1589            .collect()
1590    }
1591}
1592
1593/// Context used by the `account/deactivated.html` and `account/locked.html`
1594/// templates
1595#[derive(Serialize)]
1596pub struct AccountInactiveContext {
1597    user: User,
1598}
1599
1600impl AccountInactiveContext {
1601    /// Constructs a new context with an existing linked user
1602    #[must_use]
1603    pub fn new(user: User) -> Self {
1604        Self { user }
1605    }
1606}
1607
1608impl TemplateContext for AccountInactiveContext {
1609    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1610    where
1611        Self: Sized,
1612    {
1613        User::samples(now, rng)
1614            .into_iter()
1615            .map(|user| AccountInactiveContext { user })
1616            .collect()
1617    }
1618}
1619
1620/// Context used by the `device_name.txt` template
1621#[derive(Serialize)]
1622pub struct DeviceNameContext {
1623    client: Client,
1624    raw_user_agent: String,
1625}
1626
1627impl DeviceNameContext {
1628    /// Constructs a new context with a client and user agent
1629    #[must_use]
1630    pub fn new(client: Client, user_agent: Option<String>) -> Self {
1631        Self {
1632            client,
1633            raw_user_agent: user_agent.unwrap_or_default(),
1634        }
1635    }
1636}
1637
1638impl TemplateContext for DeviceNameContext {
1639    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1640    where
1641        Self: Sized,
1642    {
1643        Client::samples(now, rng)
1644            .into_iter()
1645            .map(|client| DeviceNameContext {
1646                client,
1647                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(),
1648            })
1649            .collect()
1650    }
1651}
1652
1653/// Context used by the `form_post.html` template
1654#[derive(Serialize)]
1655pub struct FormPostContext<T> {
1656    redirect_uri: Option<Url>,
1657    params: T,
1658}
1659
1660impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
1661    fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
1662    where
1663        Self: Sized,
1664    {
1665        let sample_params = T::sample(now, rng, locales);
1666        sample_params
1667            .into_iter()
1668            .map(|params| FormPostContext {
1669                redirect_uri: "https://example.com/callback".parse().ok(),
1670                params,
1671            })
1672            .collect()
1673    }
1674}
1675
1676impl<T> FormPostContext<T> {
1677    /// Constructs a context for the `form_post` response mode form for a given
1678    /// URL
1679    pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
1680        Self {
1681            redirect_uri: Some(redirect_uri),
1682            params,
1683        }
1684    }
1685
1686    /// Constructs a context for the `form_post` response mode form for the
1687    /// current URL
1688    pub fn new_for_current_url(params: T) -> Self {
1689        Self {
1690            redirect_uri: None,
1691            params,
1692        }
1693    }
1694
1695    /// Add the language to the context
1696    ///
1697    /// This is usually implemented by the [`TemplateContext`] trait, but it is
1698    /// annoying to make it work because of the generic parameter
1699    pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
1700        WithLanguage {
1701            lang: lang.to_string(),
1702            inner: self,
1703        }
1704    }
1705}
1706
1707/// Context used by the `error.html` template
1708#[derive(Default, Serialize, Debug, Clone)]
1709pub struct ErrorContext {
1710    code: Option<&'static str>,
1711    description: Option<String>,
1712    details: Option<String>,
1713    lang: Option<String>,
1714}
1715
1716impl std::fmt::Display for ErrorContext {
1717    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1718        if let Some(code) = &self.code {
1719            writeln!(f, "code: {code}")?;
1720        }
1721        if let Some(description) = &self.description {
1722            writeln!(f, "{description}")?;
1723        }
1724
1725        if let Some(details) = &self.details {
1726            writeln!(f, "details: {details}")?;
1727        }
1728
1729        Ok(())
1730    }
1731}
1732
1733impl TemplateContext for ErrorContext {
1734    fn sample(
1735        _now: chrono::DateTime<Utc>,
1736        _rng: &mut impl Rng,
1737        _locales: &[DataLocale],
1738    ) -> Vec<Self>
1739    where
1740        Self: Sized,
1741    {
1742        vec![
1743            Self::new()
1744                .with_code("sample_error")
1745                .with_description("A fancy description".into())
1746                .with_details("Something happened".into()),
1747            Self::new().with_code("another_error"),
1748            Self::new(),
1749        ]
1750    }
1751}
1752
1753impl ErrorContext {
1754    /// Constructs a context for the error page
1755    #[must_use]
1756    pub fn new() -> Self {
1757        Self::default()
1758    }
1759
1760    /// Add the error code to the context
1761    #[must_use]
1762    pub fn with_code(mut self, code: &'static str) -> Self {
1763        self.code = Some(code);
1764        self
1765    }
1766
1767    /// Add the error description to the context
1768    #[must_use]
1769    pub fn with_description(mut self, description: String) -> Self {
1770        self.description = Some(description);
1771        self
1772    }
1773
1774    /// Add the error details to the context
1775    #[must_use]
1776    pub fn with_details(mut self, details: String) -> Self {
1777        self.details = Some(details);
1778        self
1779    }
1780
1781    /// Add the language to the context
1782    #[must_use]
1783    pub fn with_language(mut self, lang: &DataLocale) -> Self {
1784        self.lang = Some(lang.to_string());
1785        self
1786    }
1787
1788    /// Get the error code, if any
1789    #[must_use]
1790    pub fn code(&self) -> Option<&'static str> {
1791        self.code
1792    }
1793
1794    /// Get the description, if any
1795    #[must_use]
1796    pub fn description(&self) -> Option<&str> {
1797        self.description.as_deref()
1798    }
1799
1800    /// Get the details, if any
1801    #[must_use]
1802    pub fn details(&self) -> Option<&str> {
1803        self.details.as_deref()
1804    }
1805}
1806
1807/// Context used by the not found (`404.html`) template
1808#[derive(Serialize)]
1809pub struct NotFoundContext {
1810    method: String,
1811    version: String,
1812    uri: String,
1813}
1814
1815impl NotFoundContext {
1816    /// Constructs a context for the not found page
1817    #[must_use]
1818    pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
1819        Self {
1820            method: method.to_string(),
1821            version: format!("{version:?}"),
1822            uri: uri.to_string(),
1823        }
1824    }
1825}
1826
1827impl TemplateContext for NotFoundContext {
1828    fn sample(_now: DateTime<Utc>, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1829    where
1830        Self: Sized,
1831    {
1832        vec![
1833            Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
1834            Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
1835            Self::new(
1836                &Method::PUT,
1837                Version::HTTP_10,
1838                &"/foo?bar=baz".parse().unwrap(),
1839            ),
1840        ]
1841    }
1842}