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