Remove the dedicated page to add an email address
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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 %}
|
||||
Reference in New Issue
Block a user