Skip to main content

mas_templates/
context.rs

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