mirror of
https://github.com/modrinth/code.git
synced 2026-08-27 18:14:49 +00:00
feat: email bounce check (#6774)
* feat: neverbounce API client * feat(labrinth): integrate neverbounce * chore: tombi * chore: typo * chore: clippy * chore: remove unused dep * chore: cleanup
This commit is contained in:
@@ -24,6 +24,7 @@ use crate::util::captcha::check_hcaptcha;
|
||||
use crate::util::error::Context;
|
||||
use crate::util::ext::get_image_ext;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::neverbounce::{check_email, email_check_error_generic};
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_http::header::LOCATION;
|
||||
use actix_web::http::StatusCode;
|
||||
@@ -1453,7 +1454,7 @@ fn validate_account_consent(account_consent: bool) -> Result<(), ApiError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create account with OAuth.
|
||||
/// Create account with OAuth.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -1496,6 +1497,10 @@ pub async fn create_oauth_account(
|
||||
return Err(ApiError::Internal(eyre!("invalid flow kind")));
|
||||
};
|
||||
|
||||
if let Some(email) = &user.email {
|
||||
ensure_email_is_usable(email).await?;
|
||||
}
|
||||
|
||||
let mut txn = db
|
||||
.begin()
|
||||
.await
|
||||
@@ -1542,7 +1547,7 @@ struct DiscordCommunityHandoffPayload {
|
||||
nonce: String,
|
||||
}
|
||||
|
||||
/// Link Discord community.
|
||||
/// Link Discord community.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -1619,7 +1624,7 @@ pub async fn discord_community_link(
|
||||
Ok(web::Json(DiscordCommunityLinkResponse { url }))
|
||||
}
|
||||
|
||||
/// Remove an auth provider.
|
||||
/// Remove an auth provider.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -1811,6 +1816,20 @@ impl From<NewAccount> for AccountRegisterFlow {
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_email_is_usable(email: &str) -> Result<(), ApiError> {
|
||||
let result = check_email(email).await.map_err(ApiError::Request)?;
|
||||
|
||||
if matches!(
|
||||
result,
|
||||
neverbounce::VerificationResult::Invalid
|
||||
| neverbounce::VerificationResult::Disposable
|
||||
) {
|
||||
return Err(ApiError::Request(email_check_error_generic()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AccountRegisterFlow {
|
||||
async fn validate(
|
||||
self,
|
||||
@@ -1964,7 +1983,7 @@ impl ReadyAccountRegisterFlow {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate password account creation.
|
||||
/// Validate password account creation.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -1992,7 +2011,7 @@ pub async fn validate_create_account_with_password(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create account with a password.
|
||||
/// Create account with a password.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2021,6 +2040,8 @@ pub async fn create_account_with_password(
|
||||
return Err(ApiError::Turnstile);
|
||||
}
|
||||
|
||||
ensure_email_is_usable(&new_account.email).await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let ready_flow = AccountRegisterFlow::from(new_account)
|
||||
@@ -2043,7 +2064,7 @@ pub struct Login {
|
||||
pub challenge: String,
|
||||
}
|
||||
|
||||
/// Log in with a password.
|
||||
/// Log in with a password.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2204,7 +2225,7 @@ async fn validate_2fa_code(
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete login with 2FA.
|
||||
/// Complete login with 2FA.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2264,7 +2285,7 @@ pub async fn login_2fa(
|
||||
}
|
||||
}
|
||||
|
||||
/// Start 2FA setup.
|
||||
/// Start 2FA setup.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2315,7 +2336,7 @@ pub async fn begin_2fa_flow(
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish 2FA setup.
|
||||
/// Finish 2FA setup.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2450,7 +2471,7 @@ pub struct Remove2FA {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
/// Remove 2FA.
|
||||
/// Remove 2FA.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2550,7 +2571,7 @@ pub struct ResetPassword {
|
||||
pub challenge: String,
|
||||
}
|
||||
|
||||
/// Start password reset.
|
||||
/// Start password reset.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2656,7 +2677,7 @@ pub struct ChangePassword {
|
||||
pub new_password: Option<String>,
|
||||
}
|
||||
|
||||
/// Change password.
|
||||
/// Change password.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2822,7 +2843,7 @@ pub struct SetEmail {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
/// Set email address.
|
||||
/// Set email address.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2875,6 +2896,8 @@ pub async fn set_email(
|
||||
));
|
||||
}
|
||||
|
||||
ensure_email_is_usable(&email_address.email).await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
@@ -2944,7 +2967,7 @@ pub async fn set_email(
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
/// Resend verification email.
|
||||
/// Resend verification email.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3019,7 +3042,7 @@ pub struct VerifyEmail {
|
||||
pub flow: String,
|
||||
}
|
||||
|
||||
/// Verify email address.
|
||||
/// Verify email address.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3085,7 +3108,7 @@ pub async fn verify_email(
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to the newsletter.
|
||||
/// Subscribe to the newsletter.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3134,7 +3157,7 @@ pub async fn subscribe_newsletter(
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
/// Get newsletter subscription status.
|
||||
/// Get newsletter subscription status.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3184,7 +3207,7 @@ pub struct RegisterPasskeyResponse {
|
||||
pub flow: String,
|
||||
}
|
||||
|
||||
/// Start passkey registration.
|
||||
/// Start passkey registration.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3284,7 +3307,7 @@ pub struct PasskeyResponse {
|
||||
pub last_used: Option<chrono::DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Finish passkey registration.
|
||||
/// Finish passkey registration.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3393,7 +3416,7 @@ pub struct AuthenticatePasskeyResponse {
|
||||
pub flow: String,
|
||||
}
|
||||
|
||||
/// Start passkey authentication.
|
||||
/// Start passkey authentication.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3436,7 +3459,7 @@ pub struct AuthenticatePasskeyFinish {
|
||||
pub credential: PublicKeyCredential,
|
||||
}
|
||||
|
||||
/// Finish passkey authentication.
|
||||
/// Finish passkey authentication.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3553,7 +3576,7 @@ pub async fn authenticate_passkey_finish(
|
||||
}
|
||||
}
|
||||
|
||||
/// List passkeys.
|
||||
/// List passkeys.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3603,7 +3626,7 @@ pub struct RenamePasskey {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Rename a passkey.
|
||||
/// Rename a passkey.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3658,7 +3681,7 @@ pub async fn rename_passkey(
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
/// Delete a passkey.
|
||||
/// Delete a passkey.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
|
||||
Reference in New Issue
Block a user