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