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 FancyError,
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 err,
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, FancyError> {
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
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 let email_authentication = repo
82 .user_email()
83 .lookup_authentication(email_authentication_id)
84 .await?
85 .context("Could not find email authentication")?;
86
87 if email_authentication.completed_at.is_some() {
88 return Err(FancyError::from(anyhow::anyhow!(
90 "Email authentication already completed"
91 )));
92 }
93
94 let ctx = RegisterStepsVerifyEmailContext::new(email_authentication)
95 .with_csrf(csrf_token.form_value())
96 .with_language(locale);
97
98 let content = templates.render_register_steps_verify_email(&ctx)?;
99
100 Ok((cookie_jar, Html(content)).into_response())
101}
102
103#[tracing::instrument(
104 name = "handlers.views.account_email_verify.post",
105 fields(user_email.id = %id),
106 skip_all,
107 err,
108)]
109pub(crate) async fn post(
110 clock: BoxClock,
111 mut rng: BoxRng,
112 PreferredLanguage(locale): PreferredLanguage,
113 State(templates): State<Templates>,
114 State(limiter): State<Limiter>,
115 mut repo: BoxRepository,
116 cookie_jar: CookieJar,
117 State(url_builder): State<UrlBuilder>,
118 Path(id): Path<Ulid>,
119 Form(form): Form<ProtectedForm<CodeForm>>,
120) -> Result<Response, FancyError> {
121 let form = cookie_jar.verify_form(&clock, form)?;
122
123 let registration = repo
124 .user_registration()
125 .lookup(id)
126 .await?
127 .context("Could not find user registration")?;
128
129 if registration.completed_at.is_some() {
133 let post_auth_action: Option<PostAuthAction> = registration
134 .post_auth_action
135 .map(serde_json::from_value)
136 .transpose()?;
137
138 return Ok((
139 cookie_jar,
140 OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
141 )
142 .into_response());
143 }
144
145 let email_authentication_id = registration
146 .email_authentication_id
147 .context("No email authentication started for this registration")?;
148 let email_authentication = repo
149 .user_email()
150 .lookup_authentication(email_authentication_id)
151 .await?
152 .context("Could not find email authentication")?;
153
154 if email_authentication.completed_at.is_some() {
155 return Err(FancyError::from(anyhow::anyhow!(
157 "Email authentication already completed"
158 )));
159 }
160
161 if let Err(e) = limiter.check_email_authentication_attempt(&email_authentication) {
162 tracing::warn!(error = &e as &dyn std::error::Error);
163 let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
164 let ctx = RegisterStepsVerifyEmailContext::new(email_authentication)
165 .with_form_state(
166 form.to_form_state()
167 .with_error_on_form(mas_templates::FormError::RateLimitExceeded),
168 )
169 .with_csrf(csrf_token.form_value())
170 .with_language(locale);
171
172 let content = templates.render_register_steps_verify_email(&ctx)?;
173
174 return Ok((cookie_jar, Html(content)).into_response());
175 }
176
177 let Some(code) = repo
178 .user_email()
179 .find_authentication_code(&email_authentication, &form.code)
180 .await?
181 else {
182 let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
183 let ctx =
184 RegisterStepsVerifyEmailContext::new(email_authentication)
185 .with_form_state(form.to_form_state().with_error_on_field(
186 RegisterStepsVerifyEmailFormField::Code,
187 FieldError::Invalid,
188 ))
189 .with_csrf(csrf_token.form_value())
190 .with_language(locale);
191
192 let content = templates.render_register_steps_verify_email(&ctx)?;
193
194 return Ok((cookie_jar, Html(content)).into_response());
195 };
196
197 repo.user_email()
198 .complete_authentication(&clock, email_authentication, &code)
199 .await?;
200
201 repo.save().await?;
202
203 let destination = mas_router::RegisterFinish::new(registration.id);
204 return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
205}