diff --git a/crates/handlers/src/views/recovery/finish.rs b/crates/handlers/src/views/recovery/finish.rs index 3955c015f..e5dcbe381 100644 --- a/crates/handlers/src/views/recovery/finish.rs +++ b/crates/handlers/src/views/recovery/finish.rs @@ -77,7 +77,13 @@ pub(crate) async fn get( .await? .context("Unknown session")?; - if !ticket.active(clock.now()) || session.consumed_at.is_some() { + if session.consumed_at.is_some() { + let context = EmptyContext.with_language(locale); + let rendered = templates.render_recovery_consumed(&context)?; + return Ok((cookie_jar, Html(rendered)).into_response()); + } + + if !ticket.active(clock.now()) { let context = RecoveryExpiredContext::new(session) .with_csrf(csrf_token.form_value()) .with_language(locale); @@ -118,6 +124,7 @@ pub(crate) async fn get( Ok((cookie_jar, Html(rendered)).into_response()) } +#[allow(clippy::too_many_lines)] pub(crate) async fn post( mut rng: BoxRng, clock: BoxClock, @@ -152,7 +159,13 @@ pub(crate) async fn post( .await? .context("Unknown session")?; - if !ticket.active(clock.now()) || session.consumed_at.is_some() { + if session.consumed_at.is_some() { + let context = EmptyContext.with_language(locale); + let rendered = templates.render_recovery_consumed(&context)?; + return Ok((cookie_jar, Html(rendered)).into_response()); + } + + if !ticket.active(clock.now()) { let context = RecoveryExpiredContext::new(session) .with_csrf(csrf_token.form_value()) .with_language(locale); diff --git a/crates/handlers/src/views/recovery/progress.rs b/crates/handlers/src/views/recovery/progress.rs index a6a4d5562..e852c616e 100644 --- a/crates/handlers/src/views/recovery/progress.rs +++ b/crates/handlers/src/views/recovery/progress.rs @@ -68,6 +68,12 @@ pub(crate) async fn get( .into_response()); }; + if recovery_session.consumed_at.is_some() { + let context = EmptyContext.with_language(locale); + let rendered = templates.render_recovery_consumed(&context)?; + return Ok((cookie_jar, Html(rendered)).into_response()); + } + let context = RecoveryProgressContext::new(recovery_session) .with_csrf(csrf_token.form_value()) .with_language(locale); @@ -115,6 +121,12 @@ pub(crate) async fn post( .into_response()); }; + if recovery_session.consumed_at.is_some() { + let context = EmptyContext.with_language(locale); + let rendered = templates.render_recovery_consumed(&context)?; + return Ok((cookie_jar, Html(rendered)).into_response()); + } + // Verify the CSRF token let () = cookie_jar.verify_form(&clock, form)?; diff --git a/crates/templates/src/lib.rs b/crates/templates/src/lib.rs index 5d20d0bb1..e1b27190e 100644 --- a/crates/templates/src/lib.rs +++ b/crates/templates/src/lib.rs @@ -360,6 +360,9 @@ register_templates! { /// Render the account recovery link expired page pub fn render_recovery_expired(WithLanguage>) { "pages/recovery/expired.html" } + /// Render the account recovery link consumed page + pub fn render_recovery_consumed(WithLanguage) { "pages/recovery/consumed.html" } + /// Render the account recovery disabled page pub fn render_recovery_disabled(WithLanguage) { "pages/recovery/disabled.html" } @@ -432,6 +435,7 @@ impl Templates { check::render_recovery_progress(self, now, rng)?; check::render_recovery_finish(self, now, rng)?; check::render_recovery_expired(self, now, rng)?; + check::render_recovery_consumed(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/pages/recovery/consumed.html b/templates/pages/recovery/consumed.html new file mode 100644 index 000000000..56c0e8263 --- /dev/null +++ b/templates/pages/recovery/consumed.html @@ -0,0 +1,32 @@ +{# +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.consumed.heading") }}

+

{{ _("mas.recovery.consumed.description") }}

+
+ + {{ button.link_outline(text=_("action.start_over"), href="/login") }} +
+{% endblock content %} diff --git a/translations/en.json b/translations/en.json index ea43ba2d3..2f57c73e8 100644 --- a/translations/en.json +++ b/translations/en.json @@ -26,7 +26,7 @@ }, "start_over": "Start over", "@start_over": { - "context": "pages/recovery/expired.html:38:32-54" + "context": "pages/recovery/consumed.html:30:32-54, pages/recovery/expired.html:38:32-54" } }, "app": { @@ -389,6 +389,18 @@ } }, "recovery": { + "consumed": { + "description": "To create a new password, start over and select “Forgot password”.", + "@description": { + "context": "pages/recovery/consumed.html:27:25-63", + "description": "Description on the error page shown when a user tries to use a recovery link that has already been used" + }, + "heading": "The link to reset your password has already been used", + "@heading": { + "context": "pages/recovery/consumed.html:26:27-61", + "description": "Title on the error page shown when a user tries to use a recovery link that has already been used" + } + }, "disabled": { "description": "If you have lost your credentials, please contact the administrator to recover your account.", "@description": {