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