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