mas_handlers/views/register/steps/
verify_email.rs1use anyhow::Context;
7use axum::{
8 extract::{Form, Path, State},
9 response::{Html, IntoResponse, Response},
10};
11use mas_axum_utils::{
12 InternalError,
13 cookies::CookieJar,
14 csrf::{CsrfExt, ProtectedForm},
15};
16use mas_data_model::{BoxClock, BoxRng};
17use mas_router::{PostAuthAction, UrlBuilder};
18use mas_storage::{BoxRepository, RepositoryAccess, user::UserEmailRepository};
19use mas_templates::{
20 FieldError, RegisterStepsVerifyEmailContext, RegisterStepsVerifyEmailFormField,
21 TemplateContext, Templates, ToFormState,
22};
23use serde::{Deserialize, Serialize};
24use ulid::Ulid;
25
26use crate::{Limiter, PreferredLanguage, views::shared::OptionalPostAuthAction};
27
28#[derive(Serialize, Deserialize, Debug)]
29pub struct CodeForm {
30 code: String,
31}
32
33impl ToFormState for CodeForm {
34 type Field = mas_templates::RegisterStepsVerifyEmailFormField;
35}
36
37#[tracing::instrument(
38 name = "handlers.views.register.steps.verify_email.get",
39 fields(user_registration.id = %id),
40 skip_all,
41)]
42pub(crate) async fn get(
43 mut rng: BoxRng,
44 clock: BoxClock,
45 PreferredLanguage(locale): PreferredLanguage,
46 State(templates): State<Templates>,
47 State(url_builder): State<UrlBuilder>,
48 mut repo: BoxRepository,
49 Path(id): Path<Ulid>,
50 cookie_jar: CookieJar,
51) -> Result<Response, InternalError> {
52 let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
53
54 let registration = repo
55 .user_registration()
56 .lookup(id)
57 .await?
58 .context("Could not find user registration")
59 .map_err(InternalError::from_anyhow)?;
60
61 if registration.completed_at.is_some() {
65 let post_auth_action: Option<PostAuthAction> = registration
66 .post_auth_action
67 .map(serde_json::from_value)
68 .transpose()?;
69
70 return Ok((
71 cookie_jar,
72 OptionalPostAuthAction::from(post_auth_action)
73 .go_next(&url_builder)
74 .into_response(),
75 )
76 .into_response());
77 }
78
79 let email_authentication_id = registration
80 .email_authentication_id
81 .context("No email authentication started for this registration")
82 .map_err(InternalError::from_anyhow)?;
83 let email_authentication = repo
84 .user_email()
85 .lookup_authentication(email_authentication_id)
86 .await?
87 .context("Could not find email authentication")
88 .map_err(InternalError::from_anyhow)?;
89
90 if email_authentication.completed_at.is_some() {
91 return Err(InternalError::from_anyhow(anyhow::anyhow!(
93 "Email authentication already completed"
94 )));
95 }
96
97 let ctx = RegisterStepsVerifyEmailContext::new(email_authentication)
98 .with_csrf(csrf_token.form_value())
99 .with_language(locale);
100
101 let content = templates.render_register_steps_verify_email(&ctx)?;
102
103 Ok((cookie_jar, Html(content)).into_response())
104}
105
106#[tracing::instrument(
107 name = "handlers.views.account_email_verify.post",
108 fields(user_email.id = %id),
109 skip_all,
110)]
111pub(crate) async fn post(
112 clock: BoxClock,
113 mut rng: BoxRng,
114 PreferredLanguage(locale): PreferredLanguage,
115 State(templates): State<Templates>,
116 State(limiter): State<Limiter>,
117 mut repo: BoxRepository,
118 cookie_jar: CookieJar,
119 State(url_builder): State<UrlBuilder>,
120 Path(id): Path<Ulid>,
121 Form(form): Form<ProtectedForm<CodeForm>>,
122) -> Result<Response, InternalError> {
123 let form = cookie_jar.verify_form(&clock, form)?;
124
125 let registration = repo
126 .user_registration()
127 .lookup(id)
128 .await?
129 .context("Could not find user registration")
130 .map_err(InternalError::from_anyhow)?;
131
132 if registration.completed_at.is_some() {
136 let post_auth_action: Option<PostAuthAction> = registration
137 .post_auth_action
138 .map(serde_json::from_value)
139 .transpose()?;
140
141 return Ok((
142 cookie_jar,
143 OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
144 )
145 .into_response());
146 }
147
148 let email_authentication_id = registration
149 .email_authentication_id
150 .context("No email authentication started for this registration")
151 .map_err(InternalError::from_anyhow)?;
152 let email_authentication = repo
153 .user_email()
154 .lookup_authentication(email_authentication_id)
155 .await?
156 .context("Could not find email authentication")
157 .map_err(InternalError::from_anyhow)?;
158
159 if email_authentication.completed_at.is_some() {
160 return Err(InternalError::from_anyhow(anyhow::anyhow!(
162 "Email authentication already completed"
163 )));
164 }
165
166 if let Err(e) = limiter.check_email_authentication_attempt(&email_authentication) {
167 tracing::warn!(error = &e as &dyn std::error::Error);
168 let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
169 let ctx = RegisterStepsVerifyEmailContext::new(email_authentication)
170 .with_form_state(
171 form.to_form_state()
172 .with_error_on_form(mas_templates::FormError::RateLimitExceeded),
173 )
174 .with_csrf(csrf_token.form_value())
175 .with_language(locale);
176
177 let content = templates.render_register_steps_verify_email(&ctx)?;
178
179 return Ok((cookie_jar, Html(content)).into_response());
180 }
181
182 let Some(code) = repo
183 .user_email()
184 .find_authentication_code(&email_authentication, &form.code)
185 .await?
186 else {
187 let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
188 let ctx =
189 RegisterStepsVerifyEmailContext::new(email_authentication)
190 .with_form_state(form.to_form_state().with_error_on_field(
191 RegisterStepsVerifyEmailFormField::Code,
192 FieldError::Invalid,
193 ))
194 .with_csrf(csrf_token.form_value())
195 .with_language(locale);
196
197 let content = templates.render_register_steps_verify_email(&ctx)?;
198
199 return Ok((cookie_jar, Html(content)).into_response());
200 };
201
202 repo.user_email()
203 .complete_authentication(&clock, email_authentication, &code)
204 .await?;
205
206 repo.save().await?;
207
208 let destination = mas_router::RegisterFinish::new(registration.id);
209 return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
210}