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