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