mas_handlers/views/recovery/
progress.rs1use 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 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 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 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 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 let () = cookie_jar.verify_form(&clock, form)?;
126
127 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 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}