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