Skip to main content

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