1mod branding;
10mod captcha;
11mod ext;
12mod features;
13
14use std::{
15 fmt::Formatter,
16 net::{IpAddr, Ipv4Addr},
17};
18
19use chrono::{DateTime, Duration, Utc};
20use http::{Method, Uri, Version};
21use mas_data_model::{
22 AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
23 DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
24 UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode,
25 UpstreamOAuthProviderTokenAuthMethod, User, UserAgent, UserEmailAuthentication,
26 UserEmailAuthenticationCode, UserRecoverySession, UserRegistration,
27};
28use mas_i18n::DataLocale;
29use mas_iana::jose::JsonWebSignatureAlg;
30use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder};
31use oauth2_types::scope::{OPENID, Scope};
32use rand::{
33 Rng,
34 distributions::{Alphanumeric, DistString},
35};
36use serde::{Deserialize, Serialize, ser::SerializeStruct};
37use ulid::Ulid;
38use url::Url;
39
40pub use self::{
41 branding::SiteBranding, captcha::WithCaptcha, ext::SiteConfigExt, features::SiteFeatures,
42};
43use crate::{FieldError, FormField, FormState};
44
45pub trait TemplateContext: Serialize {
47 fn with_session(self, current_session: BrowserSession) -> WithSession<Self>
49 where
50 Self: Sized,
51 {
52 WithSession {
53 current_session,
54 inner: self,
55 }
56 }
57
58 fn maybe_with_session(
60 self,
61 current_session: Option<BrowserSession>,
62 ) -> WithOptionalSession<Self>
63 where
64 Self: Sized,
65 {
66 WithOptionalSession {
67 current_session,
68 inner: self,
69 }
70 }
71
72 fn with_csrf<C>(self, csrf_token: C) -> WithCsrf<Self>
74 where
75 Self: Sized,
76 C: ToString,
77 {
78 WithCsrf {
80 csrf_token: csrf_token.to_string(),
81 inner: self,
82 }
83 }
84
85 fn with_language(self, lang: DataLocale) -> WithLanguage<Self>
87 where
88 Self: Sized,
89 {
90 WithLanguage {
91 lang: lang.to_string(),
92 inner: self,
93 }
94 }
95
96 fn with_captcha(self, captcha: Option<mas_data_model::CaptchaConfig>) -> WithCaptcha<Self>
98 where
99 Self: Sized,
100 {
101 WithCaptcha::new(captcha, self)
102 }
103
104 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
109 where
110 Self: Sized;
111}
112
113impl TemplateContext for () {
114 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
115 where
116 Self: Sized,
117 {
118 Vec::new()
119 }
120}
121
122#[derive(Serialize, Debug)]
124pub struct WithLanguage<T> {
125 lang: String,
126
127 #[serde(flatten)]
128 inner: T,
129}
130
131impl<T> WithLanguage<T> {
132 pub fn language(&self) -> &str {
134 &self.lang
135 }
136}
137
138impl<T> std::ops::Deref for WithLanguage<T> {
139 type Target = T;
140
141 fn deref(&self) -> &Self::Target {
142 &self.inner
143 }
144}
145
146impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
147 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
148 where
149 Self: Sized,
150 {
151 T::sample(now, rng)
152 .into_iter()
153 .map(|inner| WithLanguage {
154 lang: "en".into(),
155 inner,
156 })
157 .collect()
158 }
159}
160
161#[derive(Serialize, Debug)]
163pub struct WithCsrf<T> {
164 csrf_token: String,
165
166 #[serde(flatten)]
167 inner: T,
168}
169
170impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
171 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
172 where
173 Self: Sized,
174 {
175 T::sample(now, rng)
176 .into_iter()
177 .map(|inner| WithCsrf {
178 csrf_token: "fake_csrf_token".into(),
179 inner,
180 })
181 .collect()
182 }
183}
184
185#[derive(Serialize)]
187pub struct WithSession<T> {
188 current_session: BrowserSession,
189
190 #[serde(flatten)]
191 inner: T,
192}
193
194impl<T: TemplateContext> TemplateContext for WithSession<T> {
195 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
196 where
197 Self: Sized,
198 {
199 BrowserSession::samples(now, rng)
200 .into_iter()
201 .flat_map(|session| {
202 T::sample(now, rng)
203 .into_iter()
204 .map(move |inner| WithSession {
205 current_session: session.clone(),
206 inner,
207 })
208 })
209 .collect()
210 }
211}
212
213#[derive(Serialize)]
215pub struct WithOptionalSession<T> {
216 current_session: Option<BrowserSession>,
217
218 #[serde(flatten)]
219 inner: T,
220}
221
222impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
223 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
224 where
225 Self: Sized,
226 {
227 BrowserSession::samples(now, rng)
228 .into_iter()
229 .map(Some) .chain(std::iter::once(None)) .flat_map(|session| {
232 T::sample(now, rng)
233 .into_iter()
234 .map(move |inner| WithOptionalSession {
235 current_session: session.clone(),
236 inner,
237 })
238 })
239 .collect()
240 }
241}
242
243pub struct EmptyContext;
245
246impl Serialize for EmptyContext {
247 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
248 where
249 S: serde::Serializer,
250 {
251 let mut s = serializer.serialize_struct("EmptyContext", 0)?;
252 s.serialize_field("__UNUSED", &())?;
255 s.end()
256 }
257}
258
259impl TemplateContext for EmptyContext {
260 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
261 where
262 Self: Sized,
263 {
264 vec![EmptyContext]
265 }
266}
267
268#[derive(Serialize)]
270pub struct IndexContext {
271 discovery_url: Url,
272}
273
274impl IndexContext {
275 #[must_use]
278 pub fn new(discovery_url: Url) -> Self {
279 Self { discovery_url }
280 }
281}
282
283impl TemplateContext for IndexContext {
284 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
285 where
286 Self: Sized,
287 {
288 vec![Self {
289 discovery_url: "https://example.com/.well-known/openid-configuration"
290 .parse()
291 .unwrap(),
292 }]
293 }
294}
295
296#[derive(Serialize)]
298#[serde(rename_all = "camelCase")]
299pub struct AppConfig {
300 root: String,
301 graphql_endpoint: String,
302}
303
304#[derive(Serialize)]
306pub struct AppContext {
307 app_config: AppConfig,
308}
309
310impl AppContext {
311 #[must_use]
313 pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
314 let root = url_builder.relative_url_for(&Account::default());
315 let graphql_endpoint = url_builder.relative_url_for(&GraphQL);
316 Self {
317 app_config: AppConfig {
318 root,
319 graphql_endpoint,
320 },
321 }
322 }
323}
324
325impl TemplateContext for AppContext {
326 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
327 where
328 Self: Sized,
329 {
330 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
331 vec![Self::from_url_builder(&url_builder)]
332 }
333}
334
335#[derive(Serialize)]
337pub struct ApiDocContext {
338 openapi_url: Url,
339 callback_url: Url,
340}
341
342impl ApiDocContext {
343 #[must_use]
346 pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
347 Self {
348 openapi_url: url_builder.absolute_url_for(&mas_router::ApiSpec),
349 callback_url: url_builder.absolute_url_for(&mas_router::ApiDocCallback),
350 }
351 }
352}
353
354impl TemplateContext for ApiDocContext {
355 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
356 where
357 Self: Sized,
358 {
359 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
360 vec![Self::from_url_builder(&url_builder)]
361 }
362}
363
364#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
366#[serde(rename_all = "snake_case")]
367pub enum LoginFormField {
368 Username,
370
371 Password,
373}
374
375impl FormField for LoginFormField {
376 fn keep(&self) -> bool {
377 match self {
378 Self::Username => true,
379 Self::Password => false,
380 }
381 }
382}
383
384#[derive(Serialize)]
386#[serde(tag = "kind", rename_all = "snake_case")]
387pub enum PostAuthContextInner {
388 ContinueAuthorizationGrant {
390 grant: Box<AuthorizationGrant>,
392 },
393
394 ContinueDeviceCodeGrant {
396 grant: Box<DeviceCodeGrant>,
398 },
399
400 ContinueCompatSsoLogin {
403 login: Box<CompatSsoLogin>,
405 },
406
407 ChangePassword,
409
410 LinkUpstream {
412 provider: Box<UpstreamOAuthProvider>,
414
415 link: Box<UpstreamOAuthLink>,
417 },
418
419 ManageAccount,
421}
422
423#[derive(Serialize)]
425pub struct PostAuthContext {
426 pub params: PostAuthAction,
428
429 #[serde(flatten)]
431 pub ctx: PostAuthContextInner,
432}
433
434#[derive(Serialize, Default)]
436pub struct LoginContext {
437 form: FormState<LoginFormField>,
438 next: Option<PostAuthContext>,
439 providers: Vec<UpstreamOAuthProvider>,
440}
441
442impl TemplateContext for LoginContext {
443 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
444 where
445 Self: Sized,
446 {
447 vec![
449 LoginContext {
450 form: FormState::default(),
451 next: None,
452 providers: Vec::new(),
453 },
454 LoginContext {
455 form: FormState::default(),
456 next: None,
457 providers: Vec::new(),
458 },
459 LoginContext {
460 form: FormState::default()
461 .with_error_on_field(LoginFormField::Username, FieldError::Required)
462 .with_error_on_field(
463 LoginFormField::Password,
464 FieldError::Policy {
465 code: None,
466 message: "password too short".to_owned(),
467 },
468 ),
469 next: None,
470 providers: Vec::new(),
471 },
472 LoginContext {
473 form: FormState::default()
474 .with_error_on_field(LoginFormField::Username, FieldError::Exists),
475 next: None,
476 providers: Vec::new(),
477 },
478 ]
479 }
480}
481
482impl LoginContext {
483 #[must_use]
485 pub fn with_form_state(self, form: FormState<LoginFormField>) -> Self {
486 Self { form, ..self }
487 }
488
489 pub fn form_state_mut(&mut self) -> &mut FormState<LoginFormField> {
491 &mut self.form
492 }
493
494 #[must_use]
496 pub fn with_upstream_providers(self, providers: Vec<UpstreamOAuthProvider>) -> Self {
497 Self { providers, ..self }
498 }
499
500 #[must_use]
502 pub fn with_post_action(self, context: PostAuthContext) -> Self {
503 Self {
504 next: Some(context),
505 ..self
506 }
507 }
508}
509
510#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
512#[serde(rename_all = "snake_case")]
513pub enum RegisterFormField {
514 Username,
516
517 Email,
519
520 Password,
522
523 PasswordConfirm,
525
526 AcceptTerms,
528}
529
530impl FormField for RegisterFormField {
531 fn keep(&self) -> bool {
532 match self {
533 Self::Username | Self::Email | Self::AcceptTerms => true,
534 Self::Password | Self::PasswordConfirm => false,
535 }
536 }
537}
538
539#[derive(Serialize, Default)]
541pub struct RegisterContext {
542 providers: Vec<UpstreamOAuthProvider>,
543 next: Option<PostAuthContext>,
544}
545
546impl TemplateContext for RegisterContext {
547 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
548 where
549 Self: Sized,
550 {
551 vec![RegisterContext {
552 providers: Vec::new(),
553 next: None,
554 }]
555 }
556}
557
558impl RegisterContext {
559 #[must_use]
561 pub fn new(providers: Vec<UpstreamOAuthProvider>) -> Self {
562 Self {
563 providers,
564 next: None,
565 }
566 }
567
568 #[must_use]
570 pub fn with_post_action(self, next: PostAuthContext) -> Self {
571 Self {
572 next: Some(next),
573 ..self
574 }
575 }
576}
577
578#[derive(Serialize, Default)]
580pub struct PasswordRegisterContext {
581 form: FormState<RegisterFormField>,
582 next: Option<PostAuthContext>,
583}
584
585impl TemplateContext for PasswordRegisterContext {
586 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
587 where
588 Self: Sized,
589 {
590 vec![PasswordRegisterContext {
592 form: FormState::default(),
593 next: None,
594 }]
595 }
596}
597
598impl PasswordRegisterContext {
599 #[must_use]
601 pub fn with_form_state(self, form: FormState<RegisterFormField>) -> Self {
602 Self { form, ..self }
603 }
604
605 #[must_use]
607 pub fn with_post_action(self, next: PostAuthContext) -> Self {
608 Self {
609 next: Some(next),
610 ..self
611 }
612 }
613}
614
615#[derive(Serialize)]
617pub struct ConsentContext {
618 grant: AuthorizationGrant,
619 client: Client,
620 action: PostAuthAction,
621}
622
623impl TemplateContext for ConsentContext {
624 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
625 where
626 Self: Sized,
627 {
628 Client::samples(now, rng)
629 .into_iter()
630 .map(|client| {
631 let mut grant = AuthorizationGrant::sample(now, rng);
632 let action = PostAuthAction::continue_grant(grant.id);
633 grant.client_id = client.id;
635 Self {
636 grant,
637 client,
638 action,
639 }
640 })
641 .collect()
642 }
643}
644
645impl ConsentContext {
646 #[must_use]
648 pub fn new(grant: AuthorizationGrant, client: Client) -> Self {
649 let action = PostAuthAction::continue_grant(grant.id);
650 Self {
651 grant,
652 client,
653 action,
654 }
655 }
656}
657
658#[derive(Serialize)]
659#[serde(tag = "grant_type")]
660enum PolicyViolationGrant {
661 #[serde(rename = "authorization_code")]
662 Authorization(AuthorizationGrant),
663 #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
664 DeviceCode(DeviceCodeGrant),
665}
666
667#[derive(Serialize)]
669pub struct PolicyViolationContext {
670 grant: PolicyViolationGrant,
671 client: Client,
672 action: PostAuthAction,
673}
674
675impl TemplateContext for PolicyViolationContext {
676 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
677 where
678 Self: Sized,
679 {
680 Client::samples(now, rng)
681 .into_iter()
682 .flat_map(|client| {
683 let mut grant = AuthorizationGrant::sample(now, rng);
684 grant.client_id = client.id;
686
687 let authorization_grant =
688 PolicyViolationContext::for_authorization_grant(grant, client.clone());
689 let device_code_grant = PolicyViolationContext::for_device_code_grant(
690 DeviceCodeGrant {
691 id: Ulid::from_datetime_with_source(now.into(), rng),
692 state: mas_data_model::DeviceCodeGrantState::Pending,
693 client_id: client.id,
694 scope: [OPENID].into_iter().collect(),
695 user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
696 device_code: Alphanumeric.sample_string(rng, 32),
697 created_at: now - Duration::try_minutes(5).unwrap(),
698 expires_at: now + Duration::try_minutes(25).unwrap(),
699 ip_address: None,
700 user_agent: None,
701 },
702 client,
703 );
704
705 [authorization_grant, device_code_grant]
706 })
707 .collect()
708 }
709}
710
711impl PolicyViolationContext {
712 #[must_use]
715 pub const fn for_authorization_grant(grant: AuthorizationGrant, client: Client) -> Self {
716 let action = PostAuthAction::continue_grant(grant.id);
717 Self {
718 grant: PolicyViolationGrant::Authorization(grant),
719 client,
720 action,
721 }
722 }
723
724 #[must_use]
727 pub const fn for_device_code_grant(grant: DeviceCodeGrant, client: Client) -> Self {
728 let action = PostAuthAction::continue_device_code_grant(grant.id);
729 Self {
730 grant: PolicyViolationGrant::DeviceCode(grant),
731 client,
732 action,
733 }
734 }
735}
736
737#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
739#[serde(rename_all = "kebab-case")]
740pub enum ReauthFormField {
741 Password,
743}
744
745impl FormField for ReauthFormField {
746 fn keep(&self) -> bool {
747 match self {
748 Self::Password => false,
749 }
750 }
751}
752
753#[derive(Serialize, Default)]
755pub struct ReauthContext {
756 form: FormState<ReauthFormField>,
757 next: Option<PostAuthContext>,
758}
759
760impl TemplateContext for ReauthContext {
761 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
762 where
763 Self: Sized,
764 {
765 vec![ReauthContext {
767 form: FormState::default(),
768 next: None,
769 }]
770 }
771}
772
773impl ReauthContext {
774 #[must_use]
776 pub fn with_form_state(self, form: FormState<ReauthFormField>) -> Self {
777 Self { form, ..self }
778 }
779
780 #[must_use]
782 pub fn with_post_action(self, next: PostAuthContext) -> Self {
783 Self {
784 next: Some(next),
785 ..self
786 }
787 }
788}
789
790#[derive(Serialize)]
792pub struct CompatSsoContext {
793 login: CompatSsoLogin,
794 action: PostAuthAction,
795}
796
797impl TemplateContext for CompatSsoContext {
798 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
799 where
800 Self: Sized,
801 {
802 let id = Ulid::from_datetime_with_source(now.into(), rng);
803 vec![CompatSsoContext::new(CompatSsoLogin {
804 id,
805 redirect_uri: Url::parse("https://app.element.io/").unwrap(),
806 login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
807 created_at: now,
808 state: CompatSsoLoginState::Pending,
809 })]
810 }
811}
812
813impl CompatSsoContext {
814 #[must_use]
816 pub fn new(login: CompatSsoLogin) -> Self
817where {
818 let action = PostAuthAction::continue_compat_sso_login(login.id);
819 Self { login, action }
820 }
821}
822
823#[derive(Serialize)]
825pub struct EmailRecoveryContext {
826 user: User,
827 session: UserRecoverySession,
828 recovery_link: Url,
829}
830
831impl EmailRecoveryContext {
832 #[must_use]
834 pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
835 Self {
836 user,
837 session,
838 recovery_link,
839 }
840 }
841
842 #[must_use]
844 pub fn user(&self) -> &User {
845 &self.user
846 }
847
848 #[must_use]
850 pub fn session(&self) -> &UserRecoverySession {
851 &self.session
852 }
853}
854
855impl TemplateContext for EmailRecoveryContext {
856 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
857 where
858 Self: Sized,
859 {
860 User::samples(now, rng).into_iter().map(|user| {
861 let session = UserRecoverySession {
862 id: Ulid::from_datetime_with_source(now.into(), rng),
863 email: "hello@example.com".to_owned(),
864 user_agent: UserAgent::parse("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()),
865 ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
866 locale: "en".to_owned(),
867 created_at: now,
868 consumed_at: None,
869 };
870
871 let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
872
873 Self::new(user, session, link)
874 }).collect()
875 }
876}
877
878#[derive(Serialize)]
880pub struct EmailVerificationContext {
881 #[serde(skip_serializing_if = "Option::is_none")]
882 browser_session: Option<BrowserSession>,
883 #[serde(skip_serializing_if = "Option::is_none")]
884 user_registration: Option<UserRegistration>,
885 authentication_code: UserEmailAuthenticationCode,
886}
887
888impl EmailVerificationContext {
889 #[must_use]
891 pub fn new(
892 authentication_code: UserEmailAuthenticationCode,
893 browser_session: Option<BrowserSession>,
894 user_registration: Option<UserRegistration>,
895 ) -> Self {
896 Self {
897 browser_session,
898 user_registration,
899 authentication_code,
900 }
901 }
902
903 #[must_use]
905 pub fn user(&self) -> Option<&User> {
906 self.browser_session.as_ref().map(|s| &s.user)
907 }
908
909 #[must_use]
911 pub fn code(&self) -> &str {
912 &self.authentication_code.code
913 }
914}
915
916impl TemplateContext for EmailVerificationContext {
917 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
918 where
919 Self: Sized,
920 {
921 BrowserSession::samples(now, rng)
922 .into_iter()
923 .map(|browser_session| {
924 let authentication_code = UserEmailAuthenticationCode {
925 id: Ulid::from_datetime_with_source(now.into(), rng),
926 user_email_authentication_id: Ulid::from_datetime_with_source(now.into(), rng),
927 code: "123456".to_owned(),
928 created_at: now - Duration::try_minutes(5).unwrap(),
929 expires_at: now + Duration::try_minutes(25).unwrap(),
930 };
931
932 Self {
933 browser_session: Some(browser_session),
934 user_registration: None,
935 authentication_code,
936 }
937 })
938 .collect()
939 }
940}
941
942#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
944#[serde(rename_all = "snake_case")]
945pub enum RegisterStepsVerifyEmailFormField {
946 Code,
948}
949
950impl FormField for RegisterStepsVerifyEmailFormField {
951 fn keep(&self) -> bool {
952 match self {
953 Self::Code => true,
954 }
955 }
956}
957
958#[derive(Serialize)]
960pub struct RegisterStepsVerifyEmailContext {
961 form: FormState<RegisterStepsVerifyEmailFormField>,
962 authentication: UserEmailAuthentication,
963}
964
965impl RegisterStepsVerifyEmailContext {
966 #[must_use]
968 pub fn new(authentication: UserEmailAuthentication) -> Self {
969 Self {
970 form: FormState::default(),
971 authentication,
972 }
973 }
974
975 #[must_use]
977 pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
978 Self { form, ..self }
979 }
980}
981
982impl TemplateContext for RegisterStepsVerifyEmailContext {
983 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
984 where
985 Self: Sized,
986 {
987 let authentication = UserEmailAuthentication {
988 id: Ulid::from_datetime_with_source(now.into(), rng),
989 user_session_id: None,
990 user_registration_id: None,
991 email: "foobar@example.com".to_owned(),
992 created_at: now,
993 completed_at: None,
994 };
995
996 vec![Self {
997 form: FormState::default(),
998 authentication,
999 }]
1000 }
1001}
1002
1003#[derive(Serialize)]
1005pub struct RegisterStepsEmailInUseContext {
1006 email: String,
1007 action: Option<PostAuthAction>,
1008}
1009
1010impl RegisterStepsEmailInUseContext {
1011 #[must_use]
1013 pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
1014 Self { email, action }
1015 }
1016}
1017
1018impl TemplateContext for RegisterStepsEmailInUseContext {
1019 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1020 where
1021 Self: Sized,
1022 {
1023 let email = "hello@example.com".to_owned();
1024 let action = PostAuthAction::continue_grant(Ulid::nil());
1025 vec![Self::new(email, Some(action))]
1026 }
1027}
1028
1029#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1031#[serde(rename_all = "snake_case")]
1032pub enum RegisterStepsDisplayNameFormField {
1033 DisplayName,
1035}
1036
1037impl FormField for RegisterStepsDisplayNameFormField {
1038 fn keep(&self) -> bool {
1039 match self {
1040 Self::DisplayName => true,
1041 }
1042 }
1043}
1044
1045#[derive(Serialize, Default)]
1047pub struct RegisterStepsDisplayNameContext {
1048 form: FormState<RegisterStepsDisplayNameFormField>,
1049}
1050
1051impl RegisterStepsDisplayNameContext {
1052 #[must_use]
1054 pub fn new() -> Self {
1055 Self::default()
1056 }
1057
1058 #[must_use]
1060 pub fn with_form_state(
1061 mut self,
1062 form_state: FormState<RegisterStepsDisplayNameFormField>,
1063 ) -> Self {
1064 self.form = form_state;
1065 self
1066 }
1067}
1068
1069impl TemplateContext for RegisterStepsDisplayNameContext {
1070 fn sample(_now: chrono::DateTime<chrono::Utc>, _rng: &mut impl Rng) -> Vec<Self>
1071 where
1072 Self: Sized,
1073 {
1074 vec![Self {
1075 form: FormState::default(),
1076 }]
1077 }
1078}
1079
1080#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1082#[serde(rename_all = "snake_case")]
1083pub enum RecoveryStartFormField {
1084 Email,
1086}
1087
1088impl FormField for RecoveryStartFormField {
1089 fn keep(&self) -> bool {
1090 match self {
1091 Self::Email => true,
1092 }
1093 }
1094}
1095
1096#[derive(Serialize, Default)]
1098pub struct RecoveryStartContext {
1099 form: FormState<RecoveryStartFormField>,
1100}
1101
1102impl RecoveryStartContext {
1103 #[must_use]
1105 pub fn new() -> Self {
1106 Self::default()
1107 }
1108
1109 #[must_use]
1111 pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1112 Self { form }
1113 }
1114}
1115
1116impl TemplateContext for RecoveryStartContext {
1117 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1118 where
1119 Self: Sized,
1120 {
1121 vec![
1122 Self::new(),
1123 Self::new().with_form_state(
1124 FormState::default()
1125 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1126 ),
1127 Self::new().with_form_state(
1128 FormState::default()
1129 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1130 ),
1131 ]
1132 }
1133}
1134
1135#[derive(Serialize)]
1137pub struct RecoveryProgressContext {
1138 session: UserRecoverySession,
1139 resend_failed_due_to_rate_limit: bool,
1141}
1142
1143impl RecoveryProgressContext {
1144 #[must_use]
1146 pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1147 Self {
1148 session,
1149 resend_failed_due_to_rate_limit,
1150 }
1151 }
1152}
1153
1154impl TemplateContext for RecoveryProgressContext {
1155 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1156 where
1157 Self: Sized,
1158 {
1159 let session = UserRecoverySession {
1160 id: Ulid::from_datetime_with_source(now.into(), rng),
1161 email: "name@mail.com".to_owned(),
1162 user_agent: UserAgent::parse("Mozilla/5.0".to_owned()),
1163 ip_address: None,
1164 locale: "en".to_owned(),
1165 created_at: now,
1166 consumed_at: None,
1167 };
1168
1169 vec![
1170 Self {
1171 session: session.clone(),
1172 resend_failed_due_to_rate_limit: false,
1173 },
1174 Self {
1175 session,
1176 resend_failed_due_to_rate_limit: true,
1177 },
1178 ]
1179 }
1180}
1181
1182#[derive(Serialize)]
1184pub struct RecoveryExpiredContext {
1185 session: UserRecoverySession,
1186}
1187
1188impl RecoveryExpiredContext {
1189 #[must_use]
1191 pub fn new(session: UserRecoverySession) -> Self {
1192 Self { session }
1193 }
1194}
1195
1196impl TemplateContext for RecoveryExpiredContext {
1197 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1198 where
1199 Self: Sized,
1200 {
1201 let session = UserRecoverySession {
1202 id: Ulid::from_datetime_with_source(now.into(), rng),
1203 email: "name@mail.com".to_owned(),
1204 user_agent: UserAgent::parse("Mozilla/5.0".to_owned()),
1205 ip_address: None,
1206 locale: "en".to_owned(),
1207 created_at: now,
1208 consumed_at: None,
1209 };
1210
1211 vec![Self { session }]
1212 }
1213}
1214
1215#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1217#[serde(rename_all = "snake_case")]
1218pub enum RecoveryFinishFormField {
1219 NewPassword,
1221
1222 NewPasswordConfirm,
1224}
1225
1226impl FormField for RecoveryFinishFormField {
1227 fn keep(&self) -> bool {
1228 false
1229 }
1230}
1231
1232#[derive(Serialize)]
1234pub struct RecoveryFinishContext {
1235 user: User,
1236 form: FormState<RecoveryFinishFormField>,
1237}
1238
1239impl RecoveryFinishContext {
1240 #[must_use]
1242 pub fn new(user: User) -> Self {
1243 Self {
1244 user,
1245 form: FormState::default(),
1246 }
1247 }
1248
1249 #[must_use]
1251 pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1252 self.form = form;
1253 self
1254 }
1255}
1256
1257impl TemplateContext for RecoveryFinishContext {
1258 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1259 where
1260 Self: Sized,
1261 {
1262 User::samples(now, rng)
1263 .into_iter()
1264 .flat_map(|user| {
1265 vec![
1266 Self::new(user.clone()),
1267 Self::new(user.clone()).with_form_state(
1268 FormState::default().with_error_on_field(
1269 RecoveryFinishFormField::NewPassword,
1270 FieldError::Invalid,
1271 ),
1272 ),
1273 Self::new(user.clone()).with_form_state(
1274 FormState::default().with_error_on_field(
1275 RecoveryFinishFormField::NewPasswordConfirm,
1276 FieldError::Invalid,
1277 ),
1278 ),
1279 ]
1280 })
1281 .collect()
1282 }
1283}
1284
1285#[derive(Serialize)]
1288pub struct UpstreamExistingLinkContext {
1289 linked_user: User,
1290}
1291
1292impl UpstreamExistingLinkContext {
1293 #[must_use]
1295 pub fn new(linked_user: User) -> Self {
1296 Self { linked_user }
1297 }
1298}
1299
1300impl TemplateContext for UpstreamExistingLinkContext {
1301 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1302 where
1303 Self: Sized,
1304 {
1305 User::samples(now, rng)
1306 .into_iter()
1307 .map(|linked_user| Self { linked_user })
1308 .collect()
1309 }
1310}
1311
1312#[derive(Serialize)]
1315pub struct UpstreamSuggestLink {
1316 post_logout_action: PostAuthAction,
1317}
1318
1319impl UpstreamSuggestLink {
1320 #[must_use]
1322 pub fn new(link: &UpstreamOAuthLink) -> Self {
1323 Self::for_link_id(link.id)
1324 }
1325
1326 fn for_link_id(id: Ulid) -> Self {
1327 let post_logout_action = PostAuthAction::link_upstream(id);
1328 Self { post_logout_action }
1329 }
1330}
1331
1332impl TemplateContext for UpstreamSuggestLink {
1333 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1334 where
1335 Self: Sized,
1336 {
1337 let id = Ulid::from_datetime_with_source(now.into(), rng);
1338 vec![Self::for_link_id(id)]
1339 }
1340}
1341
1342#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1344#[serde(rename_all = "snake_case")]
1345pub enum UpstreamRegisterFormField {
1346 Username,
1348
1349 AcceptTerms,
1351}
1352
1353impl FormField for UpstreamRegisterFormField {
1354 fn keep(&self) -> bool {
1355 match self {
1356 Self::Username | Self::AcceptTerms => true,
1357 }
1358 }
1359}
1360
1361#[derive(Serialize)]
1364pub struct UpstreamRegister {
1365 upstream_oauth_link: UpstreamOAuthLink,
1366 upstream_oauth_provider: UpstreamOAuthProvider,
1367 imported_localpart: Option<String>,
1368 force_localpart: bool,
1369 imported_display_name: Option<String>,
1370 force_display_name: bool,
1371 imported_email: Option<String>,
1372 force_email: bool,
1373 form_state: FormState<UpstreamRegisterFormField>,
1374}
1375
1376impl UpstreamRegister {
1377 #[must_use]
1380 pub fn new(
1381 upstream_oauth_link: UpstreamOAuthLink,
1382 upstream_oauth_provider: UpstreamOAuthProvider,
1383 ) -> Self {
1384 Self {
1385 upstream_oauth_link,
1386 upstream_oauth_provider,
1387 imported_localpart: None,
1388 force_localpart: false,
1389 imported_display_name: None,
1390 force_display_name: false,
1391 imported_email: None,
1392 force_email: false,
1393 form_state: FormState::default(),
1394 }
1395 }
1396
1397 pub fn set_localpart(&mut self, localpart: String, force: bool) {
1399 self.imported_localpart = Some(localpart);
1400 self.force_localpart = force;
1401 }
1402
1403 #[must_use]
1405 pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1406 Self {
1407 imported_localpart: Some(localpart),
1408 force_localpart: force,
1409 ..self
1410 }
1411 }
1412
1413 pub fn set_display_name(&mut self, display_name: String, force: bool) {
1415 self.imported_display_name = Some(display_name);
1416 self.force_display_name = force;
1417 }
1418
1419 #[must_use]
1421 pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1422 Self {
1423 imported_display_name: Some(display_name),
1424 force_display_name: force,
1425 ..self
1426 }
1427 }
1428
1429 pub fn set_email(&mut self, email: String, force: bool) {
1431 self.imported_email = Some(email);
1432 self.force_email = force;
1433 }
1434
1435 #[must_use]
1437 pub fn with_email(self, email: String, force: bool) -> Self {
1438 Self {
1439 imported_email: Some(email),
1440 force_email: force,
1441 ..self
1442 }
1443 }
1444
1445 pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1447 self.form_state = form_state;
1448 }
1449
1450 #[must_use]
1452 pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1453 Self { form_state, ..self }
1454 }
1455}
1456
1457impl TemplateContext for UpstreamRegister {
1458 fn sample(now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1459 where
1460 Self: Sized,
1461 {
1462 vec![Self::new(
1463 UpstreamOAuthLink {
1464 id: Ulid::nil(),
1465 provider_id: Ulid::nil(),
1466 user_id: None,
1467 subject: "subject".to_owned(),
1468 human_account_name: Some("@john".to_owned()),
1469 created_at: now,
1470 },
1471 UpstreamOAuthProvider {
1472 id: Ulid::nil(),
1473 issuer: Some("https://example.com/".to_owned()),
1474 human_name: Some("Example Ltd.".to_owned()),
1475 brand_name: None,
1476 scope: Scope::from_iter([OPENID]),
1477 token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1478 token_endpoint_signing_alg: None,
1479 id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1480 client_id: "client-id".to_owned(),
1481 encrypted_client_secret: None,
1482 claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1483 authorization_endpoint_override: None,
1484 token_endpoint_override: None,
1485 jwks_uri_override: None,
1486 userinfo_endpoint_override: None,
1487 fetch_userinfo: false,
1488 userinfo_signed_response_alg: None,
1489 discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1490 pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1491 response_mode: None,
1492 additional_authorization_parameters: Vec::new(),
1493 created_at: now,
1494 disabled_at: None,
1495 },
1496 )]
1497 }
1498}
1499
1500#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1502#[serde(rename_all = "snake_case")]
1503pub enum DeviceLinkFormField {
1504 Code,
1506}
1507
1508impl FormField for DeviceLinkFormField {
1509 fn keep(&self) -> bool {
1510 match self {
1511 Self::Code => true,
1512 }
1513 }
1514}
1515
1516#[derive(Serialize, Default, Debug)]
1518pub struct DeviceLinkContext {
1519 form_state: FormState<DeviceLinkFormField>,
1520}
1521
1522impl DeviceLinkContext {
1523 #[must_use]
1525 pub fn new() -> Self {
1526 Self::default()
1527 }
1528
1529 #[must_use]
1531 pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1532 self.form_state = form_state;
1533 self
1534 }
1535}
1536
1537impl TemplateContext for DeviceLinkContext {
1538 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1539 where
1540 Self: Sized,
1541 {
1542 vec![
1543 Self::new(),
1544 Self::new().with_form_state(
1545 FormState::default()
1546 .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1547 ),
1548 ]
1549 }
1550}
1551
1552#[derive(Serialize, Debug)]
1554pub struct DeviceConsentContext {
1555 grant: DeviceCodeGrant,
1556 client: Client,
1557}
1558
1559impl DeviceConsentContext {
1560 #[must_use]
1562 pub fn new(grant: DeviceCodeGrant, client: Client) -> Self {
1563 Self { grant, client }
1564 }
1565}
1566
1567impl TemplateContext for DeviceConsentContext {
1568 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1569 where
1570 Self: Sized,
1571 {
1572 Client::samples(now, rng)
1573 .into_iter()
1574 .map(|client| {
1575 let grant = DeviceCodeGrant {
1576 id: Ulid::from_datetime_with_source(now.into(), rng),
1577 state: mas_data_model::DeviceCodeGrantState::Pending,
1578 client_id: client.id,
1579 scope: [OPENID].into_iter().collect(),
1580 user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
1581 device_code: Alphanumeric.sample_string(rng, 32),
1582 created_at: now - Duration::try_minutes(5).unwrap(),
1583 expires_at: now + Duration::try_minutes(25).unwrap(),
1584 ip_address: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1585 user_agent: Some(UserAgent::parse("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())),
1586 };
1587 Self { grant, client }
1588 })
1589 .collect()
1590 }
1591}
1592
1593#[derive(Serialize)]
1596pub struct AccountInactiveContext {
1597 user: User,
1598}
1599
1600impl AccountInactiveContext {
1601 #[must_use]
1603 pub fn new(user: User) -> Self {
1604 Self { user }
1605 }
1606}
1607
1608impl TemplateContext for AccountInactiveContext {
1609 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1610 where
1611 Self: Sized,
1612 {
1613 User::samples(now, rng)
1614 .into_iter()
1615 .map(|user| AccountInactiveContext { user })
1616 .collect()
1617 }
1618}
1619
1620#[derive(Serialize)]
1622pub struct FormPostContext<T> {
1623 redirect_uri: Option<Url>,
1624 params: T,
1625}
1626
1627impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
1628 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1629 where
1630 Self: Sized,
1631 {
1632 let sample_params = T::sample(now, rng);
1633 sample_params
1634 .into_iter()
1635 .map(|params| FormPostContext {
1636 redirect_uri: "https://example.com/callback".parse().ok(),
1637 params,
1638 })
1639 .collect()
1640 }
1641}
1642
1643impl<T> FormPostContext<T> {
1644 pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
1647 Self {
1648 redirect_uri: Some(redirect_uri),
1649 params,
1650 }
1651 }
1652
1653 pub fn new_for_current_url(params: T) -> Self {
1656 Self {
1657 redirect_uri: None,
1658 params,
1659 }
1660 }
1661
1662 pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
1667 WithLanguage {
1668 lang: lang.to_string(),
1669 inner: self,
1670 }
1671 }
1672}
1673
1674#[derive(Default, Serialize, Debug, Clone)]
1676pub struct ErrorContext {
1677 code: Option<&'static str>,
1678 description: Option<String>,
1679 details: Option<String>,
1680 lang: Option<String>,
1681}
1682
1683impl std::fmt::Display for ErrorContext {
1684 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1685 if let Some(code) = &self.code {
1686 writeln!(f, "code: {code}")?;
1687 }
1688 if let Some(description) = &self.description {
1689 writeln!(f, "{description}")?;
1690 }
1691
1692 if let Some(details) = &self.details {
1693 writeln!(f, "details: {details}")?;
1694 }
1695
1696 Ok(())
1697 }
1698}
1699
1700impl TemplateContext for ErrorContext {
1701 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1702 where
1703 Self: Sized,
1704 {
1705 vec![
1706 Self::new()
1707 .with_code("sample_error")
1708 .with_description("A fancy description".into())
1709 .with_details("Something happened".into()),
1710 Self::new().with_code("another_error"),
1711 Self::new(),
1712 ]
1713 }
1714}
1715
1716impl ErrorContext {
1717 #[must_use]
1719 pub fn new() -> Self {
1720 Self::default()
1721 }
1722
1723 #[must_use]
1725 pub fn with_code(mut self, code: &'static str) -> Self {
1726 self.code = Some(code);
1727 self
1728 }
1729
1730 #[must_use]
1732 pub fn with_description(mut self, description: String) -> Self {
1733 self.description = Some(description);
1734 self
1735 }
1736
1737 #[must_use]
1739 pub fn with_details(mut self, details: String) -> Self {
1740 self.details = Some(details);
1741 self
1742 }
1743
1744 #[must_use]
1746 pub fn with_language(mut self, lang: &DataLocale) -> Self {
1747 self.lang = Some(lang.to_string());
1748 self
1749 }
1750
1751 #[must_use]
1753 pub fn code(&self) -> Option<&'static str> {
1754 self.code
1755 }
1756
1757 #[must_use]
1759 pub fn description(&self) -> Option<&str> {
1760 self.description.as_deref()
1761 }
1762
1763 #[must_use]
1765 pub fn details(&self) -> Option<&str> {
1766 self.details.as_deref()
1767 }
1768}
1769
1770#[derive(Serialize)]
1772pub struct NotFoundContext {
1773 method: String,
1774 version: String,
1775 uri: String,
1776}
1777
1778impl NotFoundContext {
1779 #[must_use]
1781 pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
1782 Self {
1783 method: method.to_string(),
1784 version: format!("{version:?}"),
1785 uri: uri.to_string(),
1786 }
1787 }
1788}
1789
1790impl TemplateContext for NotFoundContext {
1791 fn sample(_now: DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1792 where
1793 Self: Sized,
1794 {
1795 vec![
1796 Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
1797 Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
1798 Self::new(
1799 &Method::PUT,
1800 Version::HTTP_10,
1801 &"/foo?bar=baz".parse().unwrap(),
1802 ),
1803 ]
1804 }
1805}