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