Skip to content

Commit

Permalink
Registration step to set a display name
Browse files Browse the repository at this point in the history
  • Loading branch information
sandhose committed Jan 15, 2025
1 parent 336cfd7 commit 45d4218
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 4 deletions.
5 changes: 5 additions & 0 deletions crates/handlers/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,11 @@ where
get(self::views::register::steps::verify_email::get)
.post(self::views::register::steps::verify_email::post),
)
.route(
mas_router::RegisterDisplayName::route(),
get(self::views::register::steps::display_name::get)
.post(self::views::register::steps::display_name::post),
)
.route(
mas_router::RegisterFinish::route(),
get(self::views::register::steps::finish::get),
Expand Down
182 changes: 182 additions & 0 deletions crates/handlers/src/views/register/steps/display_name.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

use anyhow::Context as _;
use axum::{
extract::{Path, State},
response::{Html, IntoResponse, Response},
Form,
};
use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt as _, ProtectedForm},
FancyError,
};
use mas_router::{PostAuthAction, UrlBuilder};
use mas_storage::{BoxClock, BoxRepository, BoxRng};
use mas_templates::{
FieldError, RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
TemplateContext as _, Templates, ToFormState,
};
use serde::{Deserialize, Serialize};
use ulid::Ulid;

use crate::{views::shared::OptionalPostAuthAction, PreferredLanguage};

#[derive(Deserialize, Default)]
#[serde(rename_all = "snake_case")]
enum FormAction {
#[default]
Set,
Skip,
}

#[derive(Deserialize, Serialize)]
pub(crate) struct DisplayNameForm {
#[serde(skip_serializing, default)]
action: FormAction,
#[serde(default)]
display_name: String,
}

impl ToFormState for DisplayNameForm {
type Field = mas_templates::RegisterStepsDisplayNameFormField;
}

#[tracing::instrument(
name = "handlers.views.register.steps.display_name.get",
fields(user_registration.id = %id),
skip_all,
err,
)]
pub(crate) async fn get(
mut rng: BoxRng,
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository,
Path(id): Path<Ulid>,
cookie_jar: CookieJar,
) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);

let registration = repo
.user_registration()
.lookup(id)
.await?
.context("Could not find user registration")?;

// If the registration is completed, we can go to the registration destination
// XXX: this might not be the right thing to do? Maybe an error page would be
// better?
if registration.completed_at.is_some() {
let post_auth_action: Option<PostAuthAction> = registration
.post_auth_action
.map(serde_json::from_value)
.transpose()?;

return Ok((
cookie_jar,
OptionalPostAuthAction::from(post_auth_action)
.go_next(&url_builder)
.into_response(),
)
.into_response());
}

let ctx = RegisterStepsDisplayNameContext::new()
.with_csrf(csrf_token.form_value())
.with_language(locale);

let content = templates.render_register_steps_display_name(&ctx)?;

Ok((cookie_jar, Html(content)).into_response())
}

#[tracing::instrument(
name = "handlers.views.register.steps.display_name.post",
fields(user_registration.id = %id),
skip_all,
err,
)]
pub(crate) async fn post(
mut rng: BoxRng,
clock: BoxClock,
PreferredLanguage(locale): PreferredLanguage,
State(templates): State<Templates>,
State(url_builder): State<UrlBuilder>,
mut repo: BoxRepository,
Path(id): Path<Ulid>,
cookie_jar: CookieJar,
Form(form): Form<ProtectedForm<DisplayNameForm>>,
) -> Result<Response, FancyError> {
let registration = repo
.user_registration()
.lookup(id)
.await?
.context("Could not find user registration")?;

// If the registration is completed, we can go to the registration destination
// XXX: this might not be the right thing to do? Maybe an error page would be
// better?
if registration.completed_at.is_some() {
let post_auth_action: Option<PostAuthAction> = registration
.post_auth_action
.map(serde_json::from_value)
.transpose()?;

return Ok((
cookie_jar,
OptionalPostAuthAction::from(post_auth_action)
.go_next(&url_builder)
.into_response(),
)
.into_response());
}

let form = cookie_jar.verify_form(&clock, form)?;

let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);

let display_name = match form.action {
FormAction::Set => {
let display_name = form.display_name.trim();

if display_name.is_empty() || display_name.len() > 255 {
let ctx = RegisterStepsDisplayNameContext::new()
.with_form_state(form.to_form_state().with_error_on_field(
RegisterStepsDisplayNameFormField::DisplayName,
FieldError::Invalid,
))
.with_csrf(csrf_token.form_value())
.with_language(locale);

return Ok((
cookie_jar,
Html(templates.render_register_steps_display_name(&ctx)?),
)
.into_response());
}

display_name.to_owned()
}
FormAction::Skip => {
// If the user chose to skip, we do the same as Synapse and use the localpart as
// default display name
registration.username.clone()
}
};

let registration = repo
.user_registration()
.set_display_name(registration, display_name)
.await?;

repo.save().await?;

let destination = mas_router::RegisterFinish::new(registration.id);
return Ok((cookie_jar, url_builder.redirect(&destination)).into_response());
}
8 changes: 8 additions & 0 deletions crates/handlers/src/views/register/steps/finish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ pub(crate) async fn get(
)));
}

// Check that the display name is set
if registration.display_name.is_none() {
return Ok((
cookie_jar,
url_builder.redirect(&mas_router::RegisterDisplayName::new(registration.id)),
));
}

// Everuthing is good, let's complete the registration
let registration = repo
.user_registration()
Expand Down
1 change: 1 addition & 0 deletions crates/handlers/src/views/register/steps/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.

pub(crate) mod display_name;
pub(crate) mod finish;
pub(crate) mod verify_email;
24 changes: 24 additions & 0 deletions crates/router/src/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,30 @@ impl From<Option<PostAuthAction>> for PasswordRegister {
}
}

/// `GET|POST /register/steps/:id/display-name`
#[derive(Debug, Clone)]
pub struct RegisterDisplayName {
id: Ulid,
}

impl RegisterDisplayName {
#[must_use]
pub fn new(id: Ulid) -> Self {
Self { id }
}
}

impl Route for RegisterDisplayName {
type Query = ();
fn route() -> &'static str {
"/register/steps/:id/display-name"
}

fn path(&self) -> std::borrow::Cow<'static, str> {
format!("/register/steps/{}/display-name", self.id).into()
}
}

/// `GET|POST /register/steps/:id/verify-email`
#[derive(Debug, Clone)]
pub struct RegisterVerifyEmail {
Expand Down
51 changes: 51 additions & 0 deletions crates/templates/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,57 @@ impl TemplateContext for RegisterStepsVerifyEmailContext {
}
}

/// Fields for the display name form
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RegisterStepsDisplayNameFormField {
/// The display name
DisplayName,
}

impl FormField for RegisterStepsDisplayNameFormField {
fn keep(&self) -> bool {
match self {
Self::DisplayName => true,
}
}
}

/// Context used by the `display_name.html` template
#[derive(Serialize, Default)]
pub struct RegisterStepsDisplayNameContext {
form: FormState<RegisterStepsDisplayNameFormField>,
}

impl RegisterStepsDisplayNameContext {
/// Constructs a context for the display name page
#[must_use]
pub fn new() -> Self {
Self::default()
}

/// Set the form state
#[must_use]
pub fn with_form_state(
mut self,
form_state: FormState<RegisterStepsDisplayNameFormField>,
) -> Self {
self.form = form_state;
self
}
}

impl TemplateContext for RegisterStepsDisplayNameContext {
fn sample(_now: chrono::DateTime<chrono::Utc>, _rng: &mut impl Rng) -> Vec<Self>
where
Self: Sized,
{
vec![Self {
form: FormState::default(),
}]
}
}

/// Fields of the account recovery start form
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
Expand Down
5 changes: 5 additions & 0 deletions crates/templates/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ pub use self::{
PostAuthContextInner, ReauthContext, ReauthFormField, RecoveryExpiredContext,
RecoveryFinishContext, RecoveryFinishFormField, RecoveryProgressContext,
RecoveryStartContext, RecoveryStartFormField, RegisterContext, RegisterFormField,
RegisterStepsDisplayNameContext, RegisterStepsDisplayNameFormField,
RegisterStepsVerifyEmailContext, RegisterStepsVerifyEmailFormField, SiteBranding,
SiteConfigExt, SiteFeatures, TemplateContext, UpstreamExistingLinkContext,
UpstreamRegister, UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf,
Expand Down Expand Up @@ -335,6 +336,9 @@ register_templates! {
/// Render the email verification page
pub fn render_register_steps_verify_email(WithLanguage<WithCsrf<RegisterStepsVerifyEmailContext>>) { "pages/register/steps/verify_email.html" }

/// Render the display name page
pub fn render_register_steps_display_name(WithLanguage<WithCsrf<RegisterStepsDisplayNameContext>>) { "pages/register/steps/display_name.html" }

/// Render the client consent page
pub fn render_consent(WithLanguage<WithCsrf<WithSession<ConsentContext>>>) { "pages/consent.html" }

Expand Down Expand Up @@ -428,6 +432,7 @@ impl Templates {
check::render_register(self, now, rng)?;
check::render_password_register(self, now, rng)?;
check::render_register_steps_verify_email(self, now, rng)?;
check::render_register_steps_display_name(self, now, rng)?;
check::render_consent(self, now, rng)?;
check::render_policy_violation(self, now, rng)?;
check::render_sso_login(self, now, rng)?;
Expand Down
3 changes: 2 additions & 1 deletion templates/components/button.html
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
class="",
value="",
disabled=False,
kind="primary",
size="lg",
autocomplete=False,
autocorrect=False,
Expand All @@ -39,7 +40,7 @@
type="{{ type }}"
{% if disabled %}disabled{% endif %}
class="cpd-button {{ class }}"
data-kind="primary"
data-kind="{{ kind }}"
data-size="{{ size }}"
{% if autocapitalize %}autocapitilize="{{ autocapitilize }}"{% endif %}
{% if autocomplete %}autocomplete="{{ autocomplete }}"{% endif %}
Expand Down
Loading

0 comments on commit 45d4218

Please sign in to comment.