Hard caps on creating projects/orgs/collections (#4430)

* implement backend limits on project creation

* implement collection, org creation hard caps

* Fix limit api

* Fix clippy

* Fix limits

* Update sqlx queries

* Address PR comments on user limit structure

* sqlx prepare and clippy

* fix test maybe
This commit is contained in:
aecsocket
2025-09-28 11:01:00 +01:00
committed by GitHub
parent 3f55711f9e
commit f466470d06
503 changed files with 14260 additions and 11 deletions

View File

@@ -8,6 +8,7 @@ use crate::file_hosting::{FileHost, FileHostPublicity};
use crate::models::collections::{Collection, CollectionStatus};
use crate::models::ids::{CollectionId, ProjectId};
use crate::models::pats::Scopes;
use crate::models::v3::user_limits::UserLimits;
use crate::queue::session::AuthQueue;
use crate::routes::ApiError;
use crate::routes::v3::project_creation::CreateError;
@@ -76,6 +77,12 @@ pub async fn collection_create(
.await?
.1;
let limits =
UserLimits::get_for_collections(&current_user, &client).await?;
if limits.current >= limits.max {
return Err(CreateError::LimitReached);
}
collection_create_data.validate().map_err(|err| {
CreateError::InvalidInput(validation_errors_to_string(err, None))
})?;

View File

@@ -0,0 +1,75 @@
use crate::{
auth::get_user_from_headers,
database::redis::RedisPool,
models::{pats::Scopes, v3::user_limits::UserLimits},
queue::session::AuthQueue,
routes::ApiError,
};
use actix_web::{HttpRequest, web};
use sqlx::PgPool;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("limits")
.route("projects", web::get().to(get_project_limits))
.route("organizations", web::get().to(get_organization_limits))
.route("collections", web::get().to(get_collection_limits)),
);
}
async fn get_project_limits(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<UserLimits>, ApiError> {
let (_, user) = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::empty(),
)
.await?;
let limits = UserLimits::get_for_projects(&user, &pool).await?;
Ok(web::Json(limits))
}
async fn get_organization_limits(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<UserLimits>, ApiError> {
let (_, user) = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::empty(),
)
.await?;
let limits = UserLimits::get_for_organizations(&user, &pool).await?;
Ok(web::Json(limits))
}
async fn get_collection_limits(
req: HttpRequest,
pool: web::Data<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
) -> Result<web::Json<UserLimits>, ApiError> {
let (_, user) = get_user_from_headers(
&req,
&**pool,
&redis,
&session_queue,
Scopes::empty(),
)
.await?;
let limits = UserLimits::get_for_collections(&user, &pool).await?;
Ok(web::Json(limits))
}

View File

@@ -7,6 +7,7 @@ pub mod analytics_get;
pub mod collections;
pub mod friends;
pub mod images;
pub mod limits;
pub mod notifications;
pub mod organizations;
pub mod payouts;
@@ -30,6 +31,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("v3")
.wrap(default_cors())
.configure(limits::config)
.configure(analytics_get::config)
.configure(collections::config)
.configure(images::config)

View File

@@ -13,6 +13,7 @@ use crate::file_hosting::{FileHost, FileHostPublicity};
use crate::models::ids::OrganizationId;
use crate::models::pats::Scopes;
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
use crate::models::v3::user_limits::UserLimits;
use crate::queue::session::AuthQueue;
use crate::routes::v3::project_creation::CreateError;
use crate::util::img::delete_old_images;
@@ -136,6 +137,12 @@ pub async fn organization_create(
.await?
.1;
let limits =
UserLimits::get_for_organizations(&current_user, &pool).await?;
if limits.current >= limits.max {
return Err(CreateError::LimitReached);
}
new_organization.validate().map_err(|err| {
CreateError::ValidationError(validation_errors_to_string(err, None))
})?;

View File

@@ -17,6 +17,7 @@ use crate::models::projects::{
};
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
use crate::models::threads::ThreadType;
use crate::models::v3::user_limits::UserLimits;
use crate::queue::session::AuthQueue;
use crate::search::indexing::IndexingError;
use crate::util::img::upload_image_optimized;
@@ -86,6 +87,8 @@ pub enum CreateError {
CustomAuthenticationError(String),
#[error("Image Parsing Error: {0}")]
ImageError(#[from] ImageError),
#[error("Project limit reached")]
LimitReached,
}
impl actix_web::ResponseError for CreateError {
@@ -117,6 +120,7 @@ impl actix_web::ResponseError for CreateError {
CreateError::ValidationError(..) => StatusCode::BAD_REQUEST,
CreateError::FileValidationError(..) => StatusCode::BAD_REQUEST,
CreateError::ImageError(..) => StatusCode::BAD_REQUEST,
CreateError::LimitReached => StatusCode::BAD_REQUEST,
}
}
@@ -143,6 +147,7 @@ impl actix_web::ResponseError for CreateError {
CreateError::ValidationError(..) => "invalid_input",
CreateError::FileValidationError(..) => "invalid_input",
CreateError::ImageError(..) => "invalid_image",
CreateError::LimitReached => "limit_reached",
},
description: self.to_string(),
})
@@ -294,9 +299,11 @@ pub async fn project_create(
/*
Project Creation Steps:
Get logged in user
- Get logged in user
Must match the author in the version creation
- Check they have not exceeded their project limit
1. Data
- Gets "data" field from multipart form; must be first
- Verification: string lengths
@@ -336,15 +343,19 @@ async fn project_create_inner(
let cdn_url = dotenvy::var("CDN_URL")?;
// The currently logged in user
let current_user = get_user_from_headers(
let (_, current_user) = get_user_from_headers(
&req,
pool,
redis,
session_queue,
Scopes::PROJECT_CREATE,
)
.await?
.1;
.await?;
let limits = UserLimits::get_for_projects(&current_user, pool).await?;
if limits.current >= limits.max {
return Err(CreateError::LimitReached);
}
let project_id: ProjectId =
models::generate_project_id(transaction).await?.into();