mas_templates/
context.rs

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