mas_handlers/views/recovery/
start.rs1use std::str::FromStr;
8
9use axum::{
10 Form,
11 extract::State,
12 response::{Html, IntoResponse, Response},
13};
14use axum_extra::typed_header::TypedHeader;
15use lettre::Address;
16use mas_axum_utils::{
17 FancyError, SessionInfoExt,
18 cookies::CookieJar,
19 csrf::{CsrfExt, ProtectedForm},
20};
21use mas_data_model::{SiteConfig, UserAgent};
22use mas_router::UrlBuilder;
23use mas_storage::{
24 BoxClock, BoxRepository, BoxRng,
25 queue::{QueueJobRepositoryExt as _, SendAccountRecoveryEmailsJob},
26};
27use mas_templates::{
28 EmptyContext, FieldError, FormError, FormState, RecoveryStartContext, RecoveryStartFormField,
29 TemplateContext, Templates,
30};
31use serde::{Deserialize, Serialize};
32
33use crate::{BoundActivityTracker, Limiter, PreferredLanguage, RequesterFingerprint};
34
35#[derive(Deserialize, Serialize)]
36pub(crate) struct StartRecoveryForm {
37 email: String,
38}
39
40pub(crate) async fn get(
41 mut rng: BoxRng,
42 clock: BoxClock,
43 mut repo: BoxRepository,
44 State(site_config): State<SiteConfig>,
45 State(templates): State<Templates>,
46 State(url_builder): State<UrlBuilder>,
47 PreferredLanguage(locale): PreferredLanguage,
48 cookie_jar: CookieJar,
49) -> Result<Response, FancyError> {
50 if !site_config.account_recovery_allowed {
51 let context = EmptyContext.with_language(locale);
52 let rendered = templates.render_recovery_disabled(&context)?;
53 return Ok((cookie_jar, Html(rendered)).into_response());
54 }
55
56 let (session_info, cookie_jar) = cookie_jar.session_info();
57 let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
58
59 let maybe_session = session_info.load_active_session(&mut repo).await?;
60 if maybe_session.is_some() {
61 return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response());
63 }
64
65 let context = RecoveryStartContext::new()
66 .with_csrf(csrf_token.form_value())
67 .with_language(locale);
68
69 repo.save().await?;
70
71 let rendered = templates.render_recovery_start(&context)?;
72
73 Ok((cookie_jar, Html(rendered)).into_response())
74}
75
76pub(crate) async fn post(
77 mut rng: BoxRng,
78 clock: BoxClock,
79 mut repo: BoxRepository,
80 user_agent: TypedHeader<headers::UserAgent>,
81 activity_tracker: BoundActivityTracker,
82 State(site_config): State<SiteConfig>,
83 State(templates): State<Templates>,
84 State(url_builder): State<UrlBuilder>,
85 (State(limiter), requester): (State<Limiter>, RequesterFingerprint),
86 PreferredLanguage(locale): PreferredLanguage,
87 cookie_jar: CookieJar,
88 Form(form): Form<ProtectedForm<StartRecoveryForm>>,
89) -> Result<impl IntoResponse, FancyError> {
90 if !site_config.account_recovery_allowed {
91 let context = EmptyContext.with_language(locale);
92 let rendered = templates.render_recovery_disabled(&context)?;
93 return Ok((cookie_jar, Html(rendered)).into_response());
94 }
95
96 let (session_info, cookie_jar) = cookie_jar.session_info();
97 let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
98
99 let maybe_session = session_info.load_active_session(&mut repo).await?;
100 if maybe_session.is_some() {
101 return Ok((cookie_jar, url_builder.redirect(&mas_router::Index)).into_response());
103 }
104
105 let user_agent = UserAgent::parse(user_agent.as_str().to_owned());
106 let ip_address = activity_tracker.ip();
107
108 let form = cookie_jar.verify_form(&clock, form)?;
109 let mut form_state = FormState::from_form(&form);
110
111 if Address::from_str(&form.email).is_err() {
112 form_state =
113 form_state.with_error_on_field(RecoveryStartFormField::Email, FieldError::Invalid);
114 }
115
116 if form_state.is_valid() {
117 if let Err(e) = limiter.check_account_recovery(requester, &form.email) {
119 tracing::warn!(error = &e as &dyn std::error::Error);
120 form_state.add_error_on_form(FormError::RateLimitExceeded);
121 }
122 }
123
124 if !form_state.is_valid() {
125 repo.save().await?;
126 let context = RecoveryStartContext::new()
127 .with_form_state(form_state)
128 .with_csrf(csrf_token.form_value())
129 .with_language(locale);
130
131 let rendered = templates.render_recovery_start(&context)?;
132
133 return Ok((cookie_jar, Html(rendered)).into_response());
134 }
135
136 let session = repo
137 .user_recovery()
138 .add_session(
139 &mut rng,
140 &clock,
141 form.email,
142 user_agent,
143 ip_address,
144 locale.to_string(),
145 )
146 .await?;
147
148 repo.queue_job()
149 .schedule_job(
150 &mut rng,
151 &clock,
152 SendAccountRecoveryEmailsJob::new(&session),
153 )
154 .await?;
155
156 repo.save().await?;
157
158 Ok((
159 cookie_jar,
160 url_builder.redirect(&mas_router::AccountRecoveryProgress::new(session.id)),
161 )
162 .into_response())
163}