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)]
739pub struct CompatSsoContext {
740 login: CompatSsoLogin,
741 action: PostAuthAction,
742}
743
744impl TemplateContext for CompatSsoContext {
745 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
746 where
747 Self: Sized,
748 {
749 let id = Ulid::from_datetime_with_source(now.into(), rng);
750 vec![CompatSsoContext::new(CompatSsoLogin {
751 id,
752 redirect_uri: Url::parse("https://app.element.io/").unwrap(),
753 login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
754 created_at: now,
755 state: CompatSsoLoginState::Pending,
756 })]
757 }
758}
759
760impl CompatSsoContext {
761 #[must_use]
763 pub fn new(login: CompatSsoLogin) -> Self
764where {
765 let action = PostAuthAction::continue_compat_sso_login(login.id);
766 Self { login, action }
767 }
768}
769
770#[derive(Serialize)]
772pub struct EmailRecoveryContext {
773 user: User,
774 session: UserRecoverySession,
775 recovery_link: Url,
776}
777
778impl EmailRecoveryContext {
779 #[must_use]
781 pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
782 Self {
783 user,
784 session,
785 recovery_link,
786 }
787 }
788
789 #[must_use]
791 pub fn user(&self) -> &User {
792 &self.user
793 }
794
795 #[must_use]
797 pub fn session(&self) -> &UserRecoverySession {
798 &self.session
799 }
800}
801
802impl TemplateContext for EmailRecoveryContext {
803 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
804 where
805 Self: Sized,
806 {
807 User::samples(now, rng).into_iter().map(|user| {
808 let session = UserRecoverySession {
809 id: Ulid::from_datetime_with_source(now.into(), rng),
810 email: "hello@example.com".to_owned(),
811 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()),
812 ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
813 locale: "en".to_owned(),
814 created_at: now,
815 consumed_at: None,
816 };
817
818 let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
819
820 Self::new(user, session, link)
821 }).collect()
822 }
823}
824
825#[derive(Serialize)]
827pub struct EmailVerificationContext {
828 #[serde(skip_serializing_if = "Option::is_none")]
829 browser_session: Option<BrowserSession>,
830 #[serde(skip_serializing_if = "Option::is_none")]
831 user_registration: Option<UserRegistration>,
832 authentication_code: UserEmailAuthenticationCode,
833}
834
835impl EmailVerificationContext {
836 #[must_use]
838 pub fn new(
839 authentication_code: UserEmailAuthenticationCode,
840 browser_session: Option<BrowserSession>,
841 user_registration: Option<UserRegistration>,
842 ) -> Self {
843 Self {
844 browser_session,
845 user_registration,
846 authentication_code,
847 }
848 }
849
850 #[must_use]
852 pub fn user(&self) -> Option<&User> {
853 self.browser_session.as_ref().map(|s| &s.user)
854 }
855
856 #[must_use]
858 pub fn code(&self) -> &str {
859 &self.authentication_code.code
860 }
861}
862
863impl TemplateContext for EmailVerificationContext {
864 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
865 where
866 Self: Sized,
867 {
868 BrowserSession::samples(now, rng)
869 .into_iter()
870 .map(|browser_session| {
871 let authentication_code = UserEmailAuthenticationCode {
872 id: Ulid::from_datetime_with_source(now.into(), rng),
873 user_email_authentication_id: Ulid::from_datetime_with_source(now.into(), rng),
874 code: "123456".to_owned(),
875 created_at: now - Duration::try_minutes(5).unwrap(),
876 expires_at: now + Duration::try_minutes(25).unwrap(),
877 };
878
879 Self {
880 browser_session: Some(browser_session),
881 user_registration: None,
882 authentication_code,
883 }
884 })
885 .collect()
886 }
887}
888
889#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
891#[serde(rename_all = "snake_case")]
892pub enum RegisterStepsVerifyEmailFormField {
893 Code,
895}
896
897impl FormField for RegisterStepsVerifyEmailFormField {
898 fn keep(&self) -> bool {
899 match self {
900 Self::Code => true,
901 }
902 }
903}
904
905#[derive(Serialize)]
907pub struct RegisterStepsVerifyEmailContext {
908 form: FormState<RegisterStepsVerifyEmailFormField>,
909 authentication: UserEmailAuthentication,
910}
911
912impl RegisterStepsVerifyEmailContext {
913 #[must_use]
915 pub fn new(authentication: UserEmailAuthentication) -> Self {
916 Self {
917 form: FormState::default(),
918 authentication,
919 }
920 }
921
922 #[must_use]
924 pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
925 Self { form, ..self }
926 }
927}
928
929impl TemplateContext for RegisterStepsVerifyEmailContext {
930 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
931 where
932 Self: Sized,
933 {
934 let authentication = UserEmailAuthentication {
935 id: Ulid::from_datetime_with_source(now.into(), rng),
936 user_session_id: None,
937 user_registration_id: None,
938 email: "foobar@example.com".to_owned(),
939 created_at: now,
940 completed_at: None,
941 };
942
943 vec![Self {
944 form: FormState::default(),
945 authentication,
946 }]
947 }
948}
949
950#[derive(Serialize)]
952pub struct RegisterStepsEmailInUseContext {
953 email: String,
954 action: Option<PostAuthAction>,
955}
956
957impl RegisterStepsEmailInUseContext {
958 #[must_use]
960 pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
961 Self { email, action }
962 }
963}
964
965impl TemplateContext for RegisterStepsEmailInUseContext {
966 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
967 where
968 Self: Sized,
969 {
970 let email = "hello@example.com".to_owned();
971 let action = PostAuthAction::continue_grant(Ulid::nil());
972 vec![Self::new(email, Some(action))]
973 }
974}
975
976#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
978#[serde(rename_all = "snake_case")]
979pub enum RegisterStepsDisplayNameFormField {
980 DisplayName,
982}
983
984impl FormField for RegisterStepsDisplayNameFormField {
985 fn keep(&self) -> bool {
986 match self {
987 Self::DisplayName => true,
988 }
989 }
990}
991
992#[derive(Serialize, Default)]
994pub struct RegisterStepsDisplayNameContext {
995 form: FormState<RegisterStepsDisplayNameFormField>,
996}
997
998impl RegisterStepsDisplayNameContext {
999 #[must_use]
1001 pub fn new() -> Self {
1002 Self::default()
1003 }
1004
1005 #[must_use]
1007 pub fn with_form_state(
1008 mut self,
1009 form_state: FormState<RegisterStepsDisplayNameFormField>,
1010 ) -> Self {
1011 self.form = form_state;
1012 self
1013 }
1014}
1015
1016impl TemplateContext for RegisterStepsDisplayNameContext {
1017 fn sample(_now: chrono::DateTime<chrono::Utc>, _rng: &mut impl Rng) -> Vec<Self>
1018 where
1019 Self: Sized,
1020 {
1021 vec![Self {
1022 form: FormState::default(),
1023 }]
1024 }
1025}
1026
1027#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1029#[serde(rename_all = "snake_case")]
1030pub enum RecoveryStartFormField {
1031 Email,
1033}
1034
1035impl FormField for RecoveryStartFormField {
1036 fn keep(&self) -> bool {
1037 match self {
1038 Self::Email => true,
1039 }
1040 }
1041}
1042
1043#[derive(Serialize, Default)]
1045pub struct RecoveryStartContext {
1046 form: FormState<RecoveryStartFormField>,
1047}
1048
1049impl RecoveryStartContext {
1050 #[must_use]
1052 pub fn new() -> Self {
1053 Self::default()
1054 }
1055
1056 #[must_use]
1058 pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1059 Self { form }
1060 }
1061}
1062
1063impl TemplateContext for RecoveryStartContext {
1064 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1065 where
1066 Self: Sized,
1067 {
1068 vec![
1069 Self::new(),
1070 Self::new().with_form_state(
1071 FormState::default()
1072 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1073 ),
1074 Self::new().with_form_state(
1075 FormState::default()
1076 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1077 ),
1078 ]
1079 }
1080}
1081
1082#[derive(Serialize)]
1084pub struct RecoveryProgressContext {
1085 session: UserRecoverySession,
1086 resend_failed_due_to_rate_limit: bool,
1088}
1089
1090impl RecoveryProgressContext {
1091 #[must_use]
1093 pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1094 Self {
1095 session,
1096 resend_failed_due_to_rate_limit,
1097 }
1098 }
1099}
1100
1101impl TemplateContext for RecoveryProgressContext {
1102 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1103 where
1104 Self: Sized,
1105 {
1106 let session = UserRecoverySession {
1107 id: Ulid::from_datetime_with_source(now.into(), rng),
1108 email: "name@mail.com".to_owned(),
1109 user_agent: UserAgent::parse("Mozilla/5.0".to_owned()),
1110 ip_address: None,
1111 locale: "en".to_owned(),
1112 created_at: now,
1113 consumed_at: None,
1114 };
1115
1116 vec![
1117 Self {
1118 session: session.clone(),
1119 resend_failed_due_to_rate_limit: false,
1120 },
1121 Self {
1122 session,
1123 resend_failed_due_to_rate_limit: true,
1124 },
1125 ]
1126 }
1127}
1128
1129#[derive(Serialize)]
1131pub struct RecoveryExpiredContext {
1132 session: UserRecoverySession,
1133}
1134
1135impl RecoveryExpiredContext {
1136 #[must_use]
1138 pub fn new(session: UserRecoverySession) -> Self {
1139 Self { session }
1140 }
1141}
1142
1143impl TemplateContext for RecoveryExpiredContext {
1144 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1145 where
1146 Self: Sized,
1147 {
1148 let session = UserRecoverySession {
1149 id: Ulid::from_datetime_with_source(now.into(), rng),
1150 email: "name@mail.com".to_owned(),
1151 user_agent: UserAgent::parse("Mozilla/5.0".to_owned()),
1152 ip_address: None,
1153 locale: "en".to_owned(),
1154 created_at: now,
1155 consumed_at: None,
1156 };
1157
1158 vec![Self { session }]
1159 }
1160}
1161
1162#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1164#[serde(rename_all = "snake_case")]
1165pub enum RecoveryFinishFormField {
1166 NewPassword,
1168
1169 NewPasswordConfirm,
1171}
1172
1173impl FormField for RecoveryFinishFormField {
1174 fn keep(&self) -> bool {
1175 false
1176 }
1177}
1178
1179#[derive(Serialize)]
1181pub struct RecoveryFinishContext {
1182 user: User,
1183 form: FormState<RecoveryFinishFormField>,
1184}
1185
1186impl RecoveryFinishContext {
1187 #[must_use]
1189 pub fn new(user: User) -> Self {
1190 Self {
1191 user,
1192 form: FormState::default(),
1193 }
1194 }
1195
1196 #[must_use]
1198 pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1199 self.form = form;
1200 self
1201 }
1202}
1203
1204impl TemplateContext for RecoveryFinishContext {
1205 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1206 where
1207 Self: Sized,
1208 {
1209 User::samples(now, rng)
1210 .into_iter()
1211 .flat_map(|user| {
1212 vec![
1213 Self::new(user.clone()),
1214 Self::new(user.clone()).with_form_state(
1215 FormState::default().with_error_on_field(
1216 RecoveryFinishFormField::NewPassword,
1217 FieldError::Invalid,
1218 ),
1219 ),
1220 Self::new(user.clone()).with_form_state(
1221 FormState::default().with_error_on_field(
1222 RecoveryFinishFormField::NewPasswordConfirm,
1223 FieldError::Invalid,
1224 ),
1225 ),
1226 ]
1227 })
1228 .collect()
1229 }
1230}
1231
1232#[derive(Serialize)]
1235pub struct UpstreamExistingLinkContext {
1236 linked_user: User,
1237}
1238
1239impl UpstreamExistingLinkContext {
1240 #[must_use]
1242 pub fn new(linked_user: User) -> Self {
1243 Self { linked_user }
1244 }
1245}
1246
1247impl TemplateContext for UpstreamExistingLinkContext {
1248 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1249 where
1250 Self: Sized,
1251 {
1252 User::samples(now, rng)
1253 .into_iter()
1254 .map(|linked_user| Self { linked_user })
1255 .collect()
1256 }
1257}
1258
1259#[derive(Serialize)]
1262pub struct UpstreamSuggestLink {
1263 post_logout_action: PostAuthAction,
1264}
1265
1266impl UpstreamSuggestLink {
1267 #[must_use]
1269 pub fn new(link: &UpstreamOAuthLink) -> Self {
1270 Self::for_link_id(link.id)
1271 }
1272
1273 fn for_link_id(id: Ulid) -> Self {
1274 let post_logout_action = PostAuthAction::link_upstream(id);
1275 Self { post_logout_action }
1276 }
1277}
1278
1279impl TemplateContext for UpstreamSuggestLink {
1280 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1281 where
1282 Self: Sized,
1283 {
1284 let id = Ulid::from_datetime_with_source(now.into(), rng);
1285 vec![Self::for_link_id(id)]
1286 }
1287}
1288
1289#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1291#[serde(rename_all = "snake_case")]
1292pub enum UpstreamRegisterFormField {
1293 Username,
1295
1296 AcceptTerms,
1298}
1299
1300impl FormField for UpstreamRegisterFormField {
1301 fn keep(&self) -> bool {
1302 match self {
1303 Self::Username | Self::AcceptTerms => true,
1304 }
1305 }
1306}
1307
1308#[derive(Serialize)]
1311pub struct UpstreamRegister {
1312 upstream_oauth_link: UpstreamOAuthLink,
1313 upstream_oauth_provider: UpstreamOAuthProvider,
1314 imported_localpart: Option<String>,
1315 force_localpart: bool,
1316 imported_display_name: Option<String>,
1317 force_display_name: bool,
1318 imported_email: Option<String>,
1319 force_email: bool,
1320 form_state: FormState<UpstreamRegisterFormField>,
1321}
1322
1323impl UpstreamRegister {
1324 #[must_use]
1327 pub fn new(
1328 upstream_oauth_link: UpstreamOAuthLink,
1329 upstream_oauth_provider: UpstreamOAuthProvider,
1330 ) -> Self {
1331 Self {
1332 upstream_oauth_link,
1333 upstream_oauth_provider,
1334 imported_localpart: None,
1335 force_localpart: false,
1336 imported_display_name: None,
1337 force_display_name: false,
1338 imported_email: None,
1339 force_email: false,
1340 form_state: FormState::default(),
1341 }
1342 }
1343
1344 pub fn set_localpart(&mut self, localpart: String, force: bool) {
1346 self.imported_localpart = Some(localpart);
1347 self.force_localpart = force;
1348 }
1349
1350 #[must_use]
1352 pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1353 Self {
1354 imported_localpart: Some(localpart),
1355 force_localpart: force,
1356 ..self
1357 }
1358 }
1359
1360 pub fn set_display_name(&mut self, display_name: String, force: bool) {
1362 self.imported_display_name = Some(display_name);
1363 self.force_display_name = force;
1364 }
1365
1366 #[must_use]
1368 pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1369 Self {
1370 imported_display_name: Some(display_name),
1371 force_display_name: force,
1372 ..self
1373 }
1374 }
1375
1376 pub fn set_email(&mut self, email: String, force: bool) {
1378 self.imported_email = Some(email);
1379 self.force_email = force;
1380 }
1381
1382 #[must_use]
1384 pub fn with_email(self, email: String, force: bool) -> Self {
1385 Self {
1386 imported_email: Some(email),
1387 force_email: force,
1388 ..self
1389 }
1390 }
1391
1392 pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1394 self.form_state = form_state;
1395 }
1396
1397 #[must_use]
1399 pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1400 Self { form_state, ..self }
1401 }
1402}
1403
1404impl TemplateContext for UpstreamRegister {
1405 fn sample(now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1406 where
1407 Self: Sized,
1408 {
1409 vec![Self::new(
1410 UpstreamOAuthLink {
1411 id: Ulid::nil(),
1412 provider_id: Ulid::nil(),
1413 user_id: None,
1414 subject: "subject".to_owned(),
1415 human_account_name: Some("@john".to_owned()),
1416 created_at: now,
1417 },
1418 UpstreamOAuthProvider {
1419 id: Ulid::nil(),
1420 issuer: Some("https://example.com/".to_owned()),
1421 human_name: Some("Example Ltd.".to_owned()),
1422 brand_name: None,
1423 scope: Scope::from_iter([OPENID]),
1424 token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1425 token_endpoint_signing_alg: None,
1426 id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1427 client_id: "client-id".to_owned(),
1428 encrypted_client_secret: None,
1429 claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1430 authorization_endpoint_override: None,
1431 token_endpoint_override: None,
1432 jwks_uri_override: None,
1433 userinfo_endpoint_override: None,
1434 fetch_userinfo: false,
1435 userinfo_signed_response_alg: None,
1436 discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1437 pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1438 response_mode: None,
1439 additional_authorization_parameters: Vec::new(),
1440 created_at: now,
1441 disabled_at: None,
1442 },
1443 )]
1444 }
1445}
1446
1447#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1449#[serde(rename_all = "snake_case")]
1450pub enum DeviceLinkFormField {
1451 Code,
1453}
1454
1455impl FormField for DeviceLinkFormField {
1456 fn keep(&self) -> bool {
1457 match self {
1458 Self::Code => true,
1459 }
1460 }
1461}
1462
1463#[derive(Serialize, Default, Debug)]
1465pub struct DeviceLinkContext {
1466 form_state: FormState<DeviceLinkFormField>,
1467}
1468
1469impl DeviceLinkContext {
1470 #[must_use]
1472 pub fn new() -> Self {
1473 Self::default()
1474 }
1475
1476 #[must_use]
1478 pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1479 self.form_state = form_state;
1480 self
1481 }
1482}
1483
1484impl TemplateContext for DeviceLinkContext {
1485 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1486 where
1487 Self: Sized,
1488 {
1489 vec![
1490 Self::new(),
1491 Self::new().with_form_state(
1492 FormState::default()
1493 .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1494 ),
1495 ]
1496 }
1497}
1498
1499#[derive(Serialize, Debug)]
1501pub struct DeviceConsentContext {
1502 grant: DeviceCodeGrant,
1503 client: Client,
1504}
1505
1506impl DeviceConsentContext {
1507 #[must_use]
1509 pub fn new(grant: DeviceCodeGrant, client: Client) -> Self {
1510 Self { grant, client }
1511 }
1512}
1513
1514impl TemplateContext for DeviceConsentContext {
1515 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1516 where
1517 Self: Sized,
1518 {
1519 Client::samples(now, rng)
1520 .into_iter()
1521 .map(|client| {
1522 let grant = DeviceCodeGrant {
1523 id: Ulid::from_datetime_with_source(now.into(), rng),
1524 state: mas_data_model::DeviceCodeGrantState::Pending,
1525 client_id: client.id,
1526 scope: [OPENID].into_iter().collect(),
1527 user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
1528 device_code: Alphanumeric.sample_string(rng, 32),
1529 created_at: now - Duration::try_minutes(5).unwrap(),
1530 expires_at: now + Duration::try_minutes(25).unwrap(),
1531 ip_address: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1532 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())),
1533 };
1534 Self { grant, client }
1535 })
1536 .collect()
1537 }
1538}
1539
1540#[derive(Serialize)]
1543pub struct AccountInactiveContext {
1544 user: User,
1545}
1546
1547impl AccountInactiveContext {
1548 #[must_use]
1550 pub fn new(user: User) -> Self {
1551 Self { user }
1552 }
1553}
1554
1555impl TemplateContext for AccountInactiveContext {
1556 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1557 where
1558 Self: Sized,
1559 {
1560 User::samples(now, rng)
1561 .into_iter()
1562 .map(|user| AccountInactiveContext { user })
1563 .collect()
1564 }
1565}
1566
1567#[derive(Serialize)]
1569pub struct FormPostContext<T> {
1570 redirect_uri: Option<Url>,
1571 params: T,
1572}
1573
1574impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
1575 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng) -> Vec<Self>
1576 where
1577 Self: Sized,
1578 {
1579 let sample_params = T::sample(now, rng);
1580 sample_params
1581 .into_iter()
1582 .map(|params| FormPostContext {
1583 redirect_uri: "https://example.com/callback".parse().ok(),
1584 params,
1585 })
1586 .collect()
1587 }
1588}
1589
1590impl<T> FormPostContext<T> {
1591 pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
1594 Self {
1595 redirect_uri: Some(redirect_uri),
1596 params,
1597 }
1598 }
1599
1600 pub fn new_for_current_url(params: T) -> Self {
1603 Self {
1604 redirect_uri: None,
1605 params,
1606 }
1607 }
1608
1609 pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
1614 WithLanguage {
1615 lang: lang.to_string(),
1616 inner: self,
1617 }
1618 }
1619}
1620
1621#[derive(Default, Serialize, Debug, Clone)]
1623pub struct ErrorContext {
1624 code: Option<&'static str>,
1625 description: Option<String>,
1626 details: Option<String>,
1627 lang: Option<String>,
1628}
1629
1630impl std::fmt::Display for ErrorContext {
1631 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1632 if let Some(code) = &self.code {
1633 writeln!(f, "code: {code}")?;
1634 }
1635 if let Some(description) = &self.description {
1636 writeln!(f, "{description}")?;
1637 }
1638
1639 if let Some(details) = &self.details {
1640 writeln!(f, "details: {details}")?;
1641 }
1642
1643 Ok(())
1644 }
1645}
1646
1647impl TemplateContext for ErrorContext {
1648 fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1649 where
1650 Self: Sized,
1651 {
1652 vec![
1653 Self::new()
1654 .with_code("sample_error")
1655 .with_description("A fancy description".into())
1656 .with_details("Something happened".into()),
1657 Self::new().with_code("another_error"),
1658 Self::new(),
1659 ]
1660 }
1661}
1662
1663impl ErrorContext {
1664 #[must_use]
1666 pub fn new() -> Self {
1667 Self::default()
1668 }
1669
1670 #[must_use]
1672 pub fn with_code(mut self, code: &'static str) -> Self {
1673 self.code = Some(code);
1674 self
1675 }
1676
1677 #[must_use]
1679 pub fn with_description(mut self, description: String) -> Self {
1680 self.description = Some(description);
1681 self
1682 }
1683
1684 #[must_use]
1686 pub fn with_details(mut self, details: String) -> Self {
1687 self.details = Some(details);
1688 self
1689 }
1690
1691 #[must_use]
1693 pub fn with_language(mut self, lang: &DataLocale) -> Self {
1694 self.lang = Some(lang.to_string());
1695 self
1696 }
1697
1698 #[must_use]
1700 pub fn code(&self) -> Option<&'static str> {
1701 self.code
1702 }
1703
1704 #[must_use]
1706 pub fn description(&self) -> Option<&str> {
1707 self.description.as_deref()
1708 }
1709
1710 #[must_use]
1712 pub fn details(&self) -> Option<&str> {
1713 self.details.as_deref()
1714 }
1715}
1716
1717#[derive(Serialize)]
1719pub struct NotFoundContext {
1720 method: String,
1721 version: String,
1722 uri: String,
1723}
1724
1725impl NotFoundContext {
1726 #[must_use]
1728 pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
1729 Self {
1730 method: method.to_string(),
1731 version: format!("{version:?}"),
1732 uri: uri.to_string(),
1733 }
1734 }
1735}
1736
1737impl TemplateContext for NotFoundContext {
1738 fn sample(_now: DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
1739 where
1740 Self: Sized,
1741 {
1742 vec![
1743 Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
1744 Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
1745 Self::new(
1746 &Method::PUT,
1747 Version::HTTP_10,
1748 &"/foo?bar=baz".parse().unwrap(),
1749 ),
1750 ]
1751 }
1752}