mas_handlers/views/register/steps/
finish.rs

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
6use std::sync::{Arc, LazyLock};
7
8use anyhow::Context as _;
9use axum::{
10    extract::{Path, State},
11    response::{Html, IntoResponse, Response},
12};
13use axum_extra::TypedHeader;
14use chrono::Duration;
15use mas_axum_utils::{FancyError, SessionInfoExt as _, cookies::CookieJar};
16use mas_matrix::HomeserverConnection;
17use mas_router::{PostAuthAction, UrlBuilder};
18use mas_storage::{
19    BoxClock, BoxRepository, BoxRng,
20    queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
21    user::UserEmailFilter,
22};
23use mas_templates::{RegisterStepsEmailInUseContext, TemplateContext as _, Templates};
24use opentelemetry::metrics::Counter;
25use ulid::Ulid;
26
27use super::super::cookie::UserRegistrationSessions;
28use crate::{
29    BoundActivityTracker, METER, PreferredLanguage, views::shared::OptionalPostAuthAction,
30};
31
32static PASSWORD_REGISTER_COUNTER: LazyLock<Counter<u64>> = LazyLock::new(|| {
33    METER
34        .u64_counter("mas.user.password_registration")
35        .with_description("Number of password registrations")
36        .with_unit("{registration}")
37        .build()
38});
39
40#[tracing::instrument(
41    name = "handlers.views.register.steps.finish.get",
42    fields(user_registration.id = %id),
43    skip_all,
44)]
45pub(crate) async fn get(
46    mut rng: BoxRng,
47    clock: BoxClock,
48    mut repo: BoxRepository,
49    activity_tracker: BoundActivityTracker,
50    user_agent: Option<TypedHeader<headers::UserAgent>>,
51    State(url_builder): State<UrlBuilder>,
52    State(homeserver): State<Arc<dyn HomeserverConnection>>,
53    State(templates): State<Templates>,
54    PreferredLanguage(lang): PreferredLanguage,
55    cookie_jar: CookieJar,
56    Path(id): Path<Ulid>,
57) -> Result<Response, FancyError> {
58    let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
59    let registration = repo
60        .user_registration()
61        .lookup(id)
62        .await?
63        .context("User registration not found")?;
64
65    // If the registration is completed, we can go to the registration destination
66    // XXX: this might not be the right thing to do? Maybe an error page would be
67    // better?
68    if registration.completed_at.is_some() {
69        let post_auth_action: Option<PostAuthAction> = registration
70            .post_auth_action
71            .map(serde_json::from_value)
72            .transpose()?;
73
74        return Ok((
75            cookie_jar,
76            OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
77        )
78            .into_response());
79    }
80
81    // Make sure the registration session hasn't expired
82    // XXX: this duration is hard-coded, could be configurable
83    if clock.now() - registration.created_at > Duration::hours(1) {
84        return Err(FancyError::from(anyhow::anyhow!(
85            "Registration session has expired"
86        )));
87    }
88
89    // Check that this registration belongs to this browser
90    let registrations = UserRegistrationSessions::load(&cookie_jar);
91    if !registrations.contains(&registration) {
92        // XXX: we should have a better error screen here
93        return Err(FancyError::from(anyhow::anyhow!(
94            "Could not find the registration in the browser cookies"
95        )));
96    }
97
98    // Let's perform last minute checks on the registration, especially to avoid
99    // race conditions where multiple users register with the same username or email
100    // address
101
102    if repo.user().exists(&registration.username).await? {
103        // XXX: this could have a better error message, but as this is unlikely to
104        // happen, we're fine with a vague message for now
105        return Err(FancyError::from(anyhow::anyhow!(
106            "Username is already taken"
107        )));
108    }
109
110    if !homeserver
111        .is_localpart_available(&registration.username)
112        .await?
113    {
114        return Err(FancyError::from(anyhow::anyhow!(
115            "Username is not available"
116        )));
117    }
118
119    // For now, we require an email address on the registration, but this might
120    // change in the future
121    let email_authentication_id = registration
122        .email_authentication_id
123        .context("No email authentication started for this registration")?;
124    let email_authentication = repo
125        .user_email()
126        .lookup_authentication(email_authentication_id)
127        .await?
128        .context("Could not load the email authentication")?;
129
130    // Check that the email authentication has been completed
131    if email_authentication.completed_at.is_none() {
132        return Ok((
133            cookie_jar,
134            url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)),
135        )
136            .into_response());
137    }
138
139    // Check that the email address isn't already used
140    // It is important to do that here, as we we're not checking during the
141    // registration, because we don't want to disclose whether an email is
142    // already being used or not before we verified it
143    if repo
144        .user_email()
145        .count(UserEmailFilter::new().for_email(&email_authentication.email))
146        .await?
147        > 0
148    {
149        let action = registration
150            .post_auth_action
151            .map(serde_json::from_value)
152            .transpose()?;
153
154        let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action)
155            .with_language(lang);
156
157        return Ok((
158            cookie_jar,
159            Html(templates.render_register_steps_email_in_use(&ctx)?),
160        )
161            .into_response());
162    }
163
164    // Check that the display name is set
165    if registration.display_name.is_none() {
166        return Ok((
167            cookie_jar,
168            url_builder.redirect(&mas_router::RegisterDisplayName::new(registration.id)),
169        )
170            .into_response());
171    }
172
173    // Everuthing is good, let's complete the registration
174    let registration = repo
175        .user_registration()
176        .complete(&clock, registration)
177        .await?;
178
179    // Consume the registration session
180    let cookie_jar = registrations
181        .consume_session(&registration)?
182        .save(cookie_jar, &clock);
183
184    // Now we can start the user creation
185    let user = repo
186        .user()
187        .add(&mut rng, &clock, registration.username)
188        .await?;
189    // Also create a browser session which will log the user in
190    let user_session = repo
191        .browser_session()
192        .add(&mut rng, &clock, &user, user_agent)
193        .await?;
194
195    repo.user_email()
196        .add(&mut rng, &clock, &user, email_authentication.email)
197        .await?;
198
199    if let Some(password) = registration.password {
200        let user_password = repo
201            .user_password()
202            .add(
203                &mut rng,
204                &clock,
205                &user,
206                password.version,
207                password.hashed_password,
208                None,
209            )
210            .await?;
211
212        repo.browser_session()
213            .authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
214            .await?;
215
216        PASSWORD_REGISTER_COUNTER.add(1, &[]);
217    }
218
219    if let Some(terms_url) = registration.terms_url {
220        repo.user_terms()
221            .accept_terms(&mut rng, &clock, &user, terms_url)
222            .await?;
223    }
224
225    let mut job = ProvisionUserJob::new(&user);
226    if let Some(display_name) = registration.display_name {
227        job = job.set_display_name(display_name);
228    }
229    repo.queue_job().schedule_job(&mut rng, &clock, job).await?;
230
231    repo.save().await?;
232
233    activity_tracker
234        .record_browser_session(&clock, &user_session)
235        .await;
236
237    let post_auth_action: Option<PostAuthAction> = registration
238        .post_auth_action
239        .map(serde_json::from_value)
240        .transpose()?;
241
242    // Login the user with the session we just created
243    let cookie_jar = cookie_jar.set_session(&user_session);
244
245    return Ok((
246        cookie_jar,
247        OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
248    )
249        .into_response());
250}