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.clone(),
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 authorization_grant_admin_scope =
855 PolicyViolationContext::for_authorization_grant(
856 grant.clone(),
857 client.clone(),
858 vec![Violation {
859 msg: "scope 'urn:mas:admin' requires admin privileges".to_owned(),
860 redirect_uri: None,
861 field: None,
862 variant: Some(ViolationVariant::AdminScopeNotAllowed),
863 }],
864 );
865
866 let authorization_grant_client_not_allowed =
867 PolicyViolationContext::for_authorization_grant(
868 grant,
869 client.clone(),
870 vec![Violation {
871 msg: "This client is not allowed to login".to_owned(),
872 redirect_uri: None,
873 field: None,
874 variant: Some(ViolationVariant::ClientNotAllowed),
875 }],
876 );
877
878 let device_code_grant = PolicyViolationContext::for_device_code_grant(
879 DeviceCodeGrant {
880 id: Ulid::from_datetime_with_rng(now, rng),
881 state: mas_data_model::DeviceCodeGrantState::Pending,
882 client_id: client.id,
883 scope: [OPENID].into_iter().collect(),
884 user_code: mas_data_model::generate_user_code(rng),
885 device_code: Alphanumeric.sample_string(rng, 32),
886 created_at: now - Duration::try_minutes(5).unwrap(),
887 expires_at: now + Duration::try_minutes(25).unwrap(),
888 ip_address: None,
889 user_agent: None,
890 locale: None,
891 },
892 client.clone(),
893 Vec::new(),
894 );
895
896 let device_code_grant_invalid_scope =
897 PolicyViolationContext::for_device_code_grant(
898 DeviceCodeGrant {
899 id: Ulid::from_datetime_with_rng(now, rng),
900 state: mas_data_model::DeviceCodeGrantState::Pending,
901 client_id: client.id,
902 scope: [OPENID].into_iter().collect(),
903 user_code: mas_data_model::generate_user_code(rng),
904 device_code: Alphanumeric.sample_string(rng, 32),
905 created_at: now - Duration::try_minutes(5).unwrap(),
906 expires_at: now + Duration::try_minutes(25).unwrap(),
907 ip_address: None,
908 user_agent: None,
909 locale: None,
910 },
911 client,
912 vec![Violation {
913 msg: "user has too many active sessions".to_owned(),
914 redirect_uri: None,
915 field: None,
916 variant: Some(ViolationVariant::TooManySessions {
917 need_to_remove: 1,
918 }),
919 }],
920 );
921
922 [
923 authorization_grant,
924 authorization_grant_invalid_scope,
925 authorization_grant_admin_scope,
926 authorization_grant_client_not_allowed,
927 device_code_grant,
928 device_code_grant_invalid_scope,
929 ]
930 })
931 .collect(),
932 )
933 }
934}
935
936impl PolicyViolationContext {
937 #[must_use]
940 pub const fn for_authorization_grant(
941 grant: AuthorizationGrant,
942 client: Client,
943 violations: Vec<Violation>,
944 ) -> Self {
945 let action = PostAuthAction::continue_grant(grant.id);
946 Self {
947 grant: PolicyViolationGrant::Authorization(grant),
948 client,
949 action,
950 violations,
951 }
952 }
953
954 #[must_use]
957 pub const fn for_device_code_grant(
958 grant: DeviceCodeGrant,
959 client: Client,
960 violations: Vec<Violation>,
961 ) -> Self {
962 let action = PostAuthAction::continue_device_code_grant(grant.id);
963 Self {
964 grant: PolicyViolationGrant::DeviceCode(grant),
965 client,
966 action,
967 violations,
968 }
969 }
970}
971
972#[derive(Serialize)]
974pub struct CompatLoginPolicyViolationContext {
975 violations: Vec<Violation>,
976}
977
978impl TemplateContext for CompatLoginPolicyViolationContext {
979 fn sample<R: Rng>(
980 _now: chrono::DateTime<Utc>,
981 _rng: &mut R,
982 _locales: &[DataLocale],
983 ) -> BTreeMap<SampleIdentifier, Self>
984 where
985 Self: Sized,
986 {
987 sample_list(vec![
988 CompatLoginPolicyViolationContext { violations: vec![] },
989 CompatLoginPolicyViolationContext {
990 violations: vec![Violation {
991 msg: "scope 'foo' not allowed".to_owned(),
992 redirect_uri: None,
993 field: None,
994 variant: None,
995 }],
996 },
997 CompatLoginPolicyViolationContext {
998 violations: vec![Violation {
999 msg: "user has too many active sessions".to_owned(),
1000 redirect_uri: None,
1001 field: None,
1002 variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1003 }],
1004 },
1005 CompatLoginPolicyViolationContext {
1006 violations: vec![Violation {
1007 msg: "This client is not allowed to login".to_owned(),
1008 redirect_uri: None,
1009 field: None,
1010 variant: Some(ViolationVariant::ClientNotAllowed),
1011 }],
1012 },
1013 CompatLoginPolicyViolationContext {
1014 violations: vec![
1015 Violation {
1016 msg: "scope 'foo' not allowed".to_owned(),
1017 redirect_uri: None,
1018 field: None,
1019 variant: None,
1020 },
1021 Violation {
1022 msg: "user has too many active sessions".to_owned(),
1023 redirect_uri: None,
1024 field: None,
1025 variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1026 },
1027 ],
1028 },
1029 ])
1030 }
1031}
1032
1033impl CompatLoginPolicyViolationContext {
1034 #[must_use]
1037 pub const fn for_violations(violations: Vec<Violation>) -> Self {
1038 Self { violations }
1039 }
1040}
1041
1042#[derive(Serialize)]
1044pub struct CompatSsoContext {
1045 login: CompatSsoLogin,
1046 action: PostAuthAction,
1047 matrix_user: MatrixUser,
1048}
1049
1050impl TemplateContext for CompatSsoContext {
1051 fn sample<R: Rng>(
1052 now: chrono::DateTime<Utc>,
1053 rng: &mut R,
1054 _locales: &[DataLocale],
1055 ) -> BTreeMap<SampleIdentifier, Self>
1056 where
1057 Self: Sized,
1058 {
1059 let id = Ulid::from_datetime_with_rng(now, rng);
1060 sample_list(vec![CompatSsoContext::new(
1061 CompatSsoLogin {
1062 id,
1063 redirect_uri: Url::parse("https://app.element.io/").unwrap(),
1064 login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
1065 created_at: now,
1066 state: CompatSsoLoginState::Pending,
1067 },
1068 MatrixUser {
1069 mxid: "@alice:example.com".to_owned(),
1070 display_name: Some("Alice".to_owned()),
1071 },
1072 )])
1073 }
1074}
1075
1076impl CompatSsoContext {
1077 #[must_use]
1079 pub fn new(login: CompatSsoLogin, matrix_user: MatrixUser) -> Self
1080where {
1081 let action = PostAuthAction::continue_compat_sso_login(login.id);
1082 Self {
1083 login,
1084 action,
1085 matrix_user,
1086 }
1087 }
1088}
1089
1090#[derive(Serialize)]
1092pub struct EmailRecoveryContext {
1093 user: User,
1094 session: UserRecoverySession,
1095 recovery_link: Url,
1096}
1097
1098impl EmailRecoveryContext {
1099 #[must_use]
1101 pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
1102 Self {
1103 user,
1104 session,
1105 recovery_link,
1106 }
1107 }
1108
1109 #[must_use]
1111 pub fn user(&self) -> &User {
1112 &self.user
1113 }
1114
1115 #[must_use]
1117 pub fn session(&self) -> &UserRecoverySession {
1118 &self.session
1119 }
1120}
1121
1122impl TemplateContext for EmailRecoveryContext {
1123 fn sample<R: Rng>(
1124 now: chrono::DateTime<Utc>,
1125 rng: &mut R,
1126 _locales: &[DataLocale],
1127 ) -> BTreeMap<SampleIdentifier, Self>
1128 where
1129 Self: Sized,
1130 {
1131 sample_list(User::samples(now, rng).into_iter().map(|user| {
1132 let session = UserRecoverySession {
1133 id: Ulid::from_datetime_with_rng(now, rng),
1134 email: "hello@example.com".to_owned(),
1135 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(),
1136 ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
1137 locale: "en".to_owned(),
1138 created_at: now,
1139 consumed_at: None,
1140 };
1141
1142 let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
1143
1144 Self::new(user, session, link)
1145 }).collect())
1146 }
1147}
1148
1149#[derive(Serialize)]
1151pub struct EmailVerificationContext {
1152 #[serde(skip_serializing_if = "Option::is_none")]
1153 browser_session: Option<BrowserSession>,
1154 #[serde(skip_serializing_if = "Option::is_none")]
1155 user_registration: Option<UserRegistration>,
1156 authentication_code: UserEmailAuthenticationCode,
1157}
1158
1159impl EmailVerificationContext {
1160 #[must_use]
1162 pub fn new(
1163 authentication_code: UserEmailAuthenticationCode,
1164 browser_session: Option<BrowserSession>,
1165 user_registration: Option<UserRegistration>,
1166 ) -> Self {
1167 Self {
1168 browser_session,
1169 user_registration,
1170 authentication_code,
1171 }
1172 }
1173
1174 #[must_use]
1176 pub fn user(&self) -> Option<&User> {
1177 self.browser_session.as_ref().map(|s| &s.user)
1178 }
1179
1180 #[must_use]
1182 pub fn code(&self) -> &str {
1183 &self.authentication_code.code
1184 }
1185}
1186
1187impl TemplateContext for EmailVerificationContext {
1188 fn sample<R: Rng>(
1189 now: chrono::DateTime<Utc>,
1190 rng: &mut R,
1191 _locales: &[DataLocale],
1192 ) -> BTreeMap<SampleIdentifier, Self>
1193 where
1194 Self: Sized,
1195 {
1196 sample_list(
1197 BrowserSession::samples(now, rng)
1198 .into_iter()
1199 .map(|browser_session| {
1200 let authentication_code = UserEmailAuthenticationCode {
1201 id: Ulid::from_datetime_with_rng(now, rng),
1202 user_email_authentication_id: Ulid::from_datetime_with_rng(now, rng),
1203 code: "123456".to_owned(),
1204 created_at: now - Duration::try_minutes(5).unwrap(),
1205 expires_at: now + Duration::try_minutes(25).unwrap(),
1206 };
1207
1208 Self {
1209 browser_session: Some(browser_session),
1210 user_registration: None,
1211 authentication_code,
1212 }
1213 })
1214 .collect(),
1215 )
1216 }
1217}
1218
1219#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1221#[serde(rename_all = "snake_case")]
1222pub enum RegisterStepsVerifyEmailFormField {
1223 Code,
1225}
1226
1227impl FormField for RegisterStepsVerifyEmailFormField {
1228 fn keep(&self) -> bool {
1229 match self {
1230 Self::Code => true,
1231 }
1232 }
1233}
1234
1235#[derive(Serialize)]
1237pub struct RegisterStepsVerifyEmailContext {
1238 form: FormState<RegisterStepsVerifyEmailFormField>,
1239 authentication: UserEmailAuthentication,
1240}
1241
1242impl RegisterStepsVerifyEmailContext {
1243 #[must_use]
1245 pub fn new(authentication: UserEmailAuthentication) -> Self {
1246 Self {
1247 form: FormState::default(),
1248 authentication,
1249 }
1250 }
1251
1252 #[must_use]
1254 pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
1255 Self { form, ..self }
1256 }
1257}
1258
1259impl TemplateContext for RegisterStepsVerifyEmailContext {
1260 fn sample<R: Rng>(
1261 now: chrono::DateTime<Utc>,
1262 rng: &mut R,
1263 _locales: &[DataLocale],
1264 ) -> BTreeMap<SampleIdentifier, Self>
1265 where
1266 Self: Sized,
1267 {
1268 let authentication = UserEmailAuthentication {
1269 id: Ulid::from_datetime_with_rng(now, rng),
1270 user_session_id: None,
1271 user_registration_id: None,
1272 email: "foobar@example.com".to_owned(),
1273 created_at: now,
1274 completed_at: None,
1275 };
1276
1277 sample_list(vec![Self {
1278 form: FormState::default(),
1279 authentication,
1280 }])
1281 }
1282}
1283
1284#[derive(Serialize)]
1286pub struct RegisterStepsEmailInUseContext {
1287 email: String,
1288 action: Option<PostAuthAction>,
1289}
1290
1291impl RegisterStepsEmailInUseContext {
1292 #[must_use]
1294 pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
1295 Self { email, action }
1296 }
1297}
1298
1299impl TemplateContext for RegisterStepsEmailInUseContext {
1300 fn sample<R: Rng>(
1301 _now: chrono::DateTime<Utc>,
1302 _rng: &mut R,
1303 _locales: &[DataLocale],
1304 ) -> BTreeMap<SampleIdentifier, Self>
1305 where
1306 Self: Sized,
1307 {
1308 let email = "hello@example.com".to_owned();
1309 let action = PostAuthAction::continue_grant(Ulid::nil());
1310 sample_list(vec![Self::new(email, Some(action))])
1311 }
1312}
1313
1314#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1316#[serde(rename_all = "snake_case")]
1317pub enum RegisterStepsDisplayNameFormField {
1318 DisplayName,
1320}
1321
1322impl FormField for RegisterStepsDisplayNameFormField {
1323 fn keep(&self) -> bool {
1324 match self {
1325 Self::DisplayName => true,
1326 }
1327 }
1328}
1329
1330#[derive(Serialize, Default)]
1332pub struct RegisterStepsDisplayNameContext {
1333 form: FormState<RegisterStepsDisplayNameFormField>,
1334}
1335
1336impl RegisterStepsDisplayNameContext {
1337 #[must_use]
1339 pub fn new() -> Self {
1340 Self::default()
1341 }
1342
1343 #[must_use]
1345 pub fn with_form_state(
1346 mut self,
1347 form_state: FormState<RegisterStepsDisplayNameFormField>,
1348 ) -> Self {
1349 self.form = form_state;
1350 self
1351 }
1352}
1353
1354impl TemplateContext for RegisterStepsDisplayNameContext {
1355 fn sample<R: Rng>(
1356 _now: chrono::DateTime<chrono::Utc>,
1357 _rng: &mut R,
1358 _locales: &[DataLocale],
1359 ) -> BTreeMap<SampleIdentifier, Self>
1360 where
1361 Self: Sized,
1362 {
1363 sample_list(vec![Self {
1364 form: FormState::default(),
1365 }])
1366 }
1367}
1368
1369#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1371#[serde(rename_all = "snake_case")]
1372pub enum RegisterStepsRegistrationTokenFormField {
1373 Token,
1375}
1376
1377impl FormField for RegisterStepsRegistrationTokenFormField {
1378 fn keep(&self) -> bool {
1379 match self {
1380 Self::Token => true,
1381 }
1382 }
1383}
1384
1385#[derive(Serialize, Default)]
1387pub struct RegisterStepsRegistrationTokenContext {
1388 form: FormState<RegisterStepsRegistrationTokenFormField>,
1389}
1390
1391impl RegisterStepsRegistrationTokenContext {
1392 #[must_use]
1394 pub fn new() -> Self {
1395 Self::default()
1396 }
1397
1398 #[must_use]
1400 pub fn with_form_state(
1401 mut self,
1402 form_state: FormState<RegisterStepsRegistrationTokenFormField>,
1403 ) -> Self {
1404 self.form = form_state;
1405 self
1406 }
1407}
1408
1409impl TemplateContext for RegisterStepsRegistrationTokenContext {
1410 fn sample<R: Rng>(
1411 _now: chrono::DateTime<chrono::Utc>,
1412 _rng: &mut R,
1413 _locales: &[DataLocale],
1414 ) -> BTreeMap<SampleIdentifier, Self>
1415 where
1416 Self: Sized,
1417 {
1418 sample_list(vec![Self {
1419 form: FormState::default(),
1420 }])
1421 }
1422}
1423
1424#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1426#[serde(rename_all = "snake_case")]
1427pub enum RecoveryStartFormField {
1428 Email,
1430}
1431
1432impl FormField for RecoveryStartFormField {
1433 fn keep(&self) -> bool {
1434 match self {
1435 Self::Email => true,
1436 }
1437 }
1438}
1439
1440#[derive(Serialize, Default)]
1442pub struct RecoveryStartContext {
1443 form: FormState<RecoveryStartFormField>,
1444}
1445
1446impl RecoveryStartContext {
1447 #[must_use]
1449 pub fn new() -> Self {
1450 Self::default()
1451 }
1452
1453 #[must_use]
1455 pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1456 Self { form }
1457 }
1458}
1459
1460impl TemplateContext for RecoveryStartContext {
1461 fn sample<R: Rng>(
1462 _now: chrono::DateTime<Utc>,
1463 _rng: &mut R,
1464 _locales: &[DataLocale],
1465 ) -> BTreeMap<SampleIdentifier, Self>
1466 where
1467 Self: Sized,
1468 {
1469 sample_list(vec![
1470 Self::new(),
1471 Self::new().with_form_state(
1472 FormState::default()
1473 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1474 ),
1475 Self::new().with_form_state(
1476 FormState::default()
1477 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1478 ),
1479 ])
1480 }
1481}
1482
1483#[derive(Serialize)]
1485pub struct RecoveryProgressContext {
1486 session: UserRecoverySession,
1487 resend_failed_due_to_rate_limit: bool,
1489}
1490
1491impl RecoveryProgressContext {
1492 #[must_use]
1494 pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1495 Self {
1496 session,
1497 resend_failed_due_to_rate_limit,
1498 }
1499 }
1500}
1501
1502impl TemplateContext for RecoveryProgressContext {
1503 fn sample<R: Rng>(
1504 now: chrono::DateTime<Utc>,
1505 rng: &mut R,
1506 _locales: &[DataLocale],
1507 ) -> BTreeMap<SampleIdentifier, Self>
1508 where
1509 Self: Sized,
1510 {
1511 let session = UserRecoverySession {
1512 id: Ulid::from_datetime_with_rng(now, rng),
1513 email: "name@mail.com".to_owned(),
1514 user_agent: "Mozilla/5.0".to_owned(),
1515 ip_address: None,
1516 locale: "en".to_owned(),
1517 created_at: now,
1518 consumed_at: None,
1519 };
1520
1521 sample_list(vec![
1522 Self {
1523 session: session.clone(),
1524 resend_failed_due_to_rate_limit: false,
1525 },
1526 Self {
1527 session,
1528 resend_failed_due_to_rate_limit: true,
1529 },
1530 ])
1531 }
1532}
1533
1534#[derive(Serialize)]
1536pub struct RecoveryExpiredContext {
1537 session: UserRecoverySession,
1538}
1539
1540impl RecoveryExpiredContext {
1541 #[must_use]
1543 pub fn new(session: UserRecoverySession) -> Self {
1544 Self { session }
1545 }
1546}
1547
1548impl TemplateContext for RecoveryExpiredContext {
1549 fn sample<R: Rng>(
1550 now: chrono::DateTime<Utc>,
1551 rng: &mut R,
1552 _locales: &[DataLocale],
1553 ) -> BTreeMap<SampleIdentifier, Self>
1554 where
1555 Self: Sized,
1556 {
1557 let session = UserRecoverySession {
1558 id: Ulid::from_datetime_with_rng(now, rng),
1559 email: "name@mail.com".to_owned(),
1560 user_agent: "Mozilla/5.0".to_owned(),
1561 ip_address: None,
1562 locale: "en".to_owned(),
1563 created_at: now,
1564 consumed_at: None,
1565 };
1566
1567 sample_list(vec![Self { session }])
1568 }
1569}
1570#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1572#[serde(rename_all = "snake_case")]
1573pub enum RecoveryFinishFormField {
1574 NewPassword,
1576
1577 NewPasswordConfirm,
1579}
1580
1581impl FormField for RecoveryFinishFormField {
1582 fn keep(&self) -> bool {
1583 false
1584 }
1585}
1586
1587#[derive(Serialize)]
1589pub struct RecoveryFinishContext {
1590 user: User,
1591 form: FormState<RecoveryFinishFormField>,
1592}
1593
1594impl RecoveryFinishContext {
1595 #[must_use]
1597 pub fn new(user: User) -> Self {
1598 Self {
1599 user,
1600 form: FormState::default(),
1601 }
1602 }
1603
1604 #[must_use]
1606 pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1607 self.form = form;
1608 self
1609 }
1610}
1611
1612impl TemplateContext for RecoveryFinishContext {
1613 fn sample<R: Rng>(
1614 now: chrono::DateTime<Utc>,
1615 rng: &mut R,
1616 _locales: &[DataLocale],
1617 ) -> BTreeMap<SampleIdentifier, Self>
1618 where
1619 Self: Sized,
1620 {
1621 sample_list(
1622 User::samples(now, rng)
1623 .into_iter()
1624 .flat_map(|user| {
1625 vec![
1626 Self::new(user.clone()),
1627 Self::new(user.clone()).with_form_state(
1628 FormState::default().with_error_on_field(
1629 RecoveryFinishFormField::NewPassword,
1630 FieldError::Invalid,
1631 ),
1632 ),
1633 Self::new(user.clone()).with_form_state(
1634 FormState::default().with_error_on_field(
1635 RecoveryFinishFormField::NewPasswordConfirm,
1636 FieldError::Invalid,
1637 ),
1638 ),
1639 ]
1640 })
1641 .collect(),
1642 )
1643 }
1644}
1645
1646#[derive(Serialize)]
1649pub struct UpstreamExistingLinkContext {
1650 linked_user: User,
1651}
1652
1653impl UpstreamExistingLinkContext {
1654 #[must_use]
1656 pub fn new(linked_user: User) -> Self {
1657 Self { linked_user }
1658 }
1659}
1660
1661impl TemplateContext for UpstreamExistingLinkContext {
1662 fn sample<R: Rng>(
1663 now: chrono::DateTime<Utc>,
1664 rng: &mut R,
1665 _locales: &[DataLocale],
1666 ) -> BTreeMap<SampleIdentifier, Self>
1667 where
1668 Self: Sized,
1669 {
1670 sample_list(
1671 User::samples(now, rng)
1672 .into_iter()
1673 .map(|linked_user| Self { linked_user })
1674 .collect(),
1675 )
1676 }
1677}
1678
1679#[derive(Serialize)]
1682pub struct UpstreamSuggestLink {
1683 post_logout_action: PostAuthAction,
1684}
1685
1686impl UpstreamSuggestLink {
1687 #[must_use]
1689 pub fn new(link: &UpstreamOAuthLink) -> Self {
1690 Self::for_link_id(link.id)
1691 }
1692
1693 fn for_link_id(id: Ulid) -> Self {
1694 let post_logout_action = PostAuthAction::link_upstream(id);
1695 Self { post_logout_action }
1696 }
1697}
1698
1699impl TemplateContext for UpstreamSuggestLink {
1700 fn sample<R: Rng>(
1701 now: chrono::DateTime<Utc>,
1702 rng: &mut R,
1703 _locales: &[DataLocale],
1704 ) -> BTreeMap<SampleIdentifier, Self>
1705 where
1706 Self: Sized,
1707 {
1708 let id = Ulid::from_datetime_with_rng(now, rng);
1709 sample_list(vec![Self::for_link_id(id)])
1710 }
1711}
1712
1713#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1715#[serde(rename_all = "snake_case")]
1716pub enum UpstreamRegisterFormField {
1717 Username,
1719
1720 AcceptTerms,
1722}
1723
1724impl FormField for UpstreamRegisterFormField {
1725 fn keep(&self) -> bool {
1726 match self {
1727 Self::Username | Self::AcceptTerms => true,
1728 }
1729 }
1730}
1731
1732#[derive(Serialize)]
1735pub struct UpstreamRegister {
1736 upstream_oauth_link: UpstreamOAuthLink,
1737 upstream_oauth_provider: UpstreamOAuthProvider,
1738 imported_localpart: Option<String>,
1739 force_localpart: bool,
1740 imported_display_name: Option<String>,
1741 force_display_name: bool,
1742 imported_email: Option<String>,
1743 force_email: bool,
1744 form_state: FormState<UpstreamRegisterFormField>,
1745}
1746
1747impl UpstreamRegister {
1748 #[must_use]
1751 pub fn new(
1752 upstream_oauth_link: UpstreamOAuthLink,
1753 upstream_oauth_provider: UpstreamOAuthProvider,
1754 ) -> Self {
1755 Self {
1756 upstream_oauth_link,
1757 upstream_oauth_provider,
1758 imported_localpart: None,
1759 force_localpart: false,
1760 imported_display_name: None,
1761 force_display_name: false,
1762 imported_email: None,
1763 force_email: false,
1764 form_state: FormState::default(),
1765 }
1766 }
1767
1768 pub fn set_localpart(&mut self, localpart: String, force: bool) {
1770 self.imported_localpart = Some(localpart);
1771 self.force_localpart = force;
1772 }
1773
1774 #[must_use]
1776 pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1777 Self {
1778 imported_localpart: Some(localpart),
1779 force_localpart: force,
1780 ..self
1781 }
1782 }
1783
1784 pub fn set_display_name(&mut self, display_name: String, force: bool) {
1786 self.imported_display_name = Some(display_name);
1787 self.force_display_name = force;
1788 }
1789
1790 #[must_use]
1792 pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1793 Self {
1794 imported_display_name: Some(display_name),
1795 force_display_name: force,
1796 ..self
1797 }
1798 }
1799
1800 pub fn set_email(&mut self, email: String, force: bool) {
1802 self.imported_email = Some(email);
1803 self.force_email = force;
1804 }
1805
1806 #[must_use]
1808 pub fn with_email(self, email: String, force: bool) -> Self {
1809 Self {
1810 imported_email: Some(email),
1811 force_email: force,
1812 ..self
1813 }
1814 }
1815
1816 pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1818 self.form_state = form_state;
1819 }
1820
1821 #[must_use]
1823 pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1824 Self { form_state, ..self }
1825 }
1826}
1827
1828impl TemplateContext for UpstreamRegister {
1829 fn sample<R: Rng>(
1830 now: chrono::DateTime<Utc>,
1831 _rng: &mut R,
1832 _locales: &[DataLocale],
1833 ) -> BTreeMap<SampleIdentifier, Self>
1834 where
1835 Self: Sized,
1836 {
1837 sample_list(vec![Self::new(
1838 UpstreamOAuthLink {
1839 id: Ulid::nil(),
1840 provider_id: Ulid::nil(),
1841 user_id: None,
1842 subject: "subject".to_owned(),
1843 human_account_name: Some("@john".to_owned()),
1844 created_at: now,
1845 },
1846 UpstreamOAuthProvider {
1847 id: Ulid::nil(),
1848 issuer: Some("https://example.com/".to_owned()),
1849 human_name: Some("Example Ltd.".to_owned()),
1850 brand_name: None,
1851 scope: Scope::from_iter([OPENID]),
1852 token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1853 token_endpoint_signing_alg: None,
1854 id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1855 client_id: "client-id".to_owned(),
1856 encrypted_client_secret: None,
1857 claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1858 authorization_endpoint_override: None,
1859 token_endpoint_override: None,
1860 jwks_uri_override: None,
1861 userinfo_endpoint_override: None,
1862 fetch_userinfo: false,
1863 userinfo_signed_response_alg: None,
1864 discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1865 pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1866 response_mode: None,
1867 additional_authorization_parameters: Vec::new(),
1868 forward_login_hint: false,
1869 created_at: now,
1870 disabled_at: None,
1871 on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
1872 registration_token_required: false,
1873 },
1874 )])
1875 }
1876}
1877
1878#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1880#[serde(rename_all = "snake_case")]
1881pub enum DeviceLinkFormField {
1882 Code,
1884}
1885
1886impl FormField for DeviceLinkFormField {
1887 fn keep(&self) -> bool {
1888 match self {
1889 Self::Code => true,
1890 }
1891 }
1892}
1893
1894#[derive(Serialize, Default, Debug)]
1896pub struct DeviceLinkContext {
1897 form_state: FormState<DeviceLinkFormField>,
1898}
1899
1900impl DeviceLinkContext {
1901 #[must_use]
1903 pub fn new() -> Self {
1904 Self::default()
1905 }
1906
1907 #[must_use]
1909 pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1910 self.form_state = form_state;
1911 self
1912 }
1913}
1914
1915impl TemplateContext for DeviceLinkContext {
1916 fn sample<R: Rng>(
1917 _now: chrono::DateTime<Utc>,
1918 _rng: &mut R,
1919 _locales: &[DataLocale],
1920 ) -> BTreeMap<SampleIdentifier, Self>
1921 where
1922 Self: Sized,
1923 {
1924 sample_list(vec![
1925 Self::new(),
1926 Self::new().with_form_state(
1927 FormState::default()
1928 .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1929 ),
1930 ])
1931 }
1932}
1933
1934#[derive(Serialize, Debug)]
1936pub struct DeviceConsentContext {
1937 grant: DeviceCodeGrant,
1938 client: Client,
1939 matrix_user: MatrixUser,
1940}
1941
1942impl DeviceConsentContext {
1943 #[must_use]
1945 pub fn new(grant: DeviceCodeGrant, client: Client, matrix_user: MatrixUser) -> Self {
1946 Self {
1947 grant,
1948 client,
1949 matrix_user,
1950 }
1951 }
1952}
1953
1954impl TemplateContext for DeviceConsentContext {
1955 fn sample<R: Rng>(
1956 now: chrono::DateTime<Utc>,
1957 rng: &mut R,
1958 _locales: &[DataLocale],
1959 ) -> BTreeMap<SampleIdentifier, Self>
1960 where
1961 Self: Sized,
1962 {
1963 sample_list(Client::samples(now, rng)
1964 .into_iter()
1965 .map(|client| {
1966 let grant = DeviceCodeGrant {
1967 id: Ulid::from_datetime_with_rng(now, rng),
1968 state: mas_data_model::DeviceCodeGrantState::Pending,
1969 client_id: client.id,
1970 scope: [OPENID].into_iter().collect(),
1971 user_code: mas_data_model::generate_user_code(rng),
1972 device_code: Alphanumeric.sample_string(rng, 32),
1973 created_at: now - Duration::try_minutes(5).unwrap(),
1974 expires_at: now + Duration::try_minutes(25).unwrap(),
1975 ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
1976 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()),
1977 locale: None,
1978 };
1979 Self {
1980 grant,
1981 client,
1982 matrix_user: MatrixUser {
1983 mxid: "@alice:example.com".to_owned(),
1984 display_name: Some("Alice".to_owned()),
1985 }
1986 }
1987 })
1988 .collect())
1989 }
1990}
1991
1992#[derive(Serialize)]
1995pub struct AccountInactiveContext {
1996 user: User,
1997
1998 #[serde(skip_serializing_if = "Option::is_none")]
2001 post_logout_action: Option<PostAuthAction>,
2002}
2003
2004impl AccountInactiveContext {
2005 #[must_use]
2007 pub fn new(user: User) -> Self {
2008 Self {
2009 user,
2010 post_logout_action: None,
2011 }
2012 }
2013
2014 #[must_use]
2016 pub fn with_post_auth_action(mut self, action: Option<PostAuthAction>) -> Self {
2017 self.post_logout_action = action;
2018 self
2019 }
2020}
2021
2022impl TemplateContext for AccountInactiveContext {
2023 fn sample<R: Rng>(
2024 now: chrono::DateTime<Utc>,
2025 rng: &mut R,
2026 _locales: &[DataLocale],
2027 ) -> BTreeMap<SampleIdentifier, Self>
2028 where
2029 Self: Sized,
2030 {
2031 let action = PostAuthAction::continue_grant(Ulid::from_datetime_with_rng(now, rng));
2032 sample_list(
2033 User::samples(now, rng)
2034 .into_iter()
2035 .flat_map(|user| {
2036 [
2039 AccountInactiveContext::new(user.clone()),
2040 AccountInactiveContext::new(user)
2041 .with_post_auth_action(Some(action.clone())),
2042 ]
2043 })
2044 .collect(),
2045 )
2046 }
2047}
2048
2049#[derive(Serialize)]
2051pub struct DeviceNameContext {
2052 client: Client,
2053 raw_user_agent: String,
2054}
2055
2056impl DeviceNameContext {
2057 #[must_use]
2059 pub fn new(client: Client, user_agent: Option<String>) -> Self {
2060 Self {
2061 client,
2062 raw_user_agent: user_agent.unwrap_or_default(),
2063 }
2064 }
2065}
2066
2067impl TemplateContext for DeviceNameContext {
2068 fn sample<R: Rng>(
2069 now: chrono::DateTime<Utc>,
2070 rng: &mut R,
2071 _locales: &[DataLocale],
2072 ) -> BTreeMap<SampleIdentifier, Self>
2073 where
2074 Self: Sized,
2075 {
2076 sample_list(Client::samples(now, rng)
2077 .into_iter()
2078 .map(|client| DeviceNameContext {
2079 client,
2080 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(),
2081 })
2082 .collect())
2083 }
2084}
2085
2086#[derive(Serialize)]
2088pub struct FormPostContext<T> {
2089 redirect_uri: Option<Url>,
2090 params: T,
2091}
2092
2093impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
2094 fn sample<R: Rng>(
2095 now: chrono::DateTime<Utc>,
2096 rng: &mut R,
2097 locales: &[DataLocale],
2098 ) -> BTreeMap<SampleIdentifier, Self>
2099 where
2100 Self: Sized,
2101 {
2102 let sample_params = T::sample(now, rng, locales);
2103 sample_params
2104 .into_iter()
2105 .map(|(k, params)| {
2106 (
2107 k,
2108 FormPostContext {
2109 redirect_uri: "https://example.com/callback".parse().ok(),
2110 params,
2111 },
2112 )
2113 })
2114 .collect()
2115 }
2116}
2117
2118impl<T> FormPostContext<T> {
2119 pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
2122 Self {
2123 redirect_uri: Some(redirect_uri),
2124 params,
2125 }
2126 }
2127
2128 pub fn new_for_current_url(params: T) -> Self {
2131 Self {
2132 redirect_uri: None,
2133 params,
2134 }
2135 }
2136
2137 pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
2142 WithLanguage {
2143 lang: lang.to_string(),
2144 inner: self,
2145 }
2146 }
2147}
2148
2149#[derive(Default, Serialize, Debug, Clone)]
2151pub struct ErrorContext {
2152 code: Option<&'static str>,
2153 description: Option<String>,
2154 details: Option<String>,
2155 lang: Option<String>,
2156}
2157
2158impl std::fmt::Display for ErrorContext {
2159 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2160 if let Some(code) = &self.code {
2161 writeln!(f, "code: {code}")?;
2162 }
2163 if let Some(description) = &self.description {
2164 writeln!(f, "{description}")?;
2165 }
2166
2167 if let Some(details) = &self.details {
2168 writeln!(f, "details: {details}")?;
2169 }
2170
2171 Ok(())
2172 }
2173}
2174
2175impl TemplateContext for ErrorContext {
2176 fn sample<R: Rng>(
2177 _now: chrono::DateTime<Utc>,
2178 _rng: &mut R,
2179 _locales: &[DataLocale],
2180 ) -> BTreeMap<SampleIdentifier, Self>
2181 where
2182 Self: Sized,
2183 {
2184 sample_list(vec![
2185 Self::new()
2186 .with_code("sample_error")
2187 .with_description("A fancy description".into())
2188 .with_details("Something happened".into()),
2189 Self::new().with_code("another_error"),
2190 Self::new(),
2191 ])
2192 }
2193}
2194
2195impl ErrorContext {
2196 #[must_use]
2198 pub fn new() -> Self {
2199 Self::default()
2200 }
2201
2202 #[must_use]
2204 pub fn with_code(mut self, code: &'static str) -> Self {
2205 self.code = Some(code);
2206 self
2207 }
2208
2209 #[must_use]
2211 pub fn with_description(mut self, description: String) -> Self {
2212 self.description = Some(description);
2213 self
2214 }
2215
2216 #[must_use]
2218 pub fn with_details(mut self, details: String) -> Self {
2219 self.details = Some(details);
2220 self
2221 }
2222
2223 #[must_use]
2225 pub fn with_language(mut self, lang: &DataLocale) -> Self {
2226 self.lang = Some(lang.to_string());
2227 self
2228 }
2229
2230 #[must_use]
2232 pub fn code(&self) -> Option<&'static str> {
2233 self.code
2234 }
2235
2236 #[must_use]
2238 pub fn description(&self) -> Option<&str> {
2239 self.description.as_deref()
2240 }
2241
2242 #[must_use]
2244 pub fn details(&self) -> Option<&str> {
2245 self.details.as_deref()
2246 }
2247}
2248
2249#[derive(Serialize)]
2251pub struct NotFoundContext {
2252 method: String,
2253 version: String,
2254 uri: String,
2255}
2256
2257impl NotFoundContext {
2258 #[must_use]
2260 pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
2261 Self {
2262 method: method.to_string(),
2263 version: format!("{version:?}"),
2264 uri: uri.to_string(),
2265 }
2266 }
2267}
2268
2269impl TemplateContext for NotFoundContext {
2270 fn sample<R: Rng>(
2271 _now: DateTime<Utc>,
2272 _rng: &mut R,
2273 _locales: &[DataLocale],
2274 ) -> BTreeMap<SampleIdentifier, Self>
2275 where
2276 Self: Sized,
2277 {
2278 sample_list(vec![
2279 Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
2280 Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
2281 Self::new(
2282 &Method::PUT,
2283 Version::HTTP_10,
2284 &"/foo?bar=baz".parse().unwrap(),
2285 ),
2286 ])
2287 }
2288}