mas_templates/
context.rs

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