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