mas_handlers/views/register/steps/
registration_token.rs

1// Copyright 2025 New Vector Ltd.
2//
3// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
4// Please see LICENSE files in the repository root for full details.
5
6use anyhow::Context as _;
7use axum::{
8    Form,
9    extract::{Path, State},
10    response::{Html, IntoResponse, Response},
11};
12use mas_axum_utils::{
13    InternalError,
14    cookies::CookieJar,
15    csrf::{CsrfExt as _, ProtectedForm},
16};
17use mas_router::{PostAuthAction, UrlBuilder};
18use mas_storage::{BoxClock, BoxRepository, BoxRng};
19use mas_templates::{
20    FieldError, RegisterStepsRegistrationTokenContext, RegisterStepsRegistrationTokenFormField,
21    TemplateContext as _, Templates, ToFormState,
22};
23use serde::{Deserialize, Serialize};
24use ulid::Ulid;
25
26use crate::{PreferredLanguage, views::shared::OptionalPostAuthAction};
27
28#[derive(Deserialize, Serialize)]
29pub(crate) struct RegistrationTokenForm {
30    #[serde(default)]
31    token: String,
32}
33
34impl ToFormState for RegistrationTokenForm {
35    type Field = mas_templates::RegisterStepsRegistrationTokenFormField;
36}
37
38#[tracing::instrument(
39    name = "handlers.views.register.steps.registration_token.get",
40    fields(user_registration.id = %id),
41    skip_all,
42)]
43pub(crate) async fn get(
44    mut rng: BoxRng,
45    clock: BoxClock,
46    PreferredLanguage(locale): PreferredLanguage,
47    State(templates): State<Templates>,
48    State(url_builder): State<UrlBuilder>,
49    mut repo: BoxRepository,
50    Path(id): Path<Ulid>,
51    cookie_jar: CookieJar,
52) -> Result<Response, InternalError> {
53    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
54
55    let registration = repo
56        .user_registration()
57        .lookup(id)
58        .await?
59        .context("Could not find user registration")
60        .map_err(InternalError::from_anyhow)?;
61
62    // If the registration is completed, we can go to the registration destination
63    if registration.completed_at.is_some() {
64        let post_auth_action: Option<PostAuthAction> = registration
65            .post_auth_action
66            .map(serde_json::from_value)
67            .transpose()?;
68
69        return Ok((
70            cookie_jar,
71            OptionalPostAuthAction::from(post_auth_action)
72                .go_next(&url_builder)
73                .into_response(),
74        )
75            .into_response());
76    }
77
78    // If the registration already has a token, skip this step
79    if registration.user_registration_token_id.is_some() {
80        let destination = mas_router::RegisterDisplayName::new(registration.id);
81        return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
82    }
83
84    let ctx = RegisterStepsRegistrationTokenContext::new()
85        .with_csrf(csrf_token.form_value())
86        .with_language(locale);
87
88    let content = templates.render_register_steps_registration_token(&ctx)?;
89
90    Ok((cookie_jar, Html(content)).into_response())
91}
92
93#[tracing::instrument(
94    name = "handlers.views.register.steps.registration_token.post",
95    fields(user_registration.id = %id),
96    skip_all,
97)]
98pub(crate) async fn post(
99    mut rng: BoxRng,
100    clock: BoxClock,
101    PreferredLanguage(locale): PreferredLanguage,
102    State(templates): State<Templates>,
103    State(url_builder): State<UrlBuilder>,
104    mut repo: BoxRepository,
105    Path(id): Path<Ulid>,
106    cookie_jar: CookieJar,
107    Form(form): Form<ProtectedForm<RegistrationTokenForm>>,
108) -> Result<Response, InternalError> {
109    let registration = repo
110        .user_registration()
111        .lookup(id)
112        .await?
113        .context("Could not find user registration")
114        .map_err(InternalError::from_anyhow)?;
115
116    // If the registration is completed, we can go to the registration destination
117    if registration.completed_at.is_some() {
118        let post_auth_action: Option<PostAuthAction> = registration
119            .post_auth_action
120            .map(serde_json::from_value)
121            .transpose()?;
122
123        return Ok((
124            cookie_jar,
125            OptionalPostAuthAction::from(post_auth_action)
126                .go_next(&url_builder)
127                .into_response(),
128        )
129            .into_response());
130    }
131
132    let form = cookie_jar.verify_form(&clock, form)?;
133
134    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
135
136    // Validate the token
137    let token = form.token.trim();
138    if token.is_empty() {
139        let ctx = RegisterStepsRegistrationTokenContext::new()
140            .with_form_state(form.to_form_state().with_error_on_field(
141                RegisterStepsRegistrationTokenFormField::Token,
142                FieldError::Required,
143            ))
144            .with_csrf(csrf_token.form_value())
145            .with_language(locale);
146
147        return Ok((
148            cookie_jar,
149            Html(templates.render_register_steps_registration_token(&ctx)?),
150        )
151            .into_response());
152    }
153
154    // Look up the token
155    let Some(registration_token) = repo.user_registration_token().find_by_token(token).await?
156    else {
157        let ctx = RegisterStepsRegistrationTokenContext::new()
158            .with_form_state(form.to_form_state().with_error_on_field(
159                RegisterStepsRegistrationTokenFormField::Token,
160                FieldError::Invalid,
161            ))
162            .with_csrf(csrf_token.form_value())
163            .with_language(locale);
164
165        return Ok((
166            cookie_jar,
167            Html(templates.render_register_steps_registration_token(&ctx)?),
168        )
169            .into_response());
170    };
171
172    // Check if the token is still valid
173    if !registration_token.is_valid(clock.now()) {
174        tracing::warn!("Registration token isn't valid (expired or already used)");
175        let ctx = RegisterStepsRegistrationTokenContext::new()
176            .with_form_state(form.to_form_state().with_error_on_field(
177                RegisterStepsRegistrationTokenFormField::Token,
178                FieldError::Invalid,
179            ))
180            .with_csrf(csrf_token.form_value())
181            .with_language(locale);
182
183        return Ok((
184            cookie_jar,
185            Html(templates.render_register_steps_registration_token(&ctx)?),
186        )
187            .into_response());
188    }
189
190    // Associate the token with the registration
191    let registration = repo
192        .user_registration()
193        .set_registration_token(registration, &registration_token)
194        .await?;
195
196    repo.save().await?;
197
198    // Continue to the next step
199    let destination = mas_router::RegisterFinish::new(registration.id);
200    Ok((cookie_jar, url_builder.redirect(&destination)).into_response())
201}