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, Login, PostAuthAction, UrlBuilder};
35use oauth2_types::{
36    requests::ResponseMode,
37    scope::{OPENID, Scope},
38};
39use rand::{
40    RngExt, SeedableRng,
41    distr::{Alphanumeric, SampleString},
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: RngExt>(
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: RngExt>(
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: RngExt>(
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(&mut *rng);
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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/// An upstream OAuth 2.0 provider, as rendered by the registration page island
652#[derive(Serialize)]
653struct RegisterPageProvider {
654    name: String,
655    brand: Option<String>,
656    /// Submitted back as the `provider` field of the registration form
657    id: String,
658}
659
660impl RegisterPageProvider {
661    fn new(provider: UpstreamOAuthProvider) -> Self {
662        let name = provider
663            .human_name
664            .or_else(|| {
665                provider
666                    .issuer
667                    .as_deref()
668                    .map(|issuer| crate::functions::simplify_url(issuer, true))
669            })
670            .filter(|name| !name.is_empty())
671            .unwrap_or_else(|| provider.id.to_string());
672
673        Self {
674            name,
675            brand: provider.brand_name,
676            id: provider.id.to_string(),
677        }
678    }
679}
680
681/// Context used by the `register/index.html` template
682#[derive(Serialize)]
683pub struct RegisterContext {
684    providers: Vec<RegisterPageProvider>,
685    login_link: String,
686    form: FormState<RegisterFormField>,
687    graphql_endpoint: String,
688}
689
690impl TemplateContext for RegisterContext {
691    fn sample<R: RngExt>(
692        _now: chrono::DateTime<Utc>,
693        _rng: &mut R,
694        _locales: &[DataLocale],
695    ) -> BTreeMap<SampleIdentifier, Self>
696    where
697        Self: Sized,
698    {
699        let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
700        // TODO: samples with errors and with upstream providers
701        sample_list(vec![RegisterContext::new(&url_builder, Vec::new(), None)])
702    }
703}
704
705impl RegisterContext {
706    /// Create a new context with the given upstream providers, resolving the
707    /// URLs used by the client-side island from the given [`UrlBuilder`]
708    #[must_use]
709    pub fn new(
710        url_builder: &UrlBuilder,
711        providers: Vec<UpstreamOAuthProvider>,
712        post_auth_action: Option<&PostAuthAction>,
713    ) -> Self {
714        Self {
715            providers: providers
716                .into_iter()
717                .map(RegisterPageProvider::new)
718                .collect(),
719            login_link: url_builder.relative_url_for(&Login::from(post_auth_action.cloned())),
720            form: FormState::default(),
721            graphql_endpoint: url_builder.relative_url_for(&GraphQL),
722        }
723    }
724
725    /// Set the state of the registration form
726    #[must_use]
727    pub fn with_form_state(self, form: FormState<RegisterFormField>) -> Self {
728        Self { form, ..self }
729    }
730}
731
732/// Context used by the `consent.html` template
733#[derive(Serialize)]
734pub struct ConsentContext {
735    grant: AuthorizationGrant,
736    client: Client,
737    action: PostAuthAction,
738    matrix_user: MatrixUser,
739}
740
741impl TemplateContext for ConsentContext {
742    fn sample<R: RngExt>(
743        now: chrono::DateTime<Utc>,
744        rng: &mut R,
745        _locales: &[DataLocale],
746    ) -> BTreeMap<SampleIdentifier, Self>
747    where
748        Self: Sized,
749    {
750        sample_list(
751            Client::samples(now, rng)
752                .into_iter()
753                .flat_map(|client| {
754                    [
755                        (None, ResponseMode::Query),
756                        (None, ResponseMode::Fragment),
757                        (None, ResponseMode::FormPost),
758                        (Some("some-state".to_owned()), ResponseMode::Query),
759                        (Some("some-state".to_owned()), ResponseMode::Fragment),
760                        (Some("some-state".to_owned()), ResponseMode::FormPost),
761                    ]
762                    .map(|(state, response_mode)| {
763                        let mut grant = AuthorizationGrant::sample(now, rng);
764                        let action = PostAuthAction::continue_grant(grant.id);
765                        // XXX
766                        grant.client_id = client.id;
767                        grant.state = state;
768                        grant.response_mode = response_mode;
769                        Self {
770                            grant,
771                            client: client.clone(),
772                            action,
773                            matrix_user: MatrixUser {
774                                mxid: "@alice:example.com".to_owned(),
775                                display_name: Some("Alice".to_owned()),
776                            },
777                        }
778                    })
779                })
780                .collect(),
781        )
782    }
783}
784
785impl ConsentContext {
786    /// Constructs a context for the client consent page
787    #[must_use]
788    pub fn new(grant: AuthorizationGrant, client: Client, matrix_user: MatrixUser) -> Self {
789        let action = PostAuthAction::continue_grant(grant.id);
790        Self {
791            grant,
792            client,
793            action,
794            matrix_user,
795        }
796    }
797}
798
799#[derive(Serialize, Debug)]
800#[serde(tag = "grant_type")]
801enum PolicyViolationGrant {
802    #[serde(rename = "authorization_code")]
803    Authorization(AuthorizationGrant),
804    #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
805    DeviceCode(DeviceCodeGrant),
806}
807
808/// Context used by the `policy_violation.html` template
809#[derive(Serialize, Debug)]
810pub struct PolicyViolationContext {
811    grant: PolicyViolationGrant,
812    client: Client,
813    action: PostAuthAction,
814    violations: Vec<Violation>,
815}
816
817impl TemplateContext for PolicyViolationContext {
818    fn sample<R: RngExt>(
819        now: chrono::DateTime<Utc>,
820        rng: &mut R,
821        _locales: &[DataLocale],
822    ) -> BTreeMap<SampleIdentifier, Self>
823    where
824        Self: Sized,
825    {
826        sample_list(
827            Client::samples(now, rng)
828                .into_iter()
829                .flat_map(|client| {
830                    let mut grant = AuthorizationGrant::sample(now, rng);
831                    // XXX
832                    grant.client_id = client.id;
833
834                    let authorization_grant = PolicyViolationContext::for_authorization_grant(
835                        grant.clone(),
836                        client.clone(),
837                        Vec::new(),
838                    );
839
840                    let authorization_grant_invalid_scope =
841                        PolicyViolationContext::for_authorization_grant(
842                            grant.clone(),
843                            client.clone(),
844                            vec![Violation {
845                                msg: "scope 'foo' not allowed".to_owned(),
846                                redirect_uri: None,
847                                field: None,
848                                variant: None,
849                            }],
850                        );
851                    let authorization_grant_admin_scope =
852                        PolicyViolationContext::for_authorization_grant(
853                            grant.clone(),
854                            client.clone(),
855                            vec![Violation {
856                                msg: "scope 'urn:mas:admin' requires admin privileges".to_owned(),
857                                redirect_uri: None,
858                                field: None,
859                                variant: Some(ViolationVariant::AdminScopeNotAllowed),
860                            }],
861                        );
862
863                    let authorization_grant_client_not_allowed =
864                        PolicyViolationContext::for_authorization_grant(
865                            grant,
866                            client.clone(),
867                            vec![Violation {
868                                msg: "This client is not allowed to login".to_owned(),
869                                redirect_uri: None,
870                                field: None,
871                                variant: Some(ViolationVariant::ClientNotAllowed),
872                            }],
873                        );
874
875                    let device_code_grant = PolicyViolationContext::for_device_code_grant(
876                        DeviceCodeGrant {
877                            id: Ulid::from_datetime_with_rng(now, rng),
878                            state: mas_data_model::DeviceCodeGrantState::Pending,
879                            client_id: client.id,
880                            scope: [OPENID].into_iter().collect(),
881                            user_code: mas_data_model::generate_user_code(rng),
882                            device_code: Alphanumeric.sample_string(rng, 32),
883                            created_at: now - Duration::try_minutes(5).unwrap(),
884                            expires_at: now + Duration::try_minutes(25).unwrap(),
885                            ip_address: None,
886                            user_agent: None,
887                            locale: None,
888                        },
889                        client.clone(),
890                        Vec::new(),
891                    );
892
893                    let device_code_grant_invalid_scope =
894                        PolicyViolationContext::for_device_code_grant(
895                            DeviceCodeGrant {
896                                id: Ulid::from_datetime_with_rng(now, rng),
897                                state: mas_data_model::DeviceCodeGrantState::Pending,
898                                client_id: client.id,
899                                scope: [OPENID].into_iter().collect(),
900                                user_code: mas_data_model::generate_user_code(rng),
901                                device_code: Alphanumeric.sample_string(rng, 32),
902                                created_at: now - Duration::try_minutes(5).unwrap(),
903                                expires_at: now + Duration::try_minutes(25).unwrap(),
904                                ip_address: None,
905                                user_agent: None,
906                                locale: None,
907                            },
908                            client,
909                            vec![Violation {
910                                msg: "user has too many active sessions".to_owned(),
911                                redirect_uri: None,
912                                field: None,
913                                variant: Some(ViolationVariant::TooManySessions {
914                                    need_to_remove: 1,
915                                }),
916                            }],
917                        );
918
919                    [
920                        authorization_grant,
921                        authorization_grant_invalid_scope,
922                        authorization_grant_admin_scope,
923                        authorization_grant_client_not_allowed,
924                        device_code_grant,
925                        device_code_grant_invalid_scope,
926                    ]
927                })
928                .collect(),
929        )
930    }
931}
932
933impl PolicyViolationContext {
934    /// Constructs a context for the policy violation page for an authorization
935    /// grant
936    #[must_use]
937    pub const fn for_authorization_grant(
938        grant: AuthorizationGrant,
939        client: Client,
940        violations: Vec<Violation>,
941    ) -> Self {
942        let action = PostAuthAction::continue_grant(grant.id);
943        Self {
944            grant: PolicyViolationGrant::Authorization(grant),
945            client,
946            action,
947            violations,
948        }
949    }
950
951    /// Constructs a context for the policy violation page for a device code
952    /// grant
953    #[must_use]
954    pub const fn for_device_code_grant(
955        grant: DeviceCodeGrant,
956        client: Client,
957        violations: Vec<Violation>,
958    ) -> Self {
959        let action = PostAuthAction::continue_device_code_grant(grant.id);
960        Self {
961            grant: PolicyViolationGrant::DeviceCode(grant),
962            client,
963            action,
964            violations,
965        }
966    }
967}
968
969/// Context used by the `compat_login_policy_violation.html` template
970#[derive(Serialize)]
971pub struct CompatLoginPolicyViolationContext {
972    violations: Vec<Violation>,
973}
974
975impl TemplateContext for CompatLoginPolicyViolationContext {
976    fn sample<R: RngExt>(
977        _now: chrono::DateTime<Utc>,
978        _rng: &mut R,
979        _locales: &[DataLocale],
980    ) -> BTreeMap<SampleIdentifier, Self>
981    where
982        Self: Sized,
983    {
984        sample_list(vec![
985            CompatLoginPolicyViolationContext { violations: vec![] },
986            CompatLoginPolicyViolationContext {
987                violations: vec![Violation {
988                    msg: "scope 'foo' not allowed".to_owned(),
989                    redirect_uri: None,
990                    field: None,
991                    variant: None,
992                }],
993            },
994            CompatLoginPolicyViolationContext {
995                violations: vec![Violation {
996                    msg: "user has too many active sessions".to_owned(),
997                    redirect_uri: None,
998                    field: None,
999                    variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1000                }],
1001            },
1002            CompatLoginPolicyViolationContext {
1003                violations: vec![Violation {
1004                    msg: "This client is not allowed to login".to_owned(),
1005                    redirect_uri: None,
1006                    field: None,
1007                    variant: Some(ViolationVariant::ClientNotAllowed),
1008                }],
1009            },
1010            CompatLoginPolicyViolationContext {
1011                violations: vec![
1012                    Violation {
1013                        msg: "scope 'foo' not allowed".to_owned(),
1014                        redirect_uri: None,
1015                        field: None,
1016                        variant: None,
1017                    },
1018                    Violation {
1019                        msg: "user has too many active sessions".to_owned(),
1020                        redirect_uri: None,
1021                        field: None,
1022                        variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1023                    },
1024                ],
1025            },
1026        ])
1027    }
1028}
1029
1030impl CompatLoginPolicyViolationContext {
1031    /// Constructs a context for the compatibility login policy violation page
1032    /// given the list of violations
1033    #[must_use]
1034    pub const fn for_violations(violations: Vec<Violation>) -> Self {
1035        Self { violations }
1036    }
1037}
1038
1039/// Context used by the `sso.html` template
1040#[derive(Serialize)]
1041pub struct CompatSsoContext {
1042    login: CompatSsoLogin,
1043    action: PostAuthAction,
1044    matrix_user: MatrixUser,
1045}
1046
1047impl TemplateContext for CompatSsoContext {
1048    fn sample<R: RngExt>(
1049        now: chrono::DateTime<Utc>,
1050        rng: &mut R,
1051        _locales: &[DataLocale],
1052    ) -> BTreeMap<SampleIdentifier, Self>
1053    where
1054        Self: Sized,
1055    {
1056        let id = Ulid::from_datetime_with_rng(now, rng);
1057        sample_list(vec![CompatSsoContext::new(
1058            CompatSsoLogin {
1059                id,
1060                redirect_uri: Url::parse("https://app.element.io/").unwrap(),
1061                login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
1062                created_at: now,
1063                state: CompatSsoLoginState::Pending,
1064            },
1065            MatrixUser {
1066                mxid: "@alice:example.com".to_owned(),
1067                display_name: Some("Alice".to_owned()),
1068            },
1069        )])
1070    }
1071}
1072
1073impl CompatSsoContext {
1074    /// Constructs a context for the legacy SSO login page
1075    #[must_use]
1076    pub fn new(login: CompatSsoLogin, matrix_user: MatrixUser) -> Self
1077where {
1078        let action = PostAuthAction::continue_compat_sso_login(login.id);
1079        Self {
1080            login,
1081            action,
1082            matrix_user,
1083        }
1084    }
1085}
1086
1087/// Context used by the `emails/recovery.{txt,html,subject}` templates
1088#[derive(Serialize)]
1089pub struct EmailRecoveryContext {
1090    user: User,
1091    session: UserRecoverySession,
1092    recovery_link: Url,
1093}
1094
1095impl EmailRecoveryContext {
1096    /// Constructs a context for the recovery email
1097    #[must_use]
1098    pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
1099        Self {
1100            user,
1101            session,
1102            recovery_link,
1103        }
1104    }
1105
1106    /// Returns the user associated with the recovery email
1107    #[must_use]
1108    pub fn user(&self) -> &User {
1109        &self.user
1110    }
1111
1112    /// Returns the recovery session associated with the recovery email
1113    #[must_use]
1114    pub fn session(&self) -> &UserRecoverySession {
1115        &self.session
1116    }
1117}
1118
1119impl TemplateContext for EmailRecoveryContext {
1120    fn sample<R: RngExt>(
1121        now: chrono::DateTime<Utc>,
1122        rng: &mut R,
1123        _locales: &[DataLocale],
1124    ) -> BTreeMap<SampleIdentifier, Self>
1125    where
1126        Self: Sized,
1127    {
1128        sample_list(User::samples(now, rng).into_iter().map(|user| {
1129            let session = UserRecoverySession {
1130                id: Ulid::from_datetime_with_rng(now, rng),
1131                email: "hello@example.com".to_owned(),
1132                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(),
1133                ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
1134                locale: "en".to_owned(),
1135                created_at: now,
1136                consumed_at: None,
1137            };
1138
1139            let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
1140
1141            Self::new(user, session, link)
1142        }).collect())
1143    }
1144}
1145
1146/// Context used by the `emails/verification.{txt,html,subject}` templates
1147#[derive(Serialize)]
1148pub struct EmailVerificationContext {
1149    #[serde(skip_serializing_if = "Option::is_none")]
1150    browser_session: Option<BrowserSession>,
1151    #[serde(skip_serializing_if = "Option::is_none")]
1152    user_registration: Option<UserRegistration>,
1153    authentication_code: UserEmailAuthenticationCode,
1154}
1155
1156impl EmailVerificationContext {
1157    /// Constructs a context for the verification email
1158    #[must_use]
1159    pub fn new(
1160        authentication_code: UserEmailAuthenticationCode,
1161        browser_session: Option<BrowserSession>,
1162        user_registration: Option<UserRegistration>,
1163    ) -> Self {
1164        Self {
1165            browser_session,
1166            user_registration,
1167            authentication_code,
1168        }
1169    }
1170
1171    /// Get the user to which this email is being sent
1172    #[must_use]
1173    pub fn user(&self) -> Option<&User> {
1174        self.browser_session.as_ref().map(|s| &s.user)
1175    }
1176
1177    /// Get the verification code being sent
1178    #[must_use]
1179    pub fn code(&self) -> &str {
1180        &self.authentication_code.code
1181    }
1182}
1183
1184impl TemplateContext for EmailVerificationContext {
1185    fn sample<R: RngExt>(
1186        now: chrono::DateTime<Utc>,
1187        rng: &mut R,
1188        _locales: &[DataLocale],
1189    ) -> BTreeMap<SampleIdentifier, Self>
1190    where
1191        Self: Sized,
1192    {
1193        sample_list(
1194            BrowserSession::samples(now, rng)
1195                .into_iter()
1196                .map(|browser_session| {
1197                    let authentication_code = UserEmailAuthenticationCode {
1198                        id: Ulid::from_datetime_with_rng(now, rng),
1199                        user_email_authentication_id: Ulid::from_datetime_with_rng(now, rng),
1200                        code: "123456".to_owned(),
1201                        created_at: now - Duration::try_minutes(5).unwrap(),
1202                        expires_at: now + Duration::try_minutes(25).unwrap(),
1203                    };
1204
1205                    Self {
1206                        browser_session: Some(browser_session),
1207                        user_registration: None,
1208                        authentication_code,
1209                    }
1210                })
1211                .collect(),
1212        )
1213    }
1214}
1215
1216/// Fields of the email verification form
1217#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1218#[serde(rename_all = "snake_case")]
1219pub enum RegisterStepsVerifyEmailFormField {
1220    /// The code field
1221    Code,
1222}
1223
1224impl FormField for RegisterStepsVerifyEmailFormField {
1225    fn keep(&self) -> bool {
1226        match self {
1227            Self::Code => true,
1228        }
1229    }
1230}
1231
1232/// Context used by the `pages/register/steps/verify_email.html` templates
1233#[derive(Serialize)]
1234pub struct RegisterStepsVerifyEmailContext {
1235    form: FormState<RegisterStepsVerifyEmailFormField>,
1236    authentication: UserEmailAuthentication,
1237}
1238
1239impl RegisterStepsVerifyEmailContext {
1240    /// Constructs a context for the email verification page
1241    #[must_use]
1242    pub fn new(authentication: UserEmailAuthentication) -> Self {
1243        Self {
1244            form: FormState::default(),
1245            authentication,
1246        }
1247    }
1248
1249    /// Set the form state
1250    #[must_use]
1251    pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
1252        Self { form, ..self }
1253    }
1254}
1255
1256impl TemplateContext for RegisterStepsVerifyEmailContext {
1257    fn sample<R: RngExt>(
1258        now: chrono::DateTime<Utc>,
1259        rng: &mut R,
1260        _locales: &[DataLocale],
1261    ) -> BTreeMap<SampleIdentifier, Self>
1262    where
1263        Self: Sized,
1264    {
1265        let authentication = UserEmailAuthentication {
1266            id: Ulid::from_datetime_with_rng(now, rng),
1267            user_session_id: None,
1268            user_registration_id: None,
1269            email: "foobar@example.com".to_owned(),
1270            created_at: now,
1271            completed_at: None,
1272        };
1273
1274        sample_list(vec![Self {
1275            form: FormState::default(),
1276            authentication,
1277        }])
1278    }
1279}
1280
1281/// Context used by the `pages/register/steps/email_in_use.html` template
1282#[derive(Serialize)]
1283pub struct RegisterStepsEmailInUseContext {
1284    email: String,
1285    action: Option<PostAuthAction>,
1286}
1287
1288impl RegisterStepsEmailInUseContext {
1289    /// Constructs a context for the email in use page
1290    #[must_use]
1291    pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
1292        Self { email, action }
1293    }
1294}
1295
1296impl TemplateContext for RegisterStepsEmailInUseContext {
1297    fn sample<R: RngExt>(
1298        _now: chrono::DateTime<Utc>,
1299        _rng: &mut R,
1300        _locales: &[DataLocale],
1301    ) -> BTreeMap<SampleIdentifier, Self>
1302    where
1303        Self: Sized,
1304    {
1305        let email = "hello@example.com".to_owned();
1306        let action = PostAuthAction::continue_grant(Ulid::nil());
1307        sample_list(vec![Self::new(email, Some(action))])
1308    }
1309}
1310
1311/// Fields for the display name form
1312#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1313#[serde(rename_all = "snake_case")]
1314pub enum RegisterStepsDisplayNameFormField {
1315    /// The display name
1316    DisplayName,
1317}
1318
1319impl FormField for RegisterStepsDisplayNameFormField {
1320    fn keep(&self) -> bool {
1321        match self {
1322            Self::DisplayName => true,
1323        }
1324    }
1325}
1326
1327/// Context used by the `display_name.html` template
1328#[derive(Serialize, Default)]
1329pub struct RegisterStepsDisplayNameContext {
1330    form: FormState<RegisterStepsDisplayNameFormField>,
1331}
1332
1333impl RegisterStepsDisplayNameContext {
1334    /// Constructs a context for the display name page
1335    #[must_use]
1336    pub fn new() -> Self {
1337        Self::default()
1338    }
1339
1340    /// Set the form state
1341    #[must_use]
1342    pub fn with_form_state(
1343        mut self,
1344        form_state: FormState<RegisterStepsDisplayNameFormField>,
1345    ) -> Self {
1346        self.form = form_state;
1347        self
1348    }
1349}
1350
1351impl TemplateContext for RegisterStepsDisplayNameContext {
1352    fn sample<R: RngExt>(
1353        _now: chrono::DateTime<chrono::Utc>,
1354        _rng: &mut R,
1355        _locales: &[DataLocale],
1356    ) -> BTreeMap<SampleIdentifier, Self>
1357    where
1358        Self: Sized,
1359    {
1360        sample_list(vec![Self {
1361            form: FormState::default(),
1362        }])
1363    }
1364}
1365
1366/// Fields of the registration token form
1367#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1368#[serde(rename_all = "snake_case")]
1369pub enum RegisterStepsRegistrationTokenFormField {
1370    /// The registration token
1371    Token,
1372}
1373
1374impl FormField for RegisterStepsRegistrationTokenFormField {
1375    fn keep(&self) -> bool {
1376        match self {
1377            Self::Token => true,
1378        }
1379    }
1380}
1381
1382/// The registration token page context
1383#[derive(Serialize, Default)]
1384pub struct RegisterStepsRegistrationTokenContext {
1385    form: FormState<RegisterStepsRegistrationTokenFormField>,
1386}
1387
1388impl RegisterStepsRegistrationTokenContext {
1389    /// Constructs a context for the registration token page
1390    #[must_use]
1391    pub fn new() -> Self {
1392        Self::default()
1393    }
1394
1395    /// Set the form state
1396    #[must_use]
1397    pub fn with_form_state(
1398        mut self,
1399        form_state: FormState<RegisterStepsRegistrationTokenFormField>,
1400    ) -> Self {
1401        self.form = form_state;
1402        self
1403    }
1404}
1405
1406impl TemplateContext for RegisterStepsRegistrationTokenContext {
1407    fn sample<R: RngExt>(
1408        _now: chrono::DateTime<chrono::Utc>,
1409        _rng: &mut R,
1410        _locales: &[DataLocale],
1411    ) -> BTreeMap<SampleIdentifier, Self>
1412    where
1413        Self: Sized,
1414    {
1415        sample_list(vec![Self {
1416            form: FormState::default(),
1417        }])
1418    }
1419}
1420
1421/// Fields of the account recovery start form
1422#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1423#[serde(rename_all = "snake_case")]
1424pub enum RecoveryStartFormField {
1425    /// The email
1426    Email,
1427}
1428
1429impl FormField for RecoveryStartFormField {
1430    fn keep(&self) -> bool {
1431        match self {
1432            Self::Email => true,
1433        }
1434    }
1435}
1436
1437/// Context used by the `pages/recovery/start.html` template
1438#[derive(Serialize, Default)]
1439pub struct RecoveryStartContext {
1440    form: FormState<RecoveryStartFormField>,
1441}
1442
1443impl RecoveryStartContext {
1444    /// Constructs a context for the recovery start page
1445    #[must_use]
1446    pub fn new() -> Self {
1447        Self::default()
1448    }
1449
1450    /// Set the form state
1451    #[must_use]
1452    pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1453        Self { form }
1454    }
1455}
1456
1457impl TemplateContext for RecoveryStartContext {
1458    fn sample<R: RngExt>(
1459        _now: chrono::DateTime<Utc>,
1460        _rng: &mut R,
1461        _locales: &[DataLocale],
1462    ) -> BTreeMap<SampleIdentifier, Self>
1463    where
1464        Self: Sized,
1465    {
1466        sample_list(vec![
1467            Self::new(),
1468            Self::new().with_form_state(
1469                FormState::default()
1470                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1471            ),
1472            Self::new().with_form_state(
1473                FormState::default()
1474                    .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1475            ),
1476        ])
1477    }
1478}
1479
1480/// Context used by the `pages/recovery/progress.html` template
1481#[derive(Serialize)]
1482pub struct RecoveryProgressContext {
1483    session: UserRecoverySession,
1484    /// Whether resending the e-mail was denied because of rate limits
1485    resend_failed_due_to_rate_limit: bool,
1486}
1487
1488impl RecoveryProgressContext {
1489    /// Constructs a context for the recovery progress page
1490    #[must_use]
1491    pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1492        Self {
1493            session,
1494            resend_failed_due_to_rate_limit,
1495        }
1496    }
1497}
1498
1499impl TemplateContext for RecoveryProgressContext {
1500    fn sample<R: RngExt>(
1501        now: chrono::DateTime<Utc>,
1502        rng: &mut R,
1503        _locales: &[DataLocale],
1504    ) -> BTreeMap<SampleIdentifier, Self>
1505    where
1506        Self: Sized,
1507    {
1508        let session = UserRecoverySession {
1509            id: Ulid::from_datetime_with_rng(now, rng),
1510            email: "name@mail.com".to_owned(),
1511            user_agent: "Mozilla/5.0".to_owned(),
1512            ip_address: None,
1513            locale: "en".to_owned(),
1514            created_at: now,
1515            consumed_at: None,
1516        };
1517
1518        sample_list(vec![
1519            Self {
1520                session: session.clone(),
1521                resend_failed_due_to_rate_limit: false,
1522            },
1523            Self {
1524                session,
1525                resend_failed_due_to_rate_limit: true,
1526            },
1527        ])
1528    }
1529}
1530
1531/// Context used by the `pages/recovery/expired.html` template
1532#[derive(Serialize)]
1533pub struct RecoveryExpiredContext {
1534    session: UserRecoverySession,
1535}
1536
1537impl RecoveryExpiredContext {
1538    /// Constructs a context for the recovery expired page
1539    #[must_use]
1540    pub fn new(session: UserRecoverySession) -> Self {
1541        Self { session }
1542    }
1543}
1544
1545impl TemplateContext for RecoveryExpiredContext {
1546    fn sample<R: RngExt>(
1547        now: chrono::DateTime<Utc>,
1548        rng: &mut R,
1549        _locales: &[DataLocale],
1550    ) -> BTreeMap<SampleIdentifier, Self>
1551    where
1552        Self: Sized,
1553    {
1554        let session = UserRecoverySession {
1555            id: Ulid::from_datetime_with_rng(now, rng),
1556            email: "name@mail.com".to_owned(),
1557            user_agent: "Mozilla/5.0".to_owned(),
1558            ip_address: None,
1559            locale: "en".to_owned(),
1560            created_at: now,
1561            consumed_at: None,
1562        };
1563
1564        sample_list(vec![Self { session }])
1565    }
1566}
1567/// Fields of the account recovery finish form
1568#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1569#[serde(rename_all = "snake_case")]
1570pub enum RecoveryFinishFormField {
1571    /// The new password
1572    NewPassword,
1573
1574    /// The new password confirmation
1575    NewPasswordConfirm,
1576}
1577
1578impl FormField for RecoveryFinishFormField {
1579    fn keep(&self) -> bool {
1580        false
1581    }
1582}
1583
1584/// Context used by the `pages/recovery/finish.html` template
1585#[derive(Serialize)]
1586pub struct RecoveryFinishContext {
1587    user: User,
1588    form: FormState<RecoveryFinishFormField>,
1589}
1590
1591impl RecoveryFinishContext {
1592    /// Constructs a context for the recovery finish page
1593    #[must_use]
1594    pub fn new(user: User) -> Self {
1595        Self {
1596            user,
1597            form: FormState::default(),
1598        }
1599    }
1600
1601    /// Set the form state
1602    #[must_use]
1603    pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1604        self.form = form;
1605        self
1606    }
1607}
1608
1609impl TemplateContext for RecoveryFinishContext {
1610    fn sample<R: RngExt>(
1611        now: chrono::DateTime<Utc>,
1612        rng: &mut R,
1613        _locales: &[DataLocale],
1614    ) -> BTreeMap<SampleIdentifier, Self>
1615    where
1616        Self: Sized,
1617    {
1618        sample_list(
1619            User::samples(now, rng)
1620                .into_iter()
1621                .flat_map(|user| {
1622                    vec![
1623                        Self::new(user.clone()),
1624                        Self::new(user.clone()).with_form_state(
1625                            FormState::default().with_error_on_field(
1626                                RecoveryFinishFormField::NewPassword,
1627                                FieldError::Invalid,
1628                            ),
1629                        ),
1630                        Self::new(user.clone()).with_form_state(
1631                            FormState::default().with_error_on_field(
1632                                RecoveryFinishFormField::NewPasswordConfirm,
1633                                FieldError::Invalid,
1634                            ),
1635                        ),
1636                    ]
1637                })
1638                .collect(),
1639        )
1640    }
1641}
1642
1643/// Context used by the `pages/upstream_oauth2/link_mismatch.html`
1644/// templates
1645#[derive(Serialize)]
1646pub struct UpstreamExistingLinkContext {
1647    linked_user: User,
1648}
1649
1650impl UpstreamExistingLinkContext {
1651    /// Constructs a new context with an existing linked user
1652    #[must_use]
1653    pub fn new(linked_user: User) -> Self {
1654        Self { linked_user }
1655    }
1656}
1657
1658impl TemplateContext for UpstreamExistingLinkContext {
1659    fn sample<R: RngExt>(
1660        now: chrono::DateTime<Utc>,
1661        rng: &mut R,
1662        _locales: &[DataLocale],
1663    ) -> BTreeMap<SampleIdentifier, Self>
1664    where
1665        Self: Sized,
1666    {
1667        sample_list(
1668            User::samples(now, rng)
1669                .into_iter()
1670                .map(|linked_user| Self { linked_user })
1671                .collect(),
1672        )
1673    }
1674}
1675
1676/// Context used by the `pages/upstream_oauth2/suggest_link.html`
1677/// templates
1678#[derive(Serialize)]
1679pub struct UpstreamSuggestLink {
1680    post_logout_action: PostAuthAction,
1681}
1682
1683impl UpstreamSuggestLink {
1684    /// Constructs a new context with an existing linked user
1685    #[must_use]
1686    pub fn new(link: &UpstreamOAuthLink) -> Self {
1687        Self::for_link_id(link.id)
1688    }
1689
1690    fn for_link_id(id: Ulid) -> Self {
1691        let post_logout_action = PostAuthAction::link_upstream(id);
1692        Self { post_logout_action }
1693    }
1694}
1695
1696impl TemplateContext for UpstreamSuggestLink {
1697    fn sample<R: RngExt>(
1698        now: chrono::DateTime<Utc>,
1699        rng: &mut R,
1700        _locales: &[DataLocale],
1701    ) -> BTreeMap<SampleIdentifier, Self>
1702    where
1703        Self: Sized,
1704    {
1705        let id = Ulid::from_datetime_with_rng(now, rng);
1706        sample_list(vec![Self::for_link_id(id)])
1707    }
1708}
1709
1710/// User-editeable fields of the upstream account link form
1711#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1712#[serde(rename_all = "snake_case")]
1713pub enum UpstreamRegisterFormField {
1714    /// The username field
1715    Username,
1716
1717    /// Accept the terms of service
1718    AcceptTerms,
1719}
1720
1721impl FormField for UpstreamRegisterFormField {
1722    fn keep(&self) -> bool {
1723        match self {
1724            Self::Username | Self::AcceptTerms => true,
1725        }
1726    }
1727}
1728
1729/// Context used by the `pages/upstream_oauth2/do_register.html`
1730/// templates
1731#[derive(Serialize)]
1732pub struct UpstreamRegister {
1733    upstream_oauth_link: UpstreamOAuthLink,
1734    upstream_oauth_provider: UpstreamOAuthProvider,
1735    imported_localpart: Option<String>,
1736    force_localpart: bool,
1737    imported_display_name: Option<String>,
1738    force_display_name: bool,
1739    imported_email: Option<String>,
1740    force_email: bool,
1741    form_state: FormState<UpstreamRegisterFormField>,
1742}
1743
1744impl UpstreamRegister {
1745    /// Constructs a new context for registering a new user from an upstream
1746    /// provider
1747    #[must_use]
1748    pub fn new(
1749        upstream_oauth_link: UpstreamOAuthLink,
1750        upstream_oauth_provider: UpstreamOAuthProvider,
1751    ) -> Self {
1752        Self {
1753            upstream_oauth_link,
1754            upstream_oauth_provider,
1755            imported_localpart: None,
1756            force_localpart: false,
1757            imported_display_name: None,
1758            force_display_name: false,
1759            imported_email: None,
1760            force_email: false,
1761            form_state: FormState::default(),
1762        }
1763    }
1764
1765    /// Set the imported localpart
1766    pub fn set_localpart(&mut self, localpart: String, force: bool) {
1767        self.imported_localpart = Some(localpart);
1768        self.force_localpart = force;
1769    }
1770
1771    /// Set the imported localpart
1772    #[must_use]
1773    pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1774        Self {
1775            imported_localpart: Some(localpart),
1776            force_localpart: force,
1777            ..self
1778        }
1779    }
1780
1781    /// Set the imported display name
1782    pub fn set_display_name(&mut self, display_name: String, force: bool) {
1783        self.imported_display_name = Some(display_name);
1784        self.force_display_name = force;
1785    }
1786
1787    /// Set the imported display name
1788    #[must_use]
1789    pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1790        Self {
1791            imported_display_name: Some(display_name),
1792            force_display_name: force,
1793            ..self
1794        }
1795    }
1796
1797    /// Set the imported email
1798    pub fn set_email(&mut self, email: String, force: bool) {
1799        self.imported_email = Some(email);
1800        self.force_email = force;
1801    }
1802
1803    /// Set the imported email
1804    #[must_use]
1805    pub fn with_email(self, email: String, force: bool) -> Self {
1806        Self {
1807            imported_email: Some(email),
1808            force_email: force,
1809            ..self
1810        }
1811    }
1812
1813    /// Set the form state
1814    pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1815        self.form_state = form_state;
1816    }
1817
1818    /// Set the form state
1819    #[must_use]
1820    pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1821        Self { form_state, ..self }
1822    }
1823}
1824
1825impl TemplateContext for UpstreamRegister {
1826    fn sample<R: RngExt>(
1827        now: chrono::DateTime<Utc>,
1828        _rng: &mut R,
1829        _locales: &[DataLocale],
1830    ) -> BTreeMap<SampleIdentifier, Self>
1831    where
1832        Self: Sized,
1833    {
1834        sample_list(vec![Self::new(
1835            UpstreamOAuthLink {
1836                id: Ulid::nil(),
1837                provider_id: Ulid::nil(),
1838                user_id: None,
1839                subject: "subject".to_owned(),
1840                human_account_name: Some("@john".to_owned()),
1841                created_at: now,
1842            },
1843            UpstreamOAuthProvider {
1844                id: Ulid::nil(),
1845                issuer: Some("https://example.com/".to_owned()),
1846                human_name: Some("Example Ltd.".to_owned()),
1847                brand_name: None,
1848                scope: Scope::from_iter([OPENID]),
1849                token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1850                token_endpoint_signing_alg: None,
1851                id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1852                client_id: "client-id".to_owned(),
1853                encrypted_client_secret: None,
1854                claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1855                authorization_endpoint_override: None,
1856                token_endpoint_override: None,
1857                jwks_uri_override: None,
1858                userinfo_endpoint_override: None,
1859                fetch_userinfo: false,
1860                userinfo_signed_response_alg: None,
1861                discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1862                pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1863                response_mode: None,
1864                additional_authorization_parameters: Vec::new(),
1865                forward_login_hint: false,
1866                created_at: now,
1867                disabled_at: None,
1868                on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
1869                registration_token_required: false,
1870            },
1871        )])
1872    }
1873}
1874
1875/// Form fields on the device link page
1876#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1877#[serde(rename_all = "snake_case")]
1878pub enum DeviceLinkFormField {
1879    /// The device code field
1880    Code,
1881}
1882
1883impl FormField for DeviceLinkFormField {
1884    fn keep(&self) -> bool {
1885        match self {
1886            Self::Code => true,
1887        }
1888    }
1889}
1890
1891/// Context used by the `device_link.html` template
1892#[derive(Serialize, Default, Debug)]
1893pub struct DeviceLinkContext {
1894    form_state: FormState<DeviceLinkFormField>,
1895}
1896
1897impl DeviceLinkContext {
1898    /// Constructs a new context with an existing linked user
1899    #[must_use]
1900    pub fn new() -> Self {
1901        Self::default()
1902    }
1903
1904    /// Set the form state
1905    #[must_use]
1906    pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1907        self.form_state = form_state;
1908        self
1909    }
1910}
1911
1912impl TemplateContext for DeviceLinkContext {
1913    fn sample<R: RngExt>(
1914        _now: chrono::DateTime<Utc>,
1915        _rng: &mut R,
1916        _locales: &[DataLocale],
1917    ) -> BTreeMap<SampleIdentifier, Self>
1918    where
1919        Self: Sized,
1920    {
1921        sample_list(vec![
1922            Self::new(),
1923            Self::new().with_form_state(
1924                FormState::default()
1925                    .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1926            ),
1927        ])
1928    }
1929}
1930
1931/// Context used by the `device_consent.html` template
1932#[derive(Serialize, Debug)]
1933pub struct DeviceConsentContext {
1934    grant: DeviceCodeGrant,
1935    client: Client,
1936    matrix_user: MatrixUser,
1937}
1938
1939impl DeviceConsentContext {
1940    /// Constructs a new context with an existing linked user
1941    #[must_use]
1942    pub fn new(grant: DeviceCodeGrant, client: Client, matrix_user: MatrixUser) -> Self {
1943        Self {
1944            grant,
1945            client,
1946            matrix_user,
1947        }
1948    }
1949}
1950
1951impl TemplateContext for DeviceConsentContext {
1952    fn sample<R: RngExt>(
1953        now: chrono::DateTime<Utc>,
1954        rng: &mut R,
1955        _locales: &[DataLocale],
1956    ) -> BTreeMap<SampleIdentifier, Self>
1957    where
1958        Self: Sized,
1959    {
1960        sample_list(Client::samples(now, rng)
1961            .into_iter()
1962            .map(|client|  {
1963                let grant = DeviceCodeGrant {
1964                    id: Ulid::from_datetime_with_rng(now, rng),
1965                    state: mas_data_model::DeviceCodeGrantState::Pending,
1966                    client_id: client.id,
1967                    scope: [OPENID].into_iter().collect(),
1968                    user_code: mas_data_model::generate_user_code(rng),
1969                    device_code: Alphanumeric.sample_string(rng, 32),
1970                    created_at: now - Duration::try_minutes(5).unwrap(),
1971                    expires_at: now + Duration::try_minutes(25).unwrap(),
1972                    ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
1973                    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()),
1974                    locale: None,
1975                };
1976                Self {
1977                    grant,
1978                    client,
1979                    matrix_user: MatrixUser {
1980                        mxid: "@alice:example.com".to_owned(),
1981                        display_name: Some("Alice".to_owned()),
1982                    }
1983                }
1984            })
1985            .collect())
1986    }
1987}
1988
1989/// Context used by the `account/deactivated.html`, `account/locked.html` and
1990/// `account/logged_out.html` templates
1991#[derive(Serialize)]
1992pub struct AccountInactiveContext {
1993    user: User,
1994
1995    /// The action to continue after signing out and back in from the
1996    /// interstitial. Absent when there is no continuation to preserve.
1997    #[serde(skip_serializing_if = "Option::is_none")]
1998    post_logout_action: Option<PostAuthAction>,
1999}
2000
2001impl AccountInactiveContext {
2002    /// Constructs a new context with an existing linked user
2003    #[must_use]
2004    pub fn new(user: User) -> Self {
2005        Self {
2006            user,
2007            post_logout_action: None,
2008        }
2009    }
2010
2011    /// Set the action to continue once the user has signed out and back in
2012    #[must_use]
2013    pub fn with_post_auth_action(mut self, action: Option<PostAuthAction>) -> Self {
2014        self.post_logout_action = action;
2015        self
2016    }
2017}
2018
2019impl TemplateContext for AccountInactiveContext {
2020    fn sample<R: RngExt>(
2021        now: chrono::DateTime<Utc>,
2022        rng: &mut R,
2023        _locales: &[DataLocale],
2024    ) -> BTreeMap<SampleIdentifier, Self>
2025    where
2026        Self: Sized,
2027    {
2028        let action = PostAuthAction::continue_grant(Ulid::from_datetime_with_rng(now, rng));
2029        sample_list(
2030            User::samples(now, rng)
2031                .into_iter()
2032                .flat_map(|user| {
2033                    // Cover both the "no continuation" and "with continuation" render
2034                    // paths so the template gallery exercises the hidden inputs.
2035                    [
2036                        AccountInactiveContext::new(user.clone()),
2037                        AccountInactiveContext::new(user)
2038                            .with_post_auth_action(Some(action.clone())),
2039                    ]
2040                })
2041                .collect(),
2042        )
2043    }
2044}
2045
2046/// Context used by the `device_name.txt` template
2047#[derive(Serialize)]
2048pub struct DeviceNameContext {
2049    client: Client,
2050    raw_user_agent: String,
2051}
2052
2053impl DeviceNameContext {
2054    /// Constructs a new context with a client and user agent
2055    #[must_use]
2056    pub fn new(client: Client, user_agent: Option<String>) -> Self {
2057        Self {
2058            client,
2059            raw_user_agent: user_agent.unwrap_or_default(),
2060        }
2061    }
2062}
2063
2064impl TemplateContext for DeviceNameContext {
2065    fn sample<R: RngExt>(
2066        now: chrono::DateTime<Utc>,
2067        rng: &mut R,
2068        _locales: &[DataLocale],
2069    ) -> BTreeMap<SampleIdentifier, Self>
2070    where
2071        Self: Sized,
2072    {
2073        sample_list(Client::samples(now, rng)
2074            .into_iter()
2075            .map(|client| DeviceNameContext {
2076                client,
2077                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(),
2078            })
2079            .collect())
2080    }
2081}
2082
2083/// Context used by the `form_post.html` template
2084#[derive(Serialize)]
2085pub struct FormPostContext<T> {
2086    redirect_uri: Option<Url>,
2087    params: T,
2088}
2089
2090impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
2091    fn sample<R: RngExt>(
2092        now: chrono::DateTime<Utc>,
2093        rng: &mut R,
2094        locales: &[DataLocale],
2095    ) -> BTreeMap<SampleIdentifier, Self>
2096    where
2097        Self: Sized,
2098    {
2099        let sample_params = T::sample(now, rng, locales);
2100        sample_params
2101            .into_iter()
2102            .map(|(k, params)| {
2103                (
2104                    k,
2105                    FormPostContext {
2106                        redirect_uri: "https://example.com/callback".parse().ok(),
2107                        params,
2108                    },
2109                )
2110            })
2111            .collect()
2112    }
2113}
2114
2115impl<T> FormPostContext<T> {
2116    /// Constructs a context for the `form_post` response mode form for a given
2117    /// URL
2118    pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
2119        Self {
2120            redirect_uri: Some(redirect_uri),
2121            params,
2122        }
2123    }
2124
2125    /// Constructs a context for the `form_post` response mode form for the
2126    /// current URL
2127    pub fn new_for_current_url(params: T) -> Self {
2128        Self {
2129            redirect_uri: None,
2130            params,
2131        }
2132    }
2133
2134    /// Add the language to the context
2135    ///
2136    /// This is usually implemented by the [`TemplateContext`] trait, but it is
2137    /// annoying to make it work because of the generic parameter
2138    pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
2139        WithLanguage {
2140            lang: lang.to_string(),
2141            inner: self,
2142        }
2143    }
2144}
2145
2146/// Context used by the `error.html` template
2147#[derive(Default, Serialize, Debug, Clone)]
2148pub struct ErrorContext {
2149    code: Option<&'static str>,
2150    description: Option<String>,
2151    details: Option<String>,
2152    lang: Option<String>,
2153}
2154
2155impl std::fmt::Display for ErrorContext {
2156    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2157        if let Some(code) = &self.code {
2158            writeln!(f, "code: {code}")?;
2159        }
2160        if let Some(description) = &self.description {
2161            writeln!(f, "{description}")?;
2162        }
2163
2164        if let Some(details) = &self.details {
2165            writeln!(f, "details: {details}")?;
2166        }
2167
2168        Ok(())
2169    }
2170}
2171
2172impl TemplateContext for ErrorContext {
2173    fn sample<R: RngExt>(
2174        _now: chrono::DateTime<Utc>,
2175        _rng: &mut R,
2176        _locales: &[DataLocale],
2177    ) -> BTreeMap<SampleIdentifier, Self>
2178    where
2179        Self: Sized,
2180    {
2181        sample_list(vec![
2182            Self::new()
2183                .with_code("sample_error")
2184                .with_description("A fancy description".into())
2185                .with_details("Something happened".into()),
2186            Self::new().with_code("another_error"),
2187            Self::new(),
2188        ])
2189    }
2190}
2191
2192impl ErrorContext {
2193    /// Constructs a context for the error page
2194    #[must_use]
2195    pub fn new() -> Self {
2196        Self::default()
2197    }
2198
2199    /// Add the error code to the context
2200    #[must_use]
2201    pub fn with_code(mut self, code: &'static str) -> Self {
2202        self.code = Some(code);
2203        self
2204    }
2205
2206    /// Add the error description to the context
2207    #[must_use]
2208    pub fn with_description(mut self, description: String) -> Self {
2209        self.description = Some(description);
2210        self
2211    }
2212
2213    /// Add the error details to the context
2214    #[must_use]
2215    pub fn with_details(mut self, details: String) -> Self {
2216        self.details = Some(details);
2217        self
2218    }
2219
2220    /// Add the language to the context
2221    #[must_use]
2222    pub fn with_language(mut self, lang: &DataLocale) -> Self {
2223        self.lang = Some(lang.to_string());
2224        self
2225    }
2226
2227    /// Get the error code, if any
2228    #[must_use]
2229    pub fn code(&self) -> Option<&'static str> {
2230        self.code
2231    }
2232
2233    /// Get the description, if any
2234    #[must_use]
2235    pub fn description(&self) -> Option<&str> {
2236        self.description.as_deref()
2237    }
2238
2239    /// Get the details, if any
2240    #[must_use]
2241    pub fn details(&self) -> Option<&str> {
2242        self.details.as_deref()
2243    }
2244}
2245
2246/// Context used by the not found (`404.html`) template
2247#[derive(Serialize)]
2248pub struct NotFoundContext {
2249    method: String,
2250    version: String,
2251    uri: String,
2252}
2253
2254impl NotFoundContext {
2255    /// Constructs a context for the not found page
2256    #[must_use]
2257    pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
2258        Self {
2259            method: method.to_string(),
2260            version: format!("{version:?}"),
2261            uri: uri.to_string(),
2262        }
2263    }
2264}
2265
2266impl TemplateContext for NotFoundContext {
2267    fn sample<R: RngExt>(
2268        _now: DateTime<Utc>,
2269        _rng: &mut R,
2270        _locales: &[DataLocale],
2271    ) -> BTreeMap<SampleIdentifier, Self>
2272    where
2273        Self: Sized,
2274    {
2275        sample_list(vec![
2276            Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
2277            Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
2278            Self::new(
2279                &Method::PUT,
2280                Version::HTTP_10,
2281                &"/foo?bar=baz".parse().unwrap(),
2282            ),
2283        ])
2284    }
2285}