1mod branding;
10mod captcha;
11mod ext;
12mod features;
13
14use std::{
15 fmt::Formatter,
16 net::{IpAddr, Ipv4Addr},
17};
18
19use chrono::{DateTime, Duration, Utc};
20use http::{Method, Uri, Version};
21use mas_data_model::{
22 AuthorizationGrant, BrowserSession, Client, CompatSsoLogin, CompatSsoLoginState,
23 DeviceCodeGrant, UpstreamOAuthLink, UpstreamOAuthProvider, UpstreamOAuthProviderClaimsImports,
24 UpstreamOAuthProviderDiscoveryMode, UpstreamOAuthProviderPkceMode,
25 UpstreamOAuthProviderTokenAuthMethod, User, UserEmailAuthentication,
26 UserEmailAuthenticationCode, UserRecoverySession, UserRegistration,
27};
28use mas_i18n::DataLocale;
29use mas_iana::jose::JsonWebSignatureAlg;
30use mas_router::{Account, GraphQL, PostAuthAction, UrlBuilder};
31use oauth2_types::scope::{OPENID, Scope};
32use rand::{
33 Rng,
34 distributions::{Alphanumeric, DistString},
35};
36use serde::{Deserialize, Serialize, ser::SerializeStruct};
37use ulid::Ulid;
38use url::Url;
39
40pub use self::{
41 branding::SiteBranding, captcha::WithCaptcha, ext::SiteConfigExt, features::SiteFeatures,
42};
43use crate::{FieldError, FormField, FormState};
44
45pub trait TemplateContext: Serialize {
47 fn with_session(self, current_session: BrowserSession) -> WithSession<Self>
49 where
50 Self: Sized,
51 {
52 WithSession {
53 current_session,
54 inner: self,
55 }
56 }
57
58 fn maybe_with_session(
60 self,
61 current_session: Option<BrowserSession>,
62 ) -> WithOptionalSession<Self>
63 where
64 Self: Sized,
65 {
66 WithOptionalSession {
67 current_session,
68 inner: self,
69 }
70 }
71
72 fn with_csrf<C>(self, csrf_token: C) -> WithCsrf<Self>
74 where
75 Self: Sized,
76 C: ToString,
77 {
78 WithCsrf {
80 csrf_token: csrf_token.to_string(),
81 inner: self,
82 }
83 }
84
85 fn with_language(self, lang: DataLocale) -> WithLanguage<Self>
87 where
88 Self: Sized,
89 {
90 WithLanguage {
91 lang: lang.to_string(),
92 inner: self,
93 }
94 }
95
96 fn with_captcha(self, captcha: Option<mas_data_model::CaptchaConfig>) -> WithCaptcha<Self>
98 where
99 Self: Sized,
100 {
101 WithCaptcha::new(captcha, self)
102 }
103
104 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
109 where
110 Self: Sized;
111}
112
113impl TemplateContext for () {
114 fn sample(
115 _now: chrono::DateTime<Utc>,
116 _rng: &mut impl Rng,
117 _locales: &[DataLocale],
118 ) -> Vec<Self>
119 where
120 Self: Sized,
121 {
122 Vec::new()
123 }
124}
125
126#[derive(Serialize, Debug)]
128pub struct WithLanguage<T> {
129 lang: String,
130
131 #[serde(flatten)]
132 inner: T,
133}
134
135impl<T> WithLanguage<T> {
136 pub fn language(&self) -> &str {
138 &self.lang
139 }
140}
141
142impl<T> std::ops::Deref for WithLanguage<T> {
143 type Target = T;
144
145 fn deref(&self) -> &Self::Target {
146 &self.inner
147 }
148}
149
150impl<T: TemplateContext> TemplateContext for WithLanguage<T> {
151 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
152 where
153 Self: Sized,
154 {
155 locales
156 .iter()
157 .flat_map(|locale| {
158 T::sample(now, rng, locales)
159 .into_iter()
160 .map(move |inner| WithLanguage {
161 lang: locale.to_string(),
162 inner,
163 })
164 })
165 .collect()
166 }
167}
168
169#[derive(Serialize, Debug)]
171pub struct WithCsrf<T> {
172 csrf_token: String,
173
174 #[serde(flatten)]
175 inner: T,
176}
177
178impl<T: TemplateContext> TemplateContext for WithCsrf<T> {
179 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
180 where
181 Self: Sized,
182 {
183 T::sample(now, rng, locales)
184 .into_iter()
185 .map(|inner| WithCsrf {
186 csrf_token: "fake_csrf_token".into(),
187 inner,
188 })
189 .collect()
190 }
191}
192
193#[derive(Serialize)]
195pub struct WithSession<T> {
196 current_session: BrowserSession,
197
198 #[serde(flatten)]
199 inner: T,
200}
201
202impl<T: TemplateContext> TemplateContext for WithSession<T> {
203 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
204 where
205 Self: Sized,
206 {
207 BrowserSession::samples(now, rng)
208 .into_iter()
209 .flat_map(|session| {
210 T::sample(now, rng, locales)
211 .into_iter()
212 .map(move |inner| WithSession {
213 current_session: session.clone(),
214 inner,
215 })
216 })
217 .collect()
218 }
219}
220
221#[derive(Serialize)]
223pub struct WithOptionalSession<T> {
224 current_session: Option<BrowserSession>,
225
226 #[serde(flatten)]
227 inner: T,
228}
229
230impl<T: TemplateContext> TemplateContext for WithOptionalSession<T> {
231 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
232 where
233 Self: Sized,
234 {
235 BrowserSession::samples(now, rng)
236 .into_iter()
237 .map(Some) .chain(std::iter::once(None)) .flat_map(|session| {
240 T::sample(now, rng, locales)
241 .into_iter()
242 .map(move |inner| WithOptionalSession {
243 current_session: session.clone(),
244 inner,
245 })
246 })
247 .collect()
248 }
249}
250
251pub struct EmptyContext;
253
254impl Serialize for EmptyContext {
255 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
256 where
257 S: serde::Serializer,
258 {
259 let mut s = serializer.serialize_struct("EmptyContext", 0)?;
260 s.serialize_field("__UNUSED", &())?;
263 s.end()
264 }
265}
266
267impl TemplateContext for EmptyContext {
268 fn sample(
269 _now: chrono::DateTime<Utc>,
270 _rng: &mut impl Rng,
271 _locales: &[DataLocale],
272 ) -> Vec<Self>
273 where
274 Self: Sized,
275 {
276 vec![EmptyContext]
277 }
278}
279
280#[derive(Serialize)]
282pub struct IndexContext {
283 discovery_url: Url,
284}
285
286impl IndexContext {
287 #[must_use]
290 pub fn new(discovery_url: Url) -> Self {
291 Self { discovery_url }
292 }
293}
294
295impl TemplateContext for IndexContext {
296 fn sample(
297 _now: chrono::DateTime<Utc>,
298 _rng: &mut impl Rng,
299 _locales: &[DataLocale],
300 ) -> Vec<Self>
301 where
302 Self: Sized,
303 {
304 vec![Self {
305 discovery_url: "https://example.com/.well-known/openid-configuration"
306 .parse()
307 .unwrap(),
308 }]
309 }
310}
311
312#[derive(Serialize)]
314#[serde(rename_all = "camelCase")]
315pub struct AppConfig {
316 root: String,
317 graphql_endpoint: String,
318}
319
320#[derive(Serialize)]
322pub struct AppContext {
323 app_config: AppConfig,
324}
325
326impl AppContext {
327 #[must_use]
329 pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
330 let root = url_builder.relative_url_for(&Account::default());
331 let graphql_endpoint = url_builder.relative_url_for(&GraphQL);
332 Self {
333 app_config: AppConfig {
334 root,
335 graphql_endpoint,
336 },
337 }
338 }
339}
340
341impl TemplateContext for AppContext {
342 fn sample(
343 _now: chrono::DateTime<Utc>,
344 _rng: &mut impl Rng,
345 _locales: &[DataLocale],
346 ) -> Vec<Self>
347 where
348 Self: Sized,
349 {
350 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
351 vec![Self::from_url_builder(&url_builder)]
352 }
353}
354
355#[derive(Serialize)]
357pub struct ApiDocContext {
358 openapi_url: Url,
359 callback_url: Url,
360}
361
362impl ApiDocContext {
363 #[must_use]
366 pub fn from_url_builder(url_builder: &UrlBuilder) -> Self {
367 Self {
368 openapi_url: url_builder.absolute_url_for(&mas_router::ApiSpec),
369 callback_url: url_builder.absolute_url_for(&mas_router::ApiDocCallback),
370 }
371 }
372}
373
374impl TemplateContext for ApiDocContext {
375 fn sample(
376 _now: chrono::DateTime<Utc>,
377 _rng: &mut impl Rng,
378 _locales: &[DataLocale],
379 ) -> Vec<Self>
380 where
381 Self: Sized,
382 {
383 let url_builder = UrlBuilder::new("https://example.com/".parse().unwrap(), None, None);
384 vec![Self::from_url_builder(&url_builder)]
385 }
386}
387
388#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
390#[serde(rename_all = "snake_case")]
391pub enum LoginFormField {
392 Username,
394
395 Password,
397}
398
399impl FormField for LoginFormField {
400 fn keep(&self) -> bool {
401 match self {
402 Self::Username => true,
403 Self::Password => false,
404 }
405 }
406}
407
408#[derive(Serialize)]
410#[serde(tag = "kind", rename_all = "snake_case")]
411pub enum PostAuthContextInner {
412 ContinueAuthorizationGrant {
414 grant: Box<AuthorizationGrant>,
416 },
417
418 ContinueDeviceCodeGrant {
420 grant: Box<DeviceCodeGrant>,
422 },
423
424 ContinueCompatSsoLogin {
427 login: Box<CompatSsoLogin>,
429 },
430
431 ChangePassword,
433
434 LinkUpstream {
436 provider: Box<UpstreamOAuthProvider>,
438
439 link: Box<UpstreamOAuthLink>,
441 },
442
443 ManageAccount,
445}
446
447#[derive(Serialize)]
449pub struct PostAuthContext {
450 pub params: PostAuthAction,
452
453 #[serde(flatten)]
455 pub ctx: PostAuthContextInner,
456}
457
458#[derive(Serialize, Default)]
460pub struct LoginContext {
461 form: FormState<LoginFormField>,
462 next: Option<PostAuthContext>,
463 providers: Vec<UpstreamOAuthProvider>,
464}
465
466impl TemplateContext for LoginContext {
467 fn sample(
468 _now: chrono::DateTime<Utc>,
469 _rng: &mut impl Rng,
470 _locales: &[DataLocale],
471 ) -> Vec<Self>
472 where
473 Self: Sized,
474 {
475 vec![
477 LoginContext {
478 form: FormState::default(),
479 next: None,
480 providers: Vec::new(),
481 },
482 LoginContext {
483 form: FormState::default(),
484 next: None,
485 providers: Vec::new(),
486 },
487 LoginContext {
488 form: FormState::default()
489 .with_error_on_field(LoginFormField::Username, FieldError::Required)
490 .with_error_on_field(
491 LoginFormField::Password,
492 FieldError::Policy {
493 code: None,
494 message: "password too short".to_owned(),
495 },
496 ),
497 next: None,
498 providers: Vec::new(),
499 },
500 LoginContext {
501 form: FormState::default()
502 .with_error_on_field(LoginFormField::Username, FieldError::Exists),
503 next: None,
504 providers: Vec::new(),
505 },
506 ]
507 }
508}
509
510impl LoginContext {
511 #[must_use]
513 pub fn with_form_state(self, form: FormState<LoginFormField>) -> Self {
514 Self { form, ..self }
515 }
516
517 pub fn form_state_mut(&mut self) -> &mut FormState<LoginFormField> {
519 &mut self.form
520 }
521
522 #[must_use]
524 pub fn with_upstream_providers(self, providers: Vec<UpstreamOAuthProvider>) -> Self {
525 Self { providers, ..self }
526 }
527
528 #[must_use]
530 pub fn with_post_action(self, context: PostAuthContext) -> Self {
531 Self {
532 next: Some(context),
533 ..self
534 }
535 }
536}
537
538#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
540#[serde(rename_all = "snake_case")]
541pub enum RegisterFormField {
542 Username,
544
545 Email,
547
548 Password,
550
551 PasswordConfirm,
553
554 AcceptTerms,
556}
557
558impl FormField for RegisterFormField {
559 fn keep(&self) -> bool {
560 match self {
561 Self::Username | Self::Email | Self::AcceptTerms => true,
562 Self::Password | Self::PasswordConfirm => false,
563 }
564 }
565}
566
567#[derive(Serialize, Default)]
569pub struct RegisterContext {
570 providers: Vec<UpstreamOAuthProvider>,
571 next: Option<PostAuthContext>,
572}
573
574impl TemplateContext for RegisterContext {
575 fn sample(
576 _now: chrono::DateTime<Utc>,
577 _rng: &mut impl Rng,
578 _locales: &[DataLocale],
579 ) -> Vec<Self>
580 where
581 Self: Sized,
582 {
583 vec![RegisterContext {
584 providers: Vec::new(),
585 next: None,
586 }]
587 }
588}
589
590impl RegisterContext {
591 #[must_use]
593 pub fn new(providers: Vec<UpstreamOAuthProvider>) -> Self {
594 Self {
595 providers,
596 next: None,
597 }
598 }
599
600 #[must_use]
602 pub fn with_post_action(self, next: PostAuthContext) -> Self {
603 Self {
604 next: Some(next),
605 ..self
606 }
607 }
608}
609
610#[derive(Serialize, Default)]
612pub struct PasswordRegisterContext {
613 form: FormState<RegisterFormField>,
614 next: Option<PostAuthContext>,
615}
616
617impl TemplateContext for PasswordRegisterContext {
618 fn sample(
619 _now: chrono::DateTime<Utc>,
620 _rng: &mut impl Rng,
621 _locales: &[DataLocale],
622 ) -> Vec<Self>
623 where
624 Self: Sized,
625 {
626 vec![PasswordRegisterContext {
628 form: FormState::default(),
629 next: None,
630 }]
631 }
632}
633
634impl PasswordRegisterContext {
635 #[must_use]
637 pub fn with_form_state(self, form: FormState<RegisterFormField>) -> Self {
638 Self { form, ..self }
639 }
640
641 #[must_use]
643 pub fn with_post_action(self, next: PostAuthContext) -> Self {
644 Self {
645 next: Some(next),
646 ..self
647 }
648 }
649}
650
651#[derive(Serialize)]
653pub struct ConsentContext {
654 grant: AuthorizationGrant,
655 client: Client,
656 action: PostAuthAction,
657}
658
659impl TemplateContext for ConsentContext {
660 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
661 where
662 Self: Sized,
663 {
664 Client::samples(now, rng)
665 .into_iter()
666 .map(|client| {
667 let mut grant = AuthorizationGrant::sample(now, rng);
668 let action = PostAuthAction::continue_grant(grant.id);
669 grant.client_id = client.id;
671 Self {
672 grant,
673 client,
674 action,
675 }
676 })
677 .collect()
678 }
679}
680
681impl ConsentContext {
682 #[must_use]
684 pub fn new(grant: AuthorizationGrant, client: Client) -> Self {
685 let action = PostAuthAction::continue_grant(grant.id);
686 Self {
687 grant,
688 client,
689 action,
690 }
691 }
692}
693
694#[derive(Serialize)]
695#[serde(tag = "grant_type")]
696enum PolicyViolationGrant {
697 #[serde(rename = "authorization_code")]
698 Authorization(AuthorizationGrant),
699 #[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
700 DeviceCode(DeviceCodeGrant),
701}
702
703#[derive(Serialize)]
705pub struct PolicyViolationContext {
706 grant: PolicyViolationGrant,
707 client: Client,
708 action: PostAuthAction,
709}
710
711impl TemplateContext for PolicyViolationContext {
712 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
713 where
714 Self: Sized,
715 {
716 Client::samples(now, rng)
717 .into_iter()
718 .flat_map(|client| {
719 let mut grant = AuthorizationGrant::sample(now, rng);
720 grant.client_id = client.id;
722
723 let authorization_grant =
724 PolicyViolationContext::for_authorization_grant(grant, client.clone());
725 let device_code_grant = PolicyViolationContext::for_device_code_grant(
726 DeviceCodeGrant {
727 id: Ulid::from_datetime_with_source(now.into(), rng),
728 state: mas_data_model::DeviceCodeGrantState::Pending,
729 client_id: client.id,
730 scope: [OPENID].into_iter().collect(),
731 user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
732 device_code: Alphanumeric.sample_string(rng, 32),
733 created_at: now - Duration::try_minutes(5).unwrap(),
734 expires_at: now + Duration::try_minutes(25).unwrap(),
735 ip_address: None,
736 user_agent: None,
737 },
738 client,
739 );
740
741 [authorization_grant, device_code_grant]
742 })
743 .collect()
744 }
745}
746
747impl PolicyViolationContext {
748 #[must_use]
751 pub const fn for_authorization_grant(grant: AuthorizationGrant, client: Client) -> Self {
752 let action = PostAuthAction::continue_grant(grant.id);
753 Self {
754 grant: PolicyViolationGrant::Authorization(grant),
755 client,
756 action,
757 }
758 }
759
760 #[must_use]
763 pub const fn for_device_code_grant(grant: DeviceCodeGrant, client: Client) -> Self {
764 let action = PostAuthAction::continue_device_code_grant(grant.id);
765 Self {
766 grant: PolicyViolationGrant::DeviceCode(grant),
767 client,
768 action,
769 }
770 }
771}
772
773#[derive(Serialize)]
775pub struct CompatSsoContext {
776 login: CompatSsoLogin,
777 action: PostAuthAction,
778}
779
780impl TemplateContext for CompatSsoContext {
781 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
782 where
783 Self: Sized,
784 {
785 let id = Ulid::from_datetime_with_source(now.into(), rng);
786 vec![CompatSsoContext::new(CompatSsoLogin {
787 id,
788 redirect_uri: Url::parse("https://app.element.io/").unwrap(),
789 login_token: "abcdefghijklmnopqrstuvwxyz012345".into(),
790 created_at: now,
791 state: CompatSsoLoginState::Pending,
792 })]
793 }
794}
795
796impl CompatSsoContext {
797 #[must_use]
799 pub fn new(login: CompatSsoLogin) -> Self
800where {
801 let action = PostAuthAction::continue_compat_sso_login(login.id);
802 Self { login, action }
803 }
804}
805
806#[derive(Serialize)]
808pub struct EmailRecoveryContext {
809 user: User,
810 session: UserRecoverySession,
811 recovery_link: Url,
812}
813
814impl EmailRecoveryContext {
815 #[must_use]
817 pub fn new(user: User, session: UserRecoverySession, recovery_link: Url) -> Self {
818 Self {
819 user,
820 session,
821 recovery_link,
822 }
823 }
824
825 #[must_use]
827 pub fn user(&self) -> &User {
828 &self.user
829 }
830
831 #[must_use]
833 pub fn session(&self) -> &UserRecoverySession {
834 &self.session
835 }
836}
837
838impl TemplateContext for EmailRecoveryContext {
839 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
840 where
841 Self: Sized,
842 {
843 User::samples(now, rng).into_iter().map(|user| {
844 let session = UserRecoverySession {
845 id: Ulid::from_datetime_with_source(now.into(), rng),
846 email: "hello@example.com".to_owned(),
847 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(),
848 ip_address: Some(IpAddr::from([192_u8, 0, 2, 1])),
849 locale: "en".to_owned(),
850 created_at: now,
851 consumed_at: None,
852 };
853
854 let link = "https://example.com/recovery/complete?ticket=abcdefghijklmnopqrstuvwxyz0123456789".parse().unwrap();
855
856 Self::new(user, session, link)
857 }).collect()
858 }
859}
860
861#[derive(Serialize)]
863pub struct EmailVerificationContext {
864 #[serde(skip_serializing_if = "Option::is_none")]
865 browser_session: Option<BrowserSession>,
866 #[serde(skip_serializing_if = "Option::is_none")]
867 user_registration: Option<UserRegistration>,
868 authentication_code: UserEmailAuthenticationCode,
869}
870
871impl EmailVerificationContext {
872 #[must_use]
874 pub fn new(
875 authentication_code: UserEmailAuthenticationCode,
876 browser_session: Option<BrowserSession>,
877 user_registration: Option<UserRegistration>,
878 ) -> Self {
879 Self {
880 browser_session,
881 user_registration,
882 authentication_code,
883 }
884 }
885
886 #[must_use]
888 pub fn user(&self) -> Option<&User> {
889 self.browser_session.as_ref().map(|s| &s.user)
890 }
891
892 #[must_use]
894 pub fn code(&self) -> &str {
895 &self.authentication_code.code
896 }
897}
898
899impl TemplateContext for EmailVerificationContext {
900 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
901 where
902 Self: Sized,
903 {
904 BrowserSession::samples(now, rng)
905 .into_iter()
906 .map(|browser_session| {
907 let authentication_code = UserEmailAuthenticationCode {
908 id: Ulid::from_datetime_with_source(now.into(), rng),
909 user_email_authentication_id: Ulid::from_datetime_with_source(now.into(), rng),
910 code: "123456".to_owned(),
911 created_at: now - Duration::try_minutes(5).unwrap(),
912 expires_at: now + Duration::try_minutes(25).unwrap(),
913 };
914
915 Self {
916 browser_session: Some(browser_session),
917 user_registration: None,
918 authentication_code,
919 }
920 })
921 .collect()
922 }
923}
924
925#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
927#[serde(rename_all = "snake_case")]
928pub enum RegisterStepsVerifyEmailFormField {
929 Code,
931}
932
933impl FormField for RegisterStepsVerifyEmailFormField {
934 fn keep(&self) -> bool {
935 match self {
936 Self::Code => true,
937 }
938 }
939}
940
941#[derive(Serialize)]
943pub struct RegisterStepsVerifyEmailContext {
944 form: FormState<RegisterStepsVerifyEmailFormField>,
945 authentication: UserEmailAuthentication,
946}
947
948impl RegisterStepsVerifyEmailContext {
949 #[must_use]
951 pub fn new(authentication: UserEmailAuthentication) -> Self {
952 Self {
953 form: FormState::default(),
954 authentication,
955 }
956 }
957
958 #[must_use]
960 pub fn with_form_state(self, form: FormState<RegisterStepsVerifyEmailFormField>) -> Self {
961 Self { form, ..self }
962 }
963}
964
965impl TemplateContext for RegisterStepsVerifyEmailContext {
966 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
967 where
968 Self: Sized,
969 {
970 let authentication = UserEmailAuthentication {
971 id: Ulid::from_datetime_with_source(now.into(), rng),
972 user_session_id: None,
973 user_registration_id: None,
974 email: "foobar@example.com".to_owned(),
975 created_at: now,
976 completed_at: None,
977 };
978
979 vec![Self {
980 form: FormState::default(),
981 authentication,
982 }]
983 }
984}
985
986#[derive(Serialize)]
988pub struct RegisterStepsEmailInUseContext {
989 email: String,
990 action: Option<PostAuthAction>,
991}
992
993impl RegisterStepsEmailInUseContext {
994 #[must_use]
996 pub fn new(email: String, action: Option<PostAuthAction>) -> Self {
997 Self { email, action }
998 }
999}
1000
1001impl TemplateContext for RegisterStepsEmailInUseContext {
1002 fn sample(
1003 _now: chrono::DateTime<Utc>,
1004 _rng: &mut impl Rng,
1005 _locales: &[DataLocale],
1006 ) -> Vec<Self>
1007 where
1008 Self: Sized,
1009 {
1010 let email = "hello@example.com".to_owned();
1011 let action = PostAuthAction::continue_grant(Ulid::nil());
1012 vec![Self::new(email, Some(action))]
1013 }
1014}
1015
1016#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1018#[serde(rename_all = "snake_case")]
1019pub enum RegisterStepsDisplayNameFormField {
1020 DisplayName,
1022}
1023
1024impl FormField for RegisterStepsDisplayNameFormField {
1025 fn keep(&self) -> bool {
1026 match self {
1027 Self::DisplayName => true,
1028 }
1029 }
1030}
1031
1032#[derive(Serialize, Default)]
1034pub struct RegisterStepsDisplayNameContext {
1035 form: FormState<RegisterStepsDisplayNameFormField>,
1036}
1037
1038impl RegisterStepsDisplayNameContext {
1039 #[must_use]
1041 pub fn new() -> Self {
1042 Self::default()
1043 }
1044
1045 #[must_use]
1047 pub fn with_form_state(
1048 mut self,
1049 form_state: FormState<RegisterStepsDisplayNameFormField>,
1050 ) -> Self {
1051 self.form = form_state;
1052 self
1053 }
1054}
1055
1056impl TemplateContext for RegisterStepsDisplayNameContext {
1057 fn sample(
1058 _now: chrono::DateTime<chrono::Utc>,
1059 _rng: &mut impl Rng,
1060 _locales: &[DataLocale],
1061 ) -> Vec<Self>
1062 where
1063 Self: Sized,
1064 {
1065 vec![Self {
1066 form: FormState::default(),
1067 }]
1068 }
1069}
1070
1071#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1073#[serde(rename_all = "snake_case")]
1074pub enum RegisterStepsRegistrationTokenFormField {
1075 Token,
1077}
1078
1079impl FormField for RegisterStepsRegistrationTokenFormField {
1080 fn keep(&self) -> bool {
1081 match self {
1082 Self::Token => true,
1083 }
1084 }
1085}
1086
1087#[derive(Serialize, Default)]
1089pub struct RegisterStepsRegistrationTokenContext {
1090 form: FormState<RegisterStepsRegistrationTokenFormField>,
1091}
1092
1093impl RegisterStepsRegistrationTokenContext {
1094 #[must_use]
1096 pub fn new() -> Self {
1097 Self::default()
1098 }
1099
1100 #[must_use]
1102 pub fn with_form_state(
1103 mut self,
1104 form_state: FormState<RegisterStepsRegistrationTokenFormField>,
1105 ) -> Self {
1106 self.form = form_state;
1107 self
1108 }
1109}
1110
1111impl TemplateContext for RegisterStepsRegistrationTokenContext {
1112 fn sample(
1113 _now: chrono::DateTime<chrono::Utc>,
1114 _rng: &mut impl Rng,
1115 _locales: &[DataLocale],
1116 ) -> Vec<Self>
1117 where
1118 Self: Sized,
1119 {
1120 vec![Self {
1121 form: FormState::default(),
1122 }]
1123 }
1124}
1125
1126#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1128#[serde(rename_all = "snake_case")]
1129pub enum RecoveryStartFormField {
1130 Email,
1132}
1133
1134impl FormField for RecoveryStartFormField {
1135 fn keep(&self) -> bool {
1136 match self {
1137 Self::Email => true,
1138 }
1139 }
1140}
1141
1142#[derive(Serialize, Default)]
1144pub struct RecoveryStartContext {
1145 form: FormState<RecoveryStartFormField>,
1146}
1147
1148impl RecoveryStartContext {
1149 #[must_use]
1151 pub fn new() -> Self {
1152 Self::default()
1153 }
1154
1155 #[must_use]
1157 pub fn with_form_state(self, form: FormState<RecoveryStartFormField>) -> Self {
1158 Self { form }
1159 }
1160}
1161
1162impl TemplateContext for RecoveryStartContext {
1163 fn sample(
1164 _now: chrono::DateTime<Utc>,
1165 _rng: &mut impl Rng,
1166 _locales: &[DataLocale],
1167 ) -> Vec<Self>
1168 where
1169 Self: Sized,
1170 {
1171 vec![
1172 Self::new(),
1173 Self::new().with_form_state(
1174 FormState::default()
1175 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Required),
1176 ),
1177 Self::new().with_form_state(
1178 FormState::default()
1179 .with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid),
1180 ),
1181 ]
1182 }
1183}
1184
1185#[derive(Serialize)]
1187pub struct RecoveryProgressContext {
1188 session: UserRecoverySession,
1189 resend_failed_due_to_rate_limit: bool,
1191}
1192
1193impl RecoveryProgressContext {
1194 #[must_use]
1196 pub fn new(session: UserRecoverySession, resend_failed_due_to_rate_limit: bool) -> Self {
1197 Self {
1198 session,
1199 resend_failed_due_to_rate_limit,
1200 }
1201 }
1202}
1203
1204impl TemplateContext for RecoveryProgressContext {
1205 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1206 where
1207 Self: Sized,
1208 {
1209 let session = UserRecoverySession {
1210 id: Ulid::from_datetime_with_source(now.into(), rng),
1211 email: "name@mail.com".to_owned(),
1212 user_agent: "Mozilla/5.0".to_owned(),
1213 ip_address: None,
1214 locale: "en".to_owned(),
1215 created_at: now,
1216 consumed_at: None,
1217 };
1218
1219 vec![
1220 Self {
1221 session: session.clone(),
1222 resend_failed_due_to_rate_limit: false,
1223 },
1224 Self {
1225 session,
1226 resend_failed_due_to_rate_limit: true,
1227 },
1228 ]
1229 }
1230}
1231
1232#[derive(Serialize)]
1234pub struct RecoveryExpiredContext {
1235 session: UserRecoverySession,
1236}
1237
1238impl RecoveryExpiredContext {
1239 #[must_use]
1241 pub fn new(session: UserRecoverySession) -> Self {
1242 Self { session }
1243 }
1244}
1245
1246impl TemplateContext for RecoveryExpiredContext {
1247 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1248 where
1249 Self: Sized,
1250 {
1251 let session = UserRecoverySession {
1252 id: Ulid::from_datetime_with_source(now.into(), rng),
1253 email: "name@mail.com".to_owned(),
1254 user_agent: "Mozilla/5.0".to_owned(),
1255 ip_address: None,
1256 locale: "en".to_owned(),
1257 created_at: now,
1258 consumed_at: None,
1259 };
1260
1261 vec![Self { session }]
1262 }
1263}
1264
1265#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1267#[serde(rename_all = "snake_case")]
1268pub enum RecoveryFinishFormField {
1269 NewPassword,
1271
1272 NewPasswordConfirm,
1274}
1275
1276impl FormField for RecoveryFinishFormField {
1277 fn keep(&self) -> bool {
1278 false
1279 }
1280}
1281
1282#[derive(Serialize)]
1284pub struct RecoveryFinishContext {
1285 user: User,
1286 form: FormState<RecoveryFinishFormField>,
1287}
1288
1289impl RecoveryFinishContext {
1290 #[must_use]
1292 pub fn new(user: User) -> Self {
1293 Self {
1294 user,
1295 form: FormState::default(),
1296 }
1297 }
1298
1299 #[must_use]
1301 pub fn with_form_state(mut self, form: FormState<RecoveryFinishFormField>) -> Self {
1302 self.form = form;
1303 self
1304 }
1305}
1306
1307impl TemplateContext for RecoveryFinishContext {
1308 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1309 where
1310 Self: Sized,
1311 {
1312 User::samples(now, rng)
1313 .into_iter()
1314 .flat_map(|user| {
1315 vec![
1316 Self::new(user.clone()),
1317 Self::new(user.clone()).with_form_state(
1318 FormState::default().with_error_on_field(
1319 RecoveryFinishFormField::NewPassword,
1320 FieldError::Invalid,
1321 ),
1322 ),
1323 Self::new(user.clone()).with_form_state(
1324 FormState::default().with_error_on_field(
1325 RecoveryFinishFormField::NewPasswordConfirm,
1326 FieldError::Invalid,
1327 ),
1328 ),
1329 ]
1330 })
1331 .collect()
1332 }
1333}
1334
1335#[derive(Serialize)]
1338pub struct UpstreamExistingLinkContext {
1339 linked_user: User,
1340}
1341
1342impl UpstreamExistingLinkContext {
1343 #[must_use]
1345 pub fn new(linked_user: User) -> Self {
1346 Self { linked_user }
1347 }
1348}
1349
1350impl TemplateContext for UpstreamExistingLinkContext {
1351 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1352 where
1353 Self: Sized,
1354 {
1355 User::samples(now, rng)
1356 .into_iter()
1357 .map(|linked_user| Self { linked_user })
1358 .collect()
1359 }
1360}
1361
1362#[derive(Serialize)]
1365pub struct UpstreamSuggestLink {
1366 post_logout_action: PostAuthAction,
1367}
1368
1369impl UpstreamSuggestLink {
1370 #[must_use]
1372 pub fn new(link: &UpstreamOAuthLink) -> Self {
1373 Self::for_link_id(link.id)
1374 }
1375
1376 fn for_link_id(id: Ulid) -> Self {
1377 let post_logout_action = PostAuthAction::link_upstream(id);
1378 Self { post_logout_action }
1379 }
1380}
1381
1382impl TemplateContext for UpstreamSuggestLink {
1383 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1384 where
1385 Self: Sized,
1386 {
1387 let id = Ulid::from_datetime_with_source(now.into(), rng);
1388 vec![Self::for_link_id(id)]
1389 }
1390}
1391
1392#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1394#[serde(rename_all = "snake_case")]
1395pub enum UpstreamRegisterFormField {
1396 Username,
1398
1399 AcceptTerms,
1401}
1402
1403impl FormField for UpstreamRegisterFormField {
1404 fn keep(&self) -> bool {
1405 match self {
1406 Self::Username | Self::AcceptTerms => true,
1407 }
1408 }
1409}
1410
1411#[derive(Serialize)]
1414pub struct UpstreamRegister {
1415 upstream_oauth_link: UpstreamOAuthLink,
1416 upstream_oauth_provider: UpstreamOAuthProvider,
1417 imported_localpart: Option<String>,
1418 force_localpart: bool,
1419 imported_display_name: Option<String>,
1420 force_display_name: bool,
1421 imported_email: Option<String>,
1422 force_email: bool,
1423 form_state: FormState<UpstreamRegisterFormField>,
1424}
1425
1426impl UpstreamRegister {
1427 #[must_use]
1430 pub fn new(
1431 upstream_oauth_link: UpstreamOAuthLink,
1432 upstream_oauth_provider: UpstreamOAuthProvider,
1433 ) -> Self {
1434 Self {
1435 upstream_oauth_link,
1436 upstream_oauth_provider,
1437 imported_localpart: None,
1438 force_localpart: false,
1439 imported_display_name: None,
1440 force_display_name: false,
1441 imported_email: None,
1442 force_email: false,
1443 form_state: FormState::default(),
1444 }
1445 }
1446
1447 pub fn set_localpart(&mut self, localpart: String, force: bool) {
1449 self.imported_localpart = Some(localpart);
1450 self.force_localpart = force;
1451 }
1452
1453 #[must_use]
1455 pub fn with_localpart(self, localpart: String, force: bool) -> Self {
1456 Self {
1457 imported_localpart: Some(localpart),
1458 force_localpart: force,
1459 ..self
1460 }
1461 }
1462
1463 pub fn set_display_name(&mut self, display_name: String, force: bool) {
1465 self.imported_display_name = Some(display_name);
1466 self.force_display_name = force;
1467 }
1468
1469 #[must_use]
1471 pub fn with_display_name(self, display_name: String, force: bool) -> Self {
1472 Self {
1473 imported_display_name: Some(display_name),
1474 force_display_name: force,
1475 ..self
1476 }
1477 }
1478
1479 pub fn set_email(&mut self, email: String, force: bool) {
1481 self.imported_email = Some(email);
1482 self.force_email = force;
1483 }
1484
1485 #[must_use]
1487 pub fn with_email(self, email: String, force: bool) -> Self {
1488 Self {
1489 imported_email: Some(email),
1490 force_email: force,
1491 ..self
1492 }
1493 }
1494
1495 pub fn set_form_state(&mut self, form_state: FormState<UpstreamRegisterFormField>) {
1497 self.form_state = form_state;
1498 }
1499
1500 #[must_use]
1502 pub fn with_form_state(self, form_state: FormState<UpstreamRegisterFormField>) -> Self {
1503 Self { form_state, ..self }
1504 }
1505}
1506
1507impl TemplateContext for UpstreamRegister {
1508 fn sample(now: chrono::DateTime<Utc>, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1509 where
1510 Self: Sized,
1511 {
1512 vec![Self::new(
1513 UpstreamOAuthLink {
1514 id: Ulid::nil(),
1515 provider_id: Ulid::nil(),
1516 user_id: None,
1517 subject: "subject".to_owned(),
1518 human_account_name: Some("@john".to_owned()),
1519 created_at: now,
1520 },
1521 UpstreamOAuthProvider {
1522 id: Ulid::nil(),
1523 issuer: Some("https://example.com/".to_owned()),
1524 human_name: Some("Example Ltd.".to_owned()),
1525 brand_name: None,
1526 scope: Scope::from_iter([OPENID]),
1527 token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretBasic,
1528 token_endpoint_signing_alg: None,
1529 id_token_signed_response_alg: JsonWebSignatureAlg::Rs256,
1530 client_id: "client-id".to_owned(),
1531 encrypted_client_secret: None,
1532 claims_imports: UpstreamOAuthProviderClaimsImports::default(),
1533 authorization_endpoint_override: None,
1534 token_endpoint_override: None,
1535 jwks_uri_override: None,
1536 userinfo_endpoint_override: None,
1537 fetch_userinfo: false,
1538 userinfo_signed_response_alg: None,
1539 discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc,
1540 pkce_mode: UpstreamOAuthProviderPkceMode::Auto,
1541 response_mode: None,
1542 additional_authorization_parameters: Vec::new(),
1543 forward_login_hint: false,
1544 created_at: now,
1545 disabled_at: None,
1546 },
1547 )]
1548 }
1549}
1550
1551#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
1553#[serde(rename_all = "snake_case")]
1554pub enum DeviceLinkFormField {
1555 Code,
1557}
1558
1559impl FormField for DeviceLinkFormField {
1560 fn keep(&self) -> bool {
1561 match self {
1562 Self::Code => true,
1563 }
1564 }
1565}
1566
1567#[derive(Serialize, Default, Debug)]
1569pub struct DeviceLinkContext {
1570 form_state: FormState<DeviceLinkFormField>,
1571}
1572
1573impl DeviceLinkContext {
1574 #[must_use]
1576 pub fn new() -> Self {
1577 Self::default()
1578 }
1579
1580 #[must_use]
1582 pub fn with_form_state(mut self, form_state: FormState<DeviceLinkFormField>) -> Self {
1583 self.form_state = form_state;
1584 self
1585 }
1586}
1587
1588impl TemplateContext for DeviceLinkContext {
1589 fn sample(
1590 _now: chrono::DateTime<Utc>,
1591 _rng: &mut impl Rng,
1592 _locales: &[DataLocale],
1593 ) -> Vec<Self>
1594 where
1595 Self: Sized,
1596 {
1597 vec![
1598 Self::new(),
1599 Self::new().with_form_state(
1600 FormState::default()
1601 .with_error_on_field(DeviceLinkFormField::Code, FieldError::Required),
1602 ),
1603 ]
1604 }
1605}
1606
1607#[derive(Serialize, Debug)]
1609pub struct DeviceConsentContext {
1610 grant: DeviceCodeGrant,
1611 client: Client,
1612}
1613
1614impl DeviceConsentContext {
1615 #[must_use]
1617 pub fn new(grant: DeviceCodeGrant, client: Client) -> Self {
1618 Self { grant, client }
1619 }
1620}
1621
1622impl TemplateContext for DeviceConsentContext {
1623 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1624 where
1625 Self: Sized,
1626 {
1627 Client::samples(now, rng)
1628 .into_iter()
1629 .map(|client| {
1630 let grant = DeviceCodeGrant {
1631 id: Ulid::from_datetime_with_source(now.into(), rng),
1632 state: mas_data_model::DeviceCodeGrantState::Pending,
1633 client_id: client.id,
1634 scope: [OPENID].into_iter().collect(),
1635 user_code: Alphanumeric.sample_string(rng, 6).to_uppercase(),
1636 device_code: Alphanumeric.sample_string(rng, 32),
1637 created_at: now - Duration::try_minutes(5).unwrap(),
1638 expires_at: now + Duration::try_minutes(25).unwrap(),
1639 ip_address: Some(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))),
1640 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()),
1641 };
1642 Self { grant, client }
1643 })
1644 .collect()
1645 }
1646}
1647
1648#[derive(Serialize)]
1651pub struct AccountInactiveContext {
1652 user: User,
1653}
1654
1655impl AccountInactiveContext {
1656 #[must_use]
1658 pub fn new(user: User) -> Self {
1659 Self { user }
1660 }
1661}
1662
1663impl TemplateContext for AccountInactiveContext {
1664 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1665 where
1666 Self: Sized,
1667 {
1668 User::samples(now, rng)
1669 .into_iter()
1670 .map(|user| AccountInactiveContext { user })
1671 .collect()
1672 }
1673}
1674
1675#[derive(Serialize)]
1677pub struct DeviceNameContext {
1678 client: Client,
1679 raw_user_agent: String,
1680}
1681
1682impl DeviceNameContext {
1683 #[must_use]
1685 pub fn new(client: Client, user_agent: Option<String>) -> Self {
1686 Self {
1687 client,
1688 raw_user_agent: user_agent.unwrap_or_default(),
1689 }
1690 }
1691}
1692
1693impl TemplateContext for DeviceNameContext {
1694 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1695 where
1696 Self: Sized,
1697 {
1698 Client::samples(now, rng)
1699 .into_iter()
1700 .map(|client| DeviceNameContext {
1701 client,
1702 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(),
1703 })
1704 .collect()
1705 }
1706}
1707
1708#[derive(Serialize)]
1710pub struct FormPostContext<T> {
1711 redirect_uri: Option<Url>,
1712 params: T,
1713}
1714
1715impl<T: TemplateContext> TemplateContext for FormPostContext<T> {
1716 fn sample(now: chrono::DateTime<Utc>, rng: &mut impl Rng, locales: &[DataLocale]) -> Vec<Self>
1717 where
1718 Self: Sized,
1719 {
1720 let sample_params = T::sample(now, rng, locales);
1721 sample_params
1722 .into_iter()
1723 .map(|params| FormPostContext {
1724 redirect_uri: "https://example.com/callback".parse().ok(),
1725 params,
1726 })
1727 .collect()
1728 }
1729}
1730
1731impl<T> FormPostContext<T> {
1732 pub fn new_for_url(redirect_uri: Url, params: T) -> Self {
1735 Self {
1736 redirect_uri: Some(redirect_uri),
1737 params,
1738 }
1739 }
1740
1741 pub fn new_for_current_url(params: T) -> Self {
1744 Self {
1745 redirect_uri: None,
1746 params,
1747 }
1748 }
1749
1750 pub fn with_language(self, lang: &DataLocale) -> WithLanguage<Self> {
1755 WithLanguage {
1756 lang: lang.to_string(),
1757 inner: self,
1758 }
1759 }
1760}
1761
1762#[derive(Default, Serialize, Debug, Clone)]
1764pub struct ErrorContext {
1765 code: Option<&'static str>,
1766 description: Option<String>,
1767 details: Option<String>,
1768 lang: Option<String>,
1769}
1770
1771impl std::fmt::Display for ErrorContext {
1772 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
1773 if let Some(code) = &self.code {
1774 writeln!(f, "code: {code}")?;
1775 }
1776 if let Some(description) = &self.description {
1777 writeln!(f, "{description}")?;
1778 }
1779
1780 if let Some(details) = &self.details {
1781 writeln!(f, "details: {details}")?;
1782 }
1783
1784 Ok(())
1785 }
1786}
1787
1788impl TemplateContext for ErrorContext {
1789 fn sample(
1790 _now: chrono::DateTime<Utc>,
1791 _rng: &mut impl Rng,
1792 _locales: &[DataLocale],
1793 ) -> Vec<Self>
1794 where
1795 Self: Sized,
1796 {
1797 vec![
1798 Self::new()
1799 .with_code("sample_error")
1800 .with_description("A fancy description".into())
1801 .with_details("Something happened".into()),
1802 Self::new().with_code("another_error"),
1803 Self::new(),
1804 ]
1805 }
1806}
1807
1808impl ErrorContext {
1809 #[must_use]
1811 pub fn new() -> Self {
1812 Self::default()
1813 }
1814
1815 #[must_use]
1817 pub fn with_code(mut self, code: &'static str) -> Self {
1818 self.code = Some(code);
1819 self
1820 }
1821
1822 #[must_use]
1824 pub fn with_description(mut self, description: String) -> Self {
1825 self.description = Some(description);
1826 self
1827 }
1828
1829 #[must_use]
1831 pub fn with_details(mut self, details: String) -> Self {
1832 self.details = Some(details);
1833 self
1834 }
1835
1836 #[must_use]
1838 pub fn with_language(mut self, lang: &DataLocale) -> Self {
1839 self.lang = Some(lang.to_string());
1840 self
1841 }
1842
1843 #[must_use]
1845 pub fn code(&self) -> Option<&'static str> {
1846 self.code
1847 }
1848
1849 #[must_use]
1851 pub fn description(&self) -> Option<&str> {
1852 self.description.as_deref()
1853 }
1854
1855 #[must_use]
1857 pub fn details(&self) -> Option<&str> {
1858 self.details.as_deref()
1859 }
1860}
1861
1862#[derive(Serialize)]
1864pub struct NotFoundContext {
1865 method: String,
1866 version: String,
1867 uri: String,
1868}
1869
1870impl NotFoundContext {
1871 #[must_use]
1873 pub fn new(method: &Method, version: Version, uri: &Uri) -> Self {
1874 Self {
1875 method: method.to_string(),
1876 version: format!("{version:?}"),
1877 uri: uri.to_string(),
1878 }
1879 }
1880}
1881
1882impl TemplateContext for NotFoundContext {
1883 fn sample(_now: DateTime<Utc>, _rng: &mut impl Rng, _locales: &[DataLocale]) -> Vec<Self>
1884 where
1885 Self: Sized,
1886 {
1887 vec![
1888 Self::new(&Method::GET, Version::HTTP_11, &"/".parse().unwrap()),
1889 Self::new(&Method::POST, Version::HTTP_2, &"/foo/bar".parse().unwrap()),
1890 Self::new(
1891 &Method::PUT,
1892 Version::HTTP_10,
1893 &"/foo?bar=baz".parse().unwrap(),
1894 ),
1895 ]
1896 }
1897}