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::{InternalError, SessionInfoExt as _, cookies::CookieJar};
16use mas_data_model::SiteConfig;
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)]
46pub(crate) async fn get(
47    mut rng: BoxRng,
48    clock: BoxClock,
49    mut repo: BoxRepository,
50    activity_tracker: BoundActivityTracker,
51    user_agent: Option<TypedHeader<headers::UserAgent>>,
52    State(url_builder): State<UrlBuilder>,
53    State(homeserver): State<Arc<dyn HomeserverConnection>>,
54    State(templates): State<Templates>,
55    State(site_config): State<SiteConfig>,
56    PreferredLanguage(lang): PreferredLanguage,
57    cookie_jar: CookieJar,
58    Path(id): Path<Ulid>,
59) -> Result<Response, InternalError> {
60    let user_agent = user_agent.map(|ua| ua.as_str().to_owned());
61    let registration = repo
62        .user_registration()
63        .lookup(id)
64        .await?
65        .context("User registration not found")
66        .map_err(InternalError::from_anyhow)?;
67
68    // If the registration is completed, we can go to the registration destination
69    // XXX: this might not be the right thing to do? Maybe an error page would be
70    // better?
71    if registration.completed_at.is_some() {
72        let post_auth_action: Option<PostAuthAction> = registration
73            .post_auth_action
74            .map(serde_json::from_value)
75            .transpose()?;
76
77        return Ok((
78            cookie_jar,
79            OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
80        )
81            .into_response());
82    }
83
84    // Make sure the registration session hasn't expired
85    // XXX: this duration is hard-coded, could be configurable
86    if clock.now() - registration.created_at > Duration::hours(1) {
87        return Err(InternalError::from_anyhow(anyhow::anyhow!(
88            "Registration session has expired"
89        )));
90    }
91
92    // Check that this registration belongs to this browser
93    let registrations = UserRegistrationSessions::load(&cookie_jar);
94    if !registrations.contains(&registration) {
95        // XXX: we should have a better error screen here
96        return Err(InternalError::from_anyhow(anyhow::anyhow!(
97            "Could not find the registration in the browser cookies"
98        )));
99    }
100
101    // Let's perform last minute checks on the registration, especially to avoid
102    // race conditions where multiple users register with the same username or email
103    // address
104
105    if repo.user().exists(&registration.username).await? {
106        // XXX: this could have a better error message, but as this is unlikely to
107        // happen, we're fine with a vague message for now
108        return Err(InternalError::from_anyhow(anyhow::anyhow!(
109            "Username is already taken"
110        )));
111    }
112
113    if !homeserver
114        .is_localpart_available(&registration.username)
115        .await
116        .map_err(InternalError::from_anyhow)?
117    {
118        return Err(InternalError::from_anyhow(anyhow::anyhow!(
119            "Username is not available"
120        )));
121    }
122
123    // Check if the registration token is required and was provided
124    let registration_token = if site_config.registration_token_required {
125        if let Some(registration_token_id) = registration.user_registration_token_id {
126            let registration_token = repo
127                .user_registration_token()
128                .lookup(registration_token_id)
129                .await?
130                .context("Could not load the registration token")
131                .map_err(InternalError::from_anyhow)?;
132
133            if !registration_token.is_valid(clock.now()) {
134                // XXX: the registration token isn't valid anymore, we should
135                // have a better error in this case?
136                return Err(InternalError::from_anyhow(anyhow::anyhow!(
137                    "Registration token used is no longer valid"
138                )));
139            }
140
141            Some(registration_token)
142        } else {
143            // Else redirect to the registration token page
144            return Ok((
145                cookie_jar,
146                url_builder.redirect(&mas_router::RegisterToken::new(registration.id)),
147            )
148                .into_response());
149        }
150    } else {
151        None
152    };
153
154    // For now, we require an email address on the registration, but this might
155    // change in the future
156    let email_authentication_id = registration
157        .email_authentication_id
158        .context("No email authentication started for this registration")
159        .map_err(InternalError::from_anyhow)?;
160    let email_authentication = repo
161        .user_email()
162        .lookup_authentication(email_authentication_id)
163        .await?
164        .context("Could not load the email authentication")
165        .map_err(InternalError::from_anyhow)?;
166
167    // Check that the email authentication has been completed
168    if email_authentication.completed_at.is_none() {
169        return Ok((
170            cookie_jar,
171            url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)),
172        )
173            .into_response());
174    }
175
176    // Check that the email address isn't already used
177    // It is important to do that here, as we we're not checking during the
178    // registration, because we don't want to disclose whether an email is
179    // already being used or not before we verified it
180    if repo
181        .user_email()
182        .count(UserEmailFilter::new().for_email(&email_authentication.email))
183        .await?
184        > 0
185    {
186        let action = registration
187            .post_auth_action
188            .map(serde_json::from_value)
189            .transpose()?;
190
191        let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action)
192            .with_language(lang);
193
194        return Ok((
195            cookie_jar,
196            Html(templates.render_register_steps_email_in_use(&ctx)?),
197        )
198            .into_response());
199    }
200
201    // Check that the display name is set
202    if registration.display_name.is_none() {
203        return Ok((
204            cookie_jar,
205            url_builder.redirect(&mas_router::RegisterDisplayName::new(registration.id)),
206        )
207            .into_response());
208    }
209
210    // Everything is good, let's complete the registration
211    let registration = repo
212        .user_registration()
213        .complete(&clock, registration)
214        .await?;
215
216    // If we used a registration token, we need to mark it as used
217    if let Some(registration_token) = registration_token {
218        repo.user_registration_token()
219            .use_token(&clock, registration_token)
220            .await?;
221    }
222
223    // Consume the registration session
224    let cookie_jar = registrations
225        .consume_session(&registration)?
226        .save(cookie_jar, &clock);
227
228    // Now we can start the user creation
229    let user = repo
230        .user()
231        .add(&mut rng, &clock, registration.username)
232        .await?;
233    // Also create a browser session which will log the user in
234    let user_session = repo
235        .browser_session()
236        .add(&mut rng, &clock, &user, user_agent)
237        .await?;
238
239    repo.user_email()
240        .add(&mut rng, &clock, &user, email_authentication.email)
241        .await?;
242
243    if let Some(password) = registration.password {
244        let user_password = repo
245            .user_password()
246            .add(
247                &mut rng,
248                &clock,
249                &user,
250                password.version,
251                password.hashed_password,
252                None,
253            )
254            .await?;
255
256        repo.browser_session()
257            .authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
258            .await?;
259
260        PASSWORD_REGISTER_COUNTER.add(1, &[]);
261    }
262
263    if let Some(terms_url) = registration.terms_url {
264        repo.user_terms()
265            .accept_terms(&mut rng, &clock, &user, terms_url)
266            .await?;
267    }
268
269    let mut job = ProvisionUserJob::new(&user);
270    if let Some(display_name) = registration.display_name {
271        job = job.set_display_name(display_name);
272    }
273    repo.queue_job().schedule_job(&mut rng, &clock, job).await?;
274
275    repo.save().await?;
276
277    activity_tracker
278        .record_browser_session(&clock, &user_session)
279        .await;
280
281    let post_auth_action: Option<PostAuthAction> = registration
282        .post_auth_action
283        .map(serde_json::from_value)
284        .transpose()?;
285
286    // Login the user with the session we just created
287    let cookie_jar = cookie_jar.set_session(&user_session);
288
289    return Ok((
290        cookie_jar,
291        OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
292    )
293        .into_response());
294}