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