Skip to main content

mas_templates/
context.rs

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