mirror of
https://github.com/element-hq/matrix-authentication-service.git
synced 2026-05-13 21:33:32 +00:00
add login by email + feature flag
This commit is contained in:
@@ -214,6 +214,7 @@ pub fn site_config_from_config(
|
||||
captcha,
|
||||
minimum_password_complexity: password_config.minimum_complexity(),
|
||||
session_expiration,
|
||||
login_with_email_allowed: account_config.login_with_email_allowed,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,12 @@ pub struct AccountConfig {
|
||||
/// `true`.
|
||||
#[serde(default = "default_true", skip_serializing_if = "is_default_true")]
|
||||
pub account_deactivation_allowed: bool,
|
||||
|
||||
/// Whether users can log in with their email address. Defaults to `false`.
|
||||
///
|
||||
/// This has no effect if password login is disabled.
|
||||
#[serde(default = "default_false", skip_serializing_if = "is_default_false")]
|
||||
pub login_with_email_allowed: bool,
|
||||
}
|
||||
|
||||
impl Default for AccountConfig {
|
||||
@@ -77,6 +83,7 @@ impl Default for AccountConfig {
|
||||
password_change_allowed: default_true(),
|
||||
password_recovery_enabled: default_false(),
|
||||
account_deactivation_allowed: default_true(),
|
||||
login_with_email_allowed: default_false(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,6 +97,7 @@ impl AccountConfig {
|
||||
&& is_default_true(&self.password_change_allowed)
|
||||
&& is_default_false(&self.password_recovery_enabled)
|
||||
&& is_default_true(&self.account_deactivation_allowed)
|
||||
&& is_default_false(&self.login_with_email_allowed)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,4 +87,7 @@ pub struct SiteConfig {
|
||||
pub minimum_password_complexity: u8,
|
||||
|
||||
pub session_expiration: Option<SessionExpirationConfig>,
|
||||
|
||||
/// Whether users can log in with their email address.
|
||||
pub login_with_email_allowed: bool,
|
||||
}
|
||||
|
||||
@@ -53,6 +53,9 @@ pub struct SiteConfig {
|
||||
/// The exact scorer (including dictionaries and other data tables)
|
||||
/// in use is <https://crates.io/crates/zxcvbn>.
|
||||
minimum_password_complexity: u8,
|
||||
|
||||
/// Whether users can log in with their email address.
|
||||
login_with_email_allowed: bool,
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
@@ -98,6 +101,7 @@ impl SiteConfig {
|
||||
password_registration_enabled: data_model.password_registration_enabled,
|
||||
account_deactivation_allowed: data_model.account_deactivation_allowed,
|
||||
minimum_password_complexity: data_model.minimum_password_complexity,
|
||||
login_with_email_allowed: data_model.login_with_email_allowed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ pub fn test_site_config() -> SiteConfig {
|
||||
captcha: None,
|
||||
minimum_password_complexity: 1,
|
||||
session_expiration: None,
|
||||
login_with_email_allowed: true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,7 @@ pub(crate) async fn post(
|
||||
.unwrap_or(&form.username);
|
||||
|
||||
// First, lookup the user
|
||||
let Some(user) = repo.user().find_by_username(username).await? else {
|
||||
let Some(user) = get_user_by_email_or_by_username(site_config, &mut repo, username).await? else {
|
||||
let form_state = form_state.with_error_on_form(FormError::InvalidCredentials);
|
||||
PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]);
|
||||
return render(
|
||||
@@ -337,6 +337,37 @@ pub(crate) async fn post(
|
||||
Ok((cookie_jar, reply).into_response())
|
||||
}
|
||||
|
||||
async fn get_user_by_email_or_by_username(
|
||||
site_config: SiteConfig,
|
||||
repo: &mut impl RepositoryAccess,
|
||||
username_or_email: &str,
|
||||
) -> Result<Option<mas_data_model::User>, Box<dyn std::error::Error>> {
|
||||
if site_config.login_with_email_allowed && username_or_email.contains('@') {
|
||||
let maybe_user_email = repo
|
||||
.user_email()
|
||||
.find_by_email(username_or_email)
|
||||
.await?;
|
||||
|
||||
if let Some(user_email) = maybe_user_email {
|
||||
let user = repo
|
||||
.user()
|
||||
.lookup(user_email.user_id)
|
||||
.await?;
|
||||
|
||||
if user.is_some() {
|
||||
return Ok(user);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let user = repo
|
||||
.user()
|
||||
.find_by_username(username_or_email)
|
||||
.await?;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
|
||||
fn handle_login_hint(
|
||||
mut ctx: LoginContext,
|
||||
next: &PostAuthContext,
|
||||
|
||||
Generated
+40
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT user_email_id\n , user_id\n , email\n , created_at\n FROM user_emails\n WHERE email = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "user_email_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "user_id",
|
||||
"type_info": "Uuid"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f3b043b69e0554b5b4d8f5cf05960632fb2ebd38916dd2e9beac232c7e14c1ec"
|
||||
}
|
||||
@@ -191,6 +191,43 @@ impl UserEmailRepository for PgUserEmailRepository<'_> {
|
||||
Ok(Some(user_email.into()))
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_email.find_by_email",
|
||||
skip_all,
|
||||
fields(
|
||||
db.query.text,
|
||||
user_email.email = email,
|
||||
),
|
||||
err,
|
||||
)]
|
||||
async fn find_by_email(&mut self, email: &str) -> Result<Option<UserEmail>, Self::Error> {
|
||||
let res = sqlx::query_as!(
|
||||
UserEmailLookup,
|
||||
r#"
|
||||
SELECT user_email_id
|
||||
, user_id
|
||||
, email
|
||||
, created_at
|
||||
FROM user_emails
|
||||
WHERE email = $1
|
||||
"#,
|
||||
email,
|
||||
)
|
||||
.traced()
|
||||
.fetch_all(&mut *self.conn)
|
||||
.await?;
|
||||
|
||||
if res.len() != 1 {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(user_email) = res.into_iter().next() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(user_email.into()))
|
||||
}
|
||||
|
||||
#[tracing::instrument(
|
||||
name = "db.user_email.all",
|
||||
skip_all,
|
||||
|
||||
@@ -93,6 +93,18 @@ pub trait UserEmailRepository: Send + Sync {
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn find(&mut self, user: &User, email: &str) -> Result<Option<UserEmail>, Self::Error>;
|
||||
|
||||
/// Lookup an [`UserEmail`] by its email address
|
||||
///
|
||||
/// Returns `None` if no matching [`UserEmail`] was found or if multiple [`UserEmail`] are found
|
||||
///
|
||||
/// # Parameters
|
||||
/// * `email`: The email address to lookup
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`Self::Error`] if the underlying repository fails
|
||||
async fn find_by_email(&mut self, email: &str) -> Result<Option<UserEmail>, Self::Error>;
|
||||
|
||||
/// Get all [`UserEmail`] of a [`User`]
|
||||
///
|
||||
/// # Parameters
|
||||
@@ -298,6 +310,7 @@ pub trait UserEmailRepository: Send + Sync {
|
||||
repository_impl!(UserEmailRepository:
|
||||
async fn lookup(&mut self, id: Ulid) -> Result<Option<UserEmail>, Self::Error>;
|
||||
async fn find(&mut self, user: &User, email: &str) -> Result<Option<UserEmail>, Self::Error>;
|
||||
async fn find_by_email(&mut self, email: &str) -> Result<Option<UserEmail>, Self::Error>;
|
||||
|
||||
async fn all(&mut self, user: &User) -> Result<Vec<UserEmail>, Self::Error>;
|
||||
async fn list(
|
||||
|
||||
@@ -47,6 +47,7 @@ impl SiteConfigExt for SiteConfig {
|
||||
password_registration: self.password_registration_enabled,
|
||||
password_login: self.password_login_enabled,
|
||||
account_recovery: self.account_recovery_allowed,
|
||||
login_with_email_allowed: self.login_with_email_allowed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use minijinja::{
|
||||
};
|
||||
|
||||
/// Site features information.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct SiteFeatures {
|
||||
/// Whether local password-based registration is enabled.
|
||||
@@ -22,6 +23,9 @@ pub struct SiteFeatures {
|
||||
|
||||
/// Whether email-based account recovery is enabled.
|
||||
pub account_recovery: bool,
|
||||
|
||||
/// Whether users can log in with their email address.
|
||||
pub login_with_email_allowed: bool,
|
||||
}
|
||||
|
||||
impl Object for SiteFeatures {
|
||||
@@ -30,6 +34,7 @@ impl Object for SiteFeatures {
|
||||
"password_registration" => Some(Value::from(self.password_registration)),
|
||||
"password_login" => Some(Value::from(self.password_login)),
|
||||
"account_recovery" => Some(Value::from(self.account_recovery)),
|
||||
"login_with_email_allowed" => Some(Value::from(self.login_with_email_allowed)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -39,6 +44,7 @@ impl Object for SiteFeatures {
|
||||
"password_registration",
|
||||
"password_login",
|
||||
"account_recovery",
|
||||
"login_with_email_allowed",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -487,6 +487,7 @@ mod tests {
|
||||
password_login: true,
|
||||
password_registration: true,
|
||||
account_recovery: true,
|
||||
login_with_email_allowed: true,
|
||||
};
|
||||
let vite_manifest_path =
|
||||
Utf8Path::new(env!("CARGO_MANIFEST_DIR")).join("../../frontend/dist/manifest.json");
|
||||
|
||||
@@ -2504,6 +2504,10 @@
|
||||
"account_deactivation_allowed": {
|
||||
"description": "Whether users are allowed to delete their own account. Defaults to `true`.",
|
||||
"type": "boolean"
|
||||
},
|
||||
"login_with_email_allowed": {
|
||||
"description": "Whether users can log in with their email address. Defaults to `false`.\n\nThis has no effect if password login is disabled.",
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -314,6 +314,12 @@ account:
|
||||
#
|
||||
# Defaults to `true`.
|
||||
account_deactivation_allowed: true
|
||||
|
||||
# Whether users can log in with their email address.
|
||||
#
|
||||
# Defaults to `false`.
|
||||
# This has no effect if password login is disabled.
|
||||
login_with_email_allowed: false
|
||||
```
|
||||
|
||||
## `captcha`
|
||||
|
||||
@@ -1662,6 +1662,10 @@ type SiteConfig implements Node {
|
||||
"""
|
||||
minimumPasswordComplexity: Int!
|
||||
"""
|
||||
Whether users can log in with their email address.
|
||||
"""
|
||||
loginWithEmailAllowed: Boolean!
|
||||
"""
|
||||
The ID of the site configuration.
|
||||
"""
|
||||
id: ID!
|
||||
|
||||
@@ -1218,6 +1218,8 @@ export type SiteConfig = Node & {
|
||||
id: Scalars['ID']['output'];
|
||||
/** Imprint to show in the footer. */
|
||||
imprint?: Maybe<Scalars['String']['output']>;
|
||||
/** Whether users can log in with their email address. */
|
||||
loginWithEmailAllowed: Scalars['Boolean']['output'];
|
||||
/**
|
||||
* Minimum password complexity, from 0 to 4, in terms of a zxcvbn score.
|
||||
* The exact scorer (including dictionaries and other data tables)
|
||||
|
||||
@@ -42,9 +42,15 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
<input type="hidden" name="csrf" value="{{ csrf_token }}" />
|
||||
|
||||
{% call(f) field.field(label=_("common.username"), name="username", form_state=form) %}
|
||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="text" autocomplete="username" autocorrect="off" autocapitalize="off" required />
|
||||
{% endcall %}
|
||||
{% if features.login_with_email_allowed %}
|
||||
{% call(f) field.field(label=_("common.username_or_email"), name="username", form_state=form) %}
|
||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="text" autocomplete="username" autocorrect="off" autocapitalize="off" required />
|
||||
{% endcall %}
|
||||
{% else %}
|
||||
{% call(f) field.field(label=_("common.username"), name="username", form_state=form) %}
|
||||
<input {{ field.attributes(f) }} class="cpd-text-control" type="text" autocomplete="username" autocorrect="off" autocapitalize="off" required />
|
||||
{% endcall %}
|
||||
{% endif %}
|
||||
|
||||
{% if features.password_login %}
|
||||
{% call(f) field.field(label=_("common.password"), name="password", form_state=form) %}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"mxid": "Matrix-ID",
|
||||
"password": "Passwort",
|
||||
"password_confirm": "Passwort wiederholen",
|
||||
"username": "Benutzername"
|
||||
"username": "Benutzername",
|
||||
"username_or_email": "Benutzername oder E-Mail-Adresse"
|
||||
},
|
||||
"error": {
|
||||
"unexpected": "Unerwarteter Fehler"
|
||||
|
||||
+12
-8
@@ -10,11 +10,11 @@
|
||||
},
|
||||
"continue": "Continue",
|
||||
"@continue": {
|
||||
"context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:123:13-33, pages/device_link.html:40:26-46, pages/login.html:62:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48"
|
||||
"context": "form_post.html:25:28-48, pages/consent.html:57:28-48, pages/device_consent.html:123:13-33, pages/device_link.html:40:26-46, pages/login.html:68:30-50, pages/reauth.html:32:28-48, pages/recovery/start.html:38:26-46, pages/register/password.html:74:26-46, pages/register/steps/display_name.html:43:28-48, pages/register/steps/verify_email.html:51:26-46, pages/sso.html:37:28-48"
|
||||
},
|
||||
"create_account": "Create Account",
|
||||
"@create_account": {
|
||||
"context": "pages/login.html:88:33-59, pages/upstream_oauth2/do_register.html:192:26-52"
|
||||
"context": "pages/login.html:94:33-59, pages/upstream_oauth2/do_register.html:192:26-52"
|
||||
},
|
||||
"sign_in": "Sign in",
|
||||
"@sign_in": {
|
||||
@@ -91,7 +91,7 @@
|
||||
},
|
||||
"password": "Password",
|
||||
"@password": {
|
||||
"context": "pages/login.html:50:37-57, pages/reauth.html:28:35-55, pages/register/password.html:42:33-53"
|
||||
"context": "pages/login.html:56:37-57, pages/reauth.html:28:35-55, pages/register/password.html:42:33-53"
|
||||
},
|
||||
"password_confirm": "Confirm password",
|
||||
"@password_confirm": {
|
||||
@@ -99,7 +99,11 @@
|
||||
},
|
||||
"username": "Username",
|
||||
"@username": {
|
||||
"context": "pages/login.html:45:35-55, pages/register/index.html:30:35-55, pages/register/password.html:34:33-53, pages/upstream_oauth2/do_register.html:101:35-55, pages/upstream_oauth2/do_register.html:106:39-59"
|
||||
"context": "pages/login.html:50:37-57, pages/register/index.html:30:35-55, pages/register/password.html:34:33-53, pages/upstream_oauth2/do_register.html:101:35-55, pages/upstream_oauth2/do_register.html:106:39-59"
|
||||
},
|
||||
"username_or_email": "Username or Email Address",
|
||||
"@username_or_email": {
|
||||
"context": "pages/login.html:46:37-66"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
@@ -403,11 +407,11 @@
|
||||
"login": {
|
||||
"call_to_register": "Don't have an account yet?",
|
||||
"@call_to_register": {
|
||||
"context": "pages/login.html:84:13-44"
|
||||
"context": "pages/login.html:90:13-44"
|
||||
},
|
||||
"continue_with_provider": "Continue with %(provider)s",
|
||||
"@continue_with_provider": {
|
||||
"context": "pages/login.html:75:15-67, pages/register/index.html:53:15-67",
|
||||
"context": "pages/login.html:81:15-67, pages/register/index.html:53:15-67",
|
||||
"description": "Button to log in with an upstream provider"
|
||||
},
|
||||
"description": "Please sign in to continue:",
|
||||
@@ -416,7 +420,7 @@
|
||||
},
|
||||
"forgot_password": "Forgot password?",
|
||||
"@forgot_password": {
|
||||
"context": "pages/login.html:55:35-65",
|
||||
"context": "pages/login.html:61:35-65",
|
||||
"description": "On the login page, link to the account recovery process"
|
||||
},
|
||||
"headline": "Sign in",
|
||||
@@ -435,7 +439,7 @@
|
||||
},
|
||||
"no_login_methods": "No login methods available.",
|
||||
"@no_login_methods": {
|
||||
"context": "pages/login.html:94:11-42"
|
||||
"context": "pages/login.html:100:11-42"
|
||||
}
|
||||
},
|
||||
"navbar": {
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
"mxid": "Matrix ID",
|
||||
"password": "Mot de passe",
|
||||
"password_confirm": "Confirmer le mot de passe",
|
||||
"username": "Nom d’utilisateur"
|
||||
"username": "Nom d’utilisateur",
|
||||
"username_or_email": "Nom d’utilisateur ou adresse e-mail"
|
||||
},
|
||||
"error": {
|
||||
"unexpected": "Erreur inattendue"
|
||||
|
||||
Reference in New Issue
Block a user