Skip to content

Commit 336cfd7

Browse files
committed
Move the finishing of registration to a dedicated view
1 parent 604ab92 commit 336cfd7

File tree

6 files changed

+225
-87
lines changed

6 files changed

+225
-87
lines changed

crates/handlers/src/lib.rs

+4
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,10 @@ where
383383
get(self::views::register::steps::verify_email::get)
384384
.post(self::views::register::steps::verify_email::post),
385385
)
386+
.route(
387+
mas_router::RegisterFinish::route(),
388+
get(self::views::register::steps::finish::get),
389+
)
386390
.route(
387391
mas_router::AccountRecoveryStart::route(),
388392
get(self::views::recovery::start::get).post(self::views::recovery::start::post),

crates/handlers/src/views/register/password.rs

+3-3
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ pub(crate) async fn post(
362362
repo.save().await?;
363363

364364
Ok(url_builder
365-
.redirect(&mas_router::RegisterVerifyEmail::new(registration.id))
365+
.redirect(&mas_router::RegisterFinish::new(registration.id))
366366
.into_response())
367367
}
368368

@@ -479,12 +479,12 @@ mod tests {
479479
response.assert_status(StatusCode::SEE_OTHER);
480480
let location = response.headers().get(LOCATION).unwrap();
481481

482-
// The handler redirects with the ID as the last portion of the path
482+
// The handler redirects with the ID as the second to last portion of the path
483483
let id = location
484484
.to_str()
485485
.unwrap()
486486
.rsplit('/')
487-
.next()
487+
.nth(1)
488488
.unwrap()
489489
.parse()
490490
.unwrap();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
// Copyright 2025 New Vector Ltd.
2+
//
3+
// SPDX-License-Identifier: AGPL-3.0-only
4+
// Please see LICENSE in the repository root for full details.
5+
6+
use anyhow::Context as _;
7+
use axum::{
8+
extract::{Path, State},
9+
response::IntoResponse,
10+
};
11+
use axum_extra::TypedHeader;
12+
use mas_axum_utils::{cookies::CookieJar, FancyError, SessionInfoExt as _};
13+
use mas_data_model::UserAgent;
14+
use mas_router::{PostAuthAction, UrlBuilder};
15+
use mas_storage::{
16+
queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
17+
user::UserEmailFilter,
18+
BoxClock, BoxRepository, BoxRng,
19+
};
20+
use ulid::Ulid;
21+
22+
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker};
23+
24+
#[tracing::instrument(
25+
name = "handlers.views.register.steps.finish.get",
26+
fields(user_registration.id = %id),
27+
skip_all,
28+
err,
29+
)]
30+
pub(crate) async fn get(
31+
mut rng: BoxRng,
32+
clock: BoxClock,
33+
mut repo: BoxRepository,
34+
activity_tracker: BoundActivityTracker,
35+
user_agent: Option<TypedHeader<headers::UserAgent>>,
36+
State(url_builder): State<UrlBuilder>,
37+
cookie_jar: CookieJar,
38+
Path(id): Path<Ulid>,
39+
) -> Result<impl IntoResponse, FancyError> {
40+
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
41+
let registration = repo
42+
.user_registration()
43+
.lookup(id)
44+
.await?
45+
.context("User registration not found")?;
46+
47+
// If the registration is completed, we can go to the registration destination
48+
// XXX: this might not be the right thing to do? Maybe an error page would be
49+
// better?
50+
if registration.completed_at.is_some() {
51+
let post_auth_action: Option<PostAuthAction> = registration
52+
.post_auth_action
53+
.map(serde_json::from_value)
54+
.transpose()?;
55+
56+
return Ok((
57+
cookie_jar,
58+
OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
59+
));
60+
}
61+
62+
// Let's perform last minute checks on the registration, especially to avoid
63+
// race conditions where multiple users register with the same username or email
64+
// address
65+
66+
if repo.user().exists(&registration.username).await? {
67+
return Err(FancyError::from(anyhow::anyhow!(
68+
"Username is already taken"
69+
)));
70+
}
71+
72+
// TODO: query the homeserver
73+
74+
// For now, we require an email address on the registration, but this might
75+
// change in the future
76+
let email_authentication_id = registration
77+
.email_authentication_id
78+
.context("No email authentication started for this registration")?;
79+
let email_authentication = repo
80+
.user_email()
81+
.lookup_authentication(email_authentication_id)
82+
.await?
83+
.context("Could not load the email authentication")?;
84+
85+
// Check that the email authentication has been completed
86+
if email_authentication.completed_at.is_none() {
87+
return Ok((
88+
cookie_jar,
89+
url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)),
90+
));
91+
}
92+
93+
// Check that the email address isn't already used
94+
if repo
95+
.user_email()
96+
.count(UserEmailFilter::new().for_email(&email_authentication.email))
97+
.await?
98+
> 0
99+
{
100+
return Err(FancyError::from(anyhow::anyhow!(
101+
"Email address is already used"
102+
)));
103+
}
104+
105+
// Everuthing is good, let's complete the registration
106+
let registration = repo
107+
.user_registration()
108+
.complete(&clock, registration)
109+
.await?;
110+
111+
// Now we can start the user creation
112+
let user = repo
113+
.user()
114+
.add(&mut rng, &clock, registration.username)
115+
.await?;
116+
// Also create a browser session which will log the user in
117+
let user_session = repo
118+
.browser_session()
119+
.add(&mut rng, &clock, &user, user_agent)
120+
.await?;
121+
122+
repo.user_email()
123+
.add(&mut rng, &clock, &user, email_authentication.email)
124+
.await?;
125+
126+
if let Some(password) = registration.password {
127+
let user_password = repo
128+
.user_password()
129+
.add(
130+
&mut rng,
131+
&clock,
132+
&user,
133+
password.version,
134+
password.hashed_password,
135+
None,
136+
)
137+
.await?;
138+
139+
repo.browser_session()
140+
.authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
141+
.await?;
142+
}
143+
144+
if let Some(terms_url) = registration.terms_url {
145+
repo.user_terms()
146+
.accept_terms(&mut rng, &clock, &user, terms_url)
147+
.await?;
148+
}
149+
150+
let mut job = ProvisionUserJob::new(&user);
151+
if let Some(display_name) = registration.display_name {
152+
job = job.set_display_name(display_name);
153+
}
154+
repo.queue_job().schedule_job(&mut rng, &clock, job).await?;
155+
156+
repo.save().await?;
157+
158+
activity_tracker
159+
.record_browser_session(&clock, &user_session)
160+
.await;
161+
162+
let post_auth_action: Option<PostAuthAction> = registration
163+
.post_auth_action
164+
.map(serde_json::from_value)
165+
.transpose()?;
166+
167+
// Login the user with the session we just created
168+
let cookie_jar = cookie_jar.set_session(&user_session);
169+
170+
return Ok((
171+
cookie_jar,
172+
OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
173+
));
174+
}

crates/handlers/src/views/register/steps/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@
33
// SPDX-License-Identifier: AGPL-3.0-only
44
// Please see LICENSE in the repository root for full details.
55

6+
pub(crate) mod finish;
67
pub(crate) mod verify_email;

crates/handlers/src/views/register/steps/verify_email.rs

+16-81
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,21 @@ use axum::{
88
extract::{Form, Path, State},
99
response::{Html, IntoResponse, Response},
1010
};
11-
use axum_extra::TypedHeader;
1211
use mas_axum_utils::{
1312
cookies::CookieJar,
1413
csrf::{CsrfExt, ProtectedForm},
15-
FancyError, SessionInfoExt,
14+
FancyError,
1615
};
17-
use mas_data_model::UserAgent;
1816
use mas_router::{PostAuthAction, UrlBuilder};
19-
use mas_storage::{
20-
queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
21-
user::UserEmailRepository,
22-
BoxClock, BoxRepository, BoxRng, RepositoryAccess,
23-
};
17+
use mas_storage::{user::UserEmailRepository, BoxClock, BoxRepository, BoxRng, RepositoryAccess};
2418
use mas_templates::{
2519
FieldError, RegisterStepsVerifyEmailContext, RegisterStepsVerifyEmailFormField,
2620
TemplateContext, Templates, ToFormState,
2721
};
2822
use serde::{Deserialize, Serialize};
2923
use ulid::Ulid;
3024

31-
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker, PreferredLanguage};
25+
use crate::{views::shared::OptionalPostAuthAction, PreferredLanguage};
3226

3327
#[derive(Serialize, Deserialize, Debug)]
3428
pub struct CodeForm {
@@ -72,8 +66,12 @@ pub(crate) async fn get(
7266
.map(serde_json::from_value)
7367
.transpose()?;
7468

75-
return Ok(OptionalPostAuthAction::from(post_auth_action)
76-
.go_next(&url_builder)
69+
return Ok((
70+
cookie_jar,
71+
OptionalPostAuthAction::from(post_auth_action)
72+
.go_next(&url_builder)
73+
.into_response(),
74+
)
7775
.into_response());
7876
}
7977

@@ -115,13 +113,10 @@ pub(crate) async fn post(
115113
State(templates): State<Templates>,
116114
mut repo: BoxRepository,
117115
cookie_jar: CookieJar,
118-
user_agent: Option<TypedHeader<headers::UserAgent>>,
119116
State(url_builder): State<UrlBuilder>,
120-
activity_tracker: BoundActivityTracker,
121117
Path(id): Path<Ulid>,
122118
Form(form): Form<ProtectedForm<CodeForm>>,
123119
) -> Result<Response, FancyError> {
124-
let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned()));
125120
let form = cookie_jar.verify_form(&clock, form)?;
126121

127122
let registration = repo
@@ -139,8 +134,10 @@ pub(crate) async fn post(
139134
.map(serde_json::from_value)
140135
.transpose()?;
141136

142-
return Ok(OptionalPostAuthAction::from(post_auth_action)
143-
.go_next(&url_builder)
137+
return Ok((
138+
cookie_jar,
139+
OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
140+
)
144141
.into_response());
145142
}
146143

@@ -180,74 +177,12 @@ pub(crate) async fn post(
180177
return Ok((cookie_jar, Html(content)).into_response());
181178
};
182179

183-
let email_authentication = repo
184-
.user_email()
185-
.complete_authentication(&clock, email_authentication, &code)
186-
.await?;
187-
188-
let registration = repo
189-
.user_registration()
190-
.complete(&clock, registration)
191-
.await?;
192-
193-
// XXX: this should move somewhere else, and it doesn't check for uniqueness
194-
let user = repo
195-
.user()
196-
.add(&mut rng, &clock, registration.username)
197-
.await?;
198-
let user_session = repo
199-
.browser_session()
200-
.add(&mut rng, &clock, &user, user_agent)
201-
.await?;
202-
203180
repo.user_email()
204-
.add(&mut rng, &clock, &user, email_authentication.email)
205-
.await?;
206-
207-
if let Some(password) = registration.password {
208-
let user_password = repo
209-
.user_password()
210-
.add(
211-
&mut rng,
212-
&clock,
213-
&user,
214-
password.version,
215-
password.hashed_password,
216-
None,
217-
)
218-
.await?;
219-
220-
repo.browser_session()
221-
.authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
222-
.await?;
223-
}
224-
225-
if let Some(terms_url) = registration.terms_url {
226-
repo.user_terms()
227-
.accept_terms(&mut rng, &clock, &user, terms_url)
228-
.await?;
229-
}
230-
231-
repo.queue_job()
232-
.schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user))
181+
.complete_authentication(&clock, email_authentication, &code)
233182
.await?;
234183

235184
repo.save().await?;
236185

237-
activity_tracker
238-
.record_browser_session(&clock, &user_session)
239-
.await;
240-
241-
let post_auth_action: Option<PostAuthAction> = registration
242-
.post_auth_action
243-
.map(serde_json::from_value)
244-
.transpose()?;
245-
246-
let cookie_jar = cookie_jar.set_session(&user_session);
247-
248-
return Ok((
249-
cookie_jar,
250-
OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
251-
)
252-
.into_response());
186+
let destination = mas_router::RegisterFinish::new(registration.id);
187+
return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
253188
}

0 commit comments

Comments
 (0)