mirror of
https://github.com/modrinth/code.git
synced 2026-08-25 00:55:25 +00:00
feat: require verified email to publish, to prevent spam (#6777)
* feat: require verified email to create projects, collections and organizations * fix(labrinth): fix tests * chore: fmt * chore: intl
This commit is contained in:
@@ -443,20 +443,23 @@
|
|||||||
:options="[
|
:options="[
|
||||||
{
|
{
|
||||||
id: 'new-project',
|
id: 'new-project',
|
||||||
action: (event) => $refs.modal_creation.show(event),
|
action: (event) => requireVerifiedEmail(() => $refs.modal_creation.show(event)),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new-server-project',
|
id: 'new-server-project',
|
||||||
action: (event) => $refs.modal_creation.show(event, { type: 'server' }),
|
action: (event) =>
|
||||||
|
requireVerifiedEmail(() => $refs.modal_creation.show(event, { type: 'server' })),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'new-collection',
|
id: 'new-collection',
|
||||||
action: (event) => $refs.modal_collection_creation.show(event),
|
action: (event) =>
|
||||||
|
requireVerifiedEmail(() => $refs.modal_collection_creation.show(event)),
|
||||||
},
|
},
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
{
|
{
|
||||||
id: 'new-organization',
|
id: 'new-organization',
|
||||||
action: (event) => $refs.modal_organization_creation.show(event),
|
action: (event) =>
|
||||||
|
requireVerifiedEmail(() => $refs.modal_organization_creation.show(event)),
|
||||||
},
|
},
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@@ -777,6 +780,7 @@ import {
|
|||||||
createHostingIntercomIdentityKey,
|
createHostingIntercomIdentityKey,
|
||||||
defineMessages,
|
defineMessages,
|
||||||
injectModrinthClient,
|
injectModrinthClient,
|
||||||
|
injectNotificationManager,
|
||||||
injectPageContext,
|
injectPageContext,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
providePageContext,
|
providePageContext,
|
||||||
@@ -814,6 +818,7 @@ const generatedState = useGeneratedState()
|
|||||||
const country = useUserCountry()
|
const country = useUserCountry()
|
||||||
|
|
||||||
const { formatMessage } = useVIntl()
|
const { formatMessage } = useVIntl()
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
const auth = await useAuth()
|
const auth = await useAuth()
|
||||||
const user = await useUser()
|
const user = await useUser()
|
||||||
@@ -906,6 +911,19 @@ async function fetchIntercomToken() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requireVerifiedEmail(action) {
|
||||||
|
if (!auth.value.user?.email_verified) {
|
||||||
|
addNotification({
|
||||||
|
title: formatMessage(messages.emailVerificationRequired),
|
||||||
|
text: formatMessage(messages.verifyEmailBeforePublishing),
|
||||||
|
type: 'error',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
|
||||||
const navMenuMessages = defineMessages({
|
const navMenuMessages = defineMessages({
|
||||||
home: {
|
home: {
|
||||||
id: 'layout.nav.home',
|
id: 'layout.nav.home',
|
||||||
@@ -962,6 +980,14 @@ const messages = defineMessages({
|
|||||||
id: 'layout.action.publish',
|
id: 'layout.action.publish',
|
||||||
defaultMessage: 'Publish',
|
defaultMessage: 'Publish',
|
||||||
},
|
},
|
||||||
|
emailVerificationRequired: {
|
||||||
|
id: 'layout.publish.email-verification-required.title',
|
||||||
|
defaultMessage: 'Email verification required',
|
||||||
|
},
|
||||||
|
verifyEmailBeforePublishing: {
|
||||||
|
id: 'layout.publish.email-verification-required.description',
|
||||||
|
defaultMessage: 'You must verify your email before publishing on Modrinth.',
|
||||||
|
},
|
||||||
reviewProjects: {
|
reviewProjects: {
|
||||||
id: 'layout.action.review-projects',
|
id: 'layout.action.review-projects',
|
||||||
defaultMessage: 'Project review',
|
defaultMessage: 'Project review',
|
||||||
|
|||||||
@@ -2672,6 +2672,12 @@
|
|||||||
"layout.nav.upgrade-to-modrinth-plus": {
|
"layout.nav.upgrade-to-modrinth-plus": {
|
||||||
"message": "Upgrade to Modrinth+"
|
"message": "Upgrade to Modrinth+"
|
||||||
},
|
},
|
||||||
|
"layout.publish.email-verification-required.description": {
|
||||||
|
"message": "You must verify your email before publishing on Modrinth."
|
||||||
|
},
|
||||||
|
"layout.publish.email-verification-required.title": {
|
||||||
|
"message": "Email verification required"
|
||||||
|
},
|
||||||
"moderation.exclude-technical-review": {
|
"moderation.exclude-technical-review": {
|
||||||
"message": "Exclude TR"
|
"message": "Exclude TR"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,6 +15,16 @@ use crate::routes::ApiError;
|
|||||||
use futures::TryStreamExt;
|
use futures::TryStreamExt;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
pub fn require_verified_email(user: &User) -> Result<(), ApiError> {
|
||||||
|
if !user.email_verified.unwrap_or(false) {
|
||||||
|
return Err(ApiError::Auth(eyre::eyre!(
|
||||||
|
"Please verify your email before publishing!"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub trait ValidateAuthorized {
|
pub trait ValidateAuthorized {
|
||||||
fn validate_authorized(
|
fn validate_authorized(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ pub mod validate;
|
|||||||
pub use checks::{
|
pub use checks::{
|
||||||
filter_enlisted_projects_ids, filter_enlisted_version_ids,
|
filter_enlisted_projects_ids, filter_enlisted_version_ids,
|
||||||
filter_visible_collections, filter_visible_project_ids,
|
filter_visible_collections, filter_visible_project_ids,
|
||||||
filter_visible_projects,
|
filter_visible_projects, require_verified_email,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
pub use validate::{
|
pub use validate::{
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use crate::auth::checks::is_visible_collection;
|
use crate::auth::checks::is_visible_collection;
|
||||||
use crate::auth::{filter_visible_collections, get_user_from_headers};
|
use crate::auth::{
|
||||||
|
filter_visible_collections, get_user_from_headers, require_verified_email,
|
||||||
|
};
|
||||||
use crate::database::PgPool;
|
use crate::database::PgPool;
|
||||||
use crate::database::models::{
|
use crate::database::models::{
|
||||||
collection_item, generate_collection_id, project_item,
|
collection_item, generate_collection_id, project_item,
|
||||||
@@ -76,6 +78,8 @@ pub async fn collection_create(
|
|||||||
.await?
|
.await?
|
||||||
.1;
|
.1;
|
||||||
|
|
||||||
|
require_verified_email(¤t_user)?;
|
||||||
|
|
||||||
let limits =
|
let limits =
|
||||||
UserLimits::get_for_collections(¤t_user, &client).await?;
|
UserLimits::get_for_collections(¤t_user, &client).await?;
|
||||||
if limits.current >= limits.max {
|
if limits.current >= limits.max {
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ use std::collections::HashMap;
|
|||||||
|
|
||||||
use super::ApiError;
|
use super::ApiError;
|
||||||
use crate::auth::checks::is_visible_organization;
|
use crate::auth::checks::is_visible_organization;
|
||||||
use crate::auth::{filter_visible_projects, get_user_from_headers};
|
use crate::auth::{
|
||||||
|
filter_visible_projects, get_user_from_headers, require_verified_email,
|
||||||
|
};
|
||||||
use crate::database::PgPool;
|
use crate::database::PgPool;
|
||||||
use crate::database::models::team_item::DBTeamMember;
|
use crate::database::models::team_item::DBTeamMember;
|
||||||
use crate::database::models::{
|
use crate::database::models::{
|
||||||
@@ -131,6 +133,8 @@ pub async fn organization_create(
|
|||||||
.await?
|
.await?
|
||||||
.1;
|
.1;
|
||||||
|
|
||||||
|
require_verified_email(¤t_user)?;
|
||||||
|
|
||||||
let limits =
|
let limits =
|
||||||
UserLimits::get_for_organizations(¤t_user, &pool).await?;
|
UserLimits::get_for_organizations(¤t_user, &pool).await?;
|
||||||
if limits.current >= limits.max {
|
if limits.current >= limits.max {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use super::version_creation::{InitialVersionData, try_create_version_fields};
|
use super::version_creation::{InitialVersionData, try_create_version_fields};
|
||||||
use crate::auth::{AuthenticationError, get_user_from_headers};
|
use crate::auth::{
|
||||||
|
AuthenticationError, get_user_from_headers, require_verified_email,
|
||||||
|
};
|
||||||
use crate::database::PgPool;
|
use crate::database::PgPool;
|
||||||
use crate::database::PgTransaction;
|
use crate::database::PgTransaction;
|
||||||
use crate::database::models::loader_fields::{
|
use crate::database::models::loader_fields::{
|
||||||
@@ -107,6 +109,9 @@ impl From<crate::routes::ApiError> for CreateError {
|
|||||||
crate::routes::ApiError::CustomAuthentication(err) => {
|
crate::routes::ApiError::CustomAuthentication(err) => {
|
||||||
Self::CustomAuthenticationError(err)
|
Self::CustomAuthenticationError(err)
|
||||||
}
|
}
|
||||||
|
crate::routes::ApiError::Auth(err) => {
|
||||||
|
Self::CustomAuthenticationError(format!("{err:#}"))
|
||||||
|
}
|
||||||
crate::routes::ApiError::InvalidInput(err)
|
crate::routes::ApiError::InvalidInput(err)
|
||||||
| crate::routes::ApiError::Validation(err) => {
|
| crate::routes::ApiError::Validation(err) => {
|
||||||
Self::InvalidInput(err)
|
Self::InvalidInput(err)
|
||||||
@@ -491,6 +496,8 @@ async fn project_create_inner(
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
require_verified_email(¤t_user)?;
|
||||||
|
|
||||||
let limits = UserLimits::get_for_projects(¤t_user, pool).await?;
|
let limits = UserLimits::get_for_projects(¤t_user, pool).await?;
|
||||||
if limits.current >= limits.max {
|
if limits.current >= limits.max {
|
||||||
return Err(CreateError::LimitReached);
|
return Err(CreateError::LimitReached);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::get_user_from_headers,
|
auth::{get_user_from_headers, require_verified_email},
|
||||||
database::{
|
database::{
|
||||||
PgPool,
|
PgPool,
|
||||||
models::{
|
models::{
|
||||||
@@ -140,6 +140,8 @@ pub async fn create(
|
|||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)?;
|
.map_err(ApiError::from)?;
|
||||||
|
|
||||||
|
require_verified_email(&user)?;
|
||||||
|
|
||||||
let limits = UserLimits::get_for_projects(&user, &db)
|
let limits = UserLimits::get_for_projects(&user, &db)
|
||||||
.await
|
.await
|
||||||
.map_err(ApiError::from)?;
|
.map_err(ApiError::from)?;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::database::Executor;
|
use crate::database::Executor;
|
||||||
use eyre::{Context, Result};
|
use eyre::{Context, Result};
|
||||||
|
|
||||||
use crate::database::PgPool;
|
use crate::database::{PgPool, models::DBUserId};
|
||||||
|
|
||||||
/// Static personal access token for use in [`AppendPat`].
|
/// Static personal access token for use in [`AppendPat`].
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -33,6 +33,25 @@ impl AppendPat for actix_web::test::TestRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mark a user's email as verified without using the email verification flow.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Errors if the user does not exist or could not be updated.
|
||||||
|
pub async fn mark_email_verified(db: &PgPool, user_id: DBUserId) -> Result<()> {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"UPDATE users SET email_verified = TRUE WHERE id = $1",
|
||||||
|
user_id.0,
|
||||||
|
)
|
||||||
|
.execute(db)
|
||||||
|
.await
|
||||||
|
.wrap_err("failed to mark user email as verified")?;
|
||||||
|
|
||||||
|
eyre::ensure!(result.rows_affected() == 1, "user does not exist");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Dummy [`DBUserId`]s.
|
/// Dummy [`DBUserId`]s.
|
||||||
///
|
///
|
||||||
/// [`DBUserId`]: crate::database::models::DBUserId
|
/// [`DBUserId`]: crate::database::models::DBUserId
|
||||||
@@ -79,5 +98,15 @@ pub async fn add_dummy_data(db: &PgPool) -> Result<()> {
|
|||||||
.await
|
.await
|
||||||
.wrap_err("failed to add dummy data")?;
|
.wrap_err("failed to add dummy data")?;
|
||||||
|
|
||||||
|
for user_id in [
|
||||||
|
user_id::ADMIN,
|
||||||
|
user_id::MODERATOR,
|
||||||
|
user_id::USER,
|
||||||
|
user_id::FRIEND,
|
||||||
|
user_id::ENEMY,
|
||||||
|
] {
|
||||||
|
mark_email_verified(db, user_id).await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ use super::{
|
|||||||
|
|
||||||
use super::{database::USER_USER_ID, get_json_val_str};
|
use super::{database::USER_USER_ID, get_json_val_str};
|
||||||
|
|
||||||
pub const DUMMY_DATA_UPDATE: i64 = 7;
|
pub const DUMMY_DATA_UPDATE: i64 = 8;
|
||||||
|
|
||||||
pub const DUMMY_CATEGORIES: &[&str] = &[
|
pub const DUMMY_CATEGORIES: &[&str] = &[
|
||||||
"combat",
|
"combat",
|
||||||
|
|||||||
Reference in New Issue
Block a user