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