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