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