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_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, InternalError> {
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        .map_err(InternalError::from_anyhow)?;
65
66    // If the registration is completed, we can go to the registration destination
67    // XXX: this might not be the right thing to do? Maybe an error page would be
68    // better?
69    if registration.completed_at.is_some() {
70        let post_auth_action: Option<PostAuthAction> = registration
71            .post_auth_action
72            .map(serde_json::from_value)
73            .transpose()?;
74
75        return Ok((
76            cookie_jar,
77            OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
78        )
79            .into_response());
80    }
81
82    // Make sure the registration session hasn't expired
83    // XXX: this duration is hard-coded, could be configurable
84    if clock.now() - registration.created_at > Duration::hours(1) {
85        return Err(InternalError::from_anyhow(anyhow::anyhow!(
86            "Registration session has expired"
87        )));
88    }
89
90    // Check that this registration belongs to this browser
91    let registrations = UserRegistrationSessions::load(&cookie_jar);
92    if !registrations.contains(&registration) {
93        // XXX: we should have a better error screen here
94        return Err(InternalError::from_anyhow(anyhow::anyhow!(
95            "Could not find the registration in the browser cookies"
96        )));
97    }
98
99    // Let's perform last minute checks on the registration, especially to avoid
100    // race conditions where multiple users register with the same username or email
101    // address
102
103    if repo.user().exists(&registration.username).await? {
104        // XXX: this could have a better error message, but as this is unlikely to
105        // happen, we're fine with a vague message for now
106        return Err(InternalError::from_anyhow(anyhow::anyhow!(
107            "Username is already taken"
108        )));
109    }
110
111    if !homeserver
112        .is_localpart_available(&registration.username)
113        .await
114        .map_err(InternalError::from_anyhow)?
115    {
116        return Err(InternalError::from_anyhow(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        .map_err(InternalError::from_anyhow)?;
127    let email_authentication = repo
128        .user_email()
129        .lookup_authentication(email_authentication_id)
130        .await?
131        .context("Could not load the email authentication")
132        .map_err(InternalError::from_anyhow)?;
133
134    // Check that the email authentication has been completed
135    if email_authentication.completed_at.is_none() {
136        return Ok((
137            cookie_jar,
138            url_builder.redirect(&mas_router::RegisterVerifyEmail::new(id)),
139        )
140            .into_response());
141    }
142
143    // Check that the email address isn't already used
144    // It is important to do that here, as we we're not checking during the
145    // registration, because we don't want to disclose whether an email is
146    // already being used or not before we verified it
147    if repo
148        .user_email()
149        .count(UserEmailFilter::new().for_email(&email_authentication.email))
150        .await?
151        > 0
152    {
153        let action = registration
154            .post_auth_action
155            .map(serde_json::from_value)
156            .transpose()?;
157
158        let ctx = RegisterStepsEmailInUseContext::new(email_authentication.email, action)
159            .with_language(lang);
160
161        return Ok((
162            cookie_jar,
163            Html(templates.render_register_steps_email_in_use(&ctx)?),
164        )
165            .into_response());
166    }
167
168    // Check that the display name is set
169    if registration.display_name.is_none() {
170        return Ok((
171            cookie_jar,
172            url_builder.redirect(&mas_router::RegisterDisplayName::new(registration.id)),
173        )
174            .into_response());
175    }
176
177    // Everuthing is good, let's complete the registration
178    let registration = repo
179        .user_registration()
180        .complete(&clock, registration)
181        .await?;
182
183    // Consume the registration session
184    let cookie_jar = registrations
185        .consume_session(&registration)?
186        .save(cookie_jar, &clock);
187
188    // Now we can start the user creation
189    let user = repo
190        .user()
191        .add(&mut rng, &clock, registration.username)
192        .await?;
193    // Also create a browser session which will log the user in
194    let user_session = repo
195        .browser_session()
196        .add(&mut rng, &clock, &user, user_agent)
197        .await?;
198
199    repo.user_email()
200        .add(&mut rng, &clock, &user, email_authentication.email)
201        .await?;
202
203    if let Some(password) = registration.password {
204        let user_password = repo
205            .user_password()
206            .add(
207                &mut rng,
208                &clock,
209                &user,
210                password.version,
211                password.hashed_password,
212                None,
213            )
214            .await?;
215
216        repo.browser_session()
217            .authenticate_with_password(&mut rng, &clock, &user_session, &user_password)
218            .await?;
219
220        PASSWORD_REGISTER_COUNTER.add(1, &[]);
221    }
222
223    if let Some(terms_url) = registration.terms_url {
224        repo.user_terms()
225            .accept_terms(&mut rng, &clock, &user, terms_url)
226            .await?;
227    }
228
229    let mut job = ProvisionUserJob::new(&user);
230    if let Some(display_name) = registration.display_name {
231        job = job.set_display_name(display_name);
232    }
233    repo.queue_job().schedule_job(&mut rng, &clock, job).await?;
234
235    repo.save().await?;
236
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    // Login the user with the session we just created
247    let cookie_jar = cookie_jar.set_session(&user_session);
248
249    return Ok((
250        cookie_jar,
251        OptionalPostAuthAction::from(post_auth_action).go_next(&url_builder),
252    )
253        .into_response());
254}