1mod branding;
10mod captcha;
11mod ext;
12mod features;
13
14use std::{
15 collections::BTreeMap,
16 fmt::Formatter,
17 net::{IpAddr, Ipv4Addr},
18};
19
20use chrono::{DateTime, Duration, Utc};
21use http::{Method, Uri, Version};
22use mas_data_model::{
23 AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
24 DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
25 UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderOnBackchannelLogout,
26 UpstreamOAuthProviderPkceMode, UpstreamOAuthProviderTokenAuthMethod, User,
27 UserEmailAuthentication, UserEmailAuthenticationCode, UserRecoverySession, UserRegistration,
28};
29use mas_i18n::DataLocale;
30use mas_iana::jose::JsonWebSignatureAlg;
31use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder};
32use oauth2_types::scope::{OPENID, Scope};
33use rand::{
34 Rng, SeedableRng,
35 distributions::{Alphanumeric, DistString},
36};
37use rand_chacha::ChaCha8Rng;
38use serde::{Deserialize, Serialize, ser::SerializeStruct};
39use ulid::Ulid;
40use url::Url;
41
42pub use self::{
43 branding::SiteBranding, captcha::WithCaptcha, ext::SiteConfigExt, features::SiteFeatures,
44};
45use crate::{FieldError, FormField, FormState};
46
47pub trait TemplateContext: Serialize {
49 fn with_session(self, current_session: BrowserSession) -> WithSession<Self>
51 where
52 Self: Sized,
53 {
54 WithSession {
55 current_session,
56 inner: self,
57 }
58 }
59
60 fn maybe_with_session(
62 self,
63 current_session: Option<BrowserSession>,
64 ) -> WithOptionalSession<Self>
65 where
66 Self: Sized,
67 {
68 WithOptionalSession {
69 current_session,
70 inner: self,
71 }
72 }
73
74 fn with_csrf<C>(self, csrf_token: C) -> WithCsrf<Self>
76 where
77 Self: Sized,
78 C: ToString,
79 {
80 WithCsrf {
82 csrf_token: csrf_token.to_string(),
83 inner: self,
84 }
85 }
86
87 fn with_language(self, lang: DataLocale) -> WithLanguage<Self>
89 where
90 Self: Sized,
91 {
92 WithLanguage {
93 lang: lang.to_string(),
94 inner: self,
95 }
96 }
97
98 fn with_captcha(self, captcha: Option<mas_data_model::CaptchaConfig>) -> WithCaptcha<Self>
100 where
101 Self: Sized,
102 {
103 WithCaptcha::new(captcha, self)
104 }
105
106 fn sample<R: Rng>(
111 now: chrono::DateTime<Utc>,
112 rng: &mut R,
113 locales: &[DataLocale],
114 ) -> BTreeMap<SampleIdentifier, Self>
115 where
116 Self: Sized;
117}
118
119#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
120pub struct SampleIdentifier {
121 pub components: Vec<(&'static str, String)>,
122}
123
124impl SampleIdentifier {
125 pub fn from_index(index: usize) -> Self {
126 Self {
127 components: Vec::default(),
128 }
129 .with_appended("index", format!("{index}"))
130 }
131
132 pub fn with_appended(&self, kind: &'static str, locale: String) -> Self {
133 let mut new = self.clone();
134 new.components.push((kind, locale));
135 new
136 }
137}
138
139pub(crate) fn sample_list<T: TemplateContext>(samples: Vec<T>) -> BTreeMap<SampleIdentifier, T> {
140 samples
141 .into_iter()
142 .enumerate()
143 .map(|(index, sample)| (SampleIdentifier::from_index(index), sample))
144 .collect()
145}
146
147impl TemplateContext for () {
148 fn sample<R: Rng>(
149 _now: chrono::DateTime<Utc>,
150 _rng: &mut R,
151 _locales: &[DataLocale],
152 ) -> BTreeMap<SampleIdentifier, Self>
153 where
154 Self: Sized,
155 {
156 BTreeMap::new()
157 }
158}
159
160#[derive(Serialize, Debug)]
162pub struct WithLanguage<T> {
163 lang: String,
164
165 #[serde(flatten)]
166 inner: T,
167}
168
169impl<T> WithLanguage<T> {
170 pub fn language(&self) -> &str {
172 &self.lang
173 }
174}
175
176impl<T> std::ops::Deref for WithLanguage<T> {
177 type Target = T;
178
179 fn deref(&self) -> &Self::Target {
180 &self.inner
181 }
182}
183
184impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
185 fn sample<R: Rng>(
186 now: chrono::DateTime<Utc>,
187 rng: &mut R,
188 locales: &[DataLocale],
189 ) -> BTreeMap<SampleIdentifier, Self>
190 where
191 Self: Sized,
192 {
193 let rng = ChaCha8Rng::from_rng(rng).unwrap();
195 locales
196 .iter()
197 .flat_map(|locale| {
198 T::sample(now, &mut rng.clone(), locales)
199 .into_iter()
200 .map(|(sample_id, sample)| {
201 (
202 sample_id.with_appended("locale", locale.to_string()),
203 WithLanguage {
204 lang: locale.to_string(),
205 inner: sample,
206 },
207 )
208 })
209 })
210 .collect()
211 }
212}
213
214#[derive(Serialize, Debug)]
216pub struct WithCsrf<T> {
217 csrf_token: String,
218
219 #[serde(flatten)]
220 inner: T,
221}
222
223impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
224 fn sample<R: Rng>(
225 now: chrono::DateTime<Utc>,
226 rng: &mut R,
227 locales: &[DataLocale],
228 ) -> BTreeMap<SampleIdentifier, Self>
229 where
230 Self: Sized,
231 {
232 T::sample(now, rng, locales)
233 .into_iter()
234 .map(|(k, inner)| {
235 (
236 k,
237 WithCsrf {
238 csrf_token: "fake_csrf_token".into(),
239 inner,
240 },
241 )
242 })
243 .collect()
244 }
245}
246
247#[derive(Serialize)]
249pub struct WithSession<T> {
250 current_session: BrowserSession,
251
252 #[serde(flatten)]
253 inner: T,
254}
255
256impl<T: TemplateContext> TemplateContext for WithSession<T> {
257 fn sample<R: Rng>(
258 now: chrono::DateTime<Utc>,
259 rng: &mut R,
260 locales: &[DataLocale],
261 ) -> BTreeMap<SampleIdentifier, Self>
262 where
263 Self: Sized,
264 {
265 BrowserSession::samples(now, rng)
266 .into_iter()
267 .enumerate()
268 .flat_map(|(session_index, session)| {
269 T::sample(now, rng, locales)
270 .into_iter()
271 .map(move |(k, inner)| {
272 (
273 k.with_appended("browser-session", session_index.to_string()),
274 WithSession {
275 current_session: session.clone(),
276 inner,
277 },
278 )
279 })
280 })
281 .collect()
282 }
283}
284
285#[derive(Serialize)]
287pub struct WithOptionalSession<T> {
288 current_session: Option<BrowserSession>,
289
290 #[serde(flatten)]
291 inner: T,
292}
293
294impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
295 fn sample<R: Rng>(
296 now: chrono::DateTime<Utc>,
297 rng: &mut R,
298 locales: &[DataLocale],
299 ) -> BTreeMap<SampleIdentifier, Self>
300 where
301 Self: Sized,
302 {
303 BrowserSession::samples(now, rng)
304 .into_iter()
305 .map(Some) .chain(std::iter::once(None)) .enumerate()
308 .flat_map(|(session_index, session)| {
309 T::sample(now, rng, locales)
310 .into_iter()
311 .map(move |(k, inner)| {
312 (
313 if session.is_some() {
314 k.with_appended("browser-session", session_index.to_string())
315 } else {
316 k
317 },
318 WithOptionalSession {
319 current_session: session.clone(),
320 inner,
321 },
322 )
323 })
324 })
325 .collect()
326 }
327}
328
329pub struct EmptyContext;
331
332impl Serialize for EmptyContext {
333 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
334 where
335 S: serde::Serializer,
336 {
337 let mut s = serializer.serialize_struct("EmptyContext", 0)?;
338 s.serialize_field("__UNUSED", &())?;
341 s.end()
342 }
343}
344
345impl TemplateContext for EmptyContext {
346 fn sample<R: Rng>(
347 _now: chrono::DateTime<Utc>,
348 _rng: &mut R,
349 _locales: &[DataLocale],
350 ) -> BTreeMap<SampleIdentifier, Self>
351 where
352 Self: Sized,
353 {
354 sample_list(vec![EmptyContext])
355 }
356}
357
358#[derive(Serialize)]
360pub struct IndexContext {
361 discovery_url: Url,
362}
363
364impl IndexContext {
365 #[must_use]
368 pub fn new(discovery_url: Url) -> Self {
369 Self { discovery_url }
370 }
371}
372
373impl TemplateContext for IndexContext {
374 fn sample<R: Rng>(
375 _now: chrono::DateTime<Utc>,
376 _rng: &mut R,
377 _locales: &[DataLocale],
378 ) -> BTreeMap<SampleIdentifier, Self>
379 where
380 Self: Sized,
381 {
382 sample_list(vec![Self {
383 discovery_url: "https://example.com/.well-known/openid-configuration"
384 .parse()
385 .unwrap(),
386 }])
387 }
388}
389
390#[derive(Serialize)]
392#[serde(rename_all = "camelCase")]
393pub struct AppConfig {
394 root: String,
395 graphql_endpoint: String,
396}
397
398#[derive(Serialize)]
400pub struct AppContext {
401 app_config: AppConfig,
402}
403
404impl AppContext {
405 #[must_use]
407 pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
408 let root = url_builder.relative_url_for(&Account::default());
409 let graphql_endpoint = url_builder.relative_url_for(&GraphQL);
410 Self {
411 app_config: AppConfig {
412 root,
413 graphql_endpoint,
414 },
415 }
416 }
417}
418
419impl TemplateContext for AppContext {
420 fn sample<R: Rng>(
421 _now: chrono::DateTime<Utc>,
422 _rng: &mut R,
423 _locales: &[DataLocale],
424 ) -> BTreeMap<SampleIdentifier, Self>
425 where
426 Self: Sized,
427 {
428 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
429 sample_list(vec![Self::from_url_builder(&url_builder)])
430 }
431}
432
433#[derive(Serialize)]
435pub struct ApiDocContext {
436 openapi_url: Url,
437 callback_url: Url,
438}
439
440impl ApiDocContext {
441 #[must_use]
444 pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
445 Self {
446 openapi_url: url_builder.absolute_url_for(&mas_router::ApiSpec),
447 callback_url: url_builder.absolute_url_for(&mas_router::ApiDocCallback),
448 }
449 }
450}
451
452impl TemplateContext for ApiDocContext {
453 fn sample<R: Rng>(
454 _now: chrono::DateTime<Utc>,
455 _rng: &mut R,
456 _locales: &[DataLocale],
457 ) -> BTreeMap<SampleIdentifier, Self>
458 where
459 Self: Sized,
460 {
461 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
462 sample_list(vec![Self::from_url_builder(&url_builder)])
463 }
464}
465
466#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
468#[serde(rename_all = "snake_case")]
469pub enum LoginFormField {
470 Username,
472
473 Password,
475}
476
477impl FormField for LoginFormField {
478 fn keep(&self) -> bool {
479 match self {
480 Self::Username => true,
481 Self::Password => false,
482 }
483 }
484}
485
486#[derive(Serialize)]
488#[serde(tag = "kind", rename_all = "snake_case")]
489pub enum PostAuthContextInner {
490 ContinueAuthorizationGrant {
492 grant: Box<AuthorizationGrant>,
494 },
495
496 ContinueDeviceCodeGrant {
498 grant: Box<DeviceCodeGrant>,
500 },
501
502 ContinueCompatSsoLogin {
505 login: Box<CompatSsoLogin>,
507 },
508
509 ChangePassword,
511
512 LinkUpstream {
514 provider: Box<UpstreamOAuthProvider>,
516
517 link: Box<UpstreamOAuthLink>,
519 },
520
521 ManageAccount,
523}
524
525#[derive(Serialize)]
527pub struct PostAuthContext {
528 pub params: PostAuthAction,
530
531 #[serde(flatten)]
533 pub ctx: PostAuthContextInner,
534}
535
536#[derive(Serialize, Default)]
538pub struct LoginContext {
539 form: FormState<LoginFormField>,
540 next: Option<PostAuthContext>,
541 providers: Vec<UpstreamOAuthProvider>,
542}
543
544impl TemplateContext for LoginContext {
545 fn sample<R: Rng>(
546 _now: chrono::DateTime<Utc>,
547 _rng: &mut R,
548 _locales: &[DataLocale],
549 ) -> BTreeMap<SampleIdentifier, Self>
550 where
551 Self: Sized,
552 {
553 sample_list(vec![
555 LoginContext {
556 form: FormState::default(),
557 next: None,
558 providers: Vec::new(),
559 },
560 LoginContext {
561 form: FormState::default(),
562 next: None,
563 providers: Vec::new(),
564 },
565 LoginContext {
566 form: FormState::default()
567 .with_error_on_field(LoginFormField::Username, FieldError::Required)
568 .with_error_on_field(
569 LoginFormField::Password,
570 FieldError::Policy {
571 code: None,
572 message: "password too short".to_owned(),
573 },
574 ),
575 next: None,
576 providers: Vec::new(),
577 },
578 LoginContext {
579 form: FormState::default()
580 .with_error_on_field(LoginFormField::Username, FieldError::Exists),
581 next: None,
582 providers: Vec::new(),
583 },
584 ])
585 }
586}
587
588impl LoginContext {
589 #[must_use]
591 pub fn with_form_state(self, form: FormState<LoginFormField>) -> Self {
592 Self { form, ..self }
593 }
594
595 pub fn form_state_mut(&mut self) -> &mut FormState<LoginFormField> {
597 &mut self.form
598 }
599
600 #[must_use]
602 pub fn with_upstream_providers(self, providers: Vec<UpstreamOAuthProvider>) -> Self {
603 Self { providers, ..self }
604 }
605
606 #[must_use]
608 pub fn with_post_action(self, context: PostAuthContext) -> Self {
609 Self {
610 next: Some(context),
611 ..self
612 }
613 }
614}
615
616#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
618#[serde(rename_all = "snake_case")]
619pub enum RegisterFormField {
620 Username,
622
623 Email,
625
626 Password,
628
629 PasswordConfirm,
631
632 AcceptTerms,
634}
635
636impl FormField for RegisterFormField {
637 fn keep(&self) -> bool {
638 match self {
639 Self::Username | Self::Email | Self::AcceptTerms => true,
640 Self::Password | Self::PasswordConfirm => false,
641 }
642 }
643}
644
645#[derive(Serialize, Default)]
647pub struct RegisterContext {
648 providers: Vec<UpstreamOAuthProvider>,
649 next: Option<PostAuthContext>,
650}
651
652impl TemplateContext for RegisterContext {
653 fn sample<R: Rng>(
654 _now: chrono::DateTime<Utc>,
655 _rng: &mut R,
656 _locales: &[DataLocale],
657 ) -> BTreeMap<SampleIdentifier, Self>
658 where
659 Self: Sized,
660 {
661 sample_list(vec![RegisterContext {
662 providers: Vec::new(),
663 next: None,
664 }])
665 }
666}
667
668impl RegisterContext {
669 #[must_use]
671 pub fn new(providers: Vec<UpstreamOAuthProvider>) -> Self {
672 Self {
673 providers,
674 next: None,
675 }
676 }
677
678 #[must_use]
680 pub fn with_post_action(self, next: PostAuthContext) -> Self {
681 Self {
682 next: Some(next),
683 ..self
684 }
685 }
686}
687
688#[derive(Serialize, Default)]
690pub struct PasswordRegisterContext {
691 form: FormState<RegisterFormField>,
692 next: Option<PostAuthContext>,
693}
694
695impl TemplateContext for PasswordRegisterContext {
696 fn sample<R: Rng>(
697 _now: chrono::DateTime<Utc>,
698 _rng: &mut R,
699 _locales: &[DataLocale],
700 ) -> BTreeMap<SampleIdentifier, Self>
701 where
702 Self: Sized,
703 {
704 sample_list(vec![PasswordRegisterContext {
706 form: FormState::default(),
707 next: None,
708 }])
709 }
710}
711
712impl PasswordRegisterContext {
713 #[must_use]
715 pub fn with_form_state(self, form: FormState<RegisterFormField>) -> Self {
716 Self { form, ..self }
717 }
718
719 #[must_use]
721 pub fn with_post_action(self, next: PostAuthContext) -> Self {
722 Self {
723 next: Some(next),
724 ..self
725 }
726 }
727}
728
729#[derive(Serialize)]
731pub struct ConsentContext {
732 grant: AuthorizationGrant,
733 client: Client,
734 action: PostAuthAction,
735}
736
737impl TemplateContext for ConsentContext {
738 fn sample<R: Rng>(
739 now: chrono::DateTime<Utc>,
740 rng: &mut R,
741 _locales: &[DataLocale],
742 ) -> BTreeMap<SampleIdentifier, Self>
743 where
744 Self: Sized,
745 {
746 sample_list(
747 Client::samples(now, rng)
748 .into_iter()
749 .map(|client| {
750 let mut grant = AuthorizationGrant::sample(now, rng);
751 let action = PostAuthAction::continue_grant(grant.id);
752 grant.client_id = client.id;
754 Self {
755 grant,
756 client,
757 action,
758 }
759 })
760 .collect(),
761 )
762 }
763}
764
765impl ConsentContext {
766 #[must_use]
768 pub fn new(grant: AuthorizationGrant, client: Client) -> Self {
769 let action = PostAuthAction::continue_grant(grant.id);
770 Self {
771 grant,
772 client,
773 action,
774 }
775 }
776}
777
778#[derive(Serialize)]
779#[serde(tag = "grant_type")]
780enum PolicyViolationGrant {
781 #[serde(rename = "authorization_code")]
782 Authorization(AuthorizationGrant),
783 #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
784 DeviceCode(DeviceCodeGrant),
785}
786
787#[derive(Serialize)]
789pub struct PolicyViolationContext {
790 grant: PolicyViolationGrant,
791 client: Client,
792 action: PostAuthAction,
793}
794
795impl TemplateContext for PolicyViolationContext {
796 fn sample<R: Rng>(
797 now: chrono::DateTime<Utc>,
798 rng: &mut R,
799 _locales: &[DataLocale],
800 ) -> BTreeMap<SampleIdentifier, Self>
801 where
802 Self: Sized,
803 {
804 sample_list(
805 Client::samples(now, rng)
806 .into_iter()
807 .flat_map(|client| {
808 let mut grant = AuthorizationGrant::sample(now, rng);
809 grant.client_id = client.id;
811
812 let authorization_grant =
813 PolicyViolationContext::for_authorization_grant(grant, client.clone());
814 let device_code_grant = PolicyViolationContext::for_device_code_grant(
815 DeviceCodeGrant {
816 id: Ulid::from_datetime_with_source(now.into(), rng),
817 state: mas_data_model::DeviceCodeGrantState::Pending,
818 client_id: client.id,
819 scope: [OPENID].into_iter().collect(),
820 user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
821 device_code: Alphanumeric.sample_string(rng, 32),
822 created_at: now - Duration::try_minutes(5).unwrap(),
823 expires_at: now + Duration::try_minutes(25).unwrap(),
824 ip_address: None,
825 user_agent: None,
826 },
827 client,
828 );
829
830 [authorization_grant, device_code_grant]
831 })
832 .collect(),
833 )
834 }
835}
836
837impl PolicyViolationContext {
838 #[must_use]
841 pub const fn for_authorization_grant(grant: AuthorizationGrant, client: Client) -> Self {
842 let action = PostAuthAction::continue_grant(grant.id);
843 Self {
844 grant: PolicyViolationGrant::Authorization(grant),
845 client,
846 action,
847 }
848 }
849
850 #[must_use]
853 pub const fn for_device_code_grant(grant: DeviceCodeGrant, client: Client) -> Self {
854 let action = PostAuthAction::continue_device_code_grant(grant.id);
855 Self {
856 grant: PolicyViolationGrant::DeviceCode(grant),
857 client,
858 action,
859 }
860 }
861}
862
863#[derive(Serialize)]
865pub struct CompatLoginPolicyViolationContext {
866 violation_codes: Vec<&'static str>,
867}
868
869impl TemplateContext for CompatLoginPolicyViolationContext {
870 fn sample<R: Rng>(
871 _now: chrono::DateTime<Utc>,
872 _rng: &mut R,
873 _locales: &[DataLocale],
874 ) -> BTreeMap<SampleIdentifier, Self>
875 where
876 Self: Sized,
877 {
878 sample_list(vec![
879 CompatLoginPolicyViolationContext {
880 violation_codes: vec![],
881 },
882 CompatLoginPolicyViolationContext {
883 violation_codes: vec!["too-many-sessions"],
884 },
885 ])
886 }
887}
888
889impl CompatLoginPolicyViolationContext {
890 #[must_use]
896 pub const fn for_violations(violation_codes: Vec<&'static str>) -> Self {
897 Self { violation_codes }
898 }
899}
900
901#[derive(Serialize)]
903pub struct CompatSsoContext {
904 login: CompatSsoLogin,
905 action: PostAuthAction,
906}
907
908impl TemplateContext for CompatSsoContext {
909 fn sample<R: Rng>(
910 now: chrono::DateTime<Utc>,
911 rng: &mut R,
912 _locales: &[DataLocale],
913 ) -> BTreeMap<SampleIdentifier, Self>
914 where
915 Self: Sized,
916 {
917 let id = Ulid::from_datetime_with_source(now.into(), rng);
918 sample_list(vec![CompatSsoContext::new(CompatSsoLogin {
919 id,
920 redirect_uri: Url::parse("https://app.element.io/").unwrap(),
921 login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
922 created_at: now,
923 state: CompatSsoLoginState::Pending,
924 })])
925 }
926}
927
928impl CompatSsoContext {
929 #[must_use]
931 pub fn new(login: CompatSsoLogin) -> Self
932where {
933 let action = PostAuthAction::continue_compat_sso_login(login.id);
934 Self { login, action }
935 }
936}
937
938#[derive(Serialize)]
940pub struct EmailRecoveryContext {
941 user: User,
942 session: UserRecoverySession,
943 recovery_link: Url,
944}
945
946impl EmailRecoveryContext {
947 #[must_use]
949 pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
950 Self {
951 user,
952 session,
953 recovery_link,
954 }
955 }
956
957 #[must_use]
959 pub fn user(&self) -> &User {
960 &self.user
961 }
962
963 #[must_use]
965 pub fn session(&self) -> &UserRecoverySession {
966 &self.session
967 }
968}
969
970impl TemplateContext for EmailRecoveryContext {
971 fn sample<R: Rng>(
972 now: chrono::DateTime<Utc>,
973 rng: &mut R,
974 _locales: &[DataLocale],
975 ) -> BTreeMap<SampleIdentifier, Self>
976 where
977 Self: Sized,
978 {
979 sample_list(User::samples(now, rng).into_iter().map(|user| {
980 let session = UserRecoverySession {
981 id: Ulid::from_datetime_with_source(now.into(), rng),
982 email: "hello@example.com".to_owned(),
983 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(),
984 ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
985 locale: "en".to_owned(),
986 created_at: now,
987 consumed_at: None,
988 };
989
990 let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
991
992 Self::new(user, session, link)
993 }).collect())
994 }
995}
996
997#[derive(Serialize)]
999pub struct EmailVerificationContext {
1000 #[serde(skip_serializing_if = "Option::is_none")]
1001 browser_session: Option<BrowserSession>,
1002 #[serde(skip_serializing_if = "Option::is_none")]
1003 user_registration: Option<UserRegistration>,
1004 authentication_code: UserEmailAuthenticationCode,
1005}
1006
1007impl EmailVerificationContext {
1008 #[must_use]
1010 pub fn new(
1011 authentication_code: UserEmailAuthenticationCode,
1012 browser_session: Option<BrowserSession>,
1013 user_registration: Option<UserRegistration>,
1014 ) -> Self {
1015 Self {
1016 browser_session,
1017 user_registration,
1018 authentication_code,
1019 }
1020 }
1021
1022 #[must_use]
1024 pub fn user(&self) -> Option<&User> {
1025 self.browser_session.as_ref().map(|s| &s.user)
1026 }
1027
1028 #[must_use]
1030 pub fn code(&self) -> &str {
1031 &self.authentication_code.code
1032 }
1033}
1034
1035impl TemplateContext for EmailVerificationContext {
1036 fn sample<R: Rng>(
1037 now: chrono::DateTime<Utc>,
1038 rng: &mut R,
1039 _locales: &[DataLocale],
1040 ) -> BTreeMap<SampleIdentifier, Self>
1041 where
1042 Self: Sized,
1043 {
1044 sample_list(
1045 BrowserSession::samples(now, rng)
1046 .into_iter()
1047 .map(|browser_session| {
1048 let authentication_code = UserEmailAuthenticationCode {
1049 id: Ulid::from_datetime_with_source(now.into(), rng),
1050 user_email_authentication_id: Ulid::from_datetime_with_source(
1051 now.into(),
1052 rng,
1053 ),
1054 code: "123456".to_owned(),
1055 created_at: now - Duration::try_minutes(5).unwrap(),
1056 expires_at: now + Duration::try_minutes(25).unwrap(),
1057 };
1058
1059 Self {
1060 browser_session: Some(browser_session),
1061 user_registration: None,
1062 authentication_code,
1063 }
1064 })
1065 .collect(),
1066 )
1067 }
1068}
1069
1070#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1072#[serde(rename_all = "snake_case")]
1073pub enum RegisterStepsVerifyEmailFormField {
1074 Code,
1076}
1077
1078impl FormField for RegisterStepsVerifyEmailFormField {
1079 fn keep(&self) -> bool {
1080 match self {
1081 Self::Code => true,
1082 }
1083 }
1084}
1085
1086#[derive(Serialize)]
1088pub struct RegisterStepsVerifyEmailContext {
1089 form: FormState<RegisterStepsVerifyEmailFormField>,
1090 authentication: UserEmailAuthentication,
1091}
1092
1093impl RegisterStepsVerifyEmailContext {
1094 #[must_use]
1096 pub fn new(authentication: UserEmailAuthentication) -> Self {
1097 Self {
1098 form: FormState::default(),
1099 authentication,
1100 }
1101 }
1102
1103 #[must_use]
1105 pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
1106 Self { form, ..self }
1107 }
1108}
1109
1110impl TemplateContext for RegisterStepsVerifyEmailContext {
1111 fn sample<R: Rng>(
1112 now: chrono::DateTime<Utc>,
1113 rng: &mut R,
1114 _locales: &[DataLocale],
1115 ) -> BTreeMap<SampleIdentifier, Self>
1116 where
1117 Self: Sized,
1118 {
1119 let authentication = UserEmailAuthentication {
1120 id: Ulid::from_datetime_with_source(now.into(), rng),
1121 user_session_id: None,
1122 user_registration_id: None,
1123 email: "foobar@example.com".to_owned(),
1124 created_at: now,
1125 completed_at: None,
1126 };
1127
1128 sample_list(vec![Self {
1129 form: FormState::default(),
1130 authentication,
1131 }])
1132 }
1133}
1134
1135#[derive(Serialize)]
1137pub struct RegisterStepsEmailInUseContext {
1138 email: String,
1139 action: Option<PostAuthAction>,
1140}
1141
1142impl RegisterStepsEmailInUseContext {
1143 #[must_use]
1145 pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
1146 Self { email, action }
1147 }
1148}
1149
1150impl TemplateContext for RegisterStepsEmailInUseContext {
1151 fn sample<R: Rng>(
1152 _now: chrono::DateTime<Utc>,
1153 _rng: &mut R,
1154 _locales: &[DataLocale],
1155 ) -> BTreeMap<SampleIdentifier, Self>
1156 where
1157 Self: Sized,
1158 {
1159 let email = "hello@example.com".to_owned();
1160 let action = PostAuthAction::continue_grant(Ulid::nil());
1161 sample_list(vec![Self::new(email, Some(action))])
1162 }
1163}
1164
1165#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1167#[serde(rename_all = "snake_case")]
1168pub enum RegisterStepsDisplayNameFormField {
1169 DisplayName,
1171}
1172
1173impl FormField for RegisterStepsDisplayNameFormField {
1174 fn keep(&self) -> bool {
1175 match self {
1176 Self::DisplayName => true,
1177 }
1178 }
1179}
1180
1181#[derive(Serialize, Default)]
1183pub struct RegisterStepsDisplayNameContext {
1184 form: FormState<RegisterStepsDisplayNameFormField>,
1185}
1186
1187impl RegisterStepsDisplayNameContext {
1188 #[must_use]
1190 pub fn new() -> Self {
1191 Self::default()
1192 }
1193
1194 #[must_use]
1196 pub fn with_form_state(
1197 mut self,
1198 form_state: FormState<RegisterStepsDisplayNameFormField>,
1199 ) -> Self {
1200 self.form = form_state;
1201 self
1202 }
1203}
1204
1205impl TemplateContext for RegisterStepsDisplayNameContext {
1206 fn sample<R: Rng>(
1207 _now: chrono::DateTime<chrono::Utc>,
1208 _rng: &mut R,
1209 _locales: &[DataLocale],
1210 ) -> BTreeMap<SampleIdentifier, Self>
1211 where
1212 Self: Sized,
1213 {
1214 sample_list(vec![Self {
1215 form: FormState::default(),
1216 }])
1217 }
1218}
1219
1220#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1222#[serde(rename_all = "snake_case")]
1223pub enum RegisterStepsRegistrationTokenFormField {
1224 Token,
1226}
1227
1228impl FormField for RegisterStepsRegistrationTokenFormField {
1229 fn keep(&self) -> bool {
1230 match self {
1231 Self::Token => true,
1232 }
1233 }
1234}
1235
1236#[derive(Serialize, Default)]
1238pub struct RegisterStepsRegistrationTokenContext {
1239 form: FormState<RegisterStepsRegistrationTokenFormField>,
1240}
1241
1242impl RegisterStepsRegistrationTokenContext {
1243 #[must_use]
1245 pub fn new() -> Self {
1246 Self::default()
1247 }
1248
1249 #[must_use]
1251 pub fn with_form_state(
1252 mut self,
1253 form_state: FormState<RegisterStepsRegistrationTokenFormField>,
1254 ) -> Self {
1255 self.form = form_state;
1256 self
1257 }
1258}
1259
1260impl TemplateContext for RegisterStepsRegistrationTokenContext {
1261 fn sample<R: Rng>(
1262 _now: chrono::DateTime<chrono::Utc>,
1263 _rng: &mut R,
1264 _locales: &[DataLocale],
1265 ) -> BTreeMap<SampleIdentifier, Self>
1266 where
1267 Self: Sized,
1268 {
1269 sample_list(vec![Self {
1270 form: FormState::default(),
1271 }])
1272 }
1273}
1274
1275#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1277#[serde(rename_all = "snake_case")]
1278pub enum RecoveryStartFormField {
1279 Email,
1281}
1282
1283impl FormField for RecoveryStartFormField {
1284 fn keep(&self) -> bool {
1285 match self {
1286 Self::Email => true,
1287 }
1288 }
1289}
1290
1291#[derive(Serialize, Default)]
1293pub struct RecoveryStartContext {
1294 form: FormState<RecoveryStartFormField>,
1295}
1296
1297impl RecoveryStartContext {
1298 #[must_use]
1300 pub fn new() -> Self {
1301 Self::default()
1302 }
1303
1304 #[must_use]
1306 pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1307 Self { form }
1308 }
1309}
1310
1311impl TemplateContext for RecoveryStartContext {
1312 fn sample<R: Rng>(
1313 _now: chrono::DateTime<Utc>,
1314 _rng: &mut R,
1315 _locales: &[DataLocale],
1316 ) -> BTreeMap<SampleIdentifier, Self>
1317 where
1318 Self: Sized,
1319 {
1320 sample_list(vec![
1321 Self::new(),
1322 Self::new().with_form_state(
1323 FormState::default()
1324 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1325 ),
1326 Self::new().with_form_state(
1327 FormState::default()
1328 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1329 ),
1330 ])
1331 }
1332}
1333
1334#[derive(Serialize)]
1336pub struct RecoveryProgressContext {
1337 session: UserRecoverySession,
1338 resend_failed_due_to_rate_limit: bool,
1340}
1341
1342impl RecoveryProgressContext {
1343 #[must_use]
1345 pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1346 Self {
1347 session,
1348 resend_failed_due_to_rate_limit,
1349 }
1350 }
1351}
1352
1353impl TemplateContext for RecoveryProgressContext {
1354 fn sample<R: Rng>(
1355 now: chrono::DateTime<Utc>,
1356 rng: &mut R,
1357 _locales: &[DataLocale],
1358 ) -> BTreeMap<SampleIdentifier, Self>
1359 where
1360 Self: Sized,
1361 {
1362 let session = UserRecoverySession {
1363 id: Ulid::from_datetime_with_source(now.into(), rng),
1364 email: "name@mail.com".to_owned(),
1365 user_agent: "Mozilla/5.0".to_owned(),
1366 ip_address: None,
1367 locale: "en".to_owned(),
1368 created_at: now,
1369 consumed_at: None,
1370 };
1371
1372 sample_list(vec![
1373 Self {
1374 session: session.clone(),
1375 resend_failed_due_to_rate_limit: false,
1376 },
1377 Self {
1378 session,
1379 resend_failed_due_to_rate_limit: true,
1380 },
1381 ])
1382 }
1383}
1384
1385#[derive(Serialize)]
1387pub struct RecoveryExpiredContext {
1388 session: UserRecoverySession,
1389}
1390
1391impl RecoveryExpiredContext {
1392 #[must_use]
1394 pub fn new(session: UserRecoverySession) -> Self {
1395 Self { session }
1396 }
1397}
1398
1399impl TemplateContext for RecoveryExpiredContext {
1400 fn sample<R: Rng>(
1401 now: chrono::DateTime<Utc>,
1402 rng: &mut R,
1403 _locales: &[DataLocale],
1404 ) -> BTreeMap<SampleIdentifier, Self>
1405 where
1406 Self: Sized,
1407 {
1408 let session = UserRecoverySession {
1409 id: Ulid::from_datetime_with_source(now.into(), rng),
1410 email: "name@mail.com".to_owned(),
1411 user_agent: "Mozilla/5.0".to_owned(),
1412 ip_address: None,
1413 locale: "en".to_owned(),
1414 created_at: now,
1415 consumed_at: None,
1416 };
1417
1418 sample_list(vec![Self { session }])
1419 }
1420}
1421#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1423#[serde(rename_all = "snake_case")]
1424pub enum RecoveryFinishFormField {
1425 NewPassword,
1427
1428 NewPasswordConfirm,
1430}
1431
1432impl FormField for RecoveryFinishFormField {
1433 fn keep(&self) -> bool {
1434 false
1435 }
1436}
1437
1438#[derive(Serialize)]
1440pub struct RecoveryFinishContext {
1441 user: User,
1442 form: FormState<RecoveryFinishFormField>,
1443}
1444
1445impl RecoveryFinishContext {
1446 #[must_use]
1448 pub fn new(user: User) -> Self {
1449 Self {
1450 user,
1451 form: FormState::default(),
1452 }
1453 }
1454
1455 #[must_use]
1457 pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1458 self.form = form;
1459 self
1460 }
1461}
1462
1463impl TemplateContext for RecoveryFinishContext {
1464 fn sample<R: Rng>(
1465 now: chrono::DateTime<Utc>,
1466 rng: &mut R,
1467 _locales: &[DataLocale],
1468 ) -> BTreeMap<SampleIdentifier, Self>
1469 where
1470 Self: Sized,
1471 {
1472 sample_list(
1473 User::samples(now, rng)
1474 .into_iter()
1475 .flat_map(|user| {
1476 vec![
1477 Self::new(user.clone()),
1478 Self::new(user.clone()).with_form_state(
1479 FormState::default().with_error_on_field(
1480 RecoveryFinishFormField::NewPassword,
1481 FieldError::Invalid,
1482 ),
1483 ),
1484 Self::new(user.clone()).with_form_state(
1485 FormState::default().with_error_on_field(
1486 RecoveryFinishFormField::NewPasswordConfirm,
1487 FieldError::Invalid,
1488 ),
1489 ),
1490 ]
1491 })
1492 .collect(),
1493 )
1494 }
1495}
1496
1497#[derive(Serialize)]
1500pub struct UpstreamExistingLinkContext {
1501 linked_user: User,
1502}
1503
1504impl UpstreamExistingLinkContext {
1505 #[must_use]
1507 pub fn new(linked_user: User) -> Self {
1508 Self { linked_user }
1509 }
1510}
1511
1512impl TemplateContext for UpstreamExistingLinkContext {
1513 fn sample<R: Rng>(
1514 now: chrono::DateTime<Utc>,
1515 rng: &mut R,
1516 _locales: &[DataLocale],
1517 ) -> BTreeMap<SampleIdentifier, Self>
1518 where
1519 Self: Sized,
1520 {
1521 sample_list(
1522 User::samples(now, rng)
1523 .into_iter()
1524 .map(|linked_user| Self { linked_user })
1525 .collect(),
1526 )
1527 }
1528}
1529
1530#[derive(Serialize)]
1533pub struct UpstreamSuggestLink {
1534 post_logout_action: PostAuthAction,
1535}
1536
1537impl UpstreamSuggestLink {
1538 #[must_use]
1540 pub fn new(link: &UpstreamOAuthLink) -> Self {
1541 Self::for_link_id(link.id)
1542 }
1543
1544 fn for_link_id(id: Ulid) -> Self {
1545 let post_logout_action = PostAuthAction::link_upstream(id);
1546 Self { post_logout_action }
1547 }
1548}
1549
1550impl TemplateContext for UpstreamSuggestLink {
1551 fn sample<R: Rng>(
1552 now: chrono::DateTime<Utc>,
1553 rng: &mut R,
1554 _locales: &[DataLocale],
1555 ) -> BTreeMap<SampleIdentifier, Self>
1556 where
1557 Self: Sized,
1558 {
1559 let id = Ulid::from_datetime_with_source(now.into(), rng);
1560 sample_list(vec![Self::for_link_id(id)])
1561 }
1562}
1563
1564#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1566#[serde(rename_all = "snake_case")]
1567pub enum UpstreamRegisterFormField {
1568 Username,
1570
1571 AcceptTerms,
1573}
1574
1575impl FormField for UpstreamRegisterFormField {
1576 fn keep(&self) -> bool {
1577 match self {
1578 Self::Username | Self::AcceptTerms => true,
1579 }
1580 }
1581}
1582
1583#[derive(Serialize)]
1586pub struct UpstreamRegister {
1587 upstream_oauth_link: UpstreamOAuthLink,
1588 upstream_oauth_provider: UpstreamOAuthProvider,
1589 imported_localpart: Option<String>,
1590 force_localpart: bool,
1591 imported_display_name: Option<String>,
1592 force_display_name: bool,
1593 imported_email: Option<String>,
1594 force_email: bool,
1595 form_state: FormState<UpstreamRegisterFormField>,
1596}
1597
1598impl UpstreamRegister {
1599 #[must_use]
1602 pub fn new(
1603 upstream_oauth_link: UpstreamOAuthLink,
1604 upstream_oauth_provider: UpstreamOAuthProvider,
1605 ) -> Self {
1606 Self {
1607 upstream_oauth_link,
1608 upstream_oauth_provider,
1609 imported_localpart: None,
1610 force_localpart: false,
1611 imported_display_name: None,
1612 force_display_name: false,
1613 imported_email: None,
1614 force_email: false,
1615 form_state: FormState::default(),
1616 }
1617 }
1618
1619 pub fn set_localpart(&mut self, localpart: String, force: bool) {
1621 self.imported_localpart = Some(localpart);
1622 self.force_localpart = force;
1623 }
1624
1625 #[must_use]
1627 pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1628 Self {
1629 imported_localpart: Some(localpart),
1630 force_localpart: force,
1631 ..self
1632 }
1633 }
1634
1635 pub fn set_display_name(&mut self, display_name: String, force: bool) {
1637 self.imported_display_name = Some(display_name);
1638 self.force_display_name = force;
1639 }
1640
1641 #[must_use]
1643 pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1644 Self {
1645 imported_display_name: Some(display_name),
1646 force_display_name: force,
1647 ..self
1648 }
1649 }
1650
1651 pub fn set_email(&mut self, email: String, force: bool) {
1653 self.imported_email = Some(email);
1654 self.force_email = force;
1655 }
1656
1657 #[must_use]
1659 pub fn with_email(self, email: String, force: bool) -> Self {
1660 Self {
1661 imported_email: Some(email),
1662 force_email: force,
1663 ..self
1664 }
1665 }
1666
1667 pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1669 self.form_state = form_state;
1670 }
1671
1672 #[must_use]
1674 pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1675 Self { form_state, ..self }
1676 }
1677}
1678
1679impl TemplateContext for UpstreamRegister {
1680 fn sample<R: Rng>(
1681 now: chrono::DateTime<Utc>,
1682 _rng: &mut R,
1683 _locales: &[DataLocale],
1684 ) -> BTreeMap<SampleIdentifier, Self>
1685 where
1686 Self: Sized,
1687 {
1688 sample_list(vec![Self::new(
1689 UpstreamOAuthLink {
1690 id: Ulid::nil(),
1691 provider_id: Ulid::nil(),
1692 user_id: None,
1693 subject: "subject".to_owned(),
1694 human_account_name: Some("@john".to_owned()),
1695 created_at: now,
1696 },
1697 UpstreamOAuthProvider {
1698 id: Ulid::nil(),
1699 issuer: Some("https://example.com/".to_owned()),
1700 human_name: Some("Example Ltd.".to_owned()),
1701 brand_name: None,
1702 scope: Scope::from_iter([OPENID]),
1703 token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1704 token_endpoint_signing_alg: None,
1705 id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1706 client_id: "client-id".to_owned(),
1707 encrypted_client_secret: None,
1708 claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1709 authorization_endpoint_override: None,
1710 token_endpoint_override: None,
1711 jwks_uri_override: None,
1712 userinfo_endpoint_override: None,
1713 fetch_userinfo: false,
1714 userinfo_signed_response_alg: None,
1715 discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1716 pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1717 response_mode: None,
1718 additional_authorization_parameters: Vec::new(),
1719 forward_login_hint: false,
1720 created_at: now,
1721 disabled_at: None,
1722 on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
1723 },
1724 )])
1725 }
1726}
1727
1728#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1730#[serde(rename_all = "snake_case")]
1731pub enum DeviceLinkFormField {
1732 Code,
1734}
1735
1736impl FormField for DeviceLinkFormField {
1737 fn keep(&self) -> bool {
1738 match self {
1739 Self::Code => true,
1740 }
1741 }
1742}
1743
1744#[derive(Serialize, Default, Debug)]
1746pub struct DeviceLinkContext {
1747 form_state: FormState<DeviceLinkFormField>,
1748}
1749
1750impl DeviceLinkContext {
1751 #[must_use]
1753 pub fn new() -> Self {
1754 Self::default()
1755 }
1756
1757 #[must_use]
1759 pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1760 self.form_state = form_state;
1761 self
1762 }
1763}
1764
1765impl TemplateContext for DeviceLinkContext {
1766 fn sample<R: Rng>(
1767 _now: chrono::DateTime<Utc>,
1768 _rng: &mut R,
1769 _locales: &[DataLocale],
1770 ) -> BTreeMap<SampleIdentifier, Self>
1771 where
1772 Self: Sized,
1773 {
1774 sample_list(vec![
1775 Self::new(),
1776 Self::new().with_form_state(
1777 FormState::default()
1778 .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1779 ),
1780 ])
1781 }
1782}
1783
1784#[derive(Serialize, Debug)]
1786pub struct DeviceConsentContext {
1787 grant: DeviceCodeGrant,
1788 client: Client,
1789}
1790
1791impl DeviceConsentContext {
1792 #[must_use]
1794 pub fn new(grant: DeviceCodeGrant, client: Client) -> Self {
1795 Self { grant, client }
1796 }
1797}
1798
1799impl TemplateContext for DeviceConsentContext {
1800 fn sample<R: Rng>(
1801 now: chrono::DateTime<Utc>,
1802 rng: &mut R,
1803 _locales: &[DataLocale],
1804 ) -> BTreeMap<SampleIdentifier, Self>
1805 where
1806 Self: Sized,
1807 {
1808 sample_list(Client::samples(now, rng)
1809 .into_iter()
1810 .map(|client| {
1811 let grant = DeviceCodeGrant {
1812 id: Ulid::from_datetime_with_source(now.into(), rng),
1813 state: mas_data_model::DeviceCodeGrantState::Pending,
1814 client_id: client.id,
1815 scope: [OPENID].into_iter().collect(),
1816 user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
1817 device_code: Alphanumeric.sample_string(rng, 32),
1818 created_at: now - Duration::try_minutes(5).unwrap(),
1819 expires_at: now + Duration::try_minutes(25).unwrap(),
1820 ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
1821 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()),
1822 };
1823 Self { grant, client }
1824 })
1825 .collect())
1826 }
1827}
1828
1829#[derive(Serialize)]
1832pub struct AccountInactiveContext {
1833 user: User,
1834}
1835
1836impl AccountInactiveContext {
1837 #[must_use]
1839 pub fn new(user: User) -> Self {
1840 Self { user }
1841 }
1842}
1843
1844impl TemplateContext for AccountInactiveContext {
1845 fn sample<R: Rng>(
1846 now: chrono::DateTime<Utc>,
1847 rng: &mut R,
1848 _locales: &[DataLocale],
1849 ) -> BTreeMap<SampleIdentifier, Self>
1850 where
1851 Self: Sized,
1852 {
1853 sample_list(
1854 User::samples(now, rng)
1855 .into_iter()
1856 .map(|user| AccountInactiveContext { user })
1857 .collect(),
1858 )
1859 }
1860}
1861
1862#[derive(Serialize)]
1864pub struct DeviceNameContext {
1865 client: Client,
1866 raw_user_agent: String,
1867}
1868
1869impl DeviceNameContext {
1870 #[must_use]
1872 pub fn new(client: Client, user_agent: Option<String>) -> Self {
1873 Self {
1874 client,
1875 raw_user_agent: user_agent.unwrap_or_default(),
1876 }
1877 }
1878}
1879
1880impl TemplateContext for DeviceNameContext {
1881 fn sample<R: Rng>(
1882 now: chrono::DateTime<Utc>,
1883 rng: &mut R,
1884 _locales: &[DataLocale],
1885 ) -> BTreeMap<SampleIdentifier, Self>
1886 where
1887 Self: Sized,
1888 {
1889 sample_list(Client::samples(now, rng)
1890 .into_iter()
1891 .map(|client| DeviceNameContext {
1892 client,
1893 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(),
1894 })
1895 .collect())
1896 }
1897}
1898
1899#[derive(Serialize)]
1901pub struct FormPostContext<T> {
1902 redirect_uri: Option<Url>,
1903 params: T,
1904}
1905
1906impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
1907 fn sample<R: Rng>(
1908 now: chrono::DateTime<Utc>,
1909 rng: &mut R,
1910 locales: &[DataLocale],
1911 ) -> BTreeMap<SampleIdentifier, Self>
1912 where
1913 Self: Sized,
1914 {
1915 let sample_params = T::sample(now, rng, locales);
1916 sample_params
1917 .into_iter()
1918 .map(|(k, params)| {
1919 (
1920 k,
1921 FormPostContext {
1922 redirect_uri: "https://example.com/callback".parse().ok(),
1923 params,
1924 },
1925 )
1926 })
1927 .collect()
1928 }
1929}
1930
1931impl<T> FormPostContext<T> {
1932 pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
1935 Self {
1936 redirect_uri: Some(redirect_uri),
1937 params,
1938 }
1939 }
1940
1941 pub fn new_for_current_url(params: T) -> Self {
1944 Self {
1945 redirect_uri: None,
1946 params,
1947 }
1948 }
1949
1950 pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
1955 WithLanguage {
1956 lang: lang.to_string(),
1957 inner: self,
1958 }
1959 }
1960}
1961
1962#[derive(Default, Serialize, Debug, Clone)]
1964pub struct ErrorContext {
1965 code: Option<&'static str>,
1966 description: Option<String>,
1967 details: Option<String>,
1968 lang: Option<String>,
1969}
1970
1971impl std::fmt::Display for ErrorContext {
1972 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1973 if let Some(code) = &self.code {
1974 writeln!(f, "code: {code}")?;
1975 }
1976 if let Some(description) = &self.description {
1977 writeln!(f, "{description}")?;
1978 }
1979
1980 if let Some(details) = &self.details {
1981 writeln!(f, "details: {details}")?;
1982 }
1983
1984 Ok(())
1985 }
1986}
1987
1988impl TemplateContext for ErrorContext {
1989 fn sample<R: Rng>(
1990 _now: chrono::DateTime<Utc>,
1991 _rng: &mut R,
1992 _locales: &[DataLocale],
1993 ) -> BTreeMap<SampleIdentifier, Self>
1994 where
1995 Self: Sized,
1996 {
1997 sample_list(vec![
1998 Self::new()
1999 .with_code("sample_error")
2000 .with_description("A fancy description".into())
2001 .with_details("Something happened".into()),
2002 Self::new().with_code("another_error"),
2003 Self::new(),
2004 ])
2005 }
2006}
2007
2008impl ErrorContext {
2009 #[must_use]
2011 pub fn new() -> Self {
2012 Self::default()
2013 }
2014
2015 #[must_use]
2017 pub fn with_code(mut self, code: &'static str) -> Self {
2018 self.code = Some(code);
2019 self
2020 }
2021
2022 #[must_use]
2024 pub fn with_description(mut self, description: String) -> Self {
2025 self.description = Some(description);
2026 self
2027 }
2028
2029 #[must_use]
2031 pub fn with_details(mut self, details: String) -> Self {
2032 self.details = Some(details);
2033 self
2034 }
2035
2036 #[must_use]
2038 pub fn with_language(mut self, lang: &DataLocale) -> Self {
2039 self.lang = Some(lang.to_string());
2040 self
2041 }
2042
2043 #[must_use]
2045 pub fn code(&self) -> Option<&'static str> {
2046 self.code
2047 }
2048
2049 #[must_use]
2051 pub fn description(&self) -> Option<&str> {
2052 self.description.as_deref()
2053 }
2054
2055 #[must_use]
2057 pub fn details(&self) -> Option<&str> {
2058 self.details.as_deref()
2059 }
2060}
2061
2062#[derive(Serialize)]
2064pub struct NotFoundContext {
2065 method: String,
2066 version: String,
2067 uri: String,
2068}
2069
2070impl NotFoundContext {
2071 #[must_use]
2073 pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
2074 Self {
2075 method: method.to_string(),
2076 version: format!("{version:?}"),
2077 uri: uri.to_string(),
2078 }
2079 }
2080}
2081
2082impl TemplateContext for NotFoundContext {
2083 fn sample<R: Rng>(
2084 _now: DateTime<Utc>,
2085 _rng: &mut R,
2086 _locales: &[DataLocale],
2087 ) -> BTreeMap<SampleIdentifier, Self>
2088 where
2089 Self: Sized,
2090 {
2091 sample_list(vec![
2092 Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
2093 Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
2094 Self::new(
2095 &Method::PUT,
2096 Version::HTTP_10,
2097 &"/foo?bar=baz".parse().unwrap(),
2098 ),
2099 ])
2100 }
2101}