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, Login, PostAuthAction, UrlBuilder};
35use oauth2_types::{
36 requests::ResponseMode,
37 scope::{OPENID, Scope},
38};
39use rand::{
40 RngExt, SeedableRng,
41 distr::{Alphanumeric, SampleString},
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: RngExt>(
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: RngExt>(
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: RngExt>(
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(&mut *rng);
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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: RngExt>(
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)]
653struct RegisterPageProvider {
654 name: String,
655 brand: Option<String>,
656 id: String,
658}
659
660impl RegisterPageProvider {
661 fn new(provider: UpstreamOAuthProvider) -> Self {
662 let name = provider
663 .human_name
664 .or_else(|| {
665 provider
666 .issuer
667 .as_deref()
668 .map(|issuer| crate::functions::simplify_url(issuer, true))
669 })
670 .filter(|name| !name.is_empty())
671 .unwrap_or_else(|| provider.id.to_string());
672
673 Self {
674 name,
675 brand: provider.brand_name,
676 id: provider.id.to_string(),
677 }
678 }
679}
680
681#[derive(Serialize)]
683pub struct RegisterContext {
684 providers: Vec<RegisterPageProvider>,
685 login_link: String,
686 form: FormState<RegisterFormField>,
687 graphql_endpoint: String,
688}
689
690impl TemplateContext for RegisterContext {
691 fn sample<R: RngExt>(
692 _now: chrono::DateTime<Utc>,
693 _rng: &mut R,
694 _locales: &[DataLocale],
695 ) -> BTreeMap<SampleIdentifier, Self>
696 where
697 Self: Sized,
698 {
699 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
700 sample_list(vec![RegisterContext::new(&url_builder, Vec::new(), None)])
702 }
703}
704
705impl RegisterContext {
706 #[must_use]
709 pub fn new(
710 url_builder: &UrlBuilder,
711 providers: Vec<UpstreamOAuthProvider>,
712 post_auth_action: Option<&PostAuthAction>,
713 ) -> Self {
714 Self {
715 providers: providers
716 .into_iter()
717 .map(RegisterPageProvider::new)
718 .collect(),
719 login_link: url_builder.relative_url_for(&Login::from(post_auth_action.cloned())),
720 form: FormState::default(),
721 graphql_endpoint: url_builder.relative_url_for(&GraphQL),
722 }
723 }
724
725 #[must_use]
727 pub fn with_form_state(self, form: FormState<RegisterFormField>) -> Self {
728 Self { form, ..self }
729 }
730}
731
732#[derive(Serialize)]
734pub struct ConsentContext {
735 grant: AuthorizationGrant,
736 client: Client,
737 action: PostAuthAction,
738 matrix_user: MatrixUser,
739}
740
741impl TemplateContext for ConsentContext {
742 fn sample<R: RngExt>(
743 now: chrono::DateTime<Utc>,
744 rng: &mut R,
745 _locales: &[DataLocale],
746 ) -> BTreeMap<SampleIdentifier, Self>
747 where
748 Self: Sized,
749 {
750 sample_list(
751 Client::samples(now, rng)
752 .into_iter()
753 .flat_map(|client| {
754 [
755 (None, ResponseMode::Query),
756 (None, ResponseMode::Fragment),
757 (None, ResponseMode::FormPost),
758 (Some("some-state".to_owned()), ResponseMode::Query),
759 (Some("some-state".to_owned()), ResponseMode::Fragment),
760 (Some("some-state".to_owned()), ResponseMode::FormPost),
761 ]
762 .map(|(state, response_mode)| {
763 let mut grant = AuthorizationGrant::sample(now, rng);
764 let action = PostAuthAction::continue_grant(grant.id);
765 grant.client_id = client.id;
767 grant.state = state;
768 grant.response_mode = response_mode;
769 Self {
770 grant,
771 client: client.clone(),
772 action,
773 matrix_user: MatrixUser {
774 mxid: "@alice:example.com".to_owned(),
775 display_name: Some("Alice".to_owned()),
776 },
777 }
778 })
779 })
780 .collect(),
781 )
782 }
783}
784
785impl ConsentContext {
786 #[must_use]
788 pub fn new(grant: AuthorizationGrant, client: Client, matrix_user: MatrixUser) -> Self {
789 let action = PostAuthAction::continue_grant(grant.id);
790 Self {
791 grant,
792 client,
793 action,
794 matrix_user,
795 }
796 }
797}
798
799#[derive(Serialize, Debug)]
800#[serde(tag = "grant_type")]
801enum PolicyViolationGrant {
802 #[serde(rename = "authorization_code")]
803 Authorization(AuthorizationGrant),
804 #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
805 DeviceCode(DeviceCodeGrant),
806}
807
808#[derive(Serialize, Debug)]
810pub struct PolicyViolationContext {
811 grant: PolicyViolationGrant,
812 client: Client,
813 action: PostAuthAction,
814 violations: Vec<Violation>,
815}
816
817impl TemplateContext for PolicyViolationContext {
818 fn sample<R: RngExt>(
819 now: chrono::DateTime<Utc>,
820 rng: &mut R,
821 _locales: &[DataLocale],
822 ) -> BTreeMap<SampleIdentifier, Self>
823 where
824 Self: Sized,
825 {
826 sample_list(
827 Client::samples(now, rng)
828 .into_iter()
829 .flat_map(|client| {
830 let mut grant = AuthorizationGrant::sample(now, rng);
831 grant.client_id = client.id;
833
834 let authorization_grant = PolicyViolationContext::for_authorization_grant(
835 grant.clone(),
836 client.clone(),
837 Vec::new(),
838 );
839
840 let authorization_grant_invalid_scope =
841 PolicyViolationContext::for_authorization_grant(
842 grant.clone(),
843 client.clone(),
844 vec![Violation {
845 msg: "scope 'foo' not allowed".to_owned(),
846 redirect_uri: None,
847 field: None,
848 variant: None,
849 }],
850 );
851 let authorization_grant_admin_scope =
852 PolicyViolationContext::for_authorization_grant(
853 grant.clone(),
854 client.clone(),
855 vec![Violation {
856 msg: "scope 'urn:mas:admin' requires admin privileges".to_owned(),
857 redirect_uri: None,
858 field: None,
859 variant: Some(ViolationVariant::AdminScopeNotAllowed),
860 }],
861 );
862
863 let authorization_grant_client_not_allowed =
864 PolicyViolationContext::for_authorization_grant(
865 grant,
866 client.clone(),
867 vec![Violation {
868 msg: "This client is not allowed to login".to_owned(),
869 redirect_uri: None,
870 field: None,
871 variant: Some(ViolationVariant::ClientNotAllowed),
872 }],
873 );
874
875 let device_code_grant = PolicyViolationContext::for_device_code_grant(
876 DeviceCodeGrant {
877 id: Ulid::from_datetime_with_rng(now, rng),
878 state: mas_data_model::DeviceCodeGrantState::Pending,
879 client_id: client.id,
880 scope: [OPENID].into_iter().collect(),
881 user_code: mas_data_model::generate_user_code(rng),
882 device_code: Alphanumeric.sample_string(rng, 32),
883 created_at: now - Duration::try_minutes(5).unwrap(),
884 expires_at: now + Duration::try_minutes(25).unwrap(),
885 ip_address: None,
886 user_agent: None,
887 locale: None,
888 },
889 client.clone(),
890 Vec::new(),
891 );
892
893 let device_code_grant_invalid_scope =
894 PolicyViolationContext::for_device_code_grant(
895 DeviceCodeGrant {
896 id: Ulid::from_datetime_with_rng(now, rng),
897 state: mas_data_model::DeviceCodeGrantState::Pending,
898 client_id: client.id,
899 scope: [OPENID].into_iter().collect(),
900 user_code: mas_data_model::generate_user_code(rng),
901 device_code: Alphanumeric.sample_string(rng, 32),
902 created_at: now - Duration::try_minutes(5).unwrap(),
903 expires_at: now + Duration::try_minutes(25).unwrap(),
904 ip_address: None,
905 user_agent: None,
906 locale: None,
907 },
908 client,
909 vec![Violation {
910 msg: "user has too many active sessions".to_owned(),
911 redirect_uri: None,
912 field: None,
913 variant: Some(ViolationVariant::TooManySessions {
914 need_to_remove: 1,
915 }),
916 }],
917 );
918
919 [
920 authorization_grant,
921 authorization_grant_invalid_scope,
922 authorization_grant_admin_scope,
923 authorization_grant_client_not_allowed,
924 device_code_grant,
925 device_code_grant_invalid_scope,
926 ]
927 })
928 .collect(),
929 )
930 }
931}
932
933impl PolicyViolationContext {
934 #[must_use]
937 pub const fn for_authorization_grant(
938 grant: AuthorizationGrant,
939 client: Client,
940 violations: Vec<Violation>,
941 ) -> Self {
942 let action = PostAuthAction::continue_grant(grant.id);
943 Self {
944 grant: PolicyViolationGrant::Authorization(grant),
945 client,
946 action,
947 violations,
948 }
949 }
950
951 #[must_use]
954 pub const fn for_device_code_grant(
955 grant: DeviceCodeGrant,
956 client: Client,
957 violations: Vec<Violation>,
958 ) -> Self {
959 let action = PostAuthAction::continue_device_code_grant(grant.id);
960 Self {
961 grant: PolicyViolationGrant::DeviceCode(grant),
962 client,
963 action,
964 violations,
965 }
966 }
967}
968
969#[derive(Serialize)]
971pub struct CompatLoginPolicyViolationContext {
972 violations: Vec<Violation>,
973}
974
975impl TemplateContext for CompatLoginPolicyViolationContext {
976 fn sample<R: RngExt>(
977 _now: chrono::DateTime<Utc>,
978 _rng: &mut R,
979 _locales: &[DataLocale],
980 ) -> BTreeMap<SampleIdentifier, Self>
981 where
982 Self: Sized,
983 {
984 sample_list(vec![
985 CompatLoginPolicyViolationContext { violations: vec![] },
986 CompatLoginPolicyViolationContext {
987 violations: vec![Violation {
988 msg: "scope 'foo' not allowed".to_owned(),
989 redirect_uri: None,
990 field: None,
991 variant: None,
992 }],
993 },
994 CompatLoginPolicyViolationContext {
995 violations: vec![Violation {
996 msg: "user has too many active sessions".to_owned(),
997 redirect_uri: None,
998 field: None,
999 variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1000 }],
1001 },
1002 CompatLoginPolicyViolationContext {
1003 violations: vec![Violation {
1004 msg: "This client is not allowed to login".to_owned(),
1005 redirect_uri: None,
1006 field: None,
1007 variant: Some(ViolationVariant::ClientNotAllowed),
1008 }],
1009 },
1010 CompatLoginPolicyViolationContext {
1011 violations: vec![
1012 Violation {
1013 msg: "scope 'foo' not allowed".to_owned(),
1014 redirect_uri: None,
1015 field: None,
1016 variant: None,
1017 },
1018 Violation {
1019 msg: "user has too many active sessions".to_owned(),
1020 redirect_uri: None,
1021 field: None,
1022 variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1023 },
1024 ],
1025 },
1026 ])
1027 }
1028}
1029
1030impl CompatLoginPolicyViolationContext {
1031 #[must_use]
1034 pub const fn for_violations(violations: Vec<Violation>) -> Self {
1035 Self { violations }
1036 }
1037}
1038
1039#[derive(Serialize)]
1041pub struct CompatSsoContext {
1042 login: CompatSsoLogin,
1043 action: PostAuthAction,
1044 matrix_user: MatrixUser,
1045}
1046
1047impl TemplateContext for CompatSsoContext {
1048 fn sample<R: RngExt>(
1049 now: chrono::DateTime<Utc>,
1050 rng: &mut R,
1051 _locales: &[DataLocale],
1052 ) -> BTreeMap<SampleIdentifier, Self>
1053 where
1054 Self: Sized,
1055 {
1056 let id = Ulid::from_datetime_with_rng(now, rng);
1057 sample_list(vec![CompatSsoContext::new(
1058 CompatSsoLogin {
1059 id,
1060 redirect_uri: Url::parse("https://app.element.io/").unwrap(),
1061 login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
1062 created_at: now,
1063 state: CompatSsoLoginState::Pending,
1064 },
1065 MatrixUser {
1066 mxid: "@alice:example.com".to_owned(),
1067 display_name: Some("Alice".to_owned()),
1068 },
1069 )])
1070 }
1071}
1072
1073impl CompatSsoContext {
1074 #[must_use]
1076 pub fn new(login: CompatSsoLogin, matrix_user: MatrixUser) -> Self
1077where {
1078 let action = PostAuthAction::continue_compat_sso_login(login.id);
1079 Self {
1080 login,
1081 action,
1082 matrix_user,
1083 }
1084 }
1085}
1086
1087#[derive(Serialize)]
1089pub struct EmailRecoveryContext {
1090 user: User,
1091 session: UserRecoverySession,
1092 recovery_link: Url,
1093}
1094
1095impl EmailRecoveryContext {
1096 #[must_use]
1098 pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
1099 Self {
1100 user,
1101 session,
1102 recovery_link,
1103 }
1104 }
1105
1106 #[must_use]
1108 pub fn user(&self) -> &User {
1109 &self.user
1110 }
1111
1112 #[must_use]
1114 pub fn session(&self) -> &UserRecoverySession {
1115 &self.session
1116 }
1117}
1118
1119impl TemplateContext for EmailRecoveryContext {
1120 fn sample<R: RngExt>(
1121 now: chrono::DateTime<Utc>,
1122 rng: &mut R,
1123 _locales: &[DataLocale],
1124 ) -> BTreeMap<SampleIdentifier, Self>
1125 where
1126 Self: Sized,
1127 {
1128 sample_list(User::samples(now, rng).into_iter().map(|user| {
1129 let session = UserRecoverySession {
1130 id: Ulid::from_datetime_with_rng(now, rng),
1131 email: "hello@example.com".to_owned(),
1132 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(),
1133 ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
1134 locale: "en".to_owned(),
1135 created_at: now,
1136 consumed_at: None,
1137 };
1138
1139 let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
1140
1141 Self::new(user, session, link)
1142 }).collect())
1143 }
1144}
1145
1146#[derive(Serialize)]
1148pub struct EmailVerificationContext {
1149 #[serde(skip_serializing_if = "Option::is_none")]
1150 browser_session: Option<BrowserSession>,
1151 #[serde(skip_serializing_if = "Option::is_none")]
1152 user_registration: Option<UserRegistration>,
1153 authentication_code: UserEmailAuthenticationCode,
1154}
1155
1156impl EmailVerificationContext {
1157 #[must_use]
1159 pub fn new(
1160 authentication_code: UserEmailAuthenticationCode,
1161 browser_session: Option<BrowserSession>,
1162 user_registration: Option<UserRegistration>,
1163 ) -> Self {
1164 Self {
1165 browser_session,
1166 user_registration,
1167 authentication_code,
1168 }
1169 }
1170
1171 #[must_use]
1173 pub fn user(&self) -> Option<&User> {
1174 self.browser_session.as_ref().map(|s| &s.user)
1175 }
1176
1177 #[must_use]
1179 pub fn code(&self) -> &str {
1180 &self.authentication_code.code
1181 }
1182}
1183
1184impl TemplateContext for EmailVerificationContext {
1185 fn sample<R: RngExt>(
1186 now: chrono::DateTime<Utc>,
1187 rng: &mut R,
1188 _locales: &[DataLocale],
1189 ) -> BTreeMap<SampleIdentifier, Self>
1190 where
1191 Self: Sized,
1192 {
1193 sample_list(
1194 BrowserSession::samples(now, rng)
1195 .into_iter()
1196 .map(|browser_session| {
1197 let authentication_code = UserEmailAuthenticationCode {
1198 id: Ulid::from_datetime_with_rng(now, rng),
1199 user_email_authentication_id: Ulid::from_datetime_with_rng(now, rng),
1200 code: "123456".to_owned(),
1201 created_at: now - Duration::try_minutes(5).unwrap(),
1202 expires_at: now + Duration::try_minutes(25).unwrap(),
1203 };
1204
1205 Self {
1206 browser_session: Some(browser_session),
1207 user_registration: None,
1208 authentication_code,
1209 }
1210 })
1211 .collect(),
1212 )
1213 }
1214}
1215
1216#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1218#[serde(rename_all = "snake_case")]
1219pub enum RegisterStepsVerifyEmailFormField {
1220 Code,
1222}
1223
1224impl FormField for RegisterStepsVerifyEmailFormField {
1225 fn keep(&self) -> bool {
1226 match self {
1227 Self::Code => true,
1228 }
1229 }
1230}
1231
1232#[derive(Serialize)]
1234pub struct RegisterStepsVerifyEmailContext {
1235 form: FormState<RegisterStepsVerifyEmailFormField>,
1236 authentication: UserEmailAuthentication,
1237}
1238
1239impl RegisterStepsVerifyEmailContext {
1240 #[must_use]
1242 pub fn new(authentication: UserEmailAuthentication) -> Self {
1243 Self {
1244 form: FormState::default(),
1245 authentication,
1246 }
1247 }
1248
1249 #[must_use]
1251 pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
1252 Self { form, ..self }
1253 }
1254}
1255
1256impl TemplateContext for RegisterStepsVerifyEmailContext {
1257 fn sample<R: RngExt>(
1258 now: chrono::DateTime<Utc>,
1259 rng: &mut R,
1260 _locales: &[DataLocale],
1261 ) -> BTreeMap<SampleIdentifier, Self>
1262 where
1263 Self: Sized,
1264 {
1265 let authentication = UserEmailAuthentication {
1266 id: Ulid::from_datetime_with_rng(now, rng),
1267 user_session_id: None,
1268 user_registration_id: None,
1269 email: "foobar@example.com".to_owned(),
1270 created_at: now,
1271 completed_at: None,
1272 };
1273
1274 sample_list(vec![Self {
1275 form: FormState::default(),
1276 authentication,
1277 }])
1278 }
1279}
1280
1281#[derive(Serialize)]
1283pub struct RegisterStepsEmailInUseContext {
1284 email: String,
1285 action: Option<PostAuthAction>,
1286}
1287
1288impl RegisterStepsEmailInUseContext {
1289 #[must_use]
1291 pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
1292 Self { email, action }
1293 }
1294}
1295
1296impl TemplateContext for RegisterStepsEmailInUseContext {
1297 fn sample<R: RngExt>(
1298 _now: chrono::DateTime<Utc>,
1299 _rng: &mut R,
1300 _locales: &[DataLocale],
1301 ) -> BTreeMap<SampleIdentifier, Self>
1302 where
1303 Self: Sized,
1304 {
1305 let email = "hello@example.com".to_owned();
1306 let action = PostAuthAction::continue_grant(Ulid::nil());
1307 sample_list(vec![Self::new(email, Some(action))])
1308 }
1309}
1310
1311#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1313#[serde(rename_all = "snake_case")]
1314pub enum RegisterStepsDisplayNameFormField {
1315 DisplayName,
1317}
1318
1319impl FormField for RegisterStepsDisplayNameFormField {
1320 fn keep(&self) -> bool {
1321 match self {
1322 Self::DisplayName => true,
1323 }
1324 }
1325}
1326
1327#[derive(Serialize, Default)]
1329pub struct RegisterStepsDisplayNameContext {
1330 form: FormState<RegisterStepsDisplayNameFormField>,
1331}
1332
1333impl RegisterStepsDisplayNameContext {
1334 #[must_use]
1336 pub fn new() -> Self {
1337 Self::default()
1338 }
1339
1340 #[must_use]
1342 pub fn with_form_state(
1343 mut self,
1344 form_state: FormState<RegisterStepsDisplayNameFormField>,
1345 ) -> Self {
1346 self.form = form_state;
1347 self
1348 }
1349}
1350
1351impl TemplateContext for RegisterStepsDisplayNameContext {
1352 fn sample<R: RngExt>(
1353 _now: chrono::DateTime<chrono::Utc>,
1354 _rng: &mut R,
1355 _locales: &[DataLocale],
1356 ) -> BTreeMap<SampleIdentifier, Self>
1357 where
1358 Self: Sized,
1359 {
1360 sample_list(vec![Self {
1361 form: FormState::default(),
1362 }])
1363 }
1364}
1365
1366#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1368#[serde(rename_all = "snake_case")]
1369pub enum RegisterStepsRegistrationTokenFormField {
1370 Token,
1372}
1373
1374impl FormField for RegisterStepsRegistrationTokenFormField {
1375 fn keep(&self) -> bool {
1376 match self {
1377 Self::Token => true,
1378 }
1379 }
1380}
1381
1382#[derive(Serialize, Default)]
1384pub struct RegisterStepsRegistrationTokenContext {
1385 form: FormState<RegisterStepsRegistrationTokenFormField>,
1386}
1387
1388impl RegisterStepsRegistrationTokenContext {
1389 #[must_use]
1391 pub fn new() -> Self {
1392 Self::default()
1393 }
1394
1395 #[must_use]
1397 pub fn with_form_state(
1398 mut self,
1399 form_state: FormState<RegisterStepsRegistrationTokenFormField>,
1400 ) -> Self {
1401 self.form = form_state;
1402 self
1403 }
1404}
1405
1406impl TemplateContext for RegisterStepsRegistrationTokenContext {
1407 fn sample<R: RngExt>(
1408 _now: chrono::DateTime<chrono::Utc>,
1409 _rng: &mut R,
1410 _locales: &[DataLocale],
1411 ) -> BTreeMap<SampleIdentifier, Self>
1412 where
1413 Self: Sized,
1414 {
1415 sample_list(vec![Self {
1416 form: FormState::default(),
1417 }])
1418 }
1419}
1420
1421#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1423#[serde(rename_all = "snake_case")]
1424pub enum RecoveryStartFormField {
1425 Email,
1427}
1428
1429impl FormField for RecoveryStartFormField {
1430 fn keep(&self) -> bool {
1431 match self {
1432 Self::Email => true,
1433 }
1434 }
1435}
1436
1437#[derive(Serialize, Default)]
1439pub struct RecoveryStartContext {
1440 form: FormState<RecoveryStartFormField>,
1441}
1442
1443impl RecoveryStartContext {
1444 #[must_use]
1446 pub fn new() -> Self {
1447 Self::default()
1448 }
1449
1450 #[must_use]
1452 pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1453 Self { form }
1454 }
1455}
1456
1457impl TemplateContext for RecoveryStartContext {
1458 fn sample<R: RngExt>(
1459 _now: chrono::DateTime<Utc>,
1460 _rng: &mut R,
1461 _locales: &[DataLocale],
1462 ) -> BTreeMap<SampleIdentifier, Self>
1463 where
1464 Self: Sized,
1465 {
1466 sample_list(vec![
1467 Self::new(),
1468 Self::new().with_form_state(
1469 FormState::default()
1470 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1471 ),
1472 Self::new().with_form_state(
1473 FormState::default()
1474 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1475 ),
1476 ])
1477 }
1478}
1479
1480#[derive(Serialize)]
1482pub struct RecoveryProgressContext {
1483 session: UserRecoverySession,
1484 resend_failed_due_to_rate_limit: bool,
1486}
1487
1488impl RecoveryProgressContext {
1489 #[must_use]
1491 pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1492 Self {
1493 session,
1494 resend_failed_due_to_rate_limit,
1495 }
1496 }
1497}
1498
1499impl TemplateContext for RecoveryProgressContext {
1500 fn sample<R: RngExt>(
1501 now: chrono::DateTime<Utc>,
1502 rng: &mut R,
1503 _locales: &[DataLocale],
1504 ) -> BTreeMap<SampleIdentifier, Self>
1505 where
1506 Self: Sized,
1507 {
1508 let session = UserRecoverySession {
1509 id: Ulid::from_datetime_with_rng(now, rng),
1510 email: "name@mail.com".to_owned(),
1511 user_agent: "Mozilla/5.0".to_owned(),
1512 ip_address: None,
1513 locale: "en".to_owned(),
1514 created_at: now,
1515 consumed_at: None,
1516 };
1517
1518 sample_list(vec![
1519 Self {
1520 session: session.clone(),
1521 resend_failed_due_to_rate_limit: false,
1522 },
1523 Self {
1524 session,
1525 resend_failed_due_to_rate_limit: true,
1526 },
1527 ])
1528 }
1529}
1530
1531#[derive(Serialize)]
1533pub struct RecoveryExpiredContext {
1534 session: UserRecoverySession,
1535}
1536
1537impl RecoveryExpiredContext {
1538 #[must_use]
1540 pub fn new(session: UserRecoverySession) -> Self {
1541 Self { session }
1542 }
1543}
1544
1545impl TemplateContext for RecoveryExpiredContext {
1546 fn sample<R: RngExt>(
1547 now: chrono::DateTime<Utc>,
1548 rng: &mut R,
1549 _locales: &[DataLocale],
1550 ) -> BTreeMap<SampleIdentifier, Self>
1551 where
1552 Self: Sized,
1553 {
1554 let session = UserRecoverySession {
1555 id: Ulid::from_datetime_with_rng(now, rng),
1556 email: "name@mail.com".to_owned(),
1557 user_agent: "Mozilla/5.0".to_owned(),
1558 ip_address: None,
1559 locale: "en".to_owned(),
1560 created_at: now,
1561 consumed_at: None,
1562 };
1563
1564 sample_list(vec![Self { session }])
1565 }
1566}
1567#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1569#[serde(rename_all = "snake_case")]
1570pub enum RecoveryFinishFormField {
1571 NewPassword,
1573
1574 NewPasswordConfirm,
1576}
1577
1578impl FormField for RecoveryFinishFormField {
1579 fn keep(&self) -> bool {
1580 false
1581 }
1582}
1583
1584#[derive(Serialize)]
1586pub struct RecoveryFinishContext {
1587 user: User,
1588 form: FormState<RecoveryFinishFormField>,
1589}
1590
1591impl RecoveryFinishContext {
1592 #[must_use]
1594 pub fn new(user: User) -> Self {
1595 Self {
1596 user,
1597 form: FormState::default(),
1598 }
1599 }
1600
1601 #[must_use]
1603 pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1604 self.form = form;
1605 self
1606 }
1607}
1608
1609impl TemplateContext for RecoveryFinishContext {
1610 fn sample<R: RngExt>(
1611 now: chrono::DateTime<Utc>,
1612 rng: &mut R,
1613 _locales: &[DataLocale],
1614 ) -> BTreeMap<SampleIdentifier, Self>
1615 where
1616 Self: Sized,
1617 {
1618 sample_list(
1619 User::samples(now, rng)
1620 .into_iter()
1621 .flat_map(|user| {
1622 vec![
1623 Self::new(user.clone()),
1624 Self::new(user.clone()).with_form_state(
1625 FormState::default().with_error_on_field(
1626 RecoveryFinishFormField::NewPassword,
1627 FieldError::Invalid,
1628 ),
1629 ),
1630 Self::new(user.clone()).with_form_state(
1631 FormState::default().with_error_on_field(
1632 RecoveryFinishFormField::NewPasswordConfirm,
1633 FieldError::Invalid,
1634 ),
1635 ),
1636 ]
1637 })
1638 .collect(),
1639 )
1640 }
1641}
1642
1643#[derive(Serialize)]
1646pub struct UpstreamExistingLinkContext {
1647 linked_user: User,
1648}
1649
1650impl UpstreamExistingLinkContext {
1651 #[must_use]
1653 pub fn new(linked_user: User) -> Self {
1654 Self { linked_user }
1655 }
1656}
1657
1658impl TemplateContext for UpstreamExistingLinkContext {
1659 fn sample<R: RngExt>(
1660 now: chrono::DateTime<Utc>,
1661 rng: &mut R,
1662 _locales: &[DataLocale],
1663 ) -> BTreeMap<SampleIdentifier, Self>
1664 where
1665 Self: Sized,
1666 {
1667 sample_list(
1668 User::samples(now, rng)
1669 .into_iter()
1670 .map(|linked_user| Self { linked_user })
1671 .collect(),
1672 )
1673 }
1674}
1675
1676#[derive(Serialize)]
1679pub struct UpstreamSuggestLink {
1680 post_logout_action: PostAuthAction,
1681}
1682
1683impl UpstreamSuggestLink {
1684 #[must_use]
1686 pub fn new(link: &UpstreamOAuthLink) -> Self {
1687 Self::for_link_id(link.id)
1688 }
1689
1690 fn for_link_id(id: Ulid) -> Self {
1691 let post_logout_action = PostAuthAction::link_upstream(id);
1692 Self { post_logout_action }
1693 }
1694}
1695
1696impl TemplateContext for UpstreamSuggestLink {
1697 fn sample<R: RngExt>(
1698 now: chrono::DateTime<Utc>,
1699 rng: &mut R,
1700 _locales: &[DataLocale],
1701 ) -> BTreeMap<SampleIdentifier, Self>
1702 where
1703 Self: Sized,
1704 {
1705 let id = Ulid::from_datetime_with_rng(now, rng);
1706 sample_list(vec![Self::for_link_id(id)])
1707 }
1708}
1709
1710#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1712#[serde(rename_all = "snake_case")]
1713pub enum UpstreamRegisterFormField {
1714 Username,
1716
1717 AcceptTerms,
1719}
1720
1721impl FormField for UpstreamRegisterFormField {
1722 fn keep(&self) -> bool {
1723 match self {
1724 Self::Username | Self::AcceptTerms => true,
1725 }
1726 }
1727}
1728
1729#[derive(Serialize)]
1732pub struct UpstreamRegister {
1733 upstream_oauth_link: UpstreamOAuthLink,
1734 upstream_oauth_provider: UpstreamOAuthProvider,
1735 imported_localpart: Option<String>,
1736 force_localpart: bool,
1737 imported_display_name: Option<String>,
1738 force_display_name: bool,
1739 imported_email: Option<String>,
1740 force_email: bool,
1741 form_state: FormState<UpstreamRegisterFormField>,
1742}
1743
1744impl UpstreamRegister {
1745 #[must_use]
1748 pub fn new(
1749 upstream_oauth_link: UpstreamOAuthLink,
1750 upstream_oauth_provider: UpstreamOAuthProvider,
1751 ) -> Self {
1752 Self {
1753 upstream_oauth_link,
1754 upstream_oauth_provider,
1755 imported_localpart: None,
1756 force_localpart: false,
1757 imported_display_name: None,
1758 force_display_name: false,
1759 imported_email: None,
1760 force_email: false,
1761 form_state: FormState::default(),
1762 }
1763 }
1764
1765 pub fn set_localpart(&mut self, localpart: String, force: bool) {
1767 self.imported_localpart = Some(localpart);
1768 self.force_localpart = force;
1769 }
1770
1771 #[must_use]
1773 pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1774 Self {
1775 imported_localpart: Some(localpart),
1776 force_localpart: force,
1777 ..self
1778 }
1779 }
1780
1781 pub fn set_display_name(&mut self, display_name: String, force: bool) {
1783 self.imported_display_name = Some(display_name);
1784 self.force_display_name = force;
1785 }
1786
1787 #[must_use]
1789 pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1790 Self {
1791 imported_display_name: Some(display_name),
1792 force_display_name: force,
1793 ..self
1794 }
1795 }
1796
1797 pub fn set_email(&mut self, email: String, force: bool) {
1799 self.imported_email = Some(email);
1800 self.force_email = force;
1801 }
1802
1803 #[must_use]
1805 pub fn with_email(self, email: String, force: bool) -> Self {
1806 Self {
1807 imported_email: Some(email),
1808 force_email: force,
1809 ..self
1810 }
1811 }
1812
1813 pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1815 self.form_state = form_state;
1816 }
1817
1818 #[must_use]
1820 pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1821 Self { form_state, ..self }
1822 }
1823}
1824
1825impl TemplateContext for UpstreamRegister {
1826 fn sample<R: RngExt>(
1827 now: chrono::DateTime<Utc>,
1828 _rng: &mut R,
1829 _locales: &[DataLocale],
1830 ) -> BTreeMap<SampleIdentifier, Self>
1831 where
1832 Self: Sized,
1833 {
1834 sample_list(vec![Self::new(
1835 UpstreamOAuthLink {
1836 id: Ulid::nil(),
1837 provider_id: Ulid::nil(),
1838 user_id: None,
1839 subject: "subject".to_owned(),
1840 human_account_name: Some("@john".to_owned()),
1841 created_at: now,
1842 },
1843 UpstreamOAuthProvider {
1844 id: Ulid::nil(),
1845 issuer: Some("https://example.com/".to_owned()),
1846 human_name: Some("Example Ltd.".to_owned()),
1847 brand_name: None,
1848 scope: Scope::from_iter([OPENID]),
1849 token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1850 token_endpoint_signing_alg: None,
1851 id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1852 client_id: "client-id".to_owned(),
1853 encrypted_client_secret: None,
1854 claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1855 authorization_endpoint_override: None,
1856 token_endpoint_override: None,
1857 jwks_uri_override: None,
1858 userinfo_endpoint_override: None,
1859 fetch_userinfo: false,
1860 userinfo_signed_response_alg: None,
1861 discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1862 pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1863 response_mode: None,
1864 additional_authorization_parameters: Vec::new(),
1865 forward_login_hint: false,
1866 created_at: now,
1867 disabled_at: None,
1868 on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
1869 registration_token_required: false,
1870 },
1871 )])
1872 }
1873}
1874
1875#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1877#[serde(rename_all = "snake_case")]
1878pub enum DeviceLinkFormField {
1879 Code,
1881}
1882
1883impl FormField for DeviceLinkFormField {
1884 fn keep(&self) -> bool {
1885 match self {
1886 Self::Code => true,
1887 }
1888 }
1889}
1890
1891#[derive(Serialize, Default, Debug)]
1893pub struct DeviceLinkContext {
1894 form_state: FormState<DeviceLinkFormField>,
1895}
1896
1897impl DeviceLinkContext {
1898 #[must_use]
1900 pub fn new() -> Self {
1901 Self::default()
1902 }
1903
1904 #[must_use]
1906 pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1907 self.form_state = form_state;
1908 self
1909 }
1910}
1911
1912impl TemplateContext for DeviceLinkContext {
1913 fn sample<R: RngExt>(
1914 _now: chrono::DateTime<Utc>,
1915 _rng: &mut R,
1916 _locales: &[DataLocale],
1917 ) -> BTreeMap<SampleIdentifier, Self>
1918 where
1919 Self: Sized,
1920 {
1921 sample_list(vec![
1922 Self::new(),
1923 Self::new().with_form_state(
1924 FormState::default()
1925 .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1926 ),
1927 ])
1928 }
1929}
1930
1931#[derive(Serialize, Debug)]
1933pub struct DeviceConsentContext {
1934 grant: DeviceCodeGrant,
1935 client: Client,
1936 matrix_user: MatrixUser,
1937}
1938
1939impl DeviceConsentContext {
1940 #[must_use]
1942 pub fn new(grant: DeviceCodeGrant, client: Client, matrix_user: MatrixUser) -> Self {
1943 Self {
1944 grant,
1945 client,
1946 matrix_user,
1947 }
1948 }
1949}
1950
1951impl TemplateContext for DeviceConsentContext {
1952 fn sample<R: RngExt>(
1953 now: chrono::DateTime<Utc>,
1954 rng: &mut R,
1955 _locales: &[DataLocale],
1956 ) -> BTreeMap<SampleIdentifier, Self>
1957 where
1958 Self: Sized,
1959 {
1960 sample_list(Client::samples(now, rng)
1961 .into_iter()
1962 .map(|client| {
1963 let grant = DeviceCodeGrant {
1964 id: Ulid::from_datetime_with_rng(now, rng),
1965 state: mas_data_model::DeviceCodeGrantState::Pending,
1966 client_id: client.id,
1967 scope: [OPENID].into_iter().collect(),
1968 user_code: mas_data_model::generate_user_code(rng),
1969 device_code: Alphanumeric.sample_string(rng, 32),
1970 created_at: now - Duration::try_minutes(5).unwrap(),
1971 expires_at: now + Duration::try_minutes(25).unwrap(),
1972 ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
1973 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()),
1974 locale: None,
1975 };
1976 Self {
1977 grant,
1978 client,
1979 matrix_user: MatrixUser {
1980 mxid: "@alice:example.com".to_owned(),
1981 display_name: Some("Alice".to_owned()),
1982 }
1983 }
1984 })
1985 .collect())
1986 }
1987}
1988
1989#[derive(Serialize)]
1992pub struct AccountInactiveContext {
1993 user: User,
1994
1995 #[serde(skip_serializing_if = "Option::is_none")]
1998 post_logout_action: Option<PostAuthAction>,
1999}
2000
2001impl AccountInactiveContext {
2002 #[must_use]
2004 pub fn new(user: User) -> Self {
2005 Self {
2006 user,
2007 post_logout_action: None,
2008 }
2009 }
2010
2011 #[must_use]
2013 pub fn with_post_auth_action(mut self, action: Option<PostAuthAction>) -> Self {
2014 self.post_logout_action = action;
2015 self
2016 }
2017}
2018
2019impl TemplateContext for AccountInactiveContext {
2020 fn sample<R: RngExt>(
2021 now: chrono::DateTime<Utc>,
2022 rng: &mut R,
2023 _locales: &[DataLocale],
2024 ) -> BTreeMap<SampleIdentifier, Self>
2025 where
2026 Self: Sized,
2027 {
2028 let action = PostAuthAction::continue_grant(Ulid::from_datetime_with_rng(now, rng));
2029 sample_list(
2030 User::samples(now, rng)
2031 .into_iter()
2032 .flat_map(|user| {
2033 [
2036 AccountInactiveContext::new(user.clone()),
2037 AccountInactiveContext::new(user)
2038 .with_post_auth_action(Some(action.clone())),
2039 ]
2040 })
2041 .collect(),
2042 )
2043 }
2044}
2045
2046#[derive(Serialize)]
2048pub struct DeviceNameContext {
2049 client: Client,
2050 raw_user_agent: String,
2051}
2052
2053impl DeviceNameContext {
2054 #[must_use]
2056 pub fn new(client: Client, user_agent: Option<String>) -> Self {
2057 Self {
2058 client,
2059 raw_user_agent: user_agent.unwrap_or_default(),
2060 }
2061 }
2062}
2063
2064impl TemplateContext for DeviceNameContext {
2065 fn sample<R: RngExt>(
2066 now: chrono::DateTime<Utc>,
2067 rng: &mut R,
2068 _locales: &[DataLocale],
2069 ) -> BTreeMap<SampleIdentifier, Self>
2070 where
2071 Self: Sized,
2072 {
2073 sample_list(Client::samples(now, rng)
2074 .into_iter()
2075 .map(|client| DeviceNameContext {
2076 client,
2077 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(),
2078 })
2079 .collect())
2080 }
2081}
2082
2083#[derive(Serialize)]
2085pub struct FormPostContext<T> {
2086 redirect_uri: Option<Url>,
2087 params: T,
2088}
2089
2090impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
2091 fn sample<R: RngExt>(
2092 now: chrono::DateTime<Utc>,
2093 rng: &mut R,
2094 locales: &[DataLocale],
2095 ) -> BTreeMap<SampleIdentifier, Self>
2096 where
2097 Self: Sized,
2098 {
2099 let sample_params = T::sample(now, rng, locales);
2100 sample_params
2101 .into_iter()
2102 .map(|(k, params)| {
2103 (
2104 k,
2105 FormPostContext {
2106 redirect_uri: "https://example.com/callback".parse().ok(),
2107 params,
2108 },
2109 )
2110 })
2111 .collect()
2112 }
2113}
2114
2115impl<T> FormPostContext<T> {
2116 pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
2119 Self {
2120 redirect_uri: Some(redirect_uri),
2121 params,
2122 }
2123 }
2124
2125 pub fn new_for_current_url(params: T) -> Self {
2128 Self {
2129 redirect_uri: None,
2130 params,
2131 }
2132 }
2133
2134 pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
2139 WithLanguage {
2140 lang: lang.to_string(),
2141 inner: self,
2142 }
2143 }
2144}
2145
2146#[derive(Default, Serialize, Debug, Clone)]
2148pub struct ErrorContext {
2149 code: Option<&'static str>,
2150 description: Option<String>,
2151 details: Option<String>,
2152 lang: Option<String>,
2153}
2154
2155impl std::fmt::Display for ErrorContext {
2156 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2157 if let Some(code) = &self.code {
2158 writeln!(f, "code: {code}")?;
2159 }
2160 if let Some(description) = &self.description {
2161 writeln!(f, "{description}")?;
2162 }
2163
2164 if let Some(details) = &self.details {
2165 writeln!(f, "details: {details}")?;
2166 }
2167
2168 Ok(())
2169 }
2170}
2171
2172impl TemplateContext for ErrorContext {
2173 fn sample<R: RngExt>(
2174 _now: chrono::DateTime<Utc>,
2175 _rng: &mut R,
2176 _locales: &[DataLocale],
2177 ) -> BTreeMap<SampleIdentifier, Self>
2178 where
2179 Self: Sized,
2180 {
2181 sample_list(vec![
2182 Self::new()
2183 .with_code("sample_error")
2184 .with_description("A fancy description".into())
2185 .with_details("Something happened".into()),
2186 Self::new().with_code("another_error"),
2187 Self::new(),
2188 ])
2189 }
2190}
2191
2192impl ErrorContext {
2193 #[must_use]
2195 pub fn new() -> Self {
2196 Self::default()
2197 }
2198
2199 #[must_use]
2201 pub fn with_code(mut self, code: &'static str) -> Self {
2202 self.code = Some(code);
2203 self
2204 }
2205
2206 #[must_use]
2208 pub fn with_description(mut self, description: String) -> Self {
2209 self.description = Some(description);
2210 self
2211 }
2212
2213 #[must_use]
2215 pub fn with_details(mut self, details: String) -> Self {
2216 self.details = Some(details);
2217 self
2218 }
2219
2220 #[must_use]
2222 pub fn with_language(mut self, lang: &DataLocale) -> Self {
2223 self.lang = Some(lang.to_string());
2224 self
2225 }
2226
2227 #[must_use]
2229 pub fn code(&self) -> Option<&'static str> {
2230 self.code
2231 }
2232
2233 #[must_use]
2235 pub fn description(&self) -> Option<&str> {
2236 self.description.as_deref()
2237 }
2238
2239 #[must_use]
2241 pub fn details(&self) -> Option<&str> {
2242 self.details.as_deref()
2243 }
2244}
2245
2246#[derive(Serialize)]
2248pub struct NotFoundContext {
2249 method: String,
2250 version: String,
2251 uri: String,
2252}
2253
2254impl NotFoundContext {
2255 #[must_use]
2257 pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
2258 Self {
2259 method: method.to_string(),
2260 version: format!("{version:?}"),
2261 uri: uri.to_string(),
2262 }
2263 }
2264}
2265
2266impl TemplateContext for NotFoundContext {
2267 fn sample<R: RngExt>(
2268 _now: DateTime<Utc>,
2269 _rng: &mut R,
2270 _locales: &[DataLocale],
2271 ) -> BTreeMap<SampleIdentifier, Self>
2272 where
2273 Self: Sized,
2274 {
2275 sample_list(vec![
2276 Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
2277 Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
2278 Self::new(
2279 &Method::PUT,
2280 Version::HTTP_10,
2281 &"/foo?bar=baz".parse().unwrap(),
2282 ),
2283 ])
2284 }
2285}