Skip to main content

mas_templates/
context.rs

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