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