feat(labrinth): edit project disclosures endpoint

This commit is contained in:
sychic
2026-07-31 13:34:26 +02:00
committed by Michael H.
parent b9c123031b
commit 6e5e33b1fd
4 changed files with 169 additions and 5 deletions
@@ -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"
}
@@ -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<bool, DatabaseError> {
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,
+125 -5
View File
@@ -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<ProjectDisclosure>,
pub remove: Vec<String>,
}
#[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<PgPool>,
redis: web::Data<RedisPool>,
session_queue: web::Data<AuthQueue>,
body: web::Json<ModifyProjectDisclosures>,
) -> 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::<Vec<_>>();
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(())
}
+1
View File
@@ -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,