From 96df94104e4e7c36fcebdef09d6f6c7ad2b8717a Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Jun 2024 15:27:19 +0200 Subject: [PATCH] Show a proper 'link expired' page --- crates/handlers/src/views/recovery/finish.rs | 24 +++++------- crates/templates/src/context.rs | 33 ++++++++++++++++ crates/templates/src/lib.rs | 16 +++++--- templates/components/button.html | 8 ++-- templates/pages/recovery/expired.html | 40 ++++++++++++++++++++ translations/en.json | 20 ++++++++++ 6 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 templates/pages/recovery/expired.html diff --git a/crates/handlers/src/views/recovery/finish.rs b/crates/handlers/src/views/recovery/finish.rs index df0b6c66..3955c015 100644 --- a/crates/handlers/src/views/recovery/finish.rs +++ b/crates/handlers/src/views/recovery/finish.rs @@ -28,8 +28,8 @@ use mas_policy::Policy; use mas_router::UrlBuilder; use mas_storage::{BoxClock, BoxRepository, BoxRng}; use mas_templates::{ - EmptyContext, ErrorContext, FieldError, FormState, RecoveryFinishContext, - RecoveryFinishFormField, TemplateContext, Templates, + EmptyContext, ErrorContext, FieldError, FormState, RecoveryExpiredContext, + RecoveryFinishContext, RecoveryFinishFormField, TemplateContext, Templates, }; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; @@ -78,12 +78,10 @@ pub(crate) async fn get( .context("Unknown session")?; if !ticket.active(clock.now()) || session.consumed_at.is_some() { - // TODO: render a 'link expired' page - let rendered = templates.render_error( - &ErrorContext::new() - .with_code("Link expired") - .with_language(&locale), - )?; + let context = RecoveryExpiredContext::new(session) + .with_csrf(csrf_token.form_value()) + .with_language(locale); + let rendered = templates.render_recovery_expired(&context)?; return Ok((cookie_jar, Html(rendered)).into_response()); } @@ -155,12 +153,10 @@ pub(crate) async fn post( .context("Unknown session")?; if !ticket.active(clock.now()) || session.consumed_at.is_some() { - // TODO: render a 'link expired' page - let rendered = templates.render_error( - &ErrorContext::new() - .with_code("Link expired") - .with_language(&locale), - )?; + let context = RecoveryExpiredContext::new(session) + .with_csrf(csrf_token.form_value()) + .with_language(locale); + let rendered = templates.render_recovery_expired(&context)?; return Ok((cookie_jar, Html(rendered)).into_response()); } diff --git a/crates/templates/src/context.rs b/crates/templates/src/context.rs index 6bb40456..a5bba03e 100644 --- a/crates/templates/src/context.rs +++ b/crates/templates/src/context.rs @@ -1056,6 +1056,39 @@ impl TemplateContext for RecoveryProgressContext { } } +/// Context used by the `pages/recovery/expired.html` template +#[derive(Serialize)] +pub struct RecoveryExpiredContext { + session: UserRecoverySession, +} + +impl RecoveryExpiredContext { + /// Constructs a context for the recovery expired page + #[must_use] + pub fn new(session: UserRecoverySession) -> Self { + Self { session } + } +} + +impl TemplateContext for RecoveryExpiredContext { + fn sample(now: chrono::DateTime, rng: &mut impl Rng) -> Vec + where + Self: Sized, + { + let session = UserRecoverySession { + id: Ulid::from_datetime_with_source(now.into(), rng), + email: "name@mail.com".to_owned(), + user_agent: UserAgent::parse("Mozilla/5.0".to_owned()), + ip_address: None, + locale: "en".to_owned(), + created_at: now, + consumed_at: None, + }; + + vec![Self { session }] + } +} + /// Fields of the account recovery finish form #[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)] #[serde(rename_all = "snake_case")] diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 4163e5c5..5d20d0bb 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -46,12 +46,12 @@ pub use self::{ DeviceLinkFormField, EmailAddContext, EmailRecoveryContext, EmailVerificationContext, EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext, PolicyViolationContext, PostAuthContext, - PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryFinishContext, - RecoveryFinishFormField, RecoveryProgressContext, RecoveryStartContext, - RecoveryStartFormField, RegisterContext, RegisterFormField, SiteBranding, SiteConfigExt, - SiteFeatures, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister, - UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf, WithLanguage, - WithOptionalSession, WithSession, + PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryExpiredContext, + RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext, + RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField, + SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext, UpstreamExistingLinkContext, + UpstreamRegister, UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf, + WithLanguage, WithOptionalSession, WithSession, }, forms::{FieldError, FormError, FormField, FormState, ToFormState}, }; @@ -357,6 +357,9 @@ register_templates! { /// Render the account recovery finish page pub fn render_recovery_finish(WithLanguage>) { "pages/recovery/finish.html" } + /// Render the account recovery link expired page + pub fn render_recovery_expired(WithLanguage>) { "pages/recovery/expired.html" } + /// Render the account recovery disabled page pub fn render_recovery_disabled(WithLanguage) { "pages/recovery/disabled.html" } @@ -428,6 +431,7 @@ impl Templates { check::render_recovery_start(self, now, rng)?; check::render_recovery_progress(self, now, rng)?; check::render_recovery_finish(self, now, rng)?; + check::render_recovery_expired(self, now, rng)?; check::render_recovery_disabled(self, now, rng)?; check::render_reauth(self, now, rng)?; check::render_form_post::(self, now, rng)?; diff --git a/templates/components/button.html b/templates/components/button.html index 9a784fe8..9c1e005b 100644 --- a/templates/components/button.html +++ b/templates/components/button.html @@ -14,16 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. #} -{% macro link(text, href="#", class="") %} - {{ text }} +{% macro link(text, href="#", class="", size="lg") %} + {{ text }} {% endmacro %} {% macro link_text(text, href="#", class="") %} {{ text }} {% endmacro %} -{% macro link_outline(text, href="#", class="") %} - {{ text }} +{% macro link_outline(text, href="#", class="", size="lg") %} + {{ text }} {% endmacro %} {% macro link_tertiary(text, href="#", class="", size="lg") %} diff --git a/templates/pages/recovery/expired.html b/templates/pages/recovery/expired.html new file mode 100644 index 00000000..59c1934f --- /dev/null +++ b/templates/pages/recovery/expired.html @@ -0,0 +1,40 @@ +{# +Copyright 2024 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +#} + +{% extends "base.html" %} + +{% block content %} +
+
+ {{ icon.error() }} +
+ +
+

{{ _("mas.recovery.expired.heading") }}

+

{{ _("mas.recovery.expired.description", email=session.email) }}

+
+
+ +
+
+ + + {{ button.button(text=_("mas.recovery.expired.resend_email"), type="submit") }} +
+ + {{ button.link_outline(text=_("action.start_over"), href="/login") }} +
+{% endblock content %} diff --git a/translations/en.json b/translations/en.json index 18256271..ea43ba2d 100644 --- a/translations/en.json +++ b/translations/en.json @@ -23,6 +23,10 @@ "sign_out": "Sign out", "@sign_out": { "context": "pages/consent.html:71:28-48, pages/device_consent.html:141:30-50, pages/index.html:36:28-48, pages/policy_violation.html:46:28-48, pages/sso.html:53:28-48, pages/upstream_oauth2/link_mismatch.html:32:24-44, pages/upstream_oauth2/suggest_link.html:40:26-46" + }, + "start_over": "Start over", + "@start_over": { + "context": "pages/recovery/expired.html:38:32-54" } }, "app": { @@ -395,6 +399,22 @@ "context": "pages/recovery/disabled.html:26:27-61" } }, + "expired": { + "description": "Request a new email that will be sent to: %(email)s.", + "@description": { + "context": "pages/recovery/expired.html:27:46-104", + "description": "Description on the page shown when a user tries to use an expired recovery link" + }, + "heading": "The link to reset your password has expired", + "@heading": { + "context": "pages/recovery/expired.html:26:27-60", + "description": "Title on the page shown when a user tries to use an expired recovery link" + }, + "resend_email": "Resend email", + "@resend_email": { + "context": "pages/recovery/expired.html:35:28-66" + } + }, "finish": { "confirm": "Enter new password again", "@confirm": {