From 6e5e33b1fda5378e50bf4d0097e311e70aa1d04b Mon Sep 17 00:00:00 2001 From: sychic <47618543+Sychic@users.noreply.github.com> Date: Thu, 30 Jul 2026 17:35:48 -0400 Subject: [PATCH] feat(labrinth): edit project disclosures endpoint --- ...61a8dff02ed316fc0b98754605a6e16e8e846.json | 23 ++++ .../models/project_disclosure_item.rs | 20 +++ apps/labrinth/src/routes/v3/disclosures.rs | 130 +++++++++++++++++- apps/labrinth/src/routes/v3/mod.rs | 1 + 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 apps/labrinth/.sqlx/query-763762b9995a6d7b00b6cf71c7661a8dff02ed316fc0b98754605a6e16e8e846.json diff --git a/apps/labrinth/.sqlx/query-763762b9995a6d7b00b6cf71c7661a8dff02ed316fc0b98754605a6e16e8e846.json b/apps/labrinth/.sqlx/query-763762b9995a6d7b00b6cf71c7661a8dff02ed316fc0b98754605a6e16e8e846.json new file mode 100644 index 000000000..153b859c1 --- /dev/null +++ b/apps/labrinth/.sqlx/query-763762b9995a6d7b00b6cf71c7661a8dff02ed316fc0b98754605a6e16e8e846.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n\t\t\tSELECT 1 FROM project_disclosures\n\t\t\tWHERE project_id = $1 AND type = ANY($2) AND set_by_moderator\n\t\t\tLIMIT 1\n\t\t\t", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "?column?", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Int8", + "TextArray" + ] + }, + "nullable": [ + null + ] + }, + "hash": "763762b9995a6d7b00b6cf71c7661a8dff02ed316fc0b98754605a6e16e8e846" +} diff --git a/apps/labrinth/src/database/models/project_disclosure_item.rs b/apps/labrinth/src/database/models/project_disclosure_item.rs index 14b13ae72..8b3114452 100644 --- a/apps/labrinth/src/database/models/project_disclosure_item.rs +++ b/apps/labrinth/src/database/models/project_disclosure_item.rs @@ -86,6 +86,26 @@ impl DBProjectDisclosure { .collect() } + pub async fn any_set_by_moderator( + project_id: DBProjectId, + types: &[String], + exec: impl crate::database::Executor<'_, Database = sqlx::Postgres>, + ) -> Result { + let existing = sqlx::query_scalar!( + r#" + SELECT 1 FROM project_disclosures + WHERE project_id = $1 AND type = ANY($2) AND set_by_moderator + LIMIT 1 + "#, + project_id as DBProjectId, + types, + ) + .fetch_optional(exec) + .await?; + + Ok(existing.is_some()) + } + pub async fn remove( project_id: DBProjectId, disclosure_type: &str, diff --git a/apps/labrinth/src/routes/v3/disclosures.rs b/apps/labrinth/src/routes/v3/disclosures.rs index 081aef70d..4ddf53dc3 100644 --- a/apps/labrinth/src/routes/v3/disclosures.rs +++ b/apps/labrinth/src/routes/v3/disclosures.rs @@ -1,5 +1,6 @@ -use actix_web::{HttpRequest, get, web}; -use serde::Serialize; +use actix_web::{HttpRequest, get, patch, web}; +use chrono::Utc; +use serde::{Deserialize, Serialize}; use utoipa::ToSchema; use xredis::RedisPool; @@ -7,14 +8,16 @@ use crate::auth::checks::is_visible_project; use crate::auth::get_user_from_headers; use crate::database::models as db_models; use crate::database::{PgPool, ReadOnlyPgPool}; -use crate::models::disclosures::ProjectDisclosureData; +use crate::models::disclosures::{ProjectDisclosure, ProjectDisclosureData}; use crate::models::pats::Scopes; +use crate::models::teams::ProjectPermissions; use crate::queue::session::AuthQueue; use crate::routes::ApiError; use crate::util::error::Context; pub fn config(cfg: &mut web::ServiceConfig) { - cfg.service(get_project_disclosures); + cfg.service(get_project_disclosures) + .service(modify_project_disclosures); } #[derive(Serialize, ToSchema)] @@ -77,6 +80,123 @@ pub async fn get_project_disclosures( .map(|disclosure| { ProjectDisclosureData::from_db(disclosure, viewer_is_moderator) }) - .collect() + .collect(), })) } + +#[derive(Deserialize, ToSchema)] +pub struct ModifyProjectDisclosures { + pub set: Vec, + pub remove: Vec, +} + +#[utoipa::path( + context_path = "/project", + tag = "project_disclosures", + request_body = ModifyProjectDisclosures, + responses((status = NO_CONTENT)) +)] +#[patch("/{project_id}/disclosures")] +pub async fn modify_project_disclosures( + req: HttpRequest, + info: web::Path<(String,)>, + pool: web::Data, + redis: web::Data, + session_queue: web::Data, + body: web::Json, +) -> Result<(), ApiError> { + let (string,) = info.into_inner(); + let body = body.into_inner(); + + let user = get_user_from_headers( + &req, + &**pool, + &redis, + &session_queue, + Scopes::PROJECT_WRITE, + ) + .await? + .1; + + let project = db_models::DBProject::get(&string, &**pool, &redis) + .await + .wrap_internal_err("failed to fetch project")? + .ok_or(ApiError::NotFound)?; + + let (team_member, organization_team_member) = + db_models::DBTeamMember::get_for_project_permissions( + &project.inner, + user.id.into(), + &**pool, + ) + .await + .wrap_internal_err("failed to fetch project permissions")?; + + let can_edit_details = ProjectPermissions::get_permissions_by_role( + &user.role, + &team_member, + &organization_team_member, + ) + .is_some_and(|perms| perms.contains(ProjectPermissions::EDIT_DETAILS)); + + if !can_edit_details { + return Err(ApiError::CustomAuthentication( + "you do not have permission to edit this project's disclosures" + .to_string(), + )); + } + + if !user.role.is_mod() { + let modified_types = body + .set + .iter() + .map(|disclosure| <&'static str>::from(disclosure).to_owned()) + .chain(body.remove.iter().cloned()) + .collect::>(); + + if db_models::DBProjectDisclosure::any_set_by_moderator( + project.inner.id, + &modified_types, + &**pool, + ) + .await + .wrap_internal_err("failed to check moderator disclosures")? + { + return Err(ApiError::CustomAuthentication( + "you cannot modify a disclosure set by a moderator".to_string(), + )); + } + } + + let mut transaction = pool.begin().await?; + + for disclosure in body.set { + db_models::DBProjectDisclosure { + project_id: project.inner.id, + disclosure, + updated_at: Utc::now(), + updated_by: user.id.into(), + set_by_moderator: user.role.is_mod(), + } + .upsert(&mut transaction) + .await + .wrap_internal_err("failed to upsert project disclosure")?; + } + + for disclosure_type in &body.remove { + db_models::DBProjectDisclosure::remove( + project.inner.id, + disclosure_type, + &mut transaction, + ) + .await + .wrap_internal_err("failed to remove project disclosure")?; + } + + transaction + .commit() + .await + .wrap_internal_err("failed to commit project disclosure changes")?; + + Ok(()) +} diff --git a/apps/labrinth/src/routes/v3/mod.rs b/apps/labrinth/src/routes/v3/mod.rs index a2e0fa3c4..7a0d92a9c 100644 --- a/apps/labrinth/src/routes/v3/mod.rs +++ b/apps/labrinth/src/routes/v3/mod.rs @@ -114,6 +114,7 @@ pub fn config(cfg: &mut web::ServiceConfig) { projects::project_get_organization, projects::dependency_list, disclosures::get_project_disclosures, + disclosures::modify_project_disclosures, project_creation::project_create, project_creation::project_create_with_id, project_creation::new::create,