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, UlidExt as _, 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_rng(now, 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                            locale: None,
839                        },
840                        client,
841                        Vec::new(),
842                    );
843
844                    [authorization_grant, device_code_grant]
845                })
846                .collect(),
847        )
848    }
849}
850
851impl PolicyViolationContext {
852    /// Constructs a context for the policy violation page for an authorization
853    /// grant
854    #[must_use]
855    pub const fn for_authorization_grant(
856        grant: AuthorizationGrant,
857        client: Client,
858        violations: Vec<Violation>,
859    ) -> Self {
860        let action = PostAuthAction::continue_grant(grant.id);
861        Self {
862            grant: PolicyViolationGrant::Authorization(grant),
863            client,
864            action,
865            violations,
866        }
867    }
868
869    /// Constructs a context for the policy violation page for a device code
870    /// grant
871    #[must_use]
872    pub const fn for_device_code_grant(
873        grant: DeviceCodeGrant,
874        client: Client,
875        violations: Vec<Violation>,
876    ) -> Self {
877        let action = PostAuthAction::continue_device_code_grant(grant.id);
878        Self {
879            grant: PolicyViolationGrant::DeviceCode(grant),
880            client,
881            action,
882            violations,
883        }
884    }
885}
886
887/// Context used by the `compat_login_policy_violation.html` template
888#[derive(Serialize)]
889pub struct CompatLoginPolicyViolationContext {
890    violations: Vec<Violation>,
891}
892
893impl TemplateContext for CompatLoginPolicyViolationContext {
894    fn sample<R: Rng>(
895        _now: chrono::DateTime<Utc>,
896        _rng: &mut R,
897        _locales: &[DataLocale],
898    ) -> BTreeMap<SampleIdentifier, Self>
899    where
900        Self: Sized,
901    {
902        sample_list(vec![
903            CompatLoginPolicyViolationContext { violations: vec![] },
904            CompatLoginPolicyViolationContext {
905                violations: vec![Violation {
906                    msg: "user has too many active sessions".to_owned(),
907                    redirect_uri: None,
908                    field: None,
909                    variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
910                }],
911            },
912        ])
913    }
914}
915
916impl CompatLoginPolicyViolationContext {
917    /// Constructs a context for the compatibility login policy violation page
918    /// given the list of violations
919    #[must_use]
920    pub const fn for_violations(violations: Vec<Violation>) -> Self {
921        Self { violations }
922    }
923}
924
925/// Context used by the `sso.html` template
926#[derive(Serialize)]
927pub struct CompatSsoContext {
928    login: CompatSsoLogin,
929    action: PostAuthAction,
930    matrix_user: MatrixUser,
931}
932
933impl TemplateContext for CompatSsoContext {
934    fn sample<R: Rng>(
935        now: chrono::DateTime<Utc>,
936        rng: &mut R,
937        _locales: &[DataLocale],
938    ) -> BTreeMap<SampleIdentifier, Self>
939    where
940        Self: Sized,
941    {
942        let id = Ulid::from_datetime_with_rng(now, rng);
943        sample_list(vec![CompatSsoContext::new(
944            CompatSsoLogin {
945                id,
946                redirect_uri: Url::parse("https://app.element.io/").unwrap(),
947                login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
948                created_at: now,
949                state: CompatSsoLoginState::Pending,
950            },
951            MatrixUser {
952                mxid: "@alice:example.com".to_owned(),
953                display_name: Some("Alice".to_owned()),
954            },
955        )])
956    }
957}
958
959impl CompatSsoContext {
960    /// Constructs a context for the legacy SSO login page
961    #[must_use]
962    pub fn new(login: CompatSsoLogin, matrix_user: MatrixUser) -> Self
963where {
964        let action = PostAuthAction::continue_compat_sso_login(login.id);
965        Self {
966            login,
967            action,
968            matrix_user,
969        }
970    }
971}
972
973/// Context used by the `emails/recovery.{txt,html,subject}` templates
974#[derive(Serialize)]
975pub struct EmailRecoveryContext {
976    user: User,
977    session: UserRecoverySession,
978    recovery_link: Url,
979}
980
981impl EmailRecoveryContext {
982    /// Constructs a context for the recovery email
983    #[must_use]
984    pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
985        Self {
986            user,
987            session,
988            recovery_link,
989        }
990    }
991
992    /// Returns the user associated with the recovery email
993    #[must_use]
994    pub fn user(&self) -> &User {
995        &self.user
996    }
997
998    /// Returns the recovery session associated with the recovery email
999    #[must_use]
1000    pub fn session(&self) -> &UserRecoverySession {
1001        &self.session
1002    }
1003}
1004
1005impl TemplateContext for EmailRecoveryContext {
1006    fn sample<R: Rng>(
1007        now: chrono::DateTime<Utc>,
1008        rng: &mut R,
1009        _locales: &[DataLocale],
1010    ) -> BTreeMap<SampleIdentifier, Self>
1011    where
1012        Self: Sized,
1013    {
1014        sample_list(User::samples(now, rng).into_iter().map(|user| {
1015            let session = UserRecoverySession {
1016                id: Ulid::from_datetime_with_rng(now, rng),
1017                email: "hello@example.com".to_owned(),
1018                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(),
1019                ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
1020                locale: "en".to_owned(),
1021                created_at: now,
1022                consumed_at: None,
1023            };
1024
1025            let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
1026
1027            Self::new(user, session, link)
1028        }).collect())
1029    }
1030}
1031
1032/// Context used by the `emails/verification.{txt,html,subject}` templates
1033#[derive(Serialize)]
1034pub struct EmailVerificationContext {
1035    #[serde(skip_serializing_if = "Option::is_none")]
1036    browser_session: Option<BrowserSession>,
1037    #[serde(skip_serializing_if = "Option::is_none")]
1038    user_registration: Option<UserRegistration>,
1039    authentication_code: UserEmailAuthenticationCode,
1040}
1041
1042impl EmailVerificationContext {
1043    /// Constructs a context for the verification email
1044    #[must_use]
1045    pub fn new(
1046        authentication_code: UserEmailAuthenticationCode,
1047        browser_session: Option<BrowserSession>,
1048        user_registration: Option<UserRegistration>,
1049    ) -> Self {
1050        Self {
1051            browser_session,
1052            user_registration,
1053            authentication_code,
1054        }
1055    }
1056
1057    /// Get the user to which this email is being sent
1058    #[must_use]
1059    pub fn user(&self) -> Option<&User> {
1060        self.browser_session.as_ref().map(|s| &s.user)
1061    }
1062
1063    /// Get the verification code being sent
1064    #[must_use]
1065    pub fn code(&self) -> &str {
1066        &self.authentication_code.code
1067    }
1068}
1069
1070impl TemplateContext for EmailVerificationContext {
1071    fn sample<R: Rng>(
1072        now: chrono::DateTime<Utc>,
1073        rng: &mut R,
1074        _locales: &[DataLocale],
1075    ) -> BTreeMap<SampleIdentifier, Self>
1076    where
1077        Self: Sized,
1078    {
1079        sample_list(
1080            BrowserSession::samples(now, rng)
1081                .into_iter()
1082                .map(|browser_session| {
1083                    let authentication_code = UserEmailAuthenticationCode {
1084                        id: Ulid::from_datetime_with_rng(now, rng),
1085                        user_email_authentication_id: Ulid::from_datetime_with_rng(now, rng),
1086                        code: "123456".to_owned(),
1087                        created_at: now - Duration::try_minutes(5).unwrap(),
1088                        expires_at: now + Duration::try_minutes(25).unwrap(),
1089                    };
1090
1091                    Self {
1092                        browser_session: Some(browser_session),
1093                        user_registration: None,
1094                        authentication_code,
1095                    }
1096                })
1097                .collect(),
1098        )
1099    }
1100}
1101
1102/// Fields of the email verification form
1103#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1104#[serde(rename_all = "snake_case")]
1105pub enum RegisterStepsVerifyEmailFormField {
1106    /// The code field
1107    Code,
1108}
1109
1110impl FormField for RegisterStepsVerifyEmailFormField {
1111    fn keep(&self) -> bool {
1112        match self {
1113            Self::Code => true,
1114        }
1115    }
1116}
1117
1118/// Context used by the `pages/register/steps/verify_email.html` templates
1119#[derive(Serialize)]
1120pub struct RegisterStepsVerifyEmailContext {
1121    form: FormState<RegisterStepsVerifyEmailFormField>,
1122    authentication: UserEmailAuthentication,
1123}
1124
1125impl RegisterStepsVerifyEmailContext {
1126    /// Constructs a context for the email verification page
1127    #[must_use]
1128    pub fn new(authentication: UserEmailAuthentication) -> Self {
1129        Self {
1130            form: FormState::default(),
1131            authentication,
1132        }
1133    }
1134
1135    /// Set the form state
1136    #[must_use]
1137    pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
1138        Self { form, ..self }
1139    }
1140}
1141
1142impl TemplateContext for RegisterStepsVerifyEmailContext {
1143    fn sample<R: Rng>(
1144        now: chrono::DateTime<Utc>,
1145        rng: &mut R,
1146        _locales: &[DataLocale],
1147    ) -> BTreeMap<SampleIdentifier, Self>
1148    where
1149        Self: Sized,
1150    {
1151        let authentication = UserEmailAuthentication {
1152            id: Ulid::from_datetime_with_rng(now, rng),
1153            user_session_id: None,
1154            user_registration_id: None,
1155            email: "foobar@example.com".to_owned(),
1156            created_at: now,
1157            completed_at: None,
1158        };
1159
1160        sample_list(vec![Self {
1161            form: FormState::default(),
1162            authentication,
1163        }])
1164    }
1165}
1166
1167/// Context used by the `pages/register/steps/email_in_use.html` template
1168#[derive(Serialize)]
1169pub struct RegisterStepsEmailInUseContext {
1170    email: String,
1171    action: Option<PostAuthAction>,
1172}
1173
1174impl RegisterStepsEmailInUseContext {
1175    /// Constructs a context for the email in use page
1176    #[must_use]
1177    pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
1178        Self { email, action }
1179    }
1180}
1181
1182impl TemplateContext for RegisterStepsEmailInUseContext {
1183    fn sample<R: Rng>(
1184        _now: chrono::DateTime<Utc>,
1185        _rng: &mut R,
1186        _locales: &[DataLocale],
1187    ) -> BTreeMap<SampleIdentifier, Self>
1188    where
1189        Self: Sized,
1190    {
1191        let email = "hello@example.com".to_owned();
1192        let action = PostAuthAction::continue_grant(Ulid::nil());
1193        sample_list(vec![Self::new(email, Some(action))])
1194    }
1195}
1196
1197/// Fields for the display name form
1198#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1199#[serde(rename_all = "snake_case")]
1200pub enum RegisterStepsDisplayNameFormField {
1201    /// The display name
1202    DisplayName,
1203}
1204
1205impl FormField for RegisterStepsDisplayNameFormField {
1206    fn keep(&self) -> bool {
1207        match self {
1208            Self::DisplayName => true,
1209        }
1210    }
1211}
1212
1213/// Context used by the `display_name.html` template
1214#[derive(Serialize, Default)]
1215pub struct RegisterStepsDisplayNameContext {
1216    form: FormState<RegisterStepsDisplayNameFormField>,
1217}
1218
1219impl RegisterStepsDisplayNameContext {
1220    /// Constructs a context for the display name page
1221    #[must_use]
1222    pub fn new() -> Self {
1223        Self::default()
1224    }
1225
1226    /// Set the form state
1227    #[must_use]
1228    pub fn with_form_state(
1229        mut self,
1230        form_state: FormState<RegisterStepsDisplayNameFormField>,
1231    ) -> Self {
1232        self.form = form_state;
1233        self
1234    }
1235}
1236
1237impl TemplateContext for RegisterStepsDisplayNameContext {
1238    fn sample<R: Rng>(
1239        _now: chrono::DateTime<chrono::Utc>,
1240        _rng: &mut R,
1241        _locales: &[DataLocale],
1242    ) -> BTreeMap<SampleIdentifier, Self>
1243    where
1244        Self: Sized,
1245    {
1246        sample_list(vec![Self {
1247            form: FormState::default(),
1248        }])
1249    }
1250}
1251
1252/// Fields of the registration token form
1253#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1254#[serde(rename_all = "snake_case")]
1255pub enum RegisterStepsRegistrationTokenFormField {
1256    /// The registration token
1257    Token,
1258}
1259
1260impl FormField for RegisterStepsRegistrationTokenFormField {
1261    fn keep(&self) -> bool {
1262        match self {
1263            Self::Token => true,
1264        }
1265    }
1266}
1267
1268/// The registration token page context
1269#[derive(Serialize, Default)]
1270pub struct RegisterStepsRegistrationTokenContext {
1271    form: FormState<RegisterStepsRegistrationTokenFormField>,
1272}
1273
1274impl RegisterStepsRegistrationTokenContext {
1275    /// Constructs a context for the registration token page
1276    #[must_use]
1277    pub fn new() -> Self {
1278        Self::default()
1279    }
1280
1281    /// Set the form state
1282    #[must_use]
1283    pub fn with_form_state(
1284        mut self,
1285        form_state: FormState<RegisterStepsRegistrationTokenFormField>,
1286    ) -> Self {
1287        self.form = form_state;
1288        self
1289    }
1290}
1291
1292impl TemplateContext for RegisterStepsRegistrationTokenContext {
1293    fn sample<R: Rng>(
1294        _now: chrono::DateTime<chrono::Utc>,
1295        _rng: &mut R,
1296        _locales: &[DataLocale],
1297    ) -> BTreeMap<SampleIdentifier, Self>
1298    where
1299        Self: Sized,
1300    {
1301        sample_list(vec![Self {
1302            form: FormState::default(),
1303        }])
1304    }
1305}
1306
1307/// Fields of the account recovery start form
1308#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1309#[serde(rename_all = "snake_case")]
1310pub enum RecoveryStartFormField {
1311    /// The email
1312    Email,
1313}
1314
1315impl FormField for RecoveryStartFormField {
1316    fn keep(&self) -> bool {
1317        match self {
1318            Self::Email => true,
1319        }
1320    }
1321}
1322
1323/// Context used by the `pages/recovery/start.html` template
1324#[derive(Serialize, Default)]
1325pub struct RecoveryStartContext {
1326    form: FormState<RecoveryStartFormField>,
1327}
1328
1329impl RecoveryStartContext {
1330    /// Constructs a context for the recovery start page
1331    #[must_use]
1332    pub fn new() -> Self {
1333        Self::default()
1334    }
1335
1336    /// Set the form state
1337    #[must_use]
1338    pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1339        Self { form }
1340    }
1341}
1342
1343impl TemplateContext for RecoveryStartContext {
1344    fn sample<R: Rng>(
1345        _now: chrono::DateTime<Utc>,
1346        _rng: &mut R,
1347        _locales: &[DataLocale],
1348    ) -> BTreeMap<SampleIdentifier, Self>
1349    where
1350        Self: Sized,
1351    {
1352        sample_list(vec![
1353            Self::new(),
1354            Self::new().with_form_state(
1355                FormState::default()
1356                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1357            ),
1358            Self::new().with_form_state(
1359                FormState::default()
1360                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1361            ),
1362        ])
1363    }
1364}
1365
1366/// Context used by the `pages/recovery/progress.html` template
1367#[derive(Serialize)]
1368pub struct RecoveryProgressContext {
1369    session: UserRecoverySession,
1370    /// Whether resending the e-mail was denied because of rate limits
1371    resend_failed_due_to_rate_limit: bool,
1372}
1373
1374impl RecoveryProgressContext {
1375    /// Constructs a context for the recovery progress page
1376    #[must_use]
1377    pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1378        Self {
1379            session,
1380            resend_failed_due_to_rate_limit,
1381        }
1382    }
1383}
1384
1385impl TemplateContext for RecoveryProgressContext {
1386    fn sample<R: Rng>(
1387        now: chrono::DateTime<Utc>,
1388        rng: &mut R,
1389        _locales: &[DataLocale],
1390    ) -> BTreeMap<SampleIdentifier, Self>
1391    where
1392        Self: Sized,
1393    {
1394        let session = UserRecoverySession {
1395            id: Ulid::from_datetime_with_rng(now, rng),
1396            email: "name@mail.com".to_owned(),
1397            user_agent: "Mozilla/5.0".to_owned(),
1398            ip_address: None,
1399            locale: "en".to_owned(),
1400            created_at: now,
1401            consumed_at: None,
1402        };
1403
1404        sample_list(vec![
1405            Self {
1406                session: session.clone(),
1407                resend_failed_due_to_rate_limit: false,
1408            },
1409            Self {
1410                session,
1411                resend_failed_due_to_rate_limit: true,
1412            },
1413        ])
1414    }
1415}
1416
1417/// Context used by the `pages/recovery/expired.html` template
1418#[derive(Serialize)]
1419pub struct RecoveryExpiredContext {
1420    session: UserRecoverySession,
1421}
1422
1423impl RecoveryExpiredContext {
1424    /// Constructs a context for the recovery expired page
1425    #[must_use]
1426    pub fn new(session: UserRecoverySession) -> Self {
1427        Self { session }
1428    }
1429}
1430
1431impl TemplateContext for RecoveryExpiredContext {
1432    fn sample<R: Rng>(
1433        now: chrono::DateTime<Utc>,
1434        rng: &mut R,
1435        _locales: &[DataLocale],
1436    ) -> BTreeMap<SampleIdentifier, Self>
1437    where
1438        Self: Sized,
1439    {
1440        let session = UserRecoverySession {
1441            id: Ulid::from_datetime_with_rng(now, rng),
1442            email: "name@mail.com".to_owned(),
1443            user_agent: "Mozilla/5.0".to_owned(),
1444            ip_address: None,
1445            locale: "en".to_owned(),
1446            created_at: now,
1447            consumed_at: None,
1448        };
1449
1450        sample_list(vec![Self { session }])
1451    }
1452}
1453/// Fields of the account recovery finish form
1454#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1455#[serde(rename_all = "snake_case")]
1456pub enum RecoveryFinishFormField {
1457    /// The new password
1458    NewPassword,
1459
1460    /// The new password confirmation
1461    NewPasswordConfirm,
1462}
1463
1464impl FormField for RecoveryFinishFormField {
1465    fn keep(&self) -> bool {
1466        false
1467    }
1468}
1469
1470/// Context used by the `pages/recovery/finish.html` template
1471#[derive(Serialize)]
1472pub struct RecoveryFinishContext {
1473    user: User,
1474    form: FormState<RecoveryFinishFormField>,
1475}
1476
1477impl RecoveryFinishContext {
1478    /// Constructs a context for the recovery finish page
1479    #[must_use]
1480    pub fn new(user: User) -> Self {
1481        Self {
1482            user,
1483            form: FormState::default(),
1484        }
1485    }
1486
1487    /// Set the form state
1488    #[must_use]
1489    pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1490        self.form = form;
1491        self
1492    }
1493}
1494
1495impl TemplateContext for RecoveryFinishContext {
1496    fn sample<R: Rng>(
1497        now: chrono::DateTime<Utc>,
1498        rng: &mut R,
1499        _locales: &[DataLocale],
1500    ) -> BTreeMap<SampleIdentifier, Self>
1501    where
1502        Self: Sized,
1503    {
1504        sample_list(
1505            User::samples(now, rng)
1506                .into_iter()
1507                .flat_map(|user| {
1508                    vec![
1509                        Self::new(user.clone()),
1510                        Self::new(user.clone()).with_form_state(
1511                            FormState::default().with_error_on_field(
1512                                RecoveryFinishFormField::NewPassword,
1513                                FieldError::Invalid,
1514                            ),
1515                        ),
1516                        Self::new(user.clone()).with_form_state(
1517                            FormState::default().with_error_on_field(
1518                                RecoveryFinishFormField::NewPasswordConfirm,
1519                                FieldError::Invalid,
1520                            ),
1521                        ),
1522                    ]
1523                })
1524                .collect(),
1525        )
1526    }
1527}
1528
1529/// Context used by the `pages/upstream_oauth2/link_mismatch.html`
1530/// templates
1531#[derive(Serialize)]
1532pub struct UpstreamExistingLinkContext {
1533    linked_user: User,
1534}
1535
1536impl UpstreamExistingLinkContext {
1537    /// Constructs a new context with an existing linked user
1538    #[must_use]
1539    pub fn new(linked_user: User) -> Self {
1540        Self { linked_user }
1541    }
1542}
1543
1544impl TemplateContext for UpstreamExistingLinkContext {
1545    fn sample<R: Rng>(
1546        now: chrono::DateTime<Utc>,
1547        rng: &mut R,
1548        _locales: &[DataLocale],
1549    ) -> BTreeMap<SampleIdentifier, Self>
1550    where
1551        Self: Sized,
1552    {
1553        sample_list(
1554            User::samples(now, rng)
1555                .into_iter()
1556                .map(|linked_user| Self { linked_user })
1557                .collect(),
1558        )
1559    }
1560}
1561
1562/// Context used by the `pages/upstream_oauth2/suggest_link.html`
1563/// templates
1564#[derive(Serialize)]
1565pub struct UpstreamSuggestLink {
1566    post_logout_action: PostAuthAction,
1567}
1568
1569impl UpstreamSuggestLink {
1570    /// Constructs a new context with an existing linked user
1571    #[must_use]
1572    pub fn new(link: &UpstreamOAuthLink) -> Self {
1573        Self::for_link_id(link.id)
1574    }
1575
1576    fn for_link_id(id: Ulid) -> Self {
1577        let post_logout_action = PostAuthAction::link_upstream(id);
1578        Self { post_logout_action }
1579    }
1580}
1581
1582impl TemplateContext for UpstreamSuggestLink {
1583    fn sample<R: Rng>(
1584        now: chrono::DateTime<Utc>,
1585        rng: &mut R,
1586        _locales: &[DataLocale],
1587    ) -> BTreeMap<SampleIdentifier, Self>
1588    where
1589        Self: Sized,
1590    {
1591        let id = Ulid::from_datetime_with_rng(now, rng);
1592        sample_list(vec![Self::for_link_id(id)])
1593    }
1594}
1595
1596/// User-editeable fields of the upstream account link form
1597#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1598#[serde(rename_all = "snake_case")]
1599pub enum UpstreamRegisterFormField {
1600    /// The username field
1601    Username,
1602
1603    /// Accept the terms of service
1604    AcceptTerms,
1605}
1606
1607impl FormField for UpstreamRegisterFormField {
1608    fn keep(&self) -> bool {
1609        match self {
1610            Self::Username | Self::AcceptTerms => true,
1611        }
1612    }
1613}
1614
1615/// Context used by the `pages/upstream_oauth2/do_register.html`
1616/// templates
1617#[derive(Serialize)]
1618pub struct UpstreamRegister {
1619    upstream_oauth_link: UpstreamOAuthLink,
1620    upstream_oauth_provider: UpstreamOAuthProvider,
1621    imported_localpart: Option<String>,
1622    force_localpart: bool,
1623    imported_display_name: Option<String>,
1624    force_display_name: bool,
1625    imported_email: Option<String>,
1626    force_email: bool,
1627    form_state: FormState<UpstreamRegisterFormField>,
1628}
1629
1630impl UpstreamRegister {
1631    /// Constructs a new context for registering a new user from an upstream
1632    /// provider
1633    #[must_use]
1634    pub fn new(
1635        upstream_oauth_link: UpstreamOAuthLink,
1636        upstream_oauth_provider: UpstreamOAuthProvider,
1637    ) -> Self {
1638        Self {
1639            upstream_oauth_link,
1640            upstream_oauth_provider,
1641            imported_localpart: None,
1642            force_localpart: false,
1643            imported_display_name: None,
1644            force_display_name: false,
1645            imported_email: None,
1646            force_email: false,
1647            form_state: FormState::default(),
1648        }
1649    }
1650
1651    /// Set the imported localpart
1652    pub fn set_localpart(&mut self, localpart: String, force: bool) {
1653        self.imported_localpart = Some(localpart);
1654        self.force_localpart = force;
1655    }
1656
1657    /// Set the imported localpart
1658    #[must_use]
1659    pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1660        Self {
1661            imported_localpart: Some(localpart),
1662            force_localpart: force,
1663            ..self
1664        }
1665    }
1666
1667    /// Set the imported display name
1668    pub fn set_display_name(&mut self, display_name: String, force: bool) {
1669        self.imported_display_name = Some(display_name);
1670        self.force_display_name = force;
1671    }
1672
1673    /// Set the imported display name
1674    #[must_use]
1675    pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1676        Self {
1677            imported_display_name: Some(display_name),
1678            force_display_name: force,
1679            ..self
1680        }
1681    }
1682
1683    /// Set the imported email
1684    pub fn set_email(&mut self, email: String, force: bool) {
1685        self.imported_email = Some(email);
1686        self.force_email = force;
1687    }
1688
1689    /// Set the imported email
1690    #[must_use]
1691    pub fn with_email(self, email: String, force: bool) -> Self {
1692        Self {
1693            imported_email: Some(email),
1694            force_email: force,
1695            ..self
1696        }
1697    }
1698
1699    /// Set the form state
1700    pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1701        self.form_state = form_state;
1702    }
1703
1704    /// Set the form state
1705    #[must_use]
1706    pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1707        Self { form_state, ..self }
1708    }
1709}
1710
1711impl TemplateContext for UpstreamRegister {
1712    fn sample<R: Rng>(
1713        now: chrono::DateTime<Utc>,
1714        _rng: &mut R,
1715        _locales: &[DataLocale],
1716    ) -> BTreeMap<SampleIdentifier, Self>
1717    where
1718        Self: Sized,
1719    {
1720        sample_list(vec![Self::new(
1721            UpstreamOAuthLink {
1722                id: Ulid::nil(),
1723                provider_id: Ulid::nil(),
1724                user_id: None,
1725                subject: "subject".to_owned(),
1726                human_account_name: Some("@john".to_owned()),
1727                created_at: now,
1728            },
1729            UpstreamOAuthProvider {
1730                id: Ulid::nil(),
1731                issuer: Some("https://example.com/".to_owned()),
1732                human_name: Some("Example Ltd.".to_owned()),
1733                brand_name: None,
1734                scope: Scope::from_iter([OPENID]),
1735                token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1736                token_endpoint_signing_alg: None,
1737                id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1738                client_id: "client-id".to_owned(),
1739                encrypted_client_secret: None,
1740                claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1741                authorization_endpoint_override: None,
1742                token_endpoint_override: None,
1743                jwks_uri_override: None,
1744                userinfo_endpoint_override: None,
1745                fetch_userinfo: false,
1746                userinfo_signed_response_alg: None,
1747                discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1748                pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1749                response_mode: None,
1750                additional_authorization_parameters: Vec::new(),
1751                forward_login_hint: false,
1752                created_at: now,
1753                disabled_at: None,
1754                on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
1755                registration_token_required: false,
1756            },
1757        )])
1758    }
1759}
1760
1761/// Form fields on the device link page
1762#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1763#[serde(rename_all = "snake_case")]
1764pub enum DeviceLinkFormField {
1765    /// The device code field
1766    Code,
1767}
1768
1769impl FormField for DeviceLinkFormField {
1770    fn keep(&self) -> bool {
1771        match self {
1772            Self::Code => true,
1773        }
1774    }
1775}
1776
1777/// Context used by the `device_link.html` template
1778#[derive(Serialize, Default, Debug)]
1779pub struct DeviceLinkContext {
1780    form_state: FormState<DeviceLinkFormField>,
1781}
1782
1783impl DeviceLinkContext {
1784    /// Constructs a new context with an existing linked user
1785    #[must_use]
1786    pub fn new() -> Self {
1787        Self::default()
1788    }
1789
1790    /// Set the form state
1791    #[must_use]
1792    pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1793        self.form_state = form_state;
1794        self
1795    }
1796}
1797
1798impl TemplateContext for DeviceLinkContext {
1799    fn sample<R: Rng>(
1800        _now: chrono::DateTime<Utc>,
1801        _rng: &mut R,
1802        _locales: &[DataLocale],
1803    ) -> BTreeMap<SampleIdentifier, Self>
1804    where
1805        Self: Sized,
1806    {
1807        sample_list(vec![
1808            Self::new(),
1809            Self::new().with_form_state(
1810                FormState::default()
1811                    .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1812            ),
1813        ])
1814    }
1815}
1816
1817/// Context used by the `device_consent.html` template
1818#[derive(Serialize, Debug)]
1819pub struct DeviceConsentContext {
1820    grant: DeviceCodeGrant,
1821    client: Client,
1822    matrix_user: MatrixUser,
1823}
1824
1825impl DeviceConsentContext {
1826    /// Constructs a new context with an existing linked user
1827    #[must_use]
1828    pub fn new(grant: DeviceCodeGrant, client: Client, matrix_user: MatrixUser) -> Self {
1829        Self {
1830            grant,
1831            client,
1832            matrix_user,
1833        }
1834    }
1835}
1836
1837impl TemplateContext for DeviceConsentContext {
1838    fn sample<R: Rng>(
1839        now: chrono::DateTime<Utc>,
1840        rng: &mut R,
1841        _locales: &[DataLocale],
1842    ) -> BTreeMap<SampleIdentifier, Self>
1843    where
1844        Self: Sized,
1845    {
1846        sample_list(Client::samples(now, rng)
1847            .into_iter()
1848            .map(|client|  {
1849                let grant = DeviceCodeGrant {
1850                    id: Ulid::from_datetime_with_rng(now, rng),
1851                    state: mas_data_model::DeviceCodeGrantState::Pending,
1852                    client_id: client.id,
1853                    scope: [OPENID].into_iter().collect(),
1854                    user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
1855                    device_code: Alphanumeric.sample_string(rng, 32),
1856                    created_at: now - Duration::try_minutes(5).unwrap(),
1857                    expires_at: now + Duration::try_minutes(25).unwrap(),
1858                    ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
1859                    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()),
1860                    locale: None,
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}