mas_templates/
context.rs

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