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, UserRegistrationToken,
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 ContinueRoomInvite,
532}
533
534#[derive(Serialize)]
536pub struct PostAuthContext {
537 pub params: PostAuthAction,
539
540 #[serde(flatten)]
542 pub ctx: PostAuthContextInner,
543}
544
545#[derive(Serialize, Default)]
547pub struct LoginContext {
548 form: FormState<LoginFormField>,
549 next: Option<PostAuthContext>,
550 providers: Vec<UpstreamOAuthProvider>,
551}
552
553impl TemplateContext for LoginContext {
554 fn sample<R: RngExt>(
555 _now: chrono::DateTime<Utc>,
556 _rng: &mut R,
557 _locales: &[DataLocale],
558 ) -> BTreeMap<SampleIdentifier, Self>
559 where
560 Self: Sized,
561 {
562 sample_list(vec![
564 LoginContext {
565 form: FormState::default(),
566 next: None,
567 providers: Vec::new(),
568 },
569 LoginContext {
570 form: FormState::default(),
571 next: None,
572 providers: Vec::new(),
573 },
574 LoginContext {
575 form: FormState::default()
576 .with_error_on_field(LoginFormField::Username, FieldError::Required)
577 .with_error_on_field(
578 LoginFormField::Password,
579 FieldError::Policy {
580 code: None,
581 message: "password too short".to_owned(),
582 },
583 ),
584 next: None,
585 providers: Vec::new(),
586 },
587 LoginContext {
588 form: FormState::default()
589 .with_error_on_field(LoginFormField::Username, FieldError::Exists),
590 next: None,
591 providers: Vec::new(),
592 },
593 ])
594 }
595}
596
597impl LoginContext {
598 #[must_use]
600 pub fn with_form_state(self, form: FormState<LoginFormField>) -> Self {
601 Self { form, ..self }
602 }
603
604 pub fn form_state_mut(&mut self) -> &mut FormState<LoginFormField> {
606 &mut self.form
607 }
608
609 #[must_use]
611 pub fn with_upstream_providers(self, providers: Vec<UpstreamOAuthProvider>) -> Self {
612 Self { providers, ..self }
613 }
614
615 #[must_use]
617 pub fn with_post_action(self, context: PostAuthContext) -> Self {
618 Self {
619 next: Some(context),
620 ..self
621 }
622 }
623}
624
625#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
627#[serde(rename_all = "snake_case")]
628pub enum RegisterFormField {
629 Username,
631
632 Email,
634
635 Password,
637
638 PasswordConfirm,
640
641 AcceptTerms,
643
644 Token,
646}
647
648impl FormField for RegisterFormField {
649 fn keep(&self) -> bool {
650 match self {
651 Self::Username | Self::Email | Self::AcceptTerms | Self::Token => true,
652 Self::Password | Self::PasswordConfirm => false,
653 }
654 }
655}
656
657#[derive(Serialize)]
659struct RegisterPageProvider {
660 name: String,
661 brand: Option<String>,
662 id: String,
664}
665
666impl RegisterPageProvider {
667 fn new(provider: UpstreamOAuthProvider) -> Self {
668 let name = provider
669 .human_name
670 .or_else(|| {
671 provider
672 .issuer
673 .as_deref()
674 .map(|issuer| crate::functions::simplify_url(issuer, true))
675 })
676 .filter(|name| !name.is_empty())
677 .unwrap_or_else(|| provider.id.to_string());
678
679 Self {
680 name,
681 brand: provider.brand_name,
682 id: provider.id.to_string(),
683 }
684 }
685}
686
687#[derive(Serialize)]
689struct InvitePinnedIdentity {
690 username: Option<String>,
691 email: Option<String>,
692 passwordless: bool,
693}
694
695#[derive(Serialize)]
697pub struct InviteContext {
698 valid: bool,
699
700 #[serde(flatten)]
701 pinned: Option<InvitePinnedIdentity>,
702}
703
704impl InviteContext {
705 #[must_use]
708 pub fn new(token: Option<&UserRegistrationToken>) -> Self {
709 Self {
710 valid: token.is_some(),
711 pinned: token.map(|token| InvitePinnedIdentity {
712 username: token.username.clone(),
713 email: token.email.clone(),
714 passwordless: token.passwordless,
715 }),
716 }
717 }
718}
719
720#[derive(Serialize)]
722pub struct RegisterContext {
723 providers: Vec<RegisterPageProvider>,
724 login_link: String,
725 form: FormState<RegisterFormField>,
726 graphql_endpoint: String,
727 invite: Option<InviteContext>,
728}
729
730impl TemplateContext for RegisterContext {
731 fn sample<R: RngExt>(
732 _now: chrono::DateTime<Utc>,
733 _rng: &mut R,
734 _locales: &[DataLocale],
735 ) -> BTreeMap<SampleIdentifier, Self>
736 where
737 Self: Sized,
738 {
739 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
740 sample_list(vec![RegisterContext::new(&url_builder, Vec::new(), None)])
742 }
743}
744
745impl RegisterContext {
746 #[must_use]
749 pub fn new(
750 url_builder: &UrlBuilder,
751 providers: Vec<UpstreamOAuthProvider>,
752 post_auth_action: Option<&PostAuthAction>,
753 ) -> Self {
754 Self {
755 providers: providers
756 .into_iter()
757 .map(RegisterPageProvider::new)
758 .collect(),
759 login_link: url_builder.relative_url_for(&Login::from(post_auth_action.cloned())),
760 form: FormState::default(),
761 graphql_endpoint: url_builder.relative_url_for(&GraphQL),
762 invite: None,
763 }
764 }
765
766 #[must_use]
768 pub fn with_form_state(self, form: FormState<RegisterFormField>) -> Self {
769 Self { form, ..self }
770 }
771
772 #[must_use]
774 pub fn with_invite(self, invite: InviteContext) -> Self {
775 Self {
776 invite: Some(invite),
777 ..self
778 }
779 }
780}
781
782#[derive(Serialize)]
784pub struct ConsentContext {
785 grant: AuthorizationGrant,
786 client: Client,
787 action: PostAuthAction,
788 matrix_user: MatrixUser,
789}
790
791impl TemplateContext for ConsentContext {
792 fn sample<R: RngExt>(
793 now: chrono::DateTime<Utc>,
794 rng: &mut R,
795 _locales: &[DataLocale],
796 ) -> BTreeMap<SampleIdentifier, Self>
797 where
798 Self: Sized,
799 {
800 sample_list(
801 Client::samples(now, rng)
802 .into_iter()
803 .flat_map(|client| {
804 [
805 (None, ResponseMode::Query),
806 (None, ResponseMode::Fragment),
807 (None, ResponseMode::FormPost),
808 (Some("some-state".to_owned()), ResponseMode::Query),
809 (Some("some-state".to_owned()), ResponseMode::Fragment),
810 (Some("some-state".to_owned()), ResponseMode::FormPost),
811 ]
812 .map(|(state, response_mode)| {
813 let mut grant = AuthorizationGrant::sample(now, rng);
814 let action = PostAuthAction::continue_grant(grant.id);
815 grant.client_id = client.id;
817 grant.state = state;
818 grant.response_mode = response_mode;
819 Self {
820 grant,
821 client: client.clone(),
822 action,
823 matrix_user: MatrixUser {
824 mxid: "@alice:example.com".to_owned(),
825 display_name: Some("Alice".to_owned()),
826 },
827 }
828 })
829 })
830 .collect(),
831 )
832 }
833}
834
835impl ConsentContext {
836 #[must_use]
838 pub fn new(grant: AuthorizationGrant, client: Client, matrix_user: MatrixUser) -> Self {
839 let action = PostAuthAction::continue_grant(grant.id);
840 Self {
841 grant,
842 client,
843 action,
844 matrix_user,
845 }
846 }
847}
848
849#[derive(Serialize, Debug)]
850#[serde(tag = "grant_type")]
851enum PolicyViolationGrant {
852 #[serde(rename = "authorization_code")]
853 Authorization(AuthorizationGrant),
854 #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
855 DeviceCode(DeviceCodeGrant),
856}
857
858#[derive(Serialize, Debug)]
860pub struct PolicyViolationContext {
861 grant: PolicyViolationGrant,
862 client: Client,
863 action: PostAuthAction,
864 violations: Vec<Violation>,
865}
866
867impl TemplateContext for PolicyViolationContext {
868 fn sample<R: RngExt>(
869 now: chrono::DateTime<Utc>,
870 rng: &mut R,
871 _locales: &[DataLocale],
872 ) -> BTreeMap<SampleIdentifier, Self>
873 where
874 Self: Sized,
875 {
876 sample_list(
877 Client::samples(now, rng)
878 .into_iter()
879 .flat_map(|client| {
880 let mut grant = AuthorizationGrant::sample(now, rng);
881 grant.client_id = client.id;
883
884 let authorization_grant = PolicyViolationContext::for_authorization_grant(
885 grant.clone(),
886 client.clone(),
887 Vec::new(),
888 );
889
890 let authorization_grant_invalid_scope =
891 PolicyViolationContext::for_authorization_grant(
892 grant.clone(),
893 client.clone(),
894 vec![Violation {
895 msg: "scope 'foo' not allowed".to_owned(),
896 redirect_uri: None,
897 field: None,
898 variant: None,
899 }],
900 );
901 let authorization_grant_admin_scope =
902 PolicyViolationContext::for_authorization_grant(
903 grant.clone(),
904 client.clone(),
905 vec![Violation {
906 msg: "scope 'urn:mas:admin' requires admin privileges".to_owned(),
907 redirect_uri: None,
908 field: None,
909 variant: Some(ViolationVariant::AdminScopeNotAllowed),
910 }],
911 );
912
913 let authorization_grant_client_not_allowed =
914 PolicyViolationContext::for_authorization_grant(
915 grant,
916 client.clone(),
917 vec![Violation {
918 msg: "This client is not allowed to login".to_owned(),
919 redirect_uri: None,
920 field: None,
921 variant: Some(ViolationVariant::ClientNotAllowed),
922 }],
923 );
924
925 let device_code_grant = PolicyViolationContext::for_device_code_grant(
926 DeviceCodeGrant {
927 id: Ulid::from_datetime_with_rng(now, rng),
928 state: mas_data_model::DeviceCodeGrantState::Pending,
929 client_id: client.id,
930 scope: [OPENID].into_iter().collect(),
931 user_code: mas_data_model::generate_user_code(rng),
932 device_code: Alphanumeric.sample_string(rng, 32),
933 created_at: now - Duration::try_minutes(5).unwrap(),
934 expires_at: now + Duration::try_minutes(25).unwrap(),
935 ip_address: None,
936 user_agent: None,
937 locale: None,
938 },
939 client.clone(),
940 Vec::new(),
941 );
942
943 let device_code_grant_invalid_scope =
944 PolicyViolationContext::for_device_code_grant(
945 DeviceCodeGrant {
946 id: Ulid::from_datetime_with_rng(now, rng),
947 state: mas_data_model::DeviceCodeGrantState::Pending,
948 client_id: client.id,
949 scope: [OPENID].into_iter().collect(),
950 user_code: mas_data_model::generate_user_code(rng),
951 device_code: Alphanumeric.sample_string(rng, 32),
952 created_at: now - Duration::try_minutes(5).unwrap(),
953 expires_at: now + Duration::try_minutes(25).unwrap(),
954 ip_address: None,
955 user_agent: None,
956 locale: None,
957 },
958 client,
959 vec![Violation {
960 msg: "user has too many active sessions".to_owned(),
961 redirect_uri: None,
962 field: None,
963 variant: Some(ViolationVariant::TooManySessions {
964 need_to_remove: 1,
965 }),
966 }],
967 );
968
969 [
970 authorization_grant,
971 authorization_grant_invalid_scope,
972 authorization_grant_admin_scope,
973 authorization_grant_client_not_allowed,
974 device_code_grant,
975 device_code_grant_invalid_scope,
976 ]
977 })
978 .collect(),
979 )
980 }
981}
982
983impl PolicyViolationContext {
984 #[must_use]
987 pub const fn for_authorization_grant(
988 grant: AuthorizationGrant,
989 client: Client,
990 violations: Vec<Violation>,
991 ) -> Self {
992 let action = PostAuthAction::continue_grant(grant.id);
993 Self {
994 grant: PolicyViolationGrant::Authorization(grant),
995 client,
996 action,
997 violations,
998 }
999 }
1000
1001 #[must_use]
1004 pub const fn for_device_code_grant(
1005 grant: DeviceCodeGrant,
1006 client: Client,
1007 violations: Vec<Violation>,
1008 ) -> Self {
1009 let action = PostAuthAction::continue_device_code_grant(grant.id);
1010 Self {
1011 grant: PolicyViolationGrant::DeviceCode(grant),
1012 client,
1013 action,
1014 violations,
1015 }
1016 }
1017}
1018
1019#[derive(Serialize)]
1021pub struct CompatLoginPolicyViolationContext {
1022 violations: Vec<Violation>,
1023}
1024
1025impl TemplateContext for CompatLoginPolicyViolationContext {
1026 fn sample<R: RngExt>(
1027 _now: chrono::DateTime<Utc>,
1028 _rng: &mut R,
1029 _locales: &[DataLocale],
1030 ) -> BTreeMap<SampleIdentifier, Self>
1031 where
1032 Self: Sized,
1033 {
1034 sample_list(vec![
1035 CompatLoginPolicyViolationContext { violations: vec![] },
1036 CompatLoginPolicyViolationContext {
1037 violations: vec![Violation {
1038 msg: "scope 'foo' not allowed".to_owned(),
1039 redirect_uri: None,
1040 field: None,
1041 variant: None,
1042 }],
1043 },
1044 CompatLoginPolicyViolationContext {
1045 violations: vec![Violation {
1046 msg: "user has too many active sessions".to_owned(),
1047 redirect_uri: None,
1048 field: None,
1049 variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1050 }],
1051 },
1052 CompatLoginPolicyViolationContext {
1053 violations: vec![Violation {
1054 msg: "This client is not allowed to login".to_owned(),
1055 redirect_uri: None,
1056 field: None,
1057 variant: Some(ViolationVariant::ClientNotAllowed),
1058 }],
1059 },
1060 CompatLoginPolicyViolationContext {
1061 violations: vec![
1062 Violation {
1063 msg: "scope 'foo' not allowed".to_owned(),
1064 redirect_uri: None,
1065 field: None,
1066 variant: None,
1067 },
1068 Violation {
1069 msg: "user has too many active sessions".to_owned(),
1070 redirect_uri: None,
1071 field: None,
1072 variant: Some(ViolationVariant::TooManySessions { need_to_remove: 1 }),
1073 },
1074 ],
1075 },
1076 ])
1077 }
1078}
1079
1080impl CompatLoginPolicyViolationContext {
1081 #[must_use]
1084 pub const fn for_violations(violations: Vec<Violation>) -> Self {
1085 Self { violations }
1086 }
1087}
1088
1089#[derive(Serialize)]
1091pub struct CompatSsoContext {
1092 login: CompatSsoLogin,
1093 action: PostAuthAction,
1094 matrix_user: MatrixUser,
1095}
1096
1097impl TemplateContext for CompatSsoContext {
1098 fn sample<R: RngExt>(
1099 now: chrono::DateTime<Utc>,
1100 rng: &mut R,
1101 _locales: &[DataLocale],
1102 ) -> BTreeMap<SampleIdentifier, Self>
1103 where
1104 Self: Sized,
1105 {
1106 let id = Ulid::from_datetime_with_rng(now, rng);
1107 sample_list(vec![CompatSsoContext::new(
1108 CompatSsoLogin {
1109 id,
1110 redirect_uri: Url::parse("https://app.element.io/").unwrap(),
1111 login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
1112 created_at: now,
1113 state: CompatSsoLoginState::Pending,
1114 },
1115 MatrixUser {
1116 mxid: "@alice:example.com".to_owned(),
1117 display_name: Some("Alice".to_owned()),
1118 },
1119 )])
1120 }
1121}
1122
1123impl CompatSsoContext {
1124 #[must_use]
1126 pub fn new(login: CompatSsoLogin, matrix_user: MatrixUser) -> Self
1127where {
1128 let action = PostAuthAction::continue_compat_sso_login(login.id);
1129 Self {
1130 login,
1131 action,
1132 matrix_user,
1133 }
1134 }
1135}
1136
1137#[derive(Serialize)]
1139pub struct EmailRecoveryContext {
1140 user: User,
1141 session: UserRecoverySession,
1142 recovery_link: Url,
1143}
1144
1145impl EmailRecoveryContext {
1146 #[must_use]
1148 pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
1149 Self {
1150 user,
1151 session,
1152 recovery_link,
1153 }
1154 }
1155
1156 #[must_use]
1158 pub fn user(&self) -> &User {
1159 &self.user
1160 }
1161
1162 #[must_use]
1164 pub fn session(&self) -> &UserRecoverySession {
1165 &self.session
1166 }
1167}
1168
1169impl TemplateContext for EmailRecoveryContext {
1170 fn sample<R: RngExt>(
1171 now: chrono::DateTime<Utc>,
1172 rng: &mut R,
1173 _locales: &[DataLocale],
1174 ) -> BTreeMap<SampleIdentifier, Self>
1175 where
1176 Self: Sized,
1177 {
1178 sample_list(User::samples(now, rng).into_iter().map(|user| {
1179 let session = UserRecoverySession {
1180 id: Ulid::from_datetime_with_rng(now, rng),
1181 email: "hello@example.com".to_owned(),
1182 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(),
1183 ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
1184 locale: "en".to_owned(),
1185 created_at: now,
1186 consumed_at: None,
1187 };
1188
1189 let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
1190
1191 Self::new(user, session, link)
1192 }).collect())
1193 }
1194}
1195
1196#[derive(Serialize)]
1198pub struct EmailVerificationContext {
1199 #[serde(skip_serializing_if = "Option::is_none")]
1200 browser_session: Option<BrowserSession>,
1201 #[serde(skip_serializing_if = "Option::is_none")]
1202 user_registration: Option<UserRegistration>,
1203 authentication_code: UserEmailAuthenticationCode,
1204}
1205
1206impl EmailVerificationContext {
1207 #[must_use]
1209 pub fn new(
1210 authentication_code: UserEmailAuthenticationCode,
1211 browser_session: Option<BrowserSession>,
1212 user_registration: Option<UserRegistration>,
1213 ) -> Self {
1214 Self {
1215 browser_session,
1216 user_registration,
1217 authentication_code,
1218 }
1219 }
1220
1221 #[must_use]
1223 pub fn user(&self) -> Option<&User> {
1224 self.browser_session.as_ref().map(|s| &s.user)
1225 }
1226
1227 #[must_use]
1229 pub fn code(&self) -> &str {
1230 &self.authentication_code.code
1231 }
1232}
1233
1234impl TemplateContext for EmailVerificationContext {
1235 fn sample<R: RngExt>(
1236 now: chrono::DateTime<Utc>,
1237 rng: &mut R,
1238 _locales: &[DataLocale],
1239 ) -> BTreeMap<SampleIdentifier, Self>
1240 where
1241 Self: Sized,
1242 {
1243 sample_list(
1244 BrowserSession::samples(now, rng)
1245 .into_iter()
1246 .map(|browser_session| {
1247 let authentication_code = UserEmailAuthenticationCode {
1248 id: Ulid::from_datetime_with_rng(now, rng),
1249 user_email_authentication_id: Ulid::from_datetime_with_rng(now, rng),
1250 code: "123456".to_owned(),
1251 created_at: now - Duration::try_minutes(5).unwrap(),
1252 expires_at: now + Duration::try_minutes(25).unwrap(),
1253 };
1254
1255 Self {
1256 browser_session: Some(browser_session),
1257 user_registration: None,
1258 authentication_code,
1259 }
1260 })
1261 .collect(),
1262 )
1263 }
1264}
1265
1266#[derive(Serialize)]
1268pub struct EmailInviteContext {
1269 opens_room: bool,
1270 room_name: Option<String>,
1271 inviter: Option<String>,
1272 inviter_name: Option<String>,
1273 invite_link: Url,
1274}
1275
1276impl EmailInviteContext {
1277 #[must_use]
1279 pub fn new(
1280 opens_room: bool,
1281 room_name: Option<String>,
1282 inviter: Option<String>,
1283 inviter_name: Option<String>,
1284 invite_link: Url,
1285 ) -> Self {
1286 Self {
1287 opens_room,
1288 room_name,
1289 inviter,
1290 inviter_name,
1291 invite_link,
1292 }
1293 }
1294}
1295
1296impl TemplateContext for EmailInviteContext {
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 url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
1306 let invite_link = url_builder.absolute_url_for(
1307 &mas_router::Register::default().with_token("abcdefghijkl".to_owned()),
1308 );
1309
1310 sample_list(vec![
1311 Self::new(
1312 true,
1313 Some("Project X".to_owned()),
1314 Some("@alice:example.com".to_owned()),
1315 Some("Alice O'Brien & Co".to_owned()),
1316 invite_link.clone(),
1317 ),
1318 Self::new(
1319 true,
1320 None,
1321 Some("@alice:example.com".to_owned()),
1322 Some("Alice O'Brien & Co".to_owned()),
1323 invite_link.clone(),
1324 ),
1325 Self::new(
1326 true,
1327 Some("Project X".to_owned()),
1328 None,
1329 Some("Alice O'Brien & Co".to_owned()),
1330 invite_link.clone(),
1331 ),
1332 Self::new(
1333 true,
1334 None,
1335 Some("@alice:example.com".to_owned()),
1336 None,
1337 invite_link.clone(),
1338 ),
1339 Self::new(
1340 true,
1341 Some("Project X".to_owned()),
1342 None,
1343 None,
1344 invite_link.clone(),
1345 ),
1346 Self::new(true, None, None, None, invite_link.clone()),
1347 Self::new(
1348 false,
1349 None,
1350 Some("@alice:example.com".to_owned()),
1351 Some("Alice O'Brien & Co".to_owned()),
1352 invite_link.clone(),
1353 ),
1354 Self::new(
1355 false,
1356 None,
1357 Some("@alice:example.com".to_owned()),
1358 None,
1359 invite_link.clone(),
1360 ),
1361 Self::new(false, None, None, None, invite_link),
1362 ])
1363 }
1364}
1365
1366#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1368#[serde(rename_all = "snake_case")]
1369pub enum RegisterStepsVerifyEmailFormField {
1370 Code,
1372}
1373
1374impl FormField for RegisterStepsVerifyEmailFormField {
1375 fn keep(&self) -> bool {
1376 match self {
1377 Self::Code => true,
1378 }
1379 }
1380}
1381
1382#[derive(Serialize)]
1384pub struct RegisterStepsVerifyEmailContext {
1385 form: FormState<RegisterStepsVerifyEmailFormField>,
1386 authentication: UserEmailAuthentication,
1387}
1388
1389impl RegisterStepsVerifyEmailContext {
1390 #[must_use]
1392 pub fn new(authentication: UserEmailAuthentication) -> Self {
1393 Self {
1394 form: FormState::default(),
1395 authentication,
1396 }
1397 }
1398
1399 #[must_use]
1401 pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
1402 Self { form, ..self }
1403 }
1404}
1405
1406impl TemplateContext for RegisterStepsVerifyEmailContext {
1407 fn sample<R: RngExt>(
1408 now: chrono::DateTime<Utc>,
1409 rng: &mut R,
1410 _locales: &[DataLocale],
1411 ) -> BTreeMap<SampleIdentifier, Self>
1412 where
1413 Self: Sized,
1414 {
1415 let authentication = UserEmailAuthentication {
1416 id: Ulid::from_datetime_with_rng(now, rng),
1417 user_session_id: None,
1418 user_registration_id: None,
1419 email: "foobar@example.com".to_owned(),
1420 created_at: now,
1421 completed_at: None,
1422 };
1423
1424 sample_list(vec![Self {
1425 form: FormState::default(),
1426 authentication,
1427 }])
1428 }
1429}
1430
1431#[derive(Serialize)]
1433pub struct RegisterStepsEmailInUseContext {
1434 email: String,
1435 action: Option<PostAuthAction>,
1436}
1437
1438impl RegisterStepsEmailInUseContext {
1439 #[must_use]
1441 pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
1442 Self { email, action }
1443 }
1444}
1445
1446impl TemplateContext for RegisterStepsEmailInUseContext {
1447 fn sample<R: RngExt>(
1448 _now: chrono::DateTime<Utc>,
1449 _rng: &mut R,
1450 _locales: &[DataLocale],
1451 ) -> BTreeMap<SampleIdentifier, Self>
1452 where
1453 Self: Sized,
1454 {
1455 let email = "hello@example.com".to_owned();
1456 let action = PostAuthAction::continue_grant(Ulid::nil());
1457 sample_list(vec![Self::new(email, Some(action))])
1458 }
1459}
1460
1461#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1463#[serde(rename_all = "snake_case")]
1464pub enum RegisterStepsDisplayNameFormField {
1465 DisplayName,
1467}
1468
1469impl FormField for RegisterStepsDisplayNameFormField {
1470 fn keep(&self) -> bool {
1471 match self {
1472 Self::DisplayName => true,
1473 }
1474 }
1475}
1476
1477#[derive(Serialize, Default)]
1479pub struct RegisterStepsDisplayNameContext {
1480 form: FormState<RegisterStepsDisplayNameFormField>,
1481}
1482
1483impl RegisterStepsDisplayNameContext {
1484 #[must_use]
1486 pub fn new() -> Self {
1487 Self::default()
1488 }
1489
1490 #[must_use]
1492 pub fn with_form_state(
1493 mut self,
1494 form_state: FormState<RegisterStepsDisplayNameFormField>,
1495 ) -> Self {
1496 self.form = form_state;
1497 self
1498 }
1499}
1500
1501impl TemplateContext for RegisterStepsDisplayNameContext {
1502 fn sample<R: RngExt>(
1503 _now: chrono::DateTime<chrono::Utc>,
1504 _rng: &mut R,
1505 _locales: &[DataLocale],
1506 ) -> BTreeMap<SampleIdentifier, Self>
1507 where
1508 Self: Sized,
1509 {
1510 sample_list(vec![Self {
1511 form: FormState::default(),
1512 }])
1513 }
1514}
1515
1516#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1518#[serde(rename_all = "snake_case")]
1519pub enum RegisterStepsRegistrationTokenFormField {
1520 Token,
1522}
1523
1524impl FormField for RegisterStepsRegistrationTokenFormField {
1525 fn keep(&self) -> bool {
1526 match self {
1527 Self::Token => true,
1528 }
1529 }
1530}
1531
1532#[derive(Serialize, Default)]
1534pub struct RegisterStepsRegistrationTokenContext {
1535 form: FormState<RegisterStepsRegistrationTokenFormField>,
1536}
1537
1538impl RegisterStepsRegistrationTokenContext {
1539 #[must_use]
1541 pub fn new() -> Self {
1542 Self::default()
1543 }
1544
1545 #[must_use]
1547 pub fn with_form_state(
1548 mut self,
1549 form_state: FormState<RegisterStepsRegistrationTokenFormField>,
1550 ) -> Self {
1551 self.form = form_state;
1552 self
1553 }
1554}
1555
1556impl TemplateContext for RegisterStepsRegistrationTokenContext {
1557 fn sample<R: RngExt>(
1558 _now: chrono::DateTime<chrono::Utc>,
1559 _rng: &mut R,
1560 _locales: &[DataLocale],
1561 ) -> BTreeMap<SampleIdentifier, Self>
1562 where
1563 Self: Sized,
1564 {
1565 sample_list(vec![Self {
1566 form: FormState::default(),
1567 }])
1568 }
1569}
1570
1571#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1573#[serde(rename_all = "snake_case")]
1574pub enum RecoveryStartFormField {
1575 Email,
1577}
1578
1579impl FormField for RecoveryStartFormField {
1580 fn keep(&self) -> bool {
1581 match self {
1582 Self::Email => true,
1583 }
1584 }
1585}
1586
1587#[derive(Serialize, Default)]
1589pub struct RecoveryStartContext {
1590 form: FormState<RecoveryStartFormField>,
1591}
1592
1593impl RecoveryStartContext {
1594 #[must_use]
1596 pub fn new() -> Self {
1597 Self::default()
1598 }
1599
1600 #[must_use]
1602 pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1603 Self { form }
1604 }
1605}
1606
1607impl TemplateContext for RecoveryStartContext {
1608 fn sample<R: RngExt>(
1609 _now: chrono::DateTime<Utc>,
1610 _rng: &mut R,
1611 _locales: &[DataLocale],
1612 ) -> BTreeMap<SampleIdentifier, Self>
1613 where
1614 Self: Sized,
1615 {
1616 sample_list(vec![
1617 Self::new(),
1618 Self::new().with_form_state(
1619 FormState::default()
1620 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1621 ),
1622 Self::new().with_form_state(
1623 FormState::default()
1624 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1625 ),
1626 ])
1627 }
1628}
1629
1630#[derive(Serialize)]
1632pub struct RecoveryProgressContext {
1633 session: UserRecoverySession,
1634 resend_failed_due_to_rate_limit: bool,
1636}
1637
1638impl RecoveryProgressContext {
1639 #[must_use]
1641 pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1642 Self {
1643 session,
1644 resend_failed_due_to_rate_limit,
1645 }
1646 }
1647}
1648
1649impl TemplateContext for RecoveryProgressContext {
1650 fn sample<R: RngExt>(
1651 now: chrono::DateTime<Utc>,
1652 rng: &mut R,
1653 _locales: &[DataLocale],
1654 ) -> BTreeMap<SampleIdentifier, Self>
1655 where
1656 Self: Sized,
1657 {
1658 let session = UserRecoverySession {
1659 id: Ulid::from_datetime_with_rng(now, rng),
1660 email: "name@mail.com".to_owned(),
1661 user_agent: "Mozilla/5.0".to_owned(),
1662 ip_address: None,
1663 locale: "en".to_owned(),
1664 created_at: now,
1665 consumed_at: None,
1666 };
1667
1668 sample_list(vec![
1669 Self {
1670 session: session.clone(),
1671 resend_failed_due_to_rate_limit: false,
1672 },
1673 Self {
1674 session,
1675 resend_failed_due_to_rate_limit: true,
1676 },
1677 ])
1678 }
1679}
1680
1681#[derive(Serialize)]
1683pub struct RecoveryExpiredContext {
1684 session: UserRecoverySession,
1685}
1686
1687impl RecoveryExpiredContext {
1688 #[must_use]
1690 pub fn new(session: UserRecoverySession) -> Self {
1691 Self { session }
1692 }
1693}
1694
1695impl TemplateContext for RecoveryExpiredContext {
1696 fn sample<R: RngExt>(
1697 now: chrono::DateTime<Utc>,
1698 rng: &mut R,
1699 _locales: &[DataLocale],
1700 ) -> BTreeMap<SampleIdentifier, Self>
1701 where
1702 Self: Sized,
1703 {
1704 let session = UserRecoverySession {
1705 id: Ulid::from_datetime_with_rng(now, rng),
1706 email: "name@mail.com".to_owned(),
1707 user_agent: "Mozilla/5.0".to_owned(),
1708 ip_address: None,
1709 locale: "en".to_owned(),
1710 created_at: now,
1711 consumed_at: None,
1712 };
1713
1714 sample_list(vec![Self { session }])
1715 }
1716}
1717#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1719#[serde(rename_all = "snake_case")]
1720pub enum RecoveryFinishFormField {
1721 NewPassword,
1723
1724 NewPasswordConfirm,
1726}
1727
1728impl FormField for RecoveryFinishFormField {
1729 fn keep(&self) -> bool {
1730 false
1731 }
1732}
1733
1734#[derive(Serialize)]
1736pub struct RecoveryFinishContext {
1737 user: User,
1738 form: FormState<RecoveryFinishFormField>,
1739}
1740
1741impl RecoveryFinishContext {
1742 #[must_use]
1744 pub fn new(user: User) -> Self {
1745 Self {
1746 user,
1747 form: FormState::default(),
1748 }
1749 }
1750
1751 #[must_use]
1753 pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1754 self.form = form;
1755 self
1756 }
1757}
1758
1759impl TemplateContext for RecoveryFinishContext {
1760 fn sample<R: RngExt>(
1761 now: chrono::DateTime<Utc>,
1762 rng: &mut R,
1763 _locales: &[DataLocale],
1764 ) -> BTreeMap<SampleIdentifier, Self>
1765 where
1766 Self: Sized,
1767 {
1768 sample_list(
1769 User::samples(now, rng)
1770 .into_iter()
1771 .flat_map(|user| {
1772 vec![
1773 Self::new(user.clone()),
1774 Self::new(user.clone()).with_form_state(
1775 FormState::default().with_error_on_field(
1776 RecoveryFinishFormField::NewPassword,
1777 FieldError::Invalid,
1778 ),
1779 ),
1780 Self::new(user.clone()).with_form_state(
1781 FormState::default().with_error_on_field(
1782 RecoveryFinishFormField::NewPasswordConfirm,
1783 FieldError::Invalid,
1784 ),
1785 ),
1786 ]
1787 })
1788 .collect(),
1789 )
1790 }
1791}
1792
1793#[derive(Serialize)]
1796pub struct UpstreamExistingLinkContext {
1797 linked_user: User,
1798}
1799
1800impl UpstreamExistingLinkContext {
1801 #[must_use]
1803 pub fn new(linked_user: User) -> Self {
1804 Self { linked_user }
1805 }
1806}
1807
1808impl TemplateContext for UpstreamExistingLinkContext {
1809 fn sample<R: RngExt>(
1810 now: chrono::DateTime<Utc>,
1811 rng: &mut R,
1812 _locales: &[DataLocale],
1813 ) -> BTreeMap<SampleIdentifier, Self>
1814 where
1815 Self: Sized,
1816 {
1817 sample_list(
1818 User::samples(now, rng)
1819 .into_iter()
1820 .map(|linked_user| Self { linked_user })
1821 .collect(),
1822 )
1823 }
1824}
1825
1826#[derive(Serialize)]
1829pub struct UpstreamSuggestLink {
1830 post_logout_action: PostAuthAction,
1831}
1832
1833impl UpstreamSuggestLink {
1834 #[must_use]
1836 pub fn new(link: &UpstreamOAuthLink) -> Self {
1837 Self::for_link_id(link.id)
1838 }
1839
1840 fn for_link_id(id: Ulid) -> Self {
1841 let post_logout_action = PostAuthAction::link_upstream(id);
1842 Self { post_logout_action }
1843 }
1844}
1845
1846impl TemplateContext for UpstreamSuggestLink {
1847 fn sample<R: RngExt>(
1848 now: chrono::DateTime<Utc>,
1849 rng: &mut R,
1850 _locales: &[DataLocale],
1851 ) -> BTreeMap<SampleIdentifier, Self>
1852 where
1853 Self: Sized,
1854 {
1855 let id = Ulid::from_datetime_with_rng(now, rng);
1856 sample_list(vec![Self::for_link_id(id)])
1857 }
1858}
1859
1860#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1862#[serde(rename_all = "snake_case")]
1863pub enum UpstreamRegisterFormField {
1864 Username,
1866
1867 AcceptTerms,
1869}
1870
1871impl FormField for UpstreamRegisterFormField {
1872 fn keep(&self) -> bool {
1873 match self {
1874 Self::Username | Self::AcceptTerms => true,
1875 }
1876 }
1877}
1878
1879#[derive(Serialize)]
1882pub struct UpstreamRegister {
1883 upstream_oauth_link: UpstreamOAuthLink,
1884 upstream_oauth_provider: UpstreamOAuthProvider,
1885 imported_localpart: Option<String>,
1886 force_localpart: bool,
1887 imported_display_name: Option<String>,
1888 force_display_name: bool,
1889 imported_email: Option<String>,
1890 force_email: bool,
1891 form_state: FormState<UpstreamRegisterFormField>,
1892}
1893
1894impl UpstreamRegister {
1895 #[must_use]
1898 pub fn new(
1899 upstream_oauth_link: UpstreamOAuthLink,
1900 upstream_oauth_provider: UpstreamOAuthProvider,
1901 ) -> Self {
1902 Self {
1903 upstream_oauth_link,
1904 upstream_oauth_provider,
1905 imported_localpart: None,
1906 force_localpart: false,
1907 imported_display_name: None,
1908 force_display_name: false,
1909 imported_email: None,
1910 force_email: false,
1911 form_state: FormState::default(),
1912 }
1913 }
1914
1915 pub fn set_localpart(&mut self, localpart: String, force: bool) {
1917 self.imported_localpart = Some(localpart);
1918 self.force_localpart = force;
1919 }
1920
1921 #[must_use]
1923 pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1924 Self {
1925 imported_localpart: Some(localpart),
1926 force_localpart: force,
1927 ..self
1928 }
1929 }
1930
1931 pub fn set_display_name(&mut self, display_name: String, force: bool) {
1933 self.imported_display_name = Some(display_name);
1934 self.force_display_name = force;
1935 }
1936
1937 #[must_use]
1939 pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1940 Self {
1941 imported_display_name: Some(display_name),
1942 force_display_name: force,
1943 ..self
1944 }
1945 }
1946
1947 pub fn set_email(&mut self, email: String, force: bool) {
1949 self.imported_email = Some(email);
1950 self.force_email = force;
1951 }
1952
1953 #[must_use]
1955 pub fn with_email(self, email: String, force: bool) -> Self {
1956 Self {
1957 imported_email: Some(email),
1958 force_email: force,
1959 ..self
1960 }
1961 }
1962
1963 pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1965 self.form_state = form_state;
1966 }
1967
1968 #[must_use]
1970 pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1971 Self { form_state, ..self }
1972 }
1973}
1974
1975impl TemplateContext for UpstreamRegister {
1976 fn sample<R: RngExt>(
1977 now: chrono::DateTime<Utc>,
1978 _rng: &mut R,
1979 _locales: &[DataLocale],
1980 ) -> BTreeMap<SampleIdentifier, Self>
1981 where
1982 Self: Sized,
1983 {
1984 sample_list(vec![Self::new(
1985 UpstreamOAuthLink {
1986 id: Ulid::nil(),
1987 provider_id: Ulid::nil(),
1988 user_id: None,
1989 subject: "subject".to_owned(),
1990 human_account_name: Some("@john".to_owned()),
1991 created_at: now,
1992 },
1993 UpstreamOAuthProvider {
1994 id: Ulid::nil(),
1995 issuer: Some("https://example.com/".to_owned()),
1996 human_name: Some("Example Ltd.".to_owned()),
1997 brand_name: None,
1998 scope: Scope::from_iter([OPENID]),
1999 token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
2000 token_endpoint_signing_alg: None,
2001 id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
2002 client_id: "client-id".to_owned(),
2003 encrypted_client_secret: None,
2004 claims_imports: UpstreamOAuthProviderClaimsImports::default(),
2005 authorization_endpoint_override: None,
2006 token_endpoint_override: None,
2007 jwks_uri_override: None,
2008 userinfo_endpoint_override: None,
2009 fetch_userinfo: false,
2010 userinfo_signed_response_alg: None,
2011 discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
2012 pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
2013 response_mode: None,
2014 additional_authorization_parameters: Vec::new(),
2015 forward_login_hint: false,
2016 created_at: now,
2017 disabled_at: None,
2018 on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing,
2019 registration_token_required: false,
2020 },
2021 )])
2022 }
2023}
2024
2025#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
2027#[serde(rename_all = "snake_case")]
2028pub enum DeviceLinkFormField {
2029 Code,
2031}
2032
2033impl FormField for DeviceLinkFormField {
2034 fn keep(&self) -> bool {
2035 match self {
2036 Self::Code => true,
2037 }
2038 }
2039}
2040
2041#[derive(Serialize, Default, Debug)]
2043pub struct DeviceLinkContext {
2044 form_state: FormState<DeviceLinkFormField>,
2045}
2046
2047impl DeviceLinkContext {
2048 #[must_use]
2050 pub fn new() -> Self {
2051 Self::default()
2052 }
2053
2054 #[must_use]
2056 pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
2057 self.form_state = form_state;
2058 self
2059 }
2060}
2061
2062impl TemplateContext for DeviceLinkContext {
2063 fn sample<R: RngExt>(
2064 _now: chrono::DateTime<Utc>,
2065 _rng: &mut R,
2066 _locales: &[DataLocale],
2067 ) -> BTreeMap<SampleIdentifier, Self>
2068 where
2069 Self: Sized,
2070 {
2071 sample_list(vec![
2072 Self::new(),
2073 Self::new().with_form_state(
2074 FormState::default()
2075 .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
2076 ),
2077 ])
2078 }
2079}
2080
2081#[derive(Serialize, Debug)]
2083pub struct DeviceConsentContext {
2084 grant: DeviceCodeGrant,
2085 client: Client,
2086 matrix_user: MatrixUser,
2087}
2088
2089impl DeviceConsentContext {
2090 #[must_use]
2092 pub fn new(grant: DeviceCodeGrant, client: Client, matrix_user: MatrixUser) -> Self {
2093 Self {
2094 grant,
2095 client,
2096 matrix_user,
2097 }
2098 }
2099}
2100
2101impl TemplateContext for DeviceConsentContext {
2102 fn sample<R: RngExt>(
2103 now: chrono::DateTime<Utc>,
2104 rng: &mut R,
2105 _locales: &[DataLocale],
2106 ) -> BTreeMap<SampleIdentifier, Self>
2107 where
2108 Self: Sized,
2109 {
2110 sample_list(Client::samples(now, rng)
2111 .into_iter()
2112 .map(|client| {
2113 let grant = DeviceCodeGrant {
2114 id: Ulid::from_datetime_with_rng(now, rng),
2115 state: mas_data_model::DeviceCodeGrantState::Pending,
2116 client_id: client.id,
2117 scope: [OPENID].into_iter().collect(),
2118 user_code: mas_data_model::generate_user_code(rng),
2119 device_code: Alphanumeric.sample_string(rng, 32),
2120 created_at: now - Duration::try_minutes(5).unwrap(),
2121 expires_at: now + Duration::try_minutes(25).unwrap(),
2122 ip_address: Some(IpAddr::V4(Ipv4Addr::LOCALHOST)),
2123 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()),
2124 locale: None,
2125 };
2126 Self {
2127 grant,
2128 client,
2129 matrix_user: MatrixUser {
2130 mxid: "@alice:example.com".to_owned(),
2131 display_name: Some("Alice".to_owned()),
2132 }
2133 }
2134 })
2135 .collect())
2136 }
2137}
2138
2139#[derive(Serialize)]
2142pub struct AccountInactiveContext {
2143 user: User,
2144
2145 #[serde(skip_serializing_if = "Option::is_none")]
2148 post_logout_action: Option<PostAuthAction>,
2149}
2150
2151impl AccountInactiveContext {
2152 #[must_use]
2154 pub fn new(user: User) -> Self {
2155 Self {
2156 user,
2157 post_logout_action: None,
2158 }
2159 }
2160
2161 #[must_use]
2163 pub fn with_post_auth_action(mut self, action: Option<PostAuthAction>) -> Self {
2164 self.post_logout_action = action;
2165 self
2166 }
2167}
2168
2169impl TemplateContext for AccountInactiveContext {
2170 fn sample<R: RngExt>(
2171 now: chrono::DateTime<Utc>,
2172 rng: &mut R,
2173 _locales: &[DataLocale],
2174 ) -> BTreeMap<SampleIdentifier, Self>
2175 where
2176 Self: Sized,
2177 {
2178 let action = PostAuthAction::continue_grant(Ulid::from_datetime_with_rng(now, rng));
2179 sample_list(
2180 User::samples(now, rng)
2181 .into_iter()
2182 .flat_map(|user| {
2183 [
2186 AccountInactiveContext::new(user.clone()),
2187 AccountInactiveContext::new(user)
2188 .with_post_auth_action(Some(action.clone())),
2189 ]
2190 })
2191 .collect(),
2192 )
2193 }
2194}
2195
2196#[derive(Serialize)]
2198pub struct DeviceNameContext {
2199 client: Client,
2200 raw_user_agent: String,
2201}
2202
2203impl DeviceNameContext {
2204 #[must_use]
2206 pub fn new(client: Client, user_agent: Option<String>) -> Self {
2207 Self {
2208 client,
2209 raw_user_agent: user_agent.unwrap_or_default(),
2210 }
2211 }
2212}
2213
2214impl TemplateContext for DeviceNameContext {
2215 fn sample<R: RngExt>(
2216 now: chrono::DateTime<Utc>,
2217 rng: &mut R,
2218 _locales: &[DataLocale],
2219 ) -> BTreeMap<SampleIdentifier, Self>
2220 where
2221 Self: Sized,
2222 {
2223 sample_list(Client::samples(now, rng)
2224 .into_iter()
2225 .map(|client| DeviceNameContext {
2226 client,
2227 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(),
2228 })
2229 .collect())
2230 }
2231}
2232
2233#[derive(Serialize)]
2235pub struct FormPostContext<T> {
2236 redirect_uri: Option<Url>,
2237 params: T,
2238}
2239
2240impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
2241 fn sample<R: RngExt>(
2242 now: chrono::DateTime<Utc>,
2243 rng: &mut R,
2244 locales: &[DataLocale],
2245 ) -> BTreeMap<SampleIdentifier, Self>
2246 where
2247 Self: Sized,
2248 {
2249 let sample_params = T::sample(now, rng, locales);
2250 sample_params
2251 .into_iter()
2252 .map(|(k, params)| {
2253 (
2254 k,
2255 FormPostContext {
2256 redirect_uri: "https://example.com/callback".parse().ok(),
2257 params,
2258 },
2259 )
2260 })
2261 .collect()
2262 }
2263}
2264
2265impl<T> FormPostContext<T> {
2266 pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
2269 Self {
2270 redirect_uri: Some(redirect_uri),
2271 params,
2272 }
2273 }
2274
2275 pub fn new_for_current_url(params: T) -> Self {
2278 Self {
2279 redirect_uri: None,
2280 params,
2281 }
2282 }
2283
2284 pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
2289 WithLanguage {
2290 lang: lang.to_string(),
2291 inner: self,
2292 }
2293 }
2294}
2295
2296#[derive(Default, Serialize, Debug, Clone)]
2298pub struct ErrorContext {
2299 code: Option<&'static str>,
2300 description: Option<String>,
2301 details: Option<String>,
2302 lang: Option<String>,
2303}
2304
2305impl std::fmt::Display for ErrorContext {
2306 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
2307 if let Some(code) = &self.code {
2308 writeln!(f, "code: {code}")?;
2309 }
2310 if let Some(description) = &self.description {
2311 writeln!(f, "{description}")?;
2312 }
2313
2314 if let Some(details) = &self.details {
2315 writeln!(f, "details: {details}")?;
2316 }
2317
2318 Ok(())
2319 }
2320}
2321
2322impl TemplateContext for ErrorContext {
2323 fn sample<R: RngExt>(
2324 _now: chrono::DateTime<Utc>,
2325 _rng: &mut R,
2326 _locales: &[DataLocale],
2327 ) -> BTreeMap<SampleIdentifier, Self>
2328 where
2329 Self: Sized,
2330 {
2331 sample_list(vec![
2332 Self::new()
2333 .with_code("sample_error")
2334 .with_description("A fancy description".into())
2335 .with_details("Something happened".into()),
2336 Self::new().with_code("another_error"),
2337 Self::new(),
2338 ])
2339 }
2340}
2341
2342impl ErrorContext {
2343 #[must_use]
2345 pub fn new() -> Self {
2346 Self::default()
2347 }
2348
2349 #[must_use]
2351 pub fn with_code(mut self, code: &'static str) -> Self {
2352 self.code = Some(code);
2353 self
2354 }
2355
2356 #[must_use]
2358 pub fn with_description(mut self, description: String) -> Self {
2359 self.description = Some(description);
2360 self
2361 }
2362
2363 #[must_use]
2365 pub fn with_details(mut self, details: String) -> Self {
2366 self.details = Some(details);
2367 self
2368 }
2369
2370 #[must_use]
2372 pub fn with_language(mut self, lang: &DataLocale) -> Self {
2373 self.lang = Some(lang.to_string());
2374 self
2375 }
2376
2377 #[must_use]
2379 pub fn code(&self) -> Option<&'static str> {
2380 self.code
2381 }
2382
2383 #[must_use]
2385 pub fn description(&self) -> Option<&str> {
2386 self.description.as_deref()
2387 }
2388
2389 #[must_use]
2391 pub fn details(&self) -> Option<&str> {
2392 self.details.as_deref()
2393 }
2394}
2395
2396#[derive(Serialize)]
2398pub struct NotFoundContext {
2399 method: String,
2400 version: String,
2401 uri: String,
2402}
2403
2404impl NotFoundContext {
2405 #[must_use]
2407 pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
2408 Self {
2409 method: method.to_string(),
2410 version: format!("{version:?}"),
2411 uri: uri.to_string(),
2412 }
2413 }
2414}
2415
2416impl TemplateContext for NotFoundContext {
2417 fn sample<R: RngExt>(
2418 _now: DateTime<Utc>,
2419 _rng: &mut R,
2420 _locales: &[DataLocale],
2421 ) -> BTreeMap<SampleIdentifier, Self>
2422 where
2423 Self: Sized,
2424 {
2425 sample_list(vec![
2426 Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
2427 Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
2428 Self::new(
2429 &Method::PUT,
2430 Version::HTTP_10,
2431 &"/foo?bar=baz".parse().unwrap(),
2432 ),
2433 ])
2434 }
2435}