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, 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, locales: &[DataLocale]) -> Vec<Self>
109 where
110 Self: Sized;
111}
112
113impl TemplateContext for () {
114 fn sample(
115 _now: chrono::DateTime<Utc>,
116 _rng: &mut impl Rng,
117 _locales: &[DataLocale],
118 ) -> Vec<Self>
119 where
120 Self: Sized,
121 {
122 Vec::new()
123 }
124}
125
126#[derive(Serialize, Debug)]
128pub struct WithLanguage<T> {
129 lang: String,
130
131 #[serde(flatten)]
132 inner: T,
133}
134
135impl<T> WithLanguage<T> {
136 pub fn language(&self) -> &str {
138 &self.lang
139 }
140}
141
142impl<T> std::ops::Deref for WithLanguage<T> {
143 type Target = T;
144
145 fn deref(&self) -> &Self::Target {
146 &self.inner
147 }
148}
149
150impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
151 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
152 where
153 Self: Sized,
154 {
155 locales
156 .iter()
157 .flat_map(|locale| {
158 T::sample(now, rng, locales)
159 .into_iter()
160 .map(move |inner| WithLanguage {
161 lang: locale.to_string(),
162 inner,
163 })
164 })
165 .collect()
166 }
167}
168
169#[derive(Serialize, Debug)]
171pub struct WithCsrf<T> {
172 csrf_token: String,
173
174 #[serde(flatten)]
175 inner: T,
176}
177
178impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
179 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
180 where
181 Self: Sized,
182 {
183 T::sample(now, rng, locales)
184 .into_iter()
185 .map(|inner| WithCsrf {
186 csrf_token: "fake_csrf_token".into(),
187 inner,
188 })
189 .collect()
190 }
191}
192
193#[derive(Serialize)]
195pub struct WithSession<T> {
196 current_session: BrowserSession,
197
198 #[serde(flatten)]
199 inner: T,
200}
201
202impl<T: TemplateContext> TemplateContext for WithSession<T> {
203 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
204 where
205 Self: Sized,
206 {
207 BrowserSession::samples(now, rng)
208 .into_iter()
209 .flat_map(|session| {
210 T::sample(now, rng, locales)
211 .into_iter()
212 .map(move |inner| WithSession {
213 current_session: session.clone(),
214 inner,
215 })
216 })
217 .collect()
218 }
219}
220
221#[derive(Serialize)]
223pub struct WithOptionalSession<T> {
224 current_session: Option<BrowserSession>,
225
226 #[serde(flatten)]
227 inner: T,
228}
229
230impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
231 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
232 where
233 Self: Sized,
234 {
235 BrowserSession::samples(now, rng)
236 .into_iter()
237 .map(Some) .chain(std::iter::once(None)) .flat_map(|session| {
240 T::sample(now, rng, locales)
241 .into_iter()
242 .map(move |inner| WithOptionalSession {
243 current_session: session.clone(),
244 inner,
245 })
246 })
247 .collect()
248 }
249}
250
251pub struct EmptyContext;
253
254impl Serialize for EmptyContext {
255 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
256 where
257 S: serde::Serializer,
258 {
259 let mut s = serializer.serialize_struct("EmptyContext", 0)?;
260 s.serialize_field("__UNUSED", &())?;
263 s.end()
264 }
265}
266
267impl TemplateContext for EmptyContext {
268 fn sample(
269 _now: chrono::DateTime<Utc>,
270 _rng: &mut impl Rng,
271 _locales: &[DataLocale],
272 ) -> Vec<Self>
273 where
274 Self: Sized,
275 {
276 vec![EmptyContext]
277 }
278}
279
280#[derive(Serialize)]
282pub struct IndexContext {
283 discovery_url: Url,
284}
285
286impl IndexContext {
287 #[must_use]
290 pub fn new(discovery_url: Url) -> Self {
291 Self { discovery_url }
292 }
293}
294
295impl TemplateContext for IndexContext {
296 fn sample(
297 _now: chrono::DateTime<Utc>,
298 _rng: &mut impl Rng,
299 _locales: &[DataLocale],
300 ) -> Vec<Self>
301 where
302 Self: Sized,
303 {
304 vec![Self {
305 discovery_url: "https://example.com/.well-known/openid-configuration"
306 .parse()
307 .unwrap(),
308 }]
309 }
310}
311
312#[derive(Serialize)]
314#[serde(rename_all = "camelCase")]
315pub struct AppConfig {
316 root: String,
317 graphql_endpoint: String,
318}
319
320#[derive(Serialize)]
322pub struct AppContext {
323 app_config: AppConfig,
324}
325
326impl AppContext {
327 #[must_use]
329 pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
330 let root = url_builder.relative_url_for(&Account::default());
331 let graphql_endpoint = url_builder.relative_url_for(&GraphQL);
332 Self {
333 app_config: AppConfig {
334 root,
335 graphql_endpoint,
336 },
337 }
338 }
339}
340
341impl TemplateContext for AppContext {
342 fn sample(
343 _now: chrono::DateTime<Utc>,
344 _rng: &mut impl Rng,
345 _locales: &[DataLocale],
346 ) -> Vec<Self>
347 where
348 Self: Sized,
349 {
350 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
351 vec![Self::from_url_builder(&url_builder)]
352 }
353}
354
355#[derive(Serialize)]
357pub struct ApiDocContext {
358 openapi_url: Url,
359 callback_url: Url,
360}
361
362impl ApiDocContext {
363 #[must_use]
366 pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
367 Self {
368 openapi_url: url_builder.absolute_url_for(&mas_router::ApiSpec),
369 callback_url: url_builder.absolute_url_for(&mas_router::ApiDocCallback),
370 }
371 }
372}
373
374impl TemplateContext for ApiDocContext {
375 fn sample(
376 _now: chrono::DateTime<Utc>,
377 _rng: &mut impl Rng,
378 _locales: &[DataLocale],
379 ) -> Vec<Self>
380 where
381 Self: Sized,
382 {
383 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
384 vec![Self::from_url_builder(&url_builder)]
385 }
386}
387
388#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
390#[serde(rename_all = "snake_case")]
391pub enum LoginFormField {
392 Username,
394
395 Password,
397}
398
399impl FormField for LoginFormField {
400 fn keep(&self) -> bool {
401 match self {
402 Self::Username => true,
403 Self::Password => false,
404 }
405 }
406}
407
408#[derive(Serialize)]
410#[serde(tag = "kind", rename_all = "snake_case")]
411pub enum PostAuthContextInner {
412 ContinueAuthorizationGrant {
414 grant: Box<AuthorizationGrant>,
416 },
417
418 ContinueDeviceCodeGrant {
420 grant: Box<DeviceCodeGrant>,
422 },
423
424 ContinueCompatSsoLogin {
427 login: Box<CompatSsoLogin>,
429 },
430
431 ChangePassword,
433
434 LinkUpstream {
436 provider: Box<UpstreamOAuthProvider>,
438
439 link: Box<UpstreamOAuthLink>,
441 },
442
443 ManageAccount,
445}
446
447#[derive(Serialize)]
449pub struct PostAuthContext {
450 pub params: PostAuthAction,
452
453 #[serde(flatten)]
455 pub ctx: PostAuthContextInner,
456}
457
458#[derive(Serialize, Default)]
460pub struct LoginContext {
461 form: FormState<LoginFormField>,
462 next: Option<PostAuthContext>,
463 providers: Vec<UpstreamOAuthProvider>,
464}
465
466impl TemplateContext for LoginContext {
467 fn sample(
468 _now: chrono::DateTime<Utc>,
469 _rng: &mut impl Rng,
470 _locales: &[DataLocale],
471 ) -> Vec<Self>
472 where
473 Self: Sized,
474 {
475 vec![
477 LoginContext {
478 form: FormState::default(),
479 next: None,
480 providers: Vec::new(),
481 },
482 LoginContext {
483 form: FormState::default(),
484 next: None,
485 providers: Vec::new(),
486 },
487 LoginContext {
488 form: FormState::default()
489 .with_error_on_field(LoginFormField::Username, FieldError::Required)
490 .with_error_on_field(
491 LoginFormField::Password,
492 FieldError::Policy {
493 code: None,
494 message: "password too short".to_owned(),
495 },
496 ),
497 next: None,
498 providers: Vec::new(),
499 },
500 LoginContext {
501 form: FormState::default()
502 .with_error_on_field(LoginFormField::Username, FieldError::Exists),
503 next: None,
504 providers: Vec::new(),
505 },
506 ]
507 }
508}
509
510impl LoginContext {
511 #[must_use]
513 pub fn with_form_state(self, form: FormState<LoginFormField>) -> Self {
514 Self { form, ..self }
515 }
516
517 pub fn form_state_mut(&mut self) -> &mut FormState<LoginFormField> {
519 &mut self.form
520 }
521
522 #[must_use]
524 pub fn with_upstream_providers(self, providers: Vec<UpstreamOAuthProvider>) -> Self {
525 Self { providers, ..self }
526 }
527
528 #[must_use]
530 pub fn with_post_action(self, context: PostAuthContext) -> Self {
531 Self {
532 next: Some(context),
533 ..self
534 }
535 }
536}
537
538#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
540#[serde(rename_all = "snake_case")]
541pub enum RegisterFormField {
542 Username,
544
545 Email,
547
548 Password,
550
551 PasswordConfirm,
553
554 AcceptTerms,
556}
557
558impl FormField for RegisterFormField {
559 fn keep(&self) -> bool {
560 match self {
561 Self::Username | Self::Email | Self::AcceptTerms => true,
562 Self::Password | Self::PasswordConfirm => false,
563 }
564 }
565}
566
567#[derive(Serialize, Default)]
569pub struct RegisterContext {
570 providers: Vec<UpstreamOAuthProvider>,
571 next: Option<PostAuthContext>,
572}
573
574impl TemplateContext for RegisterContext {
575 fn sample(
576 _now: chrono::DateTime<Utc>,
577 _rng: &mut impl Rng,
578 _locales: &[DataLocale],
579 ) -> Vec<Self>
580 where
581 Self: Sized,
582 {
583 vec![RegisterContext {
584 providers: Vec::new(),
585 next: None,
586 }]
587 }
588}
589
590impl RegisterContext {
591 #[must_use]
593 pub fn new(providers: Vec<UpstreamOAuthProvider>) -> Self {
594 Self {
595 providers,
596 next: None,
597 }
598 }
599
600 #[must_use]
602 pub fn with_post_action(self, next: PostAuthContext) -> Self {
603 Self {
604 next: Some(next),
605 ..self
606 }
607 }
608}
609
610#[derive(Serialize, Default)]
612pub struct PasswordRegisterContext {
613 form: FormState<RegisterFormField>,
614 next: Option<PostAuthContext>,
615}
616
617impl TemplateContext for PasswordRegisterContext {
618 fn sample(
619 _now: chrono::DateTime<Utc>,
620 _rng: &mut impl Rng,
621 _locales: &[DataLocale],
622 ) -> Vec<Self>
623 where
624 Self: Sized,
625 {
626 vec![PasswordRegisterContext {
628 form: FormState::default(),
629 next: None,
630 }]
631 }
632}
633
634impl PasswordRegisterContext {
635 #[must_use]
637 pub fn with_form_state(self, form: FormState<RegisterFormField>) -> Self {
638 Self { form, ..self }
639 }
640
641 #[must_use]
643 pub fn with_post_action(self, next: PostAuthContext) -> Self {
644 Self {
645 next: Some(next),
646 ..self
647 }
648 }
649}
650
651#[derive(Serialize)]
653pub struct ConsentContext {
654 grant: AuthorizationGrant,
655 client: Client,
656 action: PostAuthAction,
657}
658
659impl TemplateContext for ConsentContext {
660 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
661 where
662 Self: Sized,
663 {
664 Client::samples(now, rng)
665 .into_iter()
666 .map(|client| {
667 let mut grant = AuthorizationGrant::sample(now, rng);
668 let action = PostAuthAction::continue_grant(grant.id);
669 grant.client_id = client.id;
671 Self {
672 grant,
673 client,
674 action,
675 }
676 })
677 .collect()
678 }
679}
680
681impl ConsentContext {
682 #[must_use]
684 pub fn new(grant: AuthorizationGrant, client: Client) -> Self {
685 let action = PostAuthAction::continue_grant(grant.id);
686 Self {
687 grant,
688 client,
689 action,
690 }
691 }
692}
693
694#[derive(Serialize)]
695#[serde(tag = "grant_type")]
696enum PolicyViolationGrant {
697 #[serde(rename = "authorization_code")]
698 Authorization(AuthorizationGrant),
699 #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
700 DeviceCode(DeviceCodeGrant),
701}
702
703#[derive(Serialize)]
705pub struct PolicyViolationContext {
706 grant: PolicyViolationGrant,
707 client: Client,
708 action: PostAuthAction,
709}
710
711impl TemplateContext for PolicyViolationContext {
712 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
713 where
714 Self: Sized,
715 {
716 Client::samples(now, rng)
717 .into_iter()
718 .flat_map(|client| {
719 let mut grant = AuthorizationGrant::sample(now, rng);
720 grant.client_id = client.id;
722
723 let authorization_grant =
724 PolicyViolationContext::for_authorization_grant(grant, client.clone());
725 let device_code_grant = PolicyViolationContext::for_device_code_grant(
726 DeviceCodeGrant {
727 id: Ulid::from_datetime_with_source(now.into(), rng),
728 state: mas_data_model::DeviceCodeGrantState::Pending,
729 client_id: client.id,
730 scope: [OPENID].into_iter().collect(),
731 user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
732 device_code: Alphanumeric.sample_string(rng, 32),
733 created_at: now - Duration::try_minutes(5).unwrap(),
734 expires_at: now + Duration::try_minutes(25).unwrap(),
735 ip_address: None,
736 user_agent: None,
737 },
738 client,
739 );
740
741 [authorization_grant, device_code_grant]
742 })
743 .collect()
744 }
745}
746
747impl PolicyViolationContext {
748 #[must_use]
751 pub const fn for_authorization_grant(grant: AuthorizationGrant, client: Client) -> Self {
752 let action = PostAuthAction::continue_grant(grant.id);
753 Self {
754 grant: PolicyViolationGrant::Authorization(grant),
755 client,
756 action,
757 }
758 }
759
760 #[must_use]
763 pub const fn for_device_code_grant(grant: DeviceCodeGrant, client: Client) -> Self {
764 let action = PostAuthAction::continue_device_code_grant(grant.id);
765 Self {
766 grant: PolicyViolationGrant::DeviceCode(grant),
767 client,
768 action,
769 }
770 }
771}
772
773#[derive(Serialize)]
775pub struct CompatSsoContext {
776 login: CompatSsoLogin,
777 action: PostAuthAction,
778}
779
780impl TemplateContext for CompatSsoContext {
781 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
782 where
783 Self: Sized,
784 {
785 let id = Ulid::from_datetime_with_source(now.into(), rng);
786 vec![CompatSsoContext::new(CompatSsoLogin {
787 id,
788 redirect_uri: Url::parse("https://app.element.io/").unwrap(),
789 login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
790 created_at: now,
791 state: CompatSsoLoginState::Pending,
792 })]
793 }
794}
795
796impl CompatSsoContext {
797 #[must_use]
799 pub fn new(login: CompatSsoLogin) -> Self
800where {
801 let action = PostAuthAction::continue_compat_sso_login(login.id);
802 Self { login, action }
803 }
804}
805
806#[derive(Serialize)]
808pub struct EmailRecoveryContext {
809 user: User,
810 session: UserRecoverySession,
811 recovery_link: Url,
812}
813
814impl EmailRecoveryContext {
815 #[must_use]
817 pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
818 Self {
819 user,
820 session,
821 recovery_link,
822 }
823 }
824
825 #[must_use]
827 pub fn user(&self) -> &User {
828 &self.user
829 }
830
831 #[must_use]
833 pub fn session(&self) -> &UserRecoverySession {
834 &self.session
835 }
836}
837
838impl TemplateContext for EmailRecoveryContext {
839 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
840 where
841 Self: Sized,
842 {
843 User::samples(now, rng).into_iter().map(|user| {
844 let session = UserRecoverySession {
845 id: Ulid::from_datetime_with_source(now.into(), rng),
846 email: "hello@example.com".to_owned(),
847 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(),
848 ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
849 locale: "en".to_owned(),
850 created_at: now,
851 consumed_at: None,
852 };
853
854 let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
855
856 Self::new(user, session, link)
857 }).collect()
858 }
859}
860
861#[derive(Serialize)]
863pub struct EmailVerificationContext {
864 #[serde(skip_serializing_if = "Option::is_none")]
865 browser_session: Option<BrowserSession>,
866 #[serde(skip_serializing_if = "Option::is_none")]
867 user_registration: Option<UserRegistration>,
868 authentication_code: UserEmailAuthenticationCode,
869}
870
871impl EmailVerificationContext {
872 #[must_use]
874 pub fn new(
875 authentication_code: UserEmailAuthenticationCode,
876 browser_session: Option<BrowserSession>,
877 user_registration: Option<UserRegistration>,
878 ) -> Self {
879 Self {
880 browser_session,
881 user_registration,
882 authentication_code,
883 }
884 }
885
886 #[must_use]
888 pub fn user(&self) -> Option<&User> {
889 self.browser_session.as_ref().map(|s| &s.user)
890 }
891
892 #[must_use]
894 pub fn code(&self) -> &str {
895 &self.authentication_code.code
896 }
897}
898
899impl TemplateContext for EmailVerificationContext {
900 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
901 where
902 Self: Sized,
903 {
904 BrowserSession::samples(now, rng)
905 .into_iter()
906 .map(|browser_session| {
907 let authentication_code = UserEmailAuthenticationCode {
908 id: Ulid::from_datetime_with_source(now.into(), rng),
909 user_email_authentication_id: Ulid::from_datetime_with_source(now.into(), rng),
910 code: "123456".to_owned(),
911 created_at: now - Duration::try_minutes(5).unwrap(),
912 expires_at: now + Duration::try_minutes(25).unwrap(),
913 };
914
915 Self {
916 browser_session: Some(browser_session),
917 user_registration: None,
918 authentication_code,
919 }
920 })
921 .collect()
922 }
923}
924
925#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
927#[serde(rename_all = "snake_case")]
928pub enum RegisterStepsVerifyEmailFormField {
929 Code,
931}
932
933impl FormField for RegisterStepsVerifyEmailFormField {
934 fn keep(&self) -> bool {
935 match self {
936 Self::Code => true,
937 }
938 }
939}
940
941#[derive(Serialize)]
943pub struct RegisterStepsVerifyEmailContext {
944 form: FormState<RegisterStepsVerifyEmailFormField>,
945 authentication: UserEmailAuthentication,
946}
947
948impl RegisterStepsVerifyEmailContext {
949 #[must_use]
951 pub fn new(authentication: UserEmailAuthentication) -> Self {
952 Self {
953 form: FormState::default(),
954 authentication,
955 }
956 }
957
958 #[must_use]
960 pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
961 Self { form, ..self }
962 }
963}
964
965impl TemplateContext for RegisterStepsVerifyEmailContext {
966 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
967 where
968 Self: Sized,
969 {
970 let authentication = UserEmailAuthentication {
971 id: Ulid::from_datetime_with_source(now.into(), rng),
972 user_session_id: None,
973 user_registration_id: None,
974 email: "foobar@example.com".to_owned(),
975 created_at: now,
976 completed_at: None,
977 };
978
979 vec![Self {
980 form: FormState::default(),
981 authentication,
982 }]
983 }
984}
985
986#[derive(Serialize)]
988pub struct RegisterStepsEmailInUseContext {
989 email: String,
990 action: Option<PostAuthAction>,
991}
992
993impl RegisterStepsEmailInUseContext {
994 #[must_use]
996 pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
997 Self { email, action }
998 }
999}
1000
1001impl TemplateContext for RegisterStepsEmailInUseContext {
1002 fn sample(
1003 _now: chrono::DateTime<Utc>,
1004 _rng: &mut impl Rng,
1005 _locales: &[DataLocale],
1006 ) -> Vec<Self>
1007 where
1008 Self: Sized,
1009 {
1010 let email = "hello@example.com".to_owned();
1011 let action = PostAuthAction::continue_grant(Ulid::nil());
1012 vec![Self::new(email, Some(action))]
1013 }
1014}
1015
1016#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1018#[serde(rename_all = "snake_case")]
1019pub enum RegisterStepsDisplayNameFormField {
1020 DisplayName,
1022}
1023
1024impl FormField for RegisterStepsDisplayNameFormField {
1025 fn keep(&self) -> bool {
1026 match self {
1027 Self::DisplayName => true,
1028 }
1029 }
1030}
1031
1032#[derive(Serialize, Default)]
1034pub struct RegisterStepsDisplayNameContext {
1035 form: FormState<RegisterStepsDisplayNameFormField>,
1036}
1037
1038impl RegisterStepsDisplayNameContext {
1039 #[must_use]
1041 pub fn new() -> Self {
1042 Self::default()
1043 }
1044
1045 #[must_use]
1047 pub fn with_form_state(
1048 mut self,
1049 form_state: FormState<RegisterStepsDisplayNameFormField>,
1050 ) -> Self {
1051 self.form = form_state;
1052 self
1053 }
1054}
1055
1056impl TemplateContext for RegisterStepsDisplayNameContext {
1057 fn sample(
1058 _now: chrono::DateTime<chrono::Utc>,
1059 _rng: &mut impl Rng,
1060 _locales: &[DataLocale],
1061 ) -> Vec<Self>
1062 where
1063 Self: Sized,
1064 {
1065 vec![Self {
1066 form: FormState::default(),
1067 }]
1068 }
1069}
1070
1071#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1073#[serde(rename_all = "snake_case")]
1074pub enum RecoveryStartFormField {
1075 Email,
1077}
1078
1079impl FormField for RecoveryStartFormField {
1080 fn keep(&self) -> bool {
1081 match self {
1082 Self::Email => true,
1083 }
1084 }
1085}
1086
1087#[derive(Serialize, Default)]
1089pub struct RecoveryStartContext {
1090 form: FormState<RecoveryStartFormField>,
1091}
1092
1093impl RecoveryStartContext {
1094 #[must_use]
1096 pub fn new() -> Self {
1097 Self::default()
1098 }
1099
1100 #[must_use]
1102 pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1103 Self { form }
1104 }
1105}
1106
1107impl TemplateContext for RecoveryStartContext {
1108 fn sample(
1109 _now: chrono::DateTime<Utc>,
1110 _rng: &mut impl Rng,
1111 _locales: &[DataLocale],
1112 ) -> Vec<Self>
1113 where
1114 Self: Sized,
1115 {
1116 vec![
1117 Self::new(),
1118 Self::new().with_form_state(
1119 FormState::default()
1120 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1121 ),
1122 Self::new().with_form_state(
1123 FormState::default()
1124 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1125 ),
1126 ]
1127 }
1128}
1129
1130#[derive(Serialize)]
1132pub struct RecoveryProgressContext {
1133 session: UserRecoverySession,
1134 resend_failed_due_to_rate_limit: bool,
1136}
1137
1138impl RecoveryProgressContext {
1139 #[must_use]
1141 pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1142 Self {
1143 session,
1144 resend_failed_due_to_rate_limit,
1145 }
1146 }
1147}
1148
1149impl TemplateContext for RecoveryProgressContext {
1150 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1151 where
1152 Self: Sized,
1153 {
1154 let session = UserRecoverySession {
1155 id: Ulid::from_datetime_with_source(now.into(), rng),
1156 email: "name@mail.com".to_owned(),
1157 user_agent: "Mozilla/5.0".to_owned(),
1158 ip_address: None,
1159 locale: "en".to_owned(),
1160 created_at: now,
1161 consumed_at: None,
1162 };
1163
1164 vec![
1165 Self {
1166 session: session.clone(),
1167 resend_failed_due_to_rate_limit: false,
1168 },
1169 Self {
1170 session,
1171 resend_failed_due_to_rate_limit: true,
1172 },
1173 ]
1174 }
1175}
1176
1177#[derive(Serialize)]
1179pub struct RecoveryExpiredContext {
1180 session: UserRecoverySession,
1181}
1182
1183impl RecoveryExpiredContext {
1184 #[must_use]
1186 pub fn new(session: UserRecoverySession) -> Self {
1187 Self { session }
1188 }
1189}
1190
1191impl TemplateContext for RecoveryExpiredContext {
1192 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1193 where
1194 Self: Sized,
1195 {
1196 let session = UserRecoverySession {
1197 id: Ulid::from_datetime_with_source(now.into(), rng),
1198 email: "name@mail.com".to_owned(),
1199 user_agent: "Mozilla/5.0".to_owned(),
1200 ip_address: None,
1201 locale: "en".to_owned(),
1202 created_at: now,
1203 consumed_at: None,
1204 };
1205
1206 vec![Self { session }]
1207 }
1208}
1209
1210#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1212#[serde(rename_all = "snake_case")]
1213pub enum RecoveryFinishFormField {
1214 NewPassword,
1216
1217 NewPasswordConfirm,
1219}
1220
1221impl FormField for RecoveryFinishFormField {
1222 fn keep(&self) -> bool {
1223 false
1224 }
1225}
1226
1227#[derive(Serialize)]
1229pub struct RecoveryFinishContext {
1230 user: User,
1231 form: FormState<RecoveryFinishFormField>,
1232}
1233
1234impl RecoveryFinishContext {
1235 #[must_use]
1237 pub fn new(user: User) -> Self {
1238 Self {
1239 user,
1240 form: FormState::default(),
1241 }
1242 }
1243
1244 #[must_use]
1246 pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1247 self.form = form;
1248 self
1249 }
1250}
1251
1252impl TemplateContext for RecoveryFinishContext {
1253 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1254 where
1255 Self: Sized,
1256 {
1257 User::samples(now, rng)
1258 .into_iter()
1259 .flat_map(|user| {
1260 vec![
1261 Self::new(user.clone()),
1262 Self::new(user.clone()).with_form_state(
1263 FormState::default().with_error_on_field(
1264 RecoveryFinishFormField::NewPassword,
1265 FieldError::Invalid,
1266 ),
1267 ),
1268 Self::new(user.clone()).with_form_state(
1269 FormState::default().with_error_on_field(
1270 RecoveryFinishFormField::NewPasswordConfirm,
1271 FieldError::Invalid,
1272 ),
1273 ),
1274 ]
1275 })
1276 .collect()
1277 }
1278}
1279
1280#[derive(Serialize)]
1283pub struct UpstreamExistingLinkContext {
1284 linked_user: User,
1285}
1286
1287impl UpstreamExistingLinkContext {
1288 #[must_use]
1290 pub fn new(linked_user: User) -> Self {
1291 Self { linked_user }
1292 }
1293}
1294
1295impl TemplateContext for UpstreamExistingLinkContext {
1296 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1297 where
1298 Self: Sized,
1299 {
1300 User::samples(now, rng)
1301 .into_iter()
1302 .map(|linked_user| Self { linked_user })
1303 .collect()
1304 }
1305}
1306
1307#[derive(Serialize)]
1310pub struct UpstreamSuggestLink {
1311 post_logout_action: PostAuthAction,
1312}
1313
1314impl UpstreamSuggestLink {
1315 #[must_use]
1317 pub fn new(link: &UpstreamOAuthLink) -> Self {
1318 Self::for_link_id(link.id)
1319 }
1320
1321 fn for_link_id(id: Ulid) -> Self {
1322 let post_logout_action = PostAuthAction::link_upstream(id);
1323 Self { post_logout_action }
1324 }
1325}
1326
1327impl TemplateContext for UpstreamSuggestLink {
1328 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1329 where
1330 Self: Sized,
1331 {
1332 let id = Ulid::from_datetime_with_source(now.into(), rng);
1333 vec![Self::for_link_id(id)]
1334 }
1335}
1336
1337#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1339#[serde(rename_all = "snake_case")]
1340pub enum UpstreamRegisterFormField {
1341 Username,
1343
1344 AcceptTerms,
1346}
1347
1348impl FormField for UpstreamRegisterFormField {
1349 fn keep(&self) -> bool {
1350 match self {
1351 Self::Username | Self::AcceptTerms => true,
1352 }
1353 }
1354}
1355
1356#[derive(Serialize)]
1359pub struct UpstreamRegister {
1360 upstream_oauth_link: UpstreamOAuthLink,
1361 upstream_oauth_provider: UpstreamOAuthProvider,
1362 imported_localpart: Option<String>,
1363 force_localpart: bool,
1364 imported_display_name: Option<String>,
1365 force_display_name: bool,
1366 imported_email: Option<String>,
1367 force_email: bool,
1368 form_state: FormState<UpstreamRegisterFormField>,
1369}
1370
1371impl UpstreamRegister {
1372 #[must_use]
1375 pub fn new(
1376 upstream_oauth_link: UpstreamOAuthLink,
1377 upstream_oauth_provider: UpstreamOAuthProvider,
1378 ) -> Self {
1379 Self {
1380 upstream_oauth_link,
1381 upstream_oauth_provider,
1382 imported_localpart: None,
1383 force_localpart: false,
1384 imported_display_name: None,
1385 force_display_name: false,
1386 imported_email: None,
1387 force_email: false,
1388 form_state: FormState::default(),
1389 }
1390 }
1391
1392 pub fn set_localpart(&mut self, localpart: String, force: bool) {
1394 self.imported_localpart = Some(localpart);
1395 self.force_localpart = force;
1396 }
1397
1398 #[must_use]
1400 pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1401 Self {
1402 imported_localpart: Some(localpart),
1403 force_localpart: force,
1404 ..self
1405 }
1406 }
1407
1408 pub fn set_display_name(&mut self, display_name: String, force: bool) {
1410 self.imported_display_name = Some(display_name);
1411 self.force_display_name = force;
1412 }
1413
1414 #[must_use]
1416 pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1417 Self {
1418 imported_display_name: Some(display_name),
1419 force_display_name: force,
1420 ..self
1421 }
1422 }
1423
1424 pub fn set_email(&mut self, email: String, force: bool) {
1426 self.imported_email = Some(email);
1427 self.force_email = force;
1428 }
1429
1430 #[must_use]
1432 pub fn with_email(self, email: String, force: bool) -> Self {
1433 Self {
1434 imported_email: Some(email),
1435 force_email: force,
1436 ..self
1437 }
1438 }
1439
1440 pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1442 self.form_state = form_state;
1443 }
1444
1445 #[must_use]
1447 pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1448 Self { form_state, ..self }
1449 }
1450}
1451
1452impl TemplateContext for UpstreamRegister {
1453 fn sample(now: chrono::DateTime<Utc>, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1454 where
1455 Self: Sized,
1456 {
1457 vec![Self::new(
1458 UpstreamOAuthLink {
1459 id: Ulid::nil(),
1460 provider_id: Ulid::nil(),
1461 user_id: None,
1462 subject: "subject".to_owned(),
1463 human_account_name: Some("@john".to_owned()),
1464 created_at: now,
1465 },
1466 UpstreamOAuthProvider {
1467 id: Ulid::nil(),
1468 issuer: Some("https://example.com/".to_owned()),
1469 human_name: Some("Example Ltd.".to_owned()),
1470 brand_name: None,
1471 scope: Scope::from_iter([OPENID]),
1472 token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1473 token_endpoint_signing_alg: None,
1474 id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1475 client_id: "client-id".to_owned(),
1476 encrypted_client_secret: None,
1477 claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1478 authorization_endpoint_override: None,
1479 token_endpoint_override: None,
1480 jwks_uri_override: None,
1481 userinfo_endpoint_override: None,
1482 fetch_userinfo: false,
1483 userinfo_signed_response_alg: None,
1484 discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1485 pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1486 response_mode: None,
1487 additional_authorization_parameters: Vec::new(),
1488 forward_login_hint: false,
1489 created_at: now,
1490 disabled_at: None,
1491 },
1492 )]
1493 }
1494}
1495
1496#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1498#[serde(rename_all = "snake_case")]
1499pub enum DeviceLinkFormField {
1500 Code,
1502}
1503
1504impl FormField for DeviceLinkFormField {
1505 fn keep(&self) -> bool {
1506 match self {
1507 Self::Code => true,
1508 }
1509 }
1510}
1511
1512#[derive(Serialize, Default, Debug)]
1514pub struct DeviceLinkContext {
1515 form_state: FormState<DeviceLinkFormField>,
1516}
1517
1518impl DeviceLinkContext {
1519 #[must_use]
1521 pub fn new() -> Self {
1522 Self::default()
1523 }
1524
1525 #[must_use]
1527 pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1528 self.form_state = form_state;
1529 self
1530 }
1531}
1532
1533impl TemplateContext for DeviceLinkContext {
1534 fn sample(
1535 _now: chrono::DateTime<Utc>,
1536 _rng: &mut impl Rng,
1537 _locales: &[DataLocale],
1538 ) -> 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, _locales: &[DataLocale]) -> 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("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, _locales: &[DataLocale]) -> 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 DeviceNameContext {
1623 client: Client,
1624 raw_user_agent: String,
1625}
1626
1627impl DeviceNameContext {
1628 #[must_use]
1630 pub fn new(client: Client, user_agent: Option<String>) -> Self {
1631 Self {
1632 client,
1633 raw_user_agent: user_agent.unwrap_or_default(),
1634 }
1635 }
1636}
1637
1638impl TemplateContext for DeviceNameContext {
1639 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1640 where
1641 Self: Sized,
1642 {
1643 Client::samples(now, rng)
1644 .into_iter()
1645 .map(|client| DeviceNameContext {
1646 client,
1647 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(),
1648 })
1649 .collect()
1650 }
1651}
1652
1653#[derive(Serialize)]
1655pub struct FormPostContext<T> {
1656 redirect_uri: Option<Url>,
1657 params: T,
1658}
1659
1660impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
1661 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
1662 where
1663 Self: Sized,
1664 {
1665 let sample_params = T::sample(now, rng, locales);
1666 sample_params
1667 .into_iter()
1668 .map(|params| FormPostContext {
1669 redirect_uri: "https://example.com/callback".parse().ok(),
1670 params,
1671 })
1672 .collect()
1673 }
1674}
1675
1676impl<T> FormPostContext<T> {
1677 pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
1680 Self {
1681 redirect_uri: Some(redirect_uri),
1682 params,
1683 }
1684 }
1685
1686 pub fn new_for_current_url(params: T) -> Self {
1689 Self {
1690 redirect_uri: None,
1691 params,
1692 }
1693 }
1694
1695 pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
1700 WithLanguage {
1701 lang: lang.to_string(),
1702 inner: self,
1703 }
1704 }
1705}
1706
1707#[derive(Default, Serialize, Debug, Clone)]
1709pub struct ErrorContext {
1710 code: Option<&'static str>,
1711 description: Option<String>,
1712 details: Option<String>,
1713 lang: Option<String>,
1714}
1715
1716impl std::fmt::Display for ErrorContext {
1717 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1718 if let Some(code) = &self.code {
1719 writeln!(f, "code: {code}")?;
1720 }
1721 if let Some(description) = &self.description {
1722 writeln!(f, "{description}")?;
1723 }
1724
1725 if let Some(details) = &self.details {
1726 writeln!(f, "details: {details}")?;
1727 }
1728
1729 Ok(())
1730 }
1731}
1732
1733impl TemplateContext for ErrorContext {
1734 fn sample(
1735 _now: chrono::DateTime<Utc>,
1736 _rng: &mut impl Rng,
1737 _locales: &[DataLocale],
1738 ) -> Vec<Self>
1739 where
1740 Self: Sized,
1741 {
1742 vec![
1743 Self::new()
1744 .with_code("sample_error")
1745 .with_description("A fancy description".into())
1746 .with_details("Something happened".into()),
1747 Self::new().with_code("another_error"),
1748 Self::new(),
1749 ]
1750 }
1751}
1752
1753impl ErrorContext {
1754 #[must_use]
1756 pub fn new() -> Self {
1757 Self::default()
1758 }
1759
1760 #[must_use]
1762 pub fn with_code(mut self, code: &'static str) -> Self {
1763 self.code = Some(code);
1764 self
1765 }
1766
1767 #[must_use]
1769 pub fn with_description(mut self, description: String) -> Self {
1770 self.description = Some(description);
1771 self
1772 }
1773
1774 #[must_use]
1776 pub fn with_details(mut self, details: String) -> Self {
1777 self.details = Some(details);
1778 self
1779 }
1780
1781 #[must_use]
1783 pub fn with_language(mut self, lang: &DataLocale) -> Self {
1784 self.lang = Some(lang.to_string());
1785 self
1786 }
1787
1788 #[must_use]
1790 pub fn code(&self) -> Option<&'static str> {
1791 self.code
1792 }
1793
1794 #[must_use]
1796 pub fn description(&self) -> Option<&str> {
1797 self.description.as_deref()
1798 }
1799
1800 #[must_use]
1802 pub fn details(&self) -> Option<&str> {
1803 self.details.as_deref()
1804 }
1805}
1806
1807#[derive(Serialize)]
1809pub struct NotFoundContext {
1810 method: String,
1811 version: String,
1812 uri: String,
1813}
1814
1815impl NotFoundContext {
1816 #[must_use]
1818 pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
1819 Self {
1820 method: method.to_string(),
1821 version: format!("{version:?}"),
1822 uri: uri.to_string(),
1823 }
1824 }
1825}
1826
1827impl TemplateContext for NotFoundContext {
1828 fn sample(_now: DateTime<Utc>, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1829 where
1830 Self: Sized,
1831 {
1832 vec![
1833 Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
1834 Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
1835 Self::new(
1836 &Method::PUT,
1837 Version::HTTP_10,
1838 &"/foo?bar=baz".parse().unwrap(),
1839 ),
1840 ]
1841 }
1842}