Remove the dedicated page to add an email address

This commit is contained in:
Quentin Gliech
2025-01-09 17:21:11 +01:00
parent b697a2dfb2
commit ce256684fe
7 changed files with 10 additions and 297 deletions

View File

@@ -383,11 +383,6 @@ where
get(self::views::account::emails::verify::get)
.post(self::views::account::emails::verify::post),
)
.route(
mas_router::AccountAddEmail::route(),
get(self::views::account::emails::add::get)
.post(self::views::account::emails::add::post),
)
.route(
mas_router::AccountRecoveryStart::route(),
get(self::views::recovery::start::get).post(self::views::recovery::start::post),

View File

@@ -1,167 +0,0 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
use axum::{
extract::{Form, Query, State},
response::{Html, IntoResponse, Response},
};
use mas_axum_utils::{
cookies::CookieJar,
csrf::{CsrfExt, ProtectedForm},
FancyError, SessionInfoExt,
};
use mas_data_model::SiteConfig;
use mas_policy::Policy;
use mas_router::UrlBuilder;
use mas_storage::{
queue::{QueueJobRepositoryExt as _, VerifyEmailJob},
user::UserEmailRepository,
BoxClock, BoxRepository, BoxRng,
};
use mas_templates::{EmailAddContext, ErrorContext, TemplateContext, Templates};
use serde::Deserialize;
use crate::{views::shared::OptionalPostAuthAction, BoundActivityTracker, PreferredLanguage};
#[derive(Deserialize, Debug)]
pub struct EmailForm {
email: String,
}
#[tracing::instrument(name = "handlers.views.account_email_add.get", 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>,
State(site_config): State<SiteConfig>,
activity_tracker: BoundActivityTracker,
mut repo: BoxRepository,
cookie_jar: CookieJar,
) -> Result<Response, FancyError> {
let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng);
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let Some(session) = maybe_session else {
let login = mas_router::Login::default();
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
if !site_config.email_change_allowed {
// XXX: this may not be the best error message, it's not translatable
return Err(FancyError::new(
ErrorContext::new()
.with_description("Email change is not allowed".to_owned())
.with_details("The site configuration does not allow email changes".to_owned()),
));
}
activity_tracker
.record_browser_session(&clock, &session)
.await;
let ctx = EmailAddContext::new()
.with_session(session)
.with_csrf(csrf_token.form_value())
.with_language(locale);
let content = templates.render_account_add_email(&ctx)?;
Ok((cookie_jar, Html(content)).into_response())
}
#[tracing::instrument(name = "handlers.views.account_email_add.post", skip_all, err)]
pub(crate) async fn post(
mut rng: BoxRng,
clock: BoxClock,
mut repo: BoxRepository,
PreferredLanguage(locale): PreferredLanguage,
mut policy: Policy,
cookie_jar: CookieJar,
State(url_builder): State<UrlBuilder>,
State(site_config): State<SiteConfig>,
activity_tracker: BoundActivityTracker,
Query(query): Query<OptionalPostAuthAction>,
Form(form): Form<ProtectedForm<EmailForm>>,
) -> Result<Response, FancyError> {
let form = cookie_jar.verify_form(&clock, form)?;
let (session_info, cookie_jar) = cookie_jar.session_info();
let maybe_session = session_info.load_session(&mut repo).await?;
let Some(session) = maybe_session else {
let login = mas_router::Login::default();
return Ok((cookie_jar, url_builder.redirect(&login)).into_response());
};
// XXX: we really should show human readable errors on the form here
if !site_config.email_change_allowed {
return Err(FancyError::new(
ErrorContext::new()
.with_description("Email change is not allowed".to_owned())
.with_details("The site configuration does not allow email changes".to_owned()),
));
}
// Validate the email address
if form.email.parse::<lettre::Address>().is_err() {
return Err(anyhow::anyhow!("Invalid email address").into());
}
// Run the email policy
let res = policy.evaluate_email(&form.email).await?;
if !res.valid() {
return Err(FancyError::new(
ErrorContext::new()
.with_description(format!("Email address {:?} denied by policy", form.email))
.with_details(format!("{res}")),
));
}
// Find an existing email address
let existing_user_email = repo.user_email().find(&session.user, &form.email).await?;
let user_email = if let Some(user_email) = existing_user_email {
user_email
} else {
repo.user_email()
.add(&mut rng, &clock, &session.user, form.email)
.await?
};
// If the email was not confirmed, send a confirmation email & redirect to the
// verify page
let next = if user_email.confirmed_at.is_none() {
repo.queue_job()
.schedule_job(
&mut rng,
&clock,
VerifyEmailJob::new(&user_email).with_language(locale.to_string()),
)
.await?;
let next = mas_router::AccountVerifyEmail::new(user_email.id);
let next = if let Some(action) = query.post_auth_action {
next.and_then(action)
} else {
next
};
url_builder.redirect(&next)
} else {
query.go_next_or_default(&url_builder, &mas_router::Account::default())
};
repo.save().await?;
activity_tracker
.record_browser_session(&clock, &session)
.await;
Ok((cookie_jar, next).into_response())
}

View File

@@ -1,8 +1,7 @@
// Copyright 2024 New Vector Ltd.
// Copyright 2024, 2025 New Vector Ltd.
// Copyright 2022-2024 The Matrix.org Foundation C.I.C.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
pub mod add;
pub mod verify;

View File

@@ -488,31 +488,6 @@ impl Route for AccountVerifyEmail {
}
}
/// `GET /add-email`
#[derive(Default, Debug, Clone)]
pub struct AccountAddEmail {
post_auth_action: Option<PostAuthAction>,
}
impl Route for AccountAddEmail {
type Query = PostAuthAction;
fn route() -> &'static str {
"/add-email"
}
fn query(&self) -> Option<&Self::Query> {
self.post_auth_action.as_ref()
}
}
impl AccountAddEmail {
#[must_use]
pub fn and_then(mut self, action: PostAuthAction) -> Self {
self.post_auth_action = Some(action);
self
}
}
/// Actions parameters as defined by MSC2965
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "action")]

View File

@@ -992,51 +992,6 @@ impl TemplateContext for EmailVerificationPageContext {
}
}
/// Fields of the account email add form
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EmailAddFormField {
/// The email
Email,
}
impl FormField for EmailAddFormField {
fn keep(&self) -> bool {
match self {
Self::Email => true,
}
}
}
/// Context used by the `pages/account/verify.html` templates
#[derive(Serialize, Default)]
pub struct EmailAddContext {
form: FormState<EmailAddFormField>,
}
impl EmailAddContext {
/// Constructs a context for the email add page
#[must_use]
pub fn new() -> Self {
Self::default()
}
/// Set the form state
#[must_use]
pub fn with_form_state(form: FormState<EmailAddFormField>) -> Self {
Self { form }
}
}
impl TemplateContext for EmailAddContext {
fn sample(_now: chrono::DateTime<Utc>, _rng: &mut impl Rng) -> Vec<Self>
where
Self: Sized,
{
vec![Self::default()]
}
}
/// Fields of the account recovery start form
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]

View File

@@ -35,16 +35,15 @@ mod macros;
pub use self::{
context::{
ApiDocContext, AppContext, CompatSsoContext, ConsentContext, DeviceConsentContext,
DeviceLinkContext, DeviceLinkFormField, EmailAddContext, EmailRecoveryContext,
EmailVerificationContext, EmailVerificationPageContext, EmptyContext, ErrorContext,
FormPostContext, IndexContext, LoginContext, LoginFormField, NotFoundContext,
PasswordRegisterContext, PolicyViolationContext, PostAuthContext, PostAuthContextInner,
ReauthContext, ReauthFormField, RecoveryExpiredContext, RecoveryFinishContext,
RecoveryFinishFormField, RecoveryProgressContext, RecoveryStartContext,
RecoveryStartFormField, RegisterContext, RegisterFormField, SiteBranding, SiteConfigExt,
SiteFeatures, TemplateContext, UpstreamExistingLinkContext, UpstreamRegister,
UpstreamRegisterFormField, UpstreamSuggestLink, WithCaptcha, WithCsrf, WithLanguage,
WithOptionalSession, WithSession,
DeviceLinkContext, DeviceLinkFormField, EmailRecoveryContext, EmailVerificationContext,
EmailVerificationPageContext, EmptyContext, ErrorContext, FormPostContext, IndexContext,
LoginContext, LoginFormField, NotFoundContext, PasswordRegisterContext,
PolicyViolationContext, PostAuthContext, PostAuthContextInner, ReauthContext,
ReauthFormField, RecoveryExpiredContext, RecoveryFinishContext, RecoveryFinishFormField,
RecoveryProgressContext, RecoveryStartContext, RecoveryStartFormField, RegisterContext,
RegisterFormField, SiteBranding, SiteConfigExt, SiteFeatures, TemplateContext,
UpstreamExistingLinkContext, UpstreamRegister, UpstreamRegisterFormField,
UpstreamSuggestLink, WithCaptcha, WithCsrf, WithLanguage, WithOptionalSession, WithSession,
},
forms::{FieldError, FormError, FormField, FormState, ToFormState},
};
@@ -347,9 +346,6 @@ register_templates! {
/// Render the email verification page
pub fn render_account_verify_email(WithLanguage<WithCsrf<WithSession<EmailVerificationPageContext>>>) { "pages/account/emails/verify.html" }
/// Render the email verification page
pub fn render_account_add_email(WithLanguage<WithCsrf<WithSession<EmailAddContext>>>) { "pages/account/emails/add.html" }
/// Render the account recovery start page
pub fn render_recovery_start(WithLanguage<WithCsrf<RecoveryStartContext>>) { "pages/recovery/start.html" }
@@ -433,7 +429,6 @@ impl Templates {
check::render_policy_violation(self, now, rng)?;
check::render_sso_login(self, now, rng)?;
check::render_index(self, now, rng)?;
check::render_account_add_email(self, now, rng)?;
check::render_account_verify_email(self, now, rng)?;
check::render_recovery_start(self, now, rng)?;
check::render_recovery_progress(self, now, rng)?;

View File

@@ -1,39 +0,0 @@
{#
Copyright 2024 New Vector Ltd.
Copyright 2022-2024 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
-#}
{% extends "base.html" %}
{% block content %}
<header class="page-heading">
<div class="icon">
{{ icon.email_solid() }}
</div>
<div class="header">
<h1 class="title">{{ _("mas.add_email.heading") }}</h1>
<p class="text">{{ _("mas.add_email.description") }}</p>
</div>
</header>
<form method="POST" class="cpd-form-root">
{% if form.errors is not empty %}
{% for error in form.errors %}
<div class="text-critical font-medium">
{{ errors.form_error_message(error=error) }}
</div>
{% endfor %}
{% endif %}
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
{% call(f) field.field(label=_("common.email_address"), name="email", form_state=form) %}
<input {{ field.attributes(f) }} class="cpd-text-control" type="email" autocomplete="email" required />
{% endcall %}
{{ button.button(text=_("action.continue")) }}
</form>
{% endblock content %}