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