mas_handlers/views/register/steps/
display_name.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 anyhow::Context as _;
7use axum::{
8    Form,
9    extract::{Path, State},
10    response::{Html, IntoResponse, Response},
11};
12use mas_axum_utils::{
13    FancyError,
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, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
21    TemplateContext as _, Templates, ToFormState,
22};
23use serde::{Deserialize, Serialize};
24use ulid::Ulid;
25
26use crate::{PreferredLanguage, views::shared::OptionalPostAuthAction};
27
28#[derive(Deserialize, Default)]
29#[serde(rename_all = "snake_case")]
30enum FormAction {
31    #[default]
32    Set,
33    Skip,
34}
35
36#[derive(Deserialize, Serialize)]
37pub(crate) struct DisplayNameForm {
38    #[serde(skip_serializing, default)]
39    action: FormAction,
40    #[serde(default)]
41    display_name: String,
42}
43
44impl ToFormState for DisplayNameForm {
45    type Field = mas_templates::RegisterStepsDisplayNameFormField;
46}
47
48#[tracing::instrument(
49    name = "handlers.views.register.steps.display_name.get",
50    fields(user_registration.id = %id),
51    skip_all,
52)]
53pub(crate) async fn get(
54    mut rng: BoxRng,
55    clock: BoxClock,
56    PreferredLanguage(locale): PreferredLanguage,
57    State(templates): State<Templates>,
58    State(url_builder): State<UrlBuilder>,
59    mut repo: BoxRepository,
60    Path(id): Path<Ulid>,
61    cookie_jar: CookieJar,
62) -> Result<Response, FancyError> {
63    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
64
65    let registration = repo
66        .user_registration()
67        .lookup(id)
68        .await?
69        .context("Could not find user registration")?;
70
71    // If the registration is completed, we can go to the registration destination
72    // XXX: this might not be the right thing to do? Maybe an error page would be
73    // better?
74    if registration.completed_at.is_some() {
75        let post_auth_action: Option<PostAuthAction> = registration
76            .post_auth_action
77            .map(serde_json::from_value)
78            .transpose()?;
79
80        return Ok((
81            cookie_jar,
82            OptionalPostAuthAction::from(post_auth_action)
83                .go_next(&url_builder)
84                .into_response(),
85        )
86            .into_response());
87    }
88
89    let ctx = RegisterStepsDisplayNameContext::new()
90        .with_csrf(csrf_token.form_value())
91        .with_language(locale);
92
93    let content = templates.render_register_steps_display_name(&ctx)?;
94
95    Ok((cookie_jar, Html(content)).into_response())
96}
97
98#[tracing::instrument(
99    name = "handlers.views.register.steps.display_name.post",
100    fields(user_registration.id = %id),
101    skip_all,
102)]
103pub(crate) async fn post(
104    mut rng: BoxRng,
105    clock: BoxClock,
106    PreferredLanguage(locale): PreferredLanguage,
107    State(templates): State<Templates>,
108    State(url_builder): State<UrlBuilder>,
109    mut repo: BoxRepository,
110    Path(id): Path<Ulid>,
111    cookie_jar: CookieJar,
112    Form(form): Form<ProtectedForm<DisplayNameForm>>,
113) -> Result<Response, FancyError> {
114    let registration = repo
115        .user_registration()
116        .lookup(id)
117        .await?
118        .context("Could not find user registration")?;
119
120    // If the registration is completed, we can go to the registration destination
121    // XXX: this might not be the right thing to do? Maybe an error page would be
122    // better?
123    if registration.completed_at.is_some() {
124        let post_auth_action: Option<PostAuthAction> = registration
125            .post_auth_action
126            .map(serde_json::from_value)
127            .transpose()?;
128
129        return Ok((
130            cookie_jar,
131            OptionalPostAuthAction::from(post_auth_action)
132                .go_next(&url_builder)
133                .into_response(),
134        )
135            .into_response());
136    }
137
138    let form = cookie_jar.verify_form(&clock, form)?;
139
140    let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
141
142    let display_name = match form.action {
143        FormAction::Set => {
144            let display_name = form.display_name.trim();
145
146            if display_name.is_empty() || display_name.len() > 255 {
147                let ctx = RegisterStepsDisplayNameContext::new()
148                    .with_form_state(form.to_form_state().with_error_on_field(
149                        RegisterStepsDisplayNameFormField::DisplayName,
150                        FieldError::Invalid,
151                    ))
152                    .with_csrf(csrf_token.form_value())
153                    .with_language(locale);
154
155                return Ok((
156                    cookie_jar,
157                    Html(templates.render_register_steps_display_name(&ctx)?),
158                )
159                    .into_response());
160            }
161
162            display_name.to_owned()
163        }
164        FormAction::Skip => {
165            // If the user chose to skip, we do the same as Synapse and use the localpart as
166            // default display name
167            registration.username.clone()
168        }
169    };
170
171    let registration = repo
172        .user_registration()
173        .set_display_name(registration, display_name)
174        .await?;
175
176    repo.save().await?;
177
178    let destination = mas_router::RegisterFinish::new(registration.id);
179    return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
180}