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 CompatSsoContext {
866 login: CompatSsoLogin,
867 action: PostAuthAction,
868}
869
870impl TemplateContext for CompatSsoContext {
871 fn sample<R: Rng>(
872 now: chrono::DateTime<Utc>,
873 rng: &mut R,
874 _locales: &[DataLocale],
875 ) -> BTreeMap<SampleIdentifier, Self>
876 where
877 Self: Sized,
878 {
879 let id = Ulid::from_datetime_with_source(now.into(), rng);
880 sample_list(vec![CompatSsoContext::new(CompatSsoLogin {
881 id,
882 redirect_uri: Url::parse("https://app.element.io/").unwrap(),
883 login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
884 created_at: now,
885 state: CompatSsoLoginState::Pending,
886 })])
887 }
888}
889
890impl CompatSsoContext {
891 #[must_use]
893 pub fn new(login: CompatSsoLogin) -> Self
894where {
895 let action = PostAuthAction::continue_compat_sso_login(login.id);
896 Self { login, action }
897 }
898}
899
900#[derive(Serialize)]
902pub struct EmailRecoveryContext {
903 user: User,
904 session: UserRecoverySession,
905 recovery_link: Url,
906}
907
908impl EmailRecoveryContext {
909 #[must_use]
911 pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
912 Self {
913 user,
914 session,
915 recovery_link,
916 }
917 }
918
919 #[must_use]
921 pub fn user(&self) -> &User {
922 &self.user
923 }
924
925 #[must_use]
927 pub fn session(&self) -> &UserRecoverySession {
928 &self.session
929 }
930}
931
932impl TemplateContext for EmailRecoveryContext {
933 fn sample<R: Rng>(
934 now: chrono::DateTime<Utc>,
935 rng: &mut R,
936 _locales: &[DataLocale],
937 ) -> BTreeMap<SampleIdentifier, Self>
938 where
939 Self: Sized,
940 {
941 sample_list(User::samples(now, rng).into_iter().map(|user| {
942 let session = UserRecoverySession {
943 id: Ulid::from_datetime_with_source(now.into(), rng),
944 email: "hello@example.com".to_owned(),
945 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(),
946 ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
947 locale: "en".to_owned(),
948 created_at: now,
949 consumed_at: None,
950 };
951
952 let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
953
954 Self::new(user, session, link)
955 }).collect())
956 }
957}
958
959#[derive(Serialize)]
961pub struct EmailVerificationContext {
962 #[serde(skip_serializing_if = "Option::is_none")]
963 browser_session: Option<BrowserSession>,
964 #[serde(skip_serializing_if = "Option::is_none")]
965 user_registration: Option<UserRegistration>,
966 authentication_code: UserEmailAuthenticationCode,
967}
968
969impl EmailVerificationContext {
970 #[must_use]
972 pub fn new(
973 authentication_code: UserEmailAuthenticationCode,
974 browser_session: Option<BrowserSession>,
975 user_registration: Option<UserRegistration>,
976 ) -> Self {
977 Self {
978 browser_session,
979 user_registration,
980 authentication_code,
981 }
982 }
983
984 #[must_use]
986 pub fn user(&self) -> Option<&User> {
987 self.browser_session.as_ref().map(|s| &s.user)
988 }
989
990 #[must_use]
992 pub fn code(&self) -> &str {
993 &self.authentication_code.code
994 }
995}
996
997impl TemplateContext for EmailVerificationContext {
998 fn sample<R: Rng>(
999 now: chrono::DateTime<Utc>,
1000 rng: &mut R,
1001 _locales: &[DataLocale],
1002 ) -> BTreeMap<SampleIdentifier, Self>
1003 where
1004 Self: Sized,
1005 {
1006 sample_list(
1007 BrowserSession::samples(now, rng)
1008 .into_iter()
1009 .map(|browser_session| {
1010 let authentication_code = UserEmailAuthenticationCode {
1011 id: Ulid::from_datetime_with_source(now.into(), rng),
1012 user_email_authentication_id: Ulid::from_datetime_with_source(
1013 now.into(),
1014 rng,
1015 ),
1016 code: "123456".to_owned(),
1017 created_at: now - Duration::try_minutes(5).unwrap(),
1018 expires_at: now + Duration::try_minutes(25).unwrap(),
1019 };
1020
1021 Self {
1022 browser_session: Some(browser_session),
1023 user_registration: None,
1024 authentication_code,
1025 }
1026 })
1027 .collect(),
1028 )
1029 }
1030}
1031
1032#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1034#[serde(rename_all = "snake_case")]
1035pub enum RegisterStepsVerifyEmailFormField {
1036 Code,
1038}
1039
1040impl FormField for RegisterStepsVerifyEmailFormField {
1041 fn keep(&self) -> bool {
1042 match self {
1043 Self::Code => true,
1044 }
1045 }
1046}
1047
1048#[derive(Serialize)]
1050pub struct RegisterStepsVerifyEmailContext {
1051 form: FormState<RegisterStepsVerifyEmailFormField>,
1052 authentication: UserEmailAuthentication,
1053}
1054
1055impl RegisterStepsVerifyEmailContext {
1056 #[must_use]
1058 pub fn new(authentication: UserEmailAuthentication) -> Self {
1059 Self {
1060 form: FormState::default(),
1061 authentication,
1062 }
1063 }
1064
1065 #[must_use]
1067 pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
1068 Self { form, ..self }
1069 }
1070}
1071
1072impl TemplateContext for RegisterStepsVerifyEmailContext {
1073 fn sample<R: Rng>(
1074 now: chrono::DateTime<Utc>,
1075 rng: &mut R,
1076 _locales: &[DataLocale],
1077 ) -> BTreeMap<SampleIdentifier, Self>
1078 where
1079 Self: Sized,
1080 {
1081 let authentication = UserEmailAuthentication {
1082 id: Ulid::from_datetime_with_source(now.into(), rng),
1083 user_session_id: None,
1084 user_registration_id: None,
1085 email: "foobar@example.com".to_owned(),
1086 created_at: now,
1087 completed_at: None,
1088 };
1089
1090 sample_list(vec![Self {
1091 form: FormState::default(),
1092 authentication,
1093 }])
1094 }
1095}
1096
1097#[derive(Serialize)]
1099pub struct RegisterStepsEmailInUseContext {
1100 email: String,
1101 action: Option<PostAuthAction>,
1102}
1103
1104impl RegisterStepsEmailInUseContext {
1105 #[must_use]
1107 pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
1108 Self { email, action }
1109 }
1110}
1111
1112impl TemplateContext for RegisterStepsEmailInUseContext {
1113 fn sample<R: Rng>(
1114 _now: chrono::DateTime<Utc>,
1115 _rng: &mut R,
1116 _locales: &[DataLocale],
1117 ) -> BTreeMap<SampleIdentifier, Self>
1118 where
1119 Self: Sized,
1120 {
1121 let email = "hello@example.com".to_owned();
1122 let action = PostAuthAction::continue_grant(Ulid::nil());
1123 sample_list(vec![Self::new(email, Some(action))])
1124 }
1125}
1126
1127#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1129#[serde(rename_all = "snake_case")]
1130pub enum RegisterStepsDisplayNameFormField {
1131 DisplayName,
1133}
1134
1135impl FormField for RegisterStepsDisplayNameFormField {
1136 fn keep(&self) -> bool {
1137 match self {
1138 Self::DisplayName => true,
1139 }
1140 }
1141}
1142
1143#[derive(Serialize, Default)]
1145pub struct RegisterStepsDisplayNameContext {
1146 form: FormState<RegisterStepsDisplayNameFormField>,
1147}
1148
1149impl RegisterStepsDisplayNameContext {
1150 #[must_use]
1152 pub fn new() -> Self {
1153 Self::default()
1154 }
1155
1156 #[must_use]
1158 pub fn with_form_state(
1159 mut self,
1160 form_state: FormState<RegisterStepsDisplayNameFormField>,
1161 ) -> Self {
1162 self.form = form_state;
1163 self
1164 }
1165}
1166
1167impl TemplateContext for RegisterStepsDisplayNameContext {
1168 fn sample<R: Rng>(
1169 _now: chrono::DateTime<chrono::Utc>,
1170 _rng: &mut R,
1171 _locales: &[DataLocale],
1172 ) -> BTreeMap<SampleIdentifier, Self>
1173 where
1174 Self: Sized,
1175 {
1176 sample_list(vec![Self {
1177 form: FormState::default(),
1178 }])
1179 }
1180}
1181
1182#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1184#[serde(rename_all = "snake_case")]
1185pub enum RegisterStepsRegistrationTokenFormField {
1186 Token,
1188}
1189
1190impl FormField for RegisterStepsRegistrationTokenFormField {
1191 fn keep(&self) -> bool {
1192 match self {
1193 Self::Token => true,
1194 }
1195 }
1196}
1197
1198#[derive(Serialize, Default)]
1200pub struct RegisterStepsRegistrationTokenContext {
1201 form: FormState<RegisterStepsRegistrationTokenFormField>,
1202}
1203
1204impl RegisterStepsRegistrationTokenContext {
1205 #[must_use]
1207 pub fn new() -> Self {
1208 Self::default()
1209 }
1210
1211 #[must_use]
1213 pub fn with_form_state(
1214 mut self,
1215 form_state: FormState<RegisterStepsRegistrationTokenFormField>,
1216 ) -> Self {
1217 self.form = form_state;
1218 self
1219 }
1220}
1221
1222impl TemplateContext for RegisterStepsRegistrationTokenContext {
1223 fn sample<R: Rng>(
1224 _now: chrono::DateTime<chrono::Utc>,
1225 _rng: &mut R,
1226 _locales: &[DataLocale],
1227 ) -> BTreeMap<SampleIdentifier, Self>
1228 where
1229 Self: Sized,
1230 {
1231 sample_list(vec![Self {
1232 form: FormState::default(),
1233 }])
1234 }
1235}
1236
1237#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1239#[serde(rename_all = "snake_case")]
1240pub enum RecoveryStartFormField {
1241 Email,
1243}
1244
1245impl FormField for RecoveryStartFormField {
1246 fn keep(&self) -> bool {
1247 match self {
1248 Self::Email => true,
1249 }
1250 }
1251}
1252
1253#[derive(Serialize, Default)]
1255pub struct RecoveryStartContext {
1256 form: FormState<RecoveryStartFormField>,
1257}
1258
1259impl RecoveryStartContext {
1260 #[must_use]
1262 pub fn new() -> Self {
1263 Self::default()
1264 }
1265
1266 #[must_use]
1268 pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1269 Self { form }
1270 }
1271}
1272
1273impl TemplateContext for RecoveryStartContext {
1274 fn sample<R: Rng>(
1275 _now: chrono::DateTime<Utc>,
1276 _rng: &mut R,
1277 _locales: &[DataLocale],
1278 ) -> BTreeMap<SampleIdentifier, Self>
1279 where
1280 Self: Sized,
1281 {
1282 sample_list(vec![
1283 Self::new(),
1284 Self::new().with_form_state(
1285 FormState::default()
1286 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1287 ),
1288 Self::new().with_form_state(
1289 FormState::default()
1290 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1291 ),
1292 ])
1293 }
1294}
1295
1296#[derive(Serialize)]
1298pub struct RecoveryProgressContext {
1299 session: UserRecoverySession,
1300 resend_failed_due_to_rate_limit: bool,
1302}
1303
1304impl RecoveryProgressContext {
1305 #[must_use]
1307 pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1308 Self {
1309 session,
1310 resend_failed_due_to_rate_limit,
1311 }
1312 }
1313}
1314
1315impl TemplateContext for RecoveryProgressContext {
1316 fn sample<R: Rng>(
1317 now: chrono::DateTime<Utc>,
1318 rng: &mut R,
1319 _locales: &[DataLocale],
1320 ) -> BTreeMap<SampleIdentifier, Self>
1321 where
1322 Self: Sized,
1323 {
1324 let session = UserRecoverySession {
1325 id: Ulid::from_datetime_with_source(now.into(), rng),
1326 email: "name@mail.com".to_owned(),
1327 user_agent: "Mozilla/5.0".to_owned(),
1328 ip_address: None,
1329 locale: "en".to_owned(),
1330 created_at: now,
1331 consumed_at: None,
1332 };
1333
1334 sample_list(vec![
1335 Self {
1336 session: session.clone(),
1337 resend_failed_due_to_rate_limit: false,
1338 },
1339 Self {
1340 session,
1341 resend_failed_due_to_rate_limit: true,
1342 },
1343 ])
1344 }
1345}
1346
1347#[derive(Serialize)]
1349pub struct RecoveryExpiredContext {
1350 session: UserRecoverySession,
1351}
1352
1353impl RecoveryExpiredContext {
1354 #[must_use]
1356 pub fn new(session: UserRecoverySession) -> Self {
1357 Self { session }
1358 }
1359}
1360
1361impl TemplateContext for RecoveryExpiredContext {
1362 fn sample<R: Rng>(
1363 now: chrono::DateTime<Utc>,
1364 rng: &mut R,
1365 _locales: &[DataLocale],
1366 ) -> BTreeMap<SampleIdentifier, Self>
1367 where
1368 Self: Sized,
1369 {
1370 let session = UserRecoverySession {
1371 id: Ulid::from_datetime_with_source(now.into(), rng),
1372 email: "name@mail.com".to_owned(),
1373 user_agent: "Mozilla/5.0".to_owned(),
1374 ip_address: None,
1375 locale: "en".to_owned(),
1376 created_at: now,
1377 consumed_at: None,
1378 };
1379
1380 sample_list(vec![Self { session }])
1381 }
1382}
1383#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1385#[serde(rename_all = "snake_case")]
1386pub enum RecoveryFinishFormField {
1387 NewPassword,
1389
1390 NewPasswordConfirm,
1392}
1393
1394impl FormField for RecoveryFinishFormField {
1395 fn keep(&self) -> bool {
1396 false
1397 }
1398}
1399
1400#[derive(Serialize)]
1402pub struct RecoveryFinishContext {
1403 user: User,
1404 form: FormState<RecoveryFinishFormField>,
1405}
1406
1407impl RecoveryFinishContext {
1408 #[must_use]
1410 pub fn new(user: User) -> Self {
1411 Self {
1412 user,
1413 form: FormState::default(),
1414 }
1415 }
1416
1417 #[must_use]
1419 pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1420 self.form = form;
1421 self
1422 }
1423}
1424
1425impl TemplateContext for RecoveryFinishContext {
1426 fn sample<R: Rng>(
1427 now: chrono::DateTime<Utc>,
1428 rng: &mut R,
1429 _locales: &[DataLocale],
1430 ) -> BTreeMap<SampleIdentifier, Self>
1431 where
1432 Self: Sized,
1433 {
1434 sample_list(
1435 User::samples(now, rng)
1436 .into_iter()
1437 .flat_map(|user| {
1438 vec![
1439 Self::new(user.clone()),
1440 Self::new(user.clone()).with_form_state(
1441 FormState::default().with_error_on_field(
1442 RecoveryFinishFormField::NewPassword,
1443 FieldError::Invalid,
1444 ),
1445 ),
1446 Self::new(user.clone()).with_form_state(
1447 FormState::default().with_error_on_field(
1448 RecoveryFinishFormField::NewPasswordConfirm,
1449 FieldError::Invalid,
1450 ),
1451 ),
1452 ]
1453 })
1454 .collect(),
1455 )
1456 }
1457}
1458
1459#[derive(Serialize)]
1462pub struct UpstreamExistingLinkContext {
1463 linked_user: User,
1464}
1465
1466impl UpstreamExistingLinkContext {
1467 #[must_use]
1469 pub fn new(linked_user: User) -> Self {
1470 Self { linked_user }
1471 }
1472}
1473
1474impl TemplateContext for UpstreamExistingLinkContext {
1475 fn sample<R: Rng>(
1476 now: chrono::DateTime<Utc>,
1477 rng: &mut R,
1478 _locales: &[DataLocale],
1479 ) -> BTreeMap<SampleIdentifier, Self>
1480 where
1481 Self: Sized,
1482 {
1483 sample_list(
1484 User::samples(now, rng)
1485 .into_iter()
1486 .map(|linked_user| Self { linked_user })
1487 .collect(),
1488 )
1489 }
1490}
1491
1492#[derive(Serialize)]
1495pub struct UpstreamSuggestLink {
1496 post_logout_action: PostAuthAction,
1497}
1498
1499impl UpstreamSuggestLink {
1500 #[must_use]
1502 pub fn new(link: &UpstreamOAuthLink) -> Self {
1503 Self::for_link_id(link.id)
1504 }
1505
1506 fn for_link_id(id: Ulid) -> Self {
1507 let post_logout_action = PostAuthAction::link_upstream(id);
1508 Self { post_logout_action }
1509 }
1510}
1511
1512impl TemplateContext for UpstreamSuggestLink {
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 let id = Ulid::from_datetime_with_source(now.into(), rng);
1522 sample_list(vec![Self::for_link_id(id)])
1523 }
1524}
1525
1526#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1528#[serde(rename_all = "snake_case")]
1529pub enum UpstreamRegisterFormField {
1530 Username,
1532
1533 AcceptTerms,
1535}
1536
1537impl FormField for UpstreamRegisterFormField {
1538 fn keep(&self) -> bool {
1539 match self {
1540 Self::Username | Self::AcceptTerms => true,
1541 }
1542 }
1543}
1544
1545#[derive(Serialize)]
1548pub struct UpstreamRegister {
1549 upstream_oauth_link: UpstreamOAuthLink,
1550 upstream_oauth_provider: UpstreamOAuthProvider,
1551 imported_localpart: Option<String>,
1552 force_localpart: bool,
1553 imported_display_name: Option<String>,
1554 force_display_name: bool,
1555 imported_email: Option<String>,
1556 force_email: bool,
1557 form_state: FormState<UpstreamRegisterFormField>,
1558}
1559
1560impl UpstreamRegister {
1561 #[must_use]
1564 pub fn new(
1565 upstream_oauth_link: UpstreamOAuthLink,
1566 upstream_oauth_provider: UpstreamOAuthProvider,
1567 ) -> Self {
1568 Self {
1569 upstream_oauth_link,
1570 upstream_oauth_provider,
1571 imported_localpart: None,
1572 force_localpart: false,
1573 imported_display_name: None,
1574 force_display_name: false,
1575 imported_email: None,
1576 force_email: false,
1577 form_state: FormState::default(),
1578 }
1579 }
1580
1581 pub fn set_localpart(&mut self, localpart: String, force: bool) {
1583 self.imported_localpart = Some(localpart);
1584 self.force_localpart = force;
1585 }
1586
1587 #[must_use]
1589 pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1590 Self {
1591 imported_localpart: Some(localpart),
1592 force_localpart: force,
1593 ..self
1594 }
1595 }
1596
1597 pub fn set_display_name(&mut self, display_name: String, force: bool) {
1599 self.imported_display_name = Some(display_name);
1600 self.force_display_name = force;
1601 }
1602
1603 #[must_use]
1605 pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1606 Self {
1607 imported_display_name: Some(display_name),
1608 force_display_name: force,
1609 ..self
1610 }
1611 }
1612
1613 pub fn set_email(&mut self, email: String, force: bool) {
1615 self.imported_email = Some(email);
1616 self.force_email = force;
1617 }
1618
1619 #[must_use]
1621 pub fn with_email(self, email: String, force: bool) -> Self {
1622 Self {
1623 imported_email: Some(email),
1624 force_email: force,
1625 ..self
1626 }
1627 }
1628
1629 pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1631 self.form_state = form_state;
1632 }
1633
1634 #[must_use]
1636 pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1637 Self { form_state, ..self }
1638 }
1639}
1640
1641impl TemplateContext for UpstreamRegister {
1642 fn sample<R: Rng>(
1643 now: chrono::DateTime<Utc>,
1644 _rng: &mut R,
1645 _locales: &[DataLocale],
1646 ) -> BTreeMap<SampleIdentifier, Self>
1647 where
1648 Self: Sized,
1649 {
1650 sample_list(vec![Self::new(
1651 UpstreamOAuthLink {
1652 id: Ulid::nil(),
1653 provider_id: Ulid::nil(),
1654 user_id: None,
1655 subject: "subject".to_owned(),
1656 human_account_name: Some("@john".to_owned()),
1657 created_at: now,
1658 },
1659 UpstreamOAuthProvider {
1660 id: Ulid::nil(),
1661 issuer: Some("https://example.com/".to_owned()),
1662 human_name: Some("Example Ltd.".to_owned()),
1663 brand_name: None,
1664 scope: Scope::from_iter([OPENID]),
1665 token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1666 token_endpoint_signing_alg: None,
1667 id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1668 client_id: "client-id".to_owned(),
1669 encrypted_client_secret: None,
1670 claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1671 authorization_endpoint_override: None,
1672 token_endpoint_override: None,
1673 jwks_uri_override: None,
1674 userinfo_endpoint_override: None,
1675 fetch_userinfo: false,
1676 userinfo_signed_response_alg: None,
1677 discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1678 pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1679 response_mode: None,
1680 additional_authorization_parameters: Vec::new(),
1681 forward_login_hint: false,
1682 created_at: now,
1683 disabled_at: None,
1684 on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
1685 },
1686 )])
1687 }
1688}
1689
1690#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1692#[serde(rename_all = "snake_case")]
1693pub enum DeviceLinkFormField {
1694 Code,
1696}
1697
1698impl FormField for DeviceLinkFormField {
1699 fn keep(&self) -> bool {
1700 match self {
1701 Self::Code => true,
1702 }
1703 }
1704}
1705
1706#[derive(Serialize, Default, Debug)]
1708pub struct DeviceLinkContext {
1709 form_state: FormState<DeviceLinkFormField>,
1710}
1711
1712impl DeviceLinkContext {
1713 #[must_use]
1715 pub fn new() -> Self {
1716 Self::default()
1717 }
1718
1719 #[must_use]
1721 pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1722 self.form_state = form_state;
1723 self
1724 }
1725}
1726
1727impl TemplateContext for DeviceLinkContext {
1728 fn sample<R: Rng>(
1729 _now: chrono::DateTime<Utc>,
1730 _rng: &mut R,
1731 _locales: &[DataLocale],
1732 ) -> BTreeMap<SampleIdentifier, Self>
1733 where
1734 Self: Sized,
1735 {
1736 sample_list(vec![
1737 Self::new(),
1738 Self::new().with_form_state(
1739 FormState::default()
1740 .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1741 ),
1742 ])
1743 }
1744}
1745
1746#[derive(Serialize, Debug)]
1748pub struct DeviceConsentContext {
1749 grant: DeviceCodeGrant,
1750 client: Client,
1751}
1752
1753impl DeviceConsentContext {
1754 #[must_use]
1756 pub fn new(grant: DeviceCodeGrant, client: Client) -> Self {
1757 Self { grant, client }
1758 }
1759}
1760
1761impl TemplateContext for DeviceConsentContext {
1762 fn sample<R: Rng>(
1763 now: chrono::DateTime<Utc>,
1764 rng: &mut R,
1765 _locales: &[DataLocale],
1766 ) -> BTreeMap<SampleIdentifier, Self>
1767 where
1768 Self: Sized,
1769 {
1770 sample_list(Client::samples(now, rng)
1771 .into_iter()
1772 .map(|client| {
1773 let grant = DeviceCodeGrant {
1774 id: Ulid::from_datetime_with_source(now.into(), rng),
1775 state: mas_data_model::DeviceCodeGrantState::Pending,
1776 client_id: client.id,
1777 scope: [OPENID].into_iter().collect(),
1778 user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
1779 device_code: Alphanumeric.sample_string(rng, 32),
1780 created_at: now - Duration::try_minutes(5).unwrap(),
1781 expires_at: now + Duration::try_minutes(25).unwrap(),
1782 ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
1783 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()),
1784 };
1785 Self { grant, client }
1786 })
1787 .collect())
1788 }
1789}
1790
1791#[derive(Serialize)]
1794pub struct AccountInactiveContext {
1795 user: User,
1796}
1797
1798impl AccountInactiveContext {
1799 #[must_use]
1801 pub fn new(user: User) -> Self {
1802 Self { user }
1803 }
1804}
1805
1806impl TemplateContext for AccountInactiveContext {
1807 fn sample<R: Rng>(
1808 now: chrono::DateTime<Utc>,
1809 rng: &mut R,
1810 _locales: &[DataLocale],
1811 ) -> BTreeMap<SampleIdentifier, Self>
1812 where
1813 Self: Sized,
1814 {
1815 sample_list(
1816 User::samples(now, rng)
1817 .into_iter()
1818 .map(|user| AccountInactiveContext { user })
1819 .collect(),
1820 )
1821 }
1822}
1823
1824#[derive(Serialize)]
1826pub struct DeviceNameContext {
1827 client: Client,
1828 raw_user_agent: String,
1829}
1830
1831impl DeviceNameContext {
1832 #[must_use]
1834 pub fn new(client: Client, user_agent: Option<String>) -> Self {
1835 Self {
1836 client,
1837 raw_user_agent: user_agent.unwrap_or_default(),
1838 }
1839 }
1840}
1841
1842impl TemplateContext for DeviceNameContext {
1843 fn sample<R: Rng>(
1844 now: chrono::DateTime<Utc>,
1845 rng: &mut R,
1846 _locales: &[DataLocale],
1847 ) -> BTreeMap<SampleIdentifier, Self>
1848 where
1849 Self: Sized,
1850 {
1851 sample_list(Client::samples(now, rng)
1852 .into_iter()
1853 .map(|client| DeviceNameContext {
1854 client,
1855 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(),
1856 })
1857 .collect())
1858 }
1859}
1860
1861#[derive(Serialize)]
1863pub struct FormPostContext<T> {
1864 redirect_uri: Option<Url>,
1865 params: T,
1866}
1867
1868impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
1869 fn sample<R: Rng>(
1870 now: chrono::DateTime<Utc>,
1871 rng: &mut R,
1872 locales: &[DataLocale],
1873 ) -> BTreeMap<SampleIdentifier, Self>
1874 where
1875 Self: Sized,
1876 {
1877 let sample_params = T::sample(now, rng, locales);
1878 sample_params
1879 .into_iter()
1880 .map(|(k, params)| {
1881 (
1882 k,
1883 FormPostContext {
1884 redirect_uri: "https://example.com/callback".parse().ok(),
1885 params,
1886 },
1887 )
1888 })
1889 .collect()
1890 }
1891}
1892
1893impl<T> FormPostContext<T> {
1894 pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
1897 Self {
1898 redirect_uri: Some(redirect_uri),
1899 params,
1900 }
1901 }
1902
1903 pub fn new_for_current_url(params: T) -> Self {
1906 Self {
1907 redirect_uri: None,
1908 params,
1909 }
1910 }
1911
1912 pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
1917 WithLanguage {
1918 lang: lang.to_string(),
1919 inner: self,
1920 }
1921 }
1922}
1923
1924#[derive(Default, Serialize, Debug, Clone)]
1926pub struct ErrorContext {
1927 code: Option<&'static str>,
1928 description: Option<String>,
1929 details: Option<String>,
1930 lang: Option<String>,
1931}
1932
1933impl std::fmt::Display for ErrorContext {
1934 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1935 if let Some(code) = &self.code {
1936 writeln!(f, "code: {code}")?;
1937 }
1938 if let Some(description) = &self.description {
1939 writeln!(f, "{description}")?;
1940 }
1941
1942 if let Some(details) = &self.details {
1943 writeln!(f, "details: {details}")?;
1944 }
1945
1946 Ok(())
1947 }
1948}
1949
1950impl TemplateContext for ErrorContext {
1951 fn sample<R: Rng>(
1952 _now: chrono::DateTime<Utc>,
1953 _rng: &mut R,
1954 _locales: &[DataLocale],
1955 ) -> BTreeMap<SampleIdentifier, Self>
1956 where
1957 Self: Sized,
1958 {
1959 sample_list(vec![
1960 Self::new()
1961 .with_code("sample_error")
1962 .with_description("A fancy description".into())
1963 .with_details("Something happened".into()),
1964 Self::new().with_code("another_error"),
1965 Self::new(),
1966 ])
1967 }
1968}
1969
1970impl ErrorContext {
1971 #[must_use]
1973 pub fn new() -> Self {
1974 Self::default()
1975 }
1976
1977 #[must_use]
1979 pub fn with_code(mut self, code: &'static str) -> Self {
1980 self.code = Some(code);
1981 self
1982 }
1983
1984 #[must_use]
1986 pub fn with_description(mut self, description: String) -> Self {
1987 self.description = Some(description);
1988 self
1989 }
1990
1991 #[must_use]
1993 pub fn with_details(mut self, details: String) -> Self {
1994 self.details = Some(details);
1995 self
1996 }
1997
1998 #[must_use]
2000 pub fn with_language(mut self, lang: &DataLocale) -> Self {
2001 self.lang = Some(lang.to_string());
2002 self
2003 }
2004
2005 #[must_use]
2007 pub fn code(&self) -> Option<&'static str> {
2008 self.code
2009 }
2010
2011 #[must_use]
2013 pub fn description(&self) -> Option<&str> {
2014 self.description.as_deref()
2015 }
2016
2017 #[must_use]
2019 pub fn details(&self) -> Option<&str> {
2020 self.details.as_deref()
2021 }
2022}
2023
2024#[derive(Serialize)]
2026pub struct NotFoundContext {
2027 method: String,
2028 version: String,
2029 uri: String,
2030}
2031
2032impl NotFoundContext {
2033 #[must_use]
2035 pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
2036 Self {
2037 method: method.to_string(),
2038 version: format!("{version:?}"),
2039 uri: uri.to_string(),
2040 }
2041 }
2042}
2043
2044impl TemplateContext for NotFoundContext {
2045 fn sample<R: Rng>(
2046 _now: DateTime<Utc>,
2047 _rng: &mut R,
2048 _locales: &[DataLocale],
2049 ) -> BTreeMap<SampleIdentifier, Self>
2050 where
2051 Self: Sized,
2052 {
2053 sample_list(vec![
2054 Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
2055 Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
2056 Self::new(
2057 &Method::PUT,
2058 Version::HTTP_10,
2059 &"/foo?bar=baz".parse().unwrap(),
2060 ),
2061 ])
2062 }
2063}