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