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