mirror of
https://github.com/modrinth/code.git
synced 2026-09-03 13:36:48 +00:00
refactor: labrinth ApiError and error reporting (#6981)
* refactor: labrinth `ApiError` and error reporting * fix clippy * fix ci
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
use crate::util::error::ApiContext as _;
|
||||
use crate::util::error::Context as _;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::{HttpRequest, get, patch, post, web};
|
||||
@@ -99,9 +101,9 @@ impl LicenseId {
|
||||
match self {
|
||||
LicenseId::Number(id) => Ok(id),
|
||||
LicenseId::String(id) => id.parse().map_err(|_| {
|
||||
ApiError::InvalidInput(
|
||||
"license_id must be a valid integer".to_string(),
|
||||
)
|
||||
ApiError::Request(eyre::eyre!(
|
||||
"license_id must be a valid integer",
|
||||
))
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -143,9 +145,9 @@ fn normalize_sha1_hashes(hashes: &[String]) -> Result<Vec<String>, ApiError> {
|
||||
let hash = hash.trim().to_lowercase();
|
||||
if hash.len() != 40 || !hash.chars().all(|c| c.is_ascii_hexdigit())
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"hash must be a valid SHA1 hex string".to_string(),
|
||||
));
|
||||
return Err(ApiError::Request(eyre::eyre!(
|
||||
"hash must be a valid SHA1 hex string",
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(hash)
|
||||
@@ -196,7 +198,8 @@ async fn fetch_linked_files(
|
||||
license_ids,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("fetching file rows from database")?;
|
||||
|
||||
let mut map: HashMap<i64, Vec<LinkedFile>> = HashMap::new();
|
||||
for row in file_rows {
|
||||
@@ -247,10 +250,12 @@ async fn fetch_by_hashes(
|
||||
&hash_bytes,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
.await.wrap_internal_err("querying database for `fetch_by_hashes`")?;
|
||||
|
||||
let license_ids = rows.iter().map(|row| row.id).collect::<Vec<_>>();
|
||||
let files_map = fetch_linked_files(pool, &license_ids).await?;
|
||||
let files_map = fetch_linked_files(pool, &license_ids)
|
||||
.await
|
||||
.wrap_api_err("fetching linked files")?;
|
||||
|
||||
let mut results = HashMap::new();
|
||||
for row in rows {
|
||||
@@ -309,10 +314,13 @@ async fn fetch_by_flame_ids(
|
||||
flame_ids,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("querying database for `fetch_by_flame_ids`")?;
|
||||
|
||||
let license_ids = rows.iter().map(|row| row.id).collect::<Vec<_>>();
|
||||
let files_map = fetch_linked_files(pool, &license_ids).await?;
|
||||
let files_map = fetch_linked_files(pool, &license_ids)
|
||||
.await
|
||||
.wrap_api_err("fetching linked files")?;
|
||||
|
||||
let mut results: HashMap<i32, Vec<ExternalProject>> = HashMap::new();
|
||||
for row in rows {
|
||||
@@ -350,7 +358,8 @@ pub async fn search(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating external license search")?;
|
||||
|
||||
let rows = sqlx::query_as!(
|
||||
LicenseRow,
|
||||
@@ -381,10 +390,13 @@ pub async fn search(
|
||||
body.flame_ids.as_deref(),
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("querying database for `search`")?;
|
||||
|
||||
let license_ids: Vec<i64> = rows.iter().map(|r| r.id).collect();
|
||||
let files_map = fetch_linked_files(&pool, &license_ids).await?;
|
||||
let files_map = fetch_linked_files(&pool, &license_ids)
|
||||
.await
|
||||
.wrap_api_err("fetching linked files")?;
|
||||
|
||||
let results = rows
|
||||
.into_iter()
|
||||
@@ -419,12 +431,18 @@ pub async fn lookup(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let body = body.into_inner();
|
||||
let hashes = normalize_sha1_hashes(&body.hashes)?;
|
||||
let flame_ids = fetch_by_flame_ids(&pool, &body.flame_ids).await?;
|
||||
let hashes = fetch_by_hashes(&pool, &hashes).await?;
|
||||
let hashes = normalize_sha1_hashes(&body.hashes)
|
||||
.wrap_api_err("executing `normalize_sha1_hashes`")?;
|
||||
let flame_ids = fetch_by_flame_ids(&pool, &body.flame_ids)
|
||||
.await
|
||||
.wrap_api_err("fetching by flame ids")?;
|
||||
let hashes = fetch_by_hashes(&pool, &hashes)
|
||||
.await
|
||||
.wrap_api_err("fetching by hashes")?;
|
||||
|
||||
Ok(web::Json(ExternalLicenseLookupResponse {
|
||||
flame_ids,
|
||||
@@ -453,12 +471,18 @@ pub async fn get_by_sha1(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let hashes = normalize_sha1_hashes(&[path.into_inner().0])?;
|
||||
let hash = hashes.first().ok_or(ApiError::NotFound)?;
|
||||
let mut results = fetch_by_hashes(&pool, &hashes).await?;
|
||||
let result = results.remove(hash).ok_or(ApiError::NotFound)?;
|
||||
let hashes = normalize_sha1_hashes(&[path.into_inner().0])
|
||||
.wrap_api_err("normalizing SHA-1 hash")?;
|
||||
let hash = hashes.first().wrap_not_found_err("resource not found")?;
|
||||
let mut results = fetch_by_hashes(&pool, &hashes)
|
||||
.await
|
||||
.wrap_api_err("fetching by hashes")?;
|
||||
let result = results
|
||||
.remove(hash)
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
Ok(web::Json(result))
|
||||
}
|
||||
@@ -484,10 +508,14 @@ pub async fn get_by_sha1_bulk(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let hashes = normalize_sha1_hashes(&body.hashes)?;
|
||||
let results = fetch_by_hashes(&pool, &hashes).await?;
|
||||
let hashes = normalize_sha1_hashes(&body.hashes)
|
||||
.wrap_api_err("executing `normalize_sha1_hashes`")?;
|
||||
let results = fetch_by_hashes(&pool, &hashes)
|
||||
.await
|
||||
.wrap_api_err("fetching by hashes")?;
|
||||
|
||||
Ok(web::Json(results))
|
||||
}
|
||||
@@ -540,16 +568,21 @@ async fn upsert_file_license(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let body = body.into_inner();
|
||||
let license_id = body.license_id.parse()?;
|
||||
let license_id = body
|
||||
.license_id
|
||||
.parse()
|
||||
.wrap_api_err("parsing external license ID")?;
|
||||
if body.hashes.is_empty() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"hashes must contain at least one SHA1 hex string".to_string(),
|
||||
));
|
||||
return Err(ApiError::Request(eyre::eyre!(
|
||||
"hashes must contain at least one SHA1 hex string",
|
||||
)));
|
||||
}
|
||||
let hashes = normalize_sha1_hashes(&body.hashes)?;
|
||||
let hashes = normalize_sha1_hashes(&body.hashes)
|
||||
.wrap_api_err("executing `normalize_sha1_hashes`")?;
|
||||
let hash_bytes = hashes
|
||||
.iter()
|
||||
.map(|hash| hash.as_bytes().to_vec())
|
||||
@@ -557,7 +590,10 @@ async fn upsert_file_license(
|
||||
let filenames = vec![None; hashes.len()];
|
||||
let license_ids = vec![license_id; hashes.len()];
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("starting database transaction")?;
|
||||
|
||||
let license = sqlx::query!(
|
||||
r#"
|
||||
@@ -579,8 +615,9 @@ async fn upsert_file_license(
|
||||
license_id,
|
||||
)
|
||||
.fetch_optional(&mut transaction)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.await
|
||||
.wrap_internal_err("fetching license from database")?
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
ExternalLicense::insert_files(
|
||||
&mut transaction,
|
||||
@@ -589,11 +626,19 @@ async fn upsert_file_license(
|
||||
&license_ids,
|
||||
DBUserId(user.id.0 as i64),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err(
|
||||
"inserting database records for `upsert_file_license`",
|
||||
)?;
|
||||
|
||||
transaction.commit().await?;
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_internal_err("committing database transaction")?;
|
||||
|
||||
let files_map = fetch_linked_files(&pool, &[license_id]).await?;
|
||||
let files_map = fetch_linked_files(&pool, &[license_id])
|
||||
.await
|
||||
.wrap_api_err("fetching linked files")?;
|
||||
let linked_files = files_map.get(&license_id).cloned().unwrap_or_default();
|
||||
|
||||
Ok(web::Json(
|
||||
@@ -636,7 +681,8 @@ pub async fn update_license(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let id = path.into_inner().0;
|
||||
|
||||
@@ -666,10 +712,13 @@ pub async fn update_license(
|
||||
user.id.0 as i64,
|
||||
)
|
||||
.fetch_optional(&**pool)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.await
|
||||
.wrap_internal_err("querying database for `update_license`")?
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let files_map = fetch_linked_files(&pool, &[id]).await?;
|
||||
let files_map = fetch_linked_files(&pool, &[id])
|
||||
.await
|
||||
.wrap_api_err("fetching linked files")?;
|
||||
let linked_files = files_map.get(&id).cloned().unwrap_or_default();
|
||||
|
||||
Ok(web::Json(
|
||||
|
||||
@@ -8,6 +8,7 @@ use crate::models::ids::{OrganizationId, ProjectId};
|
||||
use crate::models::projects::{ProjectStatus, VersionStatus};
|
||||
use crate::queue::moderation::{ApprovalType, IdentifiedFile, MissingMetadata};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::util::error::ApiContext as _;
|
||||
use crate::util::error::Context;
|
||||
use crate::{
|
||||
auth::{check_is_moderator_from_headers, get_user_from_bearer_token},
|
||||
@@ -254,7 +255,8 @@ pub async fn get_projects_internal(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let request_opts = request_opts.into_inner();
|
||||
let query = normalize_optional_string(request_opts.query.as_deref());
|
||||
@@ -518,7 +520,9 @@ pub async fn get_projects_internal(
|
||||
row.owner_icon_url,
|
||||
row.project_types,
|
||||
row.external_dependencies_count,
|
||||
)? {
|
||||
)
|
||||
.wrap_api_err("executing `row_to_queue_project`")?
|
||||
{
|
||||
projects.push(project);
|
||||
}
|
||||
}
|
||||
@@ -693,7 +697,9 @@ pub async fn get_projects_internal(
|
||||
row.owner_icon_url,
|
||||
row.project_types,
|
||||
row.external_dependencies_count,
|
||||
)? {
|
||||
)
|
||||
.wrap_api_err("executing `row_to_queue_project`")?
|
||||
{
|
||||
projects.push(project);
|
||||
}
|
||||
}
|
||||
@@ -731,7 +737,8 @@ pub async fn get_project_ids(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let request_opts = request_opts.into_inner();
|
||||
let query = normalize_optional_string(request_opts.query.as_deref());
|
||||
@@ -978,7 +985,8 @@ fn row_to_queue_project(
|
||||
owner_id,
|
||||
owner_name,
|
||||
owner_icon_url,
|
||||
)?;
|
||||
)
|
||||
.wrap_api_err("executing `row_to_ownership`")?;
|
||||
|
||||
Ok(Some(ModerationQueueProject {
|
||||
id: project_id,
|
||||
@@ -1076,11 +1084,14 @@ pub async fn get_project_meta(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let project_id = info.into_inner().0;
|
||||
let project =
|
||||
database::models::DBProject::get(&project_id, &**pool, &redis).await?;
|
||||
database::models::DBProject::get(&project_id, &**pool, &redis)
|
||||
.await
|
||||
.wrap_api_err("fetching project from database")?;
|
||||
|
||||
if let Some(project) = project {
|
||||
let rows = sqlx::query!(
|
||||
@@ -1094,7 +1105,8 @@ pub async fn get_project_meta(
|
||||
project.inner.id.0
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("querying database for `get_project_meta`")?;
|
||||
|
||||
let mut merged = MissingMetadata {
|
||||
identified: HashMap::new(),
|
||||
@@ -1134,7 +1146,7 @@ pub async fn get_project_meta(
|
||||
.collect::<Vec<_>>()
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
.await.wrap_internal_err("querying database for `get_project_meta`")?;
|
||||
|
||||
for row in rows {
|
||||
if let Some(sha1) = row.sha1 {
|
||||
@@ -1169,7 +1181,8 @@ pub async fn get_project_meta(
|
||||
&check_flames,
|
||||
)
|
||||
.fetch_all(&**pool)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("querying database for `get_project_meta`")?;
|
||||
|
||||
for row in rows {
|
||||
if let Some(sha1) = merged
|
||||
@@ -1192,7 +1205,7 @@ pub async fn get_project_meta(
|
||||
|
||||
Ok(web::Json(merged))
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
Err(ApiError::NotFound(eyre::eyre!("resource not found")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1234,9 +1247,13 @@ pub async fn set_project_meta(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
let mut transaction = pool
|
||||
.begin()
|
||||
.await
|
||||
.wrap_internal_err("starting database transaction")?;
|
||||
|
||||
let mut licenses = Vec::new();
|
||||
let mut file_hashes = Vec::new();
|
||||
@@ -1287,7 +1304,8 @@ pub async fn set_project_meta(
|
||||
&licenses,
|
||||
user_id,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("inserting database records for `set_project_meta`")?;
|
||||
|
||||
moderation_external_item::ExternalLicense::insert_files(
|
||||
&mut transaction,
|
||||
@@ -1299,9 +1317,13 @@ pub async fn set_project_meta(
|
||||
&file_license_ids,
|
||||
user_id,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_internal_err("inserting database records for `set_project_meta`")?;
|
||||
|
||||
transaction.commit().await?;
|
||||
transaction
|
||||
.commit()
|
||||
.await
|
||||
.wrap_internal_err("committing database transaction")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1331,18 +1353,23 @@ pub async fn acquire_lock(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let project_id_str = path.into_inner().0;
|
||||
let project =
|
||||
database::models::DBProject::get(&project_id_str, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.await
|
||||
.wrap_api_err("fetching project from database")?
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let db_project_id = project.inner.id;
|
||||
let db_user_id = database::models::DBUserId::from(user.id);
|
||||
|
||||
match DBModerationLock::acquire(db_project_id, db_user_id, &pool).await? {
|
||||
match DBModerationLock::acquire(db_project_id, db_user_id, &pool)
|
||||
.await
|
||||
.wrap_internal_err("executing `DBModerationLock::acquire`")?
|
||||
{
|
||||
Ok(()) => Ok(web::Json(LockAcquireResponse {
|
||||
success: true,
|
||||
is_own_lock: true,
|
||||
@@ -1390,18 +1417,22 @@ pub async fn override_lock(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let project_id_str = path.into_inner().0;
|
||||
let project =
|
||||
database::models::DBProject::get(&project_id_str, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.await
|
||||
.wrap_api_err("fetching project from database")?
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let db_project_id = project.inner.id;
|
||||
let db_user_id = database::models::DBUserId::from(user.id);
|
||||
|
||||
DBModerationLock::force_acquire(db_project_id, db_user_id, &pool).await?;
|
||||
DBModerationLock::force_acquire(db_project_id, db_user_id, &pool)
|
||||
.await
|
||||
.wrap_internal_err("executing `DBModerationLock::force_acquire`")?;
|
||||
|
||||
Ok(web::Json(LockAcquireResponse {
|
||||
success: true,
|
||||
@@ -1437,18 +1468,23 @@ pub async fn get_lock_status(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let project_id_str = path.into_inner().0;
|
||||
let project =
|
||||
database::models::DBProject::get(&project_id_str, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.await
|
||||
.wrap_api_err("fetching project from database")?
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let db_project_id = project.inner.id;
|
||||
let db_user_id = database::models::DBUserId::from(user.id);
|
||||
|
||||
match DBModerationLock::get_with_user(db_project_id, &pool).await? {
|
||||
match DBModerationLock::get_with_user(db_project_id, &pool)
|
||||
.await
|
||||
.wrap_internal_err("fetching moderation lock from database")?
|
||||
{
|
||||
Some(lock) => {
|
||||
let is_own_lock = lock.moderator_id == db_user_id;
|
||||
Ok(web::Json(LockStatusResponse {
|
||||
@@ -1499,19 +1535,22 @@ pub async fn release_lock(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let project_id_str = path.into_inner().0;
|
||||
let project =
|
||||
database::models::DBProject::get(&project_id_str, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.await
|
||||
.wrap_api_err("fetching project from database")?
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let db_project_id = project.inner.id;
|
||||
let db_user_id = database::models::DBUserId::from(user.id);
|
||||
|
||||
let released =
|
||||
DBModerationLock::release(db_project_id, db_user_id, &pool).await?;
|
||||
let released = DBModerationLock::release(db_project_id, db_user_id, &pool)
|
||||
.await
|
||||
.wrap_internal_err("executing `DBModerationLock::release`")?;
|
||||
|
||||
let _ = DBModerationLock::cleanup_expired(&pool).await;
|
||||
|
||||
@@ -1547,9 +1586,9 @@ pub async fn release_lock_beacon(
|
||||
) -> Result<web::Json<LockReleaseResponse>, ApiError> {
|
||||
let token = body.trim();
|
||||
if token.is_empty() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"missing token in request body".to_string(),
|
||||
));
|
||||
return Err(ApiError::Request(eyre::eyre!(
|
||||
"missing token in request body",
|
||||
)));
|
||||
}
|
||||
let token = token.strip_prefix("Bearer ").unwrap_or(token).trim();
|
||||
|
||||
@@ -1561,30 +1600,33 @@ pub async fn release_lock_beacon(
|
||||
&session_queue,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
if !scopes.contains(Scopes::PROJECT_WRITE) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"token is missing required scopes".to_string(),
|
||||
));
|
||||
return Err(ApiError::Auth(eyre::eyre!(
|
||||
"token is missing required scopes",
|
||||
)));
|
||||
}
|
||||
if !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"only moderators may release moderation locks".to_string(),
|
||||
));
|
||||
return Err(ApiError::Auth(eyre::eyre!(
|
||||
"only moderators may release moderation locks",
|
||||
)));
|
||||
}
|
||||
|
||||
let project_id_str = path.into_inner().0;
|
||||
let project =
|
||||
database::models::DBProject::get(&project_id_str, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.await
|
||||
.wrap_api_err("fetching project from database")?
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let db_project_id = project.inner.id;
|
||||
let db_user_id = database::models::DBUserId::from(user.id);
|
||||
|
||||
let released =
|
||||
DBModerationLock::release(db_project_id, db_user_id, &pool).await?;
|
||||
let released = DBModerationLock::release(db_project_id, db_user_id, &pool)
|
||||
.await
|
||||
.wrap_internal_err("executing `DBModerationLock::release`")?;
|
||||
|
||||
let _ = DBModerationLock::cleanup_expired(&pool).await;
|
||||
|
||||
@@ -1614,16 +1656,19 @@ pub async fn delete_all_locks(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?
|
||||
.1;
|
||||
|
||||
if !user.role.is_admin() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You must be an admin to delete all locks".to_string(),
|
||||
));
|
||||
return Err(ApiError::Auth(eyre::eyre!(
|
||||
"You must be an admin to delete all locks",
|
||||
)));
|
||||
}
|
||||
|
||||
let deleted_count = DBModerationLock::delete_all(&pool).await?;
|
||||
let deleted_count = DBModerationLock::delete_all(&pool)
|
||||
.await
|
||||
.wrap_internal_err("deleting moderation locks from database")?;
|
||||
|
||||
Ok(web::Json(DeleteAllLocksResponse { deleted_count }))
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::util::error::ApiContext as _;
|
||||
use std::{collections::HashMap, fmt};
|
||||
use xredis::RedisPool;
|
||||
|
||||
@@ -225,7 +226,8 @@ pub async fn get_issue(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let (issue_id,) = path.into_inner();
|
||||
let row = sqlx::query!(
|
||||
@@ -261,7 +263,7 @@ pub async fn get_issue(
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch issue from database")?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
Ok(web::Json(row.data.0))
|
||||
}
|
||||
@@ -288,7 +290,8 @@ pub async fn get_report(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let (report_id,) = path.into_inner();
|
||||
|
||||
@@ -348,7 +351,7 @@ pub async fn get_report(
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch report from database")?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
Ok(web::Json(row.data.0))
|
||||
}
|
||||
@@ -703,7 +706,8 @@ pub async fn search_projects(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let sort_by = search_req.sort_by.to_string();
|
||||
let limit = search_req.limit.max(50);
|
||||
@@ -819,8 +823,9 @@ pub async fn search_projects(
|
||||
thread_ids.push(row.thread_id);
|
||||
}
|
||||
|
||||
let project_reports =
|
||||
fetch_project_reports(&project_ids, &pool, &redis).await?;
|
||||
let project_reports = fetch_project_reports(&project_ids, &pool, &redis)
|
||||
.await
|
||||
.wrap_api_err("fetching project reports")?;
|
||||
|
||||
let projects = DBProject::get_many_ids(&project_ids, &**pool, &redis)
|
||||
.await
|
||||
@@ -913,7 +918,8 @@ pub async fn get_project_report(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
|
||||
let (project_id,) = path.into_inner();
|
||||
let db_project_id = DBProjectId::from(project_id);
|
||||
@@ -929,10 +935,12 @@ pub async fn get_project_report(
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch thread")?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let project_reports =
|
||||
fetch_project_reports(&[db_project_id], &pool, &redis).await?;
|
||||
fetch_project_reports(&[db_project_id], &pool, &redis)
|
||||
.await
|
||||
.wrap_api_err("fetching project reports")?;
|
||||
|
||||
let project_report = project_reports.into_iter().next();
|
||||
|
||||
@@ -966,7 +974,7 @@ pub async fn get_project_report(
|
||||
let thread = threads
|
||||
.get(&row.thread_id.into())
|
||||
.cloned()
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
Ok(web::Json(ProjectReportResponse {
|
||||
project_report,
|
||||
@@ -988,8 +996,8 @@ pub struct SubmitReport {
|
||||
///
|
||||
/// Before this is called, all issues for this project's reports must have been
|
||||
/// marked as either safe or unsafe. Otherwise, this will error with
|
||||
/// [`ApiError::TechReviewIssuesWithNoVerdict`], providing the issue IDs which
|
||||
/// are still unmarked.
|
||||
/// A request error is returned with the issue detail IDs which are still
|
||||
/// unmarked.
|
||||
#[utoipa::path(
|
||||
context_path = "/moderation/tech-review",
|
||||
tag = "moderation",
|
||||
@@ -1013,7 +1021,8 @@ pub async fn submit_report(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating API request")?;
|
||||
let (project_id,) = path.into_inner();
|
||||
let project_id = DBProjectId::from(project_id);
|
||||
|
||||
@@ -1045,14 +1054,13 @@ pub async fn submit_report(
|
||||
.wrap_internal_err("failed to fetch pending issues")?;
|
||||
|
||||
if !pending_issue_details.is_empty() {
|
||||
return Err(ApiError::TechReviewDetailsWithNoVerdict {
|
||||
details: pending_issue_details
|
||||
.into_iter()
|
||||
.map(|record| {
|
||||
DelphiReportIssueDetailsId(record.issue_detail_id)
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
let details = pending_issue_details
|
||||
.into_iter()
|
||||
.map(|record| DelphiReportIssueDetailsId(record.issue_detail_id))
|
||||
.collect_vec();
|
||||
return Err(ApiError::Request(eyre::eyre!(
|
||||
"report still has issue details with no verdict: {details:?}"
|
||||
)));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
@@ -1171,7 +1179,10 @@ pub async fn submit_report(
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_api_err(
|
||||
"executing `projects::clear_project_cache_and_queue_search`",
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -1224,7 +1235,8 @@ pub async fn update_issue_details(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("updating database records for `update_issue_details`")?;
|
||||
|
||||
let mut txn = pool
|
||||
.begin()
|
||||
@@ -1347,7 +1359,10 @@ pub async fn update_issue_details(
|
||||
TechReviewExitReason::Resolved,
|
||||
&mut txn,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_api_err(
|
||||
"executing `tech_review_sync::sync_project_tech_review_state`",
|
||||
)?;
|
||||
|
||||
txn.commit()
|
||||
.await
|
||||
@@ -1381,7 +1396,10 @@ pub async fn update_global_issue_details(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err(
|
||||
"updating database records for `update_global_issue_details`",
|
||||
)?;
|
||||
|
||||
let updates = update_reqs.into_inner();
|
||||
|
||||
@@ -1461,7 +1479,10 @@ pub async fn update_global_issue_details(
|
||||
TechReviewExitReason::Resolved,
|
||||
&mut txn,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_api_err(
|
||||
"executing `tech_review_sync::sync_detail_key_tech_review_state`",
|
||||
)?;
|
||||
|
||||
txn.commit()
|
||||
.await
|
||||
@@ -1498,7 +1519,8 @@ pub async fn add_report(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_WRITE,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("inserting database records for `add_report`")?;
|
||||
let file_id = add_report.file_id;
|
||||
|
||||
let mut txn = pool
|
||||
|
||||
@@ -148,7 +148,8 @@ pub async fn search_global_issue_details(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating global issue search")?;
|
||||
|
||||
let query = search_req
|
||||
.query
|
||||
@@ -370,7 +371,8 @@ pub async fn get_global_issue_detail(
|
||||
&session_queue,
|
||||
Scopes::PROJECT_READ,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.wrap_auth_err("authenticating global issue detail request")?;
|
||||
|
||||
let detail_key = get_req.detail_key.trim();
|
||||
if detail_key.is_empty() {
|
||||
@@ -404,7 +406,7 @@ pub async fn get_global_issue_detail(
|
||||
.fetch_optional(&**pool)
|
||||
.await
|
||||
.wrap_internal_err("failed to fetch global issue detail")?
|
||||
.ok_or(ApiError::NotFound)?;
|
||||
.wrap_not_found_err("resource not found")?;
|
||||
|
||||
let local_rows = sqlx::query!(
|
||||
r#"
|
||||
|
||||
Reference in New Issue
Block a user