mas_handlers/views/recovery/
progress.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use axum::{
8    Form,
9    extract::{Path, State},
10    response::{Html, IntoResponse, Response},
11};
12use hyper::StatusCode;
13use mas_axum_utils::{
14    FancyError, SessionInfoExt,
15    cookies::CookieJar,
16    csrf::{CsrfExt, ProtectedForm},
17};
18use mas_data_model::SiteConfig;
19use mas_router::UrlBuilder;
20use mas_storage::{
21    BoxClock, BoxRepository, BoxRng,
22    queue::{QueueJobRepositoryExt as _, SendAccountRecoveryEmailsJob},
23};
24use mas_templates::{EmptyContext, RecoveryProgressContext, TemplateContext, Templates};
25use ulid::Ulid;
26
27use crate::{Limiter, PreferredLanguage, RequesterFingerprint};
28
29pub(crate) async fn get(
30    mut rng: BoxRng,
31    clock: BoxClock,
32    mut repo: BoxRepository,
33    State(site_config): State<SiteConfig>,
34    State(templates): State<Templates>,
35    State(url_builder): State<UrlBuilder>,
36    PreferredLanguage(locale): PreferredLanguage,
37    cookie_jar: CookieJar,
38    Path(id): Path<Ulid>,
39) -> Result<Response, FancyError> {
40    if !site_config.account_recovery_allowed {
41        let context = EmptyContext.with_language(locale);
42        let rendered = templates.render_recovery_disabled(&context)?;
43        return Ok((cookie_jar, Html(rendered)).into_response());
44    }
45
46    let (session_info, cookie_jar) = cookie_jar.session_info();
47    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
48
49    let maybe_session = session_info.load_active_session(&mut repo).await?;
50    if maybe_session.is_some() {
51        // TODO: redirect to continue whatever action was going on
52        return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response());
53    }
54
55    let Some(recovery_session) = repo.user_recovery().lookup_session(id).await? else {
56        // XXX: is that the right thing to do?
57        return Ok((
58            cookie_jar,
59            url_builder.redirect(&mas_router::AccountRecoveryStart),
60        )
61            .into_response());
62    };
63
64    if recovery_session.consumed_at.is_some() {
65        let context = EmptyContext.with_language(locale);
66        let rendered = templates.render_recovery_consumed(&context)?;
67        return Ok((cookie_jar, Html(rendered)).into_response());
68    }
69
70    let context = RecoveryProgressContext::new(recovery_session, false)
71        .with_csrf(csrf_token.form_value())
72        .with_language(locale);
73
74    repo.save().await?;
75
76    let rendered = templates.render_recovery_progress(&context)?;
77
78    Ok((cookie_jar, Html(rendered)).into_response())
79}
80
81pub(crate) async fn post(
82    mut rng: BoxRng,
83    clock: BoxClock,
84    mut repo: BoxRepository,
85    State(site_config): State<SiteConfig>,
86    State(templates): State<Templates>,
87    State(url_builder): State<UrlBuilder>,
88    (State(limiter), requester): (State<Limiter>, RequesterFingerprint),
89    PreferredLanguage(locale): PreferredLanguage,
90    cookie_jar: CookieJar,
91    Path(id): Path<Ulid>,
92    Form(form): Form<ProtectedForm<()>>,
93) -> Result<Response, FancyError> {
94    if !site_config.account_recovery_allowed {
95        let context = EmptyContext.with_language(locale);
96        let rendered = templates.render_recovery_disabled(&context)?;
97        return Ok((cookie_jar, Html(rendered)).into_response());
98    }
99
100    let (session_info, cookie_jar) = cookie_jar.session_info();
101    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
102
103    let maybe_session = session_info.load_active_session(&mut repo).await?;
104    if maybe_session.is_some() {
105        // TODO: redirect to continue whatever action was going on
106        return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response());
107    }
108
109    let Some(recovery_session) = repo.user_recovery().lookup_session(id).await? else {
110        // XXX: is that the right thing to do?
111        return Ok((
112            cookie_jar,
113            url_builder.redirect(&mas_router::AccountRecoveryStart),
114        )
115            .into_response());
116    };
117
118    if recovery_session.consumed_at.is_some() {
119        let context = EmptyContext.with_language(locale);
120        let rendered = templates.render_recovery_consumed(&context)?;
121        return Ok((cookie_jar, Html(rendered)).into_response());
122    }
123
124    // Verify the CSRF token
125    let () = cookie_jar.verify_form(&clock, form)?;
126
127    // Check the rate limit if we are about to process the form
128    if let Err(e) = limiter.check_account_recovery(requester, &recovery_session.email) {
129        tracing::warn!(error = &e as &dyn std::error::Error);
130        let context = RecoveryProgressContext::new(recovery_session, true)
131            .with_csrf(csrf_token.form_value())
132            .with_language(locale);
133        let rendered = templates.render_recovery_progress(&context)?;
134
135        return Ok((StatusCode::TOO_MANY_REQUESTS, (cookie_jar, Html(rendered))).into_response());
136    }
137
138    // Schedule a new batch of emails
139    repo.queue_job()
140        .schedule_job(
141            &mut rng,
142            &clock,
143            SendAccountRecoveryEmailsJob::new(&recovery_session),
144        )
145        .await?;
146
147    repo.save().await?;
148
149    let context = RecoveryProgressContext::new(recovery_session, false)
150        .with_csrf(csrf_token.form_value())
151        .with_language(locale);
152
153    let rendered = templates.render_recovery_progress(&context)?;
154
155    Ok((cookie_jar, Html(rendered)).into_response())
156}