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