Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions apps/frontend/src/layouts/default.vue
Original file line number Diff line number Diff line change
Expand Up @@ -443,20 +443,23 @@
:options="[
{
id: 'new-project',
action: (event) => $refs.modal_creation.show(event),
action: (event) => requireVerifiedEmail(() => $refs.modal_creation.show(event)),
},
{
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',
action: (event) => $refs.modal_collection_creation.show(event),
action: (event) =>
requireVerifiedEmail(() => $refs.modal_collection_creation.show(event)),
},
{ divider: true },
{
id: 'new-organization',
action: (event) => $refs.modal_organization_creation.show(event),
action: (event) =>
requireVerifiedEmail(() => $refs.modal_organization_creation.show(event)),
},
]"
>
Expand Down Expand Up @@ -777,6 +780,7 @@ import {
createHostingIntercomIdentityKey,
defineMessages,
injectModrinthClient,
injectNotificationManager,
injectPageContext,
OverflowMenu,
providePageContext,
Expand Down Expand Up @@ -814,6 +818,7 @@ const generatedState = useGeneratedState()
const country = useUserCountry()

const { formatMessage } = useVIntl()
const { addNotification } = injectNotificationManager()

const auth = await useAuth()
const user = await useUser()
Expand Down Expand Up @@ -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({
home: {
id: 'layout.nav.home',
Expand Down Expand Up @@ -962,6 +980,14 @@ const messages = defineMessages({
id: 'layout.action.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: {
id: 'layout.action.review-projects',
defaultMessage: 'Project review',
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/src/locales/en-US/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -2672,6 +2672,12 @@
"layout.nav.upgrade-to-modrinth-plus": {
"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": {
"message": "Exclude TR"
},
Expand Down
10 changes: 10 additions & 0 deletions apps/labrinth/src/auth/checks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,16 @@ use crate::routes::ApiError;
use futures::TryStreamExt;
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 {
fn validate_authorized(
&self,
Expand Down
2 changes: 1 addition & 1 deletion apps/labrinth/src/auth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pub mod validate;
pub use checks::{
filter_enlisted_projects_ids, filter_enlisted_version_ids,
filter_visible_collections, filter_visible_project_ids,
filter_visible_projects,
filter_visible_projects, require_verified_email,
};
use serde::{Deserialize, Serialize};
pub use validate::{
Expand Down
6 changes: 5 additions & 1 deletion apps/labrinth/src/routes/v3/collections.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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::models::{
collection_item, generate_collection_id, project_item,
Expand Down Expand Up @@ -76,6 +78,8 @@ pub async fn collection_create(
.await?
.1;

require_verified_email(&current_user)?;

let limits =
UserLimits::get_for_collections(&current_user, &client).await?;
if limits.current >= limits.max {
Expand Down
6 changes: 5 additions & 1 deletion apps/labrinth/src/routes/v3/organizations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ use std::collections::HashMap;

use super::ApiError;
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::models::team_item::DBTeamMember;
use crate::database::models::{
Expand Down Expand Up @@ -131,6 +133,8 @@ pub async fn organization_create(
.await?
.1;

require_verified_email(&current_user)?;

let limits =
UserLimits::get_for_organizations(&current_user, &pool).await?;
if limits.current >= limits.max {
Expand Down
9 changes: 8 additions & 1 deletion apps/labrinth/src/routes/v3/project_creation.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
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::PgTransaction;
use crate::database::models::loader_fields::{
Expand Down Expand Up @@ -107,6 +109,9 @@ impl From<crate::routes::ApiError> for CreateError {
crate::routes::ApiError::CustomAuthentication(err) => {
Self::CustomAuthenticationError(err)
}
crate::routes::ApiError::Auth(err) => {
Self::CustomAuthenticationError(format!("{err:#}"))
}
crate::routes::ApiError::InvalidInput(err)
| crate::routes::ApiError::Validation(err) => {
Self::InvalidInput(err)
Expand Down Expand Up @@ -491,6 +496,8 @@ async fn project_create_inner(
)
.await?;

require_verified_email(&current_user)?;

let limits = UserLimits::get_for_projects(&current_user, pool).await?;
if limits.current >= limits.max {
return Err(CreateError::LimitReached);
Expand Down
4 changes: 3 additions & 1 deletion apps/labrinth/src/routes/v3/project_creation/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use validator::Validate;

use crate::{
auth::get_user_from_headers,
auth::{get_user_from_headers, require_verified_email},
database::{
PgPool,
models::{
Expand Down Expand Up @@ -140,6 +140,8 @@ pub async fn create(
.await
.map_err(ApiError::from)?;

require_verified_email(&user)?;

let limits = UserLimits::get_for_projects(&user, &db)
.await
.map_err(ApiError::from)?;
Expand Down
31 changes: 30 additions & 1 deletion apps/labrinth/src/test/db.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::database::Executor;
use eyre::{Context, Result};

use crate::database::PgPool;
use crate::database::{PgPool, models::DBUserId};

/// Static personal access token for use in [`AppendPat`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -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.
///
/// [`DBUserId`]: crate::database::models::DBUserId
Expand Down Expand Up @@ -79,5 +98,15 @@ pub async fn add_dummy_data(db: &PgPool) -> Result<()> {
.await
.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(())
}
2 changes: 1 addition & 1 deletion apps/labrinth/src/test/dummy_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use super::{

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] = &[
"combat",
Expand Down
Loading