diff --git a/.sqlx/query-06a4ce8197437b76cc6686c4cfd12ede5cd9545c0874278148ab9e268b20b969.json b/.sqlx/query-06a4ce8197437b76cc6686c4cfd12ede5cd9545c0874278148ab9e268b20b969.json new file mode 100644 index 0000000..5c827db --- /dev/null +++ b/.sqlx/query-06a4ce8197437b76cc6686c4cfd12ede5cd9545c0874278148ab9e268b20b969.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE mod_statuses\n SET status = $1,\n actor_id = $2,\n info = $3,\n locked = $4,\n updated_at = NOW()\n WHERE mod_id = $5", + "describe": { + "columns": [], + "parameters": { + "Left": [ + { + "Custom": { + "name": "mod_status", + "kind": { + "Enum": [ + "default", + "archived", + "unlisted" + ] + } + } + }, + "Int4", + "Text", + "Bool", + "Text" + ] + }, + "nullable": [] + }, + "hash": "06a4ce8197437b76cc6686c4cfd12ede5cd9545c0874278148ab9e268b20b969" +} diff --git a/.sqlx/query-2894a3bf8b4f43496a3a1532ba1e630726cf78d354b586094b1ee8c16a6fe9ee.json b/.sqlx/query-1464e6f2388cbbefe41ab7c467e0c308b73c7bd0e08fb07e3a171cd0ae6d6ab3.json similarity index 85% rename from .sqlx/query-2894a3bf8b4f43496a3a1532ba1e630726cf78d354b586094b1ee8c16a6fe9ee.json rename to .sqlx/query-1464e6f2388cbbefe41ab7c467e0c308b73c7bd0e08fb07e3a171cd0ae6d6ab3.json index 91dd8c2..d5677e0 100644 --- a/.sqlx/query-2894a3bf8b4f43496a3a1532ba1e630726cf78d354b586094b1ee8c16a6fe9ee.json +++ b/.sqlx/query-1464e6f2388cbbefe41ab7c467e0c308b73c7bd0e08fb07e3a171cd0ae6d6ab3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n m.id, m.repository, m.about, m.changelog, m.featured, m.download_count as mod_download_count, m.created_at, m.updated_at,\n mv.id as version_id, mv.name, mv.description, mv.version, mv.download_link, mv.managed_download_link,\n mv.download_count as mod_version_download_count, mv.created_at as mod_version_created_at, mv.updated_at as mod_version_updated_at, mv.hash,\n format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as \"geode!: _\",\n mv.early_load, mv.requires_patching, mv.api, mv.mod_id, mvs.status as \"status: _\", mvs.info\n FROM mods m\n INNER JOIN mod_versions mv ON m.id = mv.mod_id\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id\n WHERE m.id = $1\n AND ($2 = false OR mvs.status = 'accepted')\n ORDER BY mv.id DESC", + "query": "SELECT\n m.id, m.repository, m.about, m.changelog, m.featured, m.download_count as mod_download_count, m.created_at, m.updated_at,\n mv.id as version_id, mv.name, mv.description, mv.version, mv.download_link, mv.managed_download_link,\n mv.download_count as mod_version_download_count, mv.created_at as mod_version_created_at, mv.updated_at as mod_version_updated_at, mv.hash,\n format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as \"geode!: _\",\n mv.early_load, mv.requires_patching, mv.api, mv.mod_id, mvs.status as \"status: _\", mvs.info,\n ms.status AS \"mod_status: _\", ms.info AS mod_status_info\n FROM mods m\n INNER JOIN mod_versions mv ON m.id = mv.mod_id\n INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id\n INNER JOIN mod_statuses ms ON ms.mod_id = m.id\n WHERE m.id = $1\n AND ($2 = false OR mvs.status = 'accepted')\n ORDER BY mv.id DESC", "describe": { "columns": [ { @@ -284,6 +284,39 @@ "name": "info" } } + }, + { + "ordinal": 25, + "name": "mod_status: _", + "type_info": { + "Custom": { + "name": "mod_status", + "kind": { + "Enum": [ + "default", + "archived", + "unlisted" + ] + } + } + }, + "origin": { + "Table": { + "table": "mod_statuses", + "name": "status" + } + } + }, + { + "ordinal": 26, + "name": "mod_status_info", + "type_info": "Text", + "origin": { + "Table": { + "table": "mod_statuses", + "name": "info" + } + } } ], "parameters": { @@ -317,8 +350,10 @@ false, false, false, + true, + false, true ] }, - "hash": "2894a3bf8b4f43496a3a1532ba1e630726cf78d354b586094b1ee8c16a6fe9ee" + "hash": "1464e6f2388cbbefe41ab7c467e0c308b73c7bd0e08fb07e3a171cd0ae6d6ab3" } diff --git a/.sqlx/query-a9131f0ab03fa27cc7e6531397dc1d9343a95ff30a648d9c9b21120651a1caaa.json b/.sqlx/query-2abe4cd1be35381a41369ced2e76001b2e94a405715248f8a3064f6cb87f901a.json similarity index 68% rename from .sqlx/query-a9131f0ab03fa27cc7e6531397dc1d9343a95ff30a648d9c9b21120651a1caaa.json rename to .sqlx/query-2abe4cd1be35381a41369ced2e76001b2e94a405715248f8a3064f6cb87f901a.json index e7c2506..59c0292 100644 --- a/.sqlx/query-a9131f0ab03fa27cc7e6531397dc1d9343a95ff30a648d9c9b21120651a1caaa.json +++ b/.sqlx/query-2abe4cd1be35381a41369ced2e76001b2e94a405715248f8a3064f6cb87f901a.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n m.id, m.repository, NULL as about, NULL as changelog, m.featured,\n m.download_count, m.created_at, m.updated_at\n FROM mods m\n WHERE id = $1", + "query": "SELECT\n m.id, m.repository, NULL as about, NULL as changelog, m.featured,\n m.download_count, m.created_at, m.updated_at,\n ms.status AS \"status: _\", NULL AS status_info\n FROM mods m\n INNER JOIN mod_statuses ms ON ms.mod_id = m.id\n WHERE m.id = $1", "describe": { "columns": [ { @@ -80,6 +80,34 @@ "name": "updated_at" } } + }, + { + "ordinal": 8, + "name": "status: _", + "type_info": { + "Custom": { + "name": "mod_status", + "kind": { + "Enum": [ + "default", + "archived", + "unlisted" + ] + } + } + }, + "origin": { + "Table": { + "table": "mod_statuses", + "name": "status" + } + } + }, + { + "ordinal": 9, + "name": "status_info", + "type_info": "Text", + "origin": "Expression" } ], "parameters": { @@ -95,8 +123,10 @@ false, false, false, - false + false, + false, + null ] }, - "hash": "a9131f0ab03fa27cc7e6531397dc1d9343a95ff30a648d9c9b21120651a1caaa" + "hash": "2abe4cd1be35381a41369ced2e76001b2e94a405715248f8a3064f6cb87f901a" } diff --git a/.sqlx/query-3f065288e13b327a83b3b6abeaa3733dc7c826dea02aeb06fd202c093d9902e0.json b/.sqlx/query-3f065288e13b327a83b3b6abeaa3733dc7c826dea02aeb06fd202c093d9902e0.json new file mode 100644 index 0000000..0b001b9 --- /dev/null +++ b/.sqlx/query-3f065288e13b327a83b3b6abeaa3733dc7c826dea02aeb06fd202c093d9902e0.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(\n SELECT 1 FROM mods m\n INNER JOIN mod_statuses ms ON m.id = ms.mod_id\n WHERE m.id = $1 AND ms.locked = TRUE\n ) AS \"exists!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Bool", + "origin": "Expression" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null + ] + }, + "hash": "3f065288e13b327a83b3b6abeaa3733dc7c826dea02aeb06fd202c093d9902e0" +} diff --git a/.sqlx/query-53c75f49c92aba5582d18e974185e2f48b760b8ee773531de1c1a36083590a84.json b/.sqlx/query-53c75f49c92aba5582d18e974185e2f48b760b8ee773531de1c1a36083590a84.json new file mode 100644 index 0000000..5e10aad --- /dev/null +++ b/.sqlx/query-53c75f49c92aba5582d18e974185e2f48b760b8ee773531de1c1a36083590a84.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "SET CONSTRAINTS mods_status_id_fkey DEFERRED", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "53c75f49c92aba5582d18e974185e2f48b760b8ee773531de1c1a36083590a84" +} diff --git a/.sqlx/query-588edf3d01b00e2210aa1577b76b4696ac11b5da72b20653a1f847862bb7ad5f.json b/.sqlx/query-588edf3d01b00e2210aa1577b76b4696ac11b5da72b20653a1f847862bb7ad5f.json new file mode 100644 index 0000000..45f1ba4 --- /dev/null +++ b/.sqlx/query-588edf3d01b00e2210aa1577b76b4696ac11b5da72b20653a1f847862bb7ad5f.json @@ -0,0 +1,35 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT EXISTS(\n SELECT 1 FROM mods m\n INNER JOIN mod_statuses ms ON m.id = ms.mod_id\n WHERE m.id = $1 AND ms.status = $2\n ) AS \"exists!\"", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "exists!", + "type_info": "Bool", + "origin": "Expression" + } + ], + "parameters": { + "Left": [ + "Text", + { + "Custom": { + "name": "mod_status", + "kind": { + "Enum": [ + "default", + "archived", + "unlisted" + ] + } + } + } + ] + }, + "nullable": [ + null + ] + }, + "hash": "588edf3d01b00e2210aa1577b76b4696ac11b5da72b20653a1f847862bb7ad5f" +} diff --git a/.sqlx/query-f98fe5c88a7a234cc0b68fb39c156a741fbad1d2e4682e1e748037bb08962912.json b/.sqlx/query-66186328d37a03e0bc17364fa2be10f02b814dc887b71a0c995eafd0dd8b1426.json similarity index 87% rename from .sqlx/query-f98fe5c88a7a234cc0b68fb39c156a741fbad1d2e4682e1e748037bb08962912.json rename to .sqlx/query-66186328d37a03e0bc17364fa2be10f02b814dc887b71a0c995eafd0dd8b1426.json index df6a0a8..d5931bf 100644 --- a/.sqlx/query-f98fe5c88a7a234cc0b68fb39c156a741fbad1d2e4682e1e748037bb08962912.json +++ b/.sqlx/query-66186328d37a03e0bc17364fa2be10f02b814dc887b71a0c995eafd0dd8b1426.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO mods (\n id,\n repository,\n changelog,\n about,\n image\n ) VALUES ($1, $2, $3, $4, $5)\n RETURNING\n id, repository, about,\n changelog, featured,\n download_count, created_at,\n updated_at", + "query": "INSERT INTO mods (\n id,\n repository,\n changelog,\n about,\n image,\n status_id\n ) VALUES ($1, $2, $3, $4, $5, 0)\n RETURNING\n id, repository, about,\n changelog, featured,\n download_count, created_at,\n updated_at", "describe": { "columns": [ { @@ -112,5 +112,5 @@ false ] }, - "hash": "f98fe5c88a7a234cc0b68fb39c156a741fbad1d2e4682e1e748037bb08962912" + "hash": "66186328d37a03e0bc17364fa2be10f02b814dc887b71a0c995eafd0dd8b1426" } diff --git a/.sqlx/query-7f8c391eb1a2a0e3ac12d0598b9ae6b200cee6f038f52e633b0dbba06852653f.json b/.sqlx/query-7f8c391eb1a2a0e3ac12d0598b9ae6b200cee6f038f52e633b0dbba06852653f.json new file mode 100644 index 0000000..ad9acd0 --- /dev/null +++ b/.sqlx/query-7f8c391eb1a2a0e3ac12d0598b9ae6b200cee6f038f52e633b0dbba06852653f.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE mods SET status_id = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Text" + ] + }, + "nullable": [] + }, + "hash": "7f8c391eb1a2a0e3ac12d0598b9ae6b200cee6f038f52e633b0dbba06852653f" +} diff --git a/.sqlx/query-e03e835c24369349bbec1639b7f5afd42551899edcef89ab4812339e195b4a6b.json b/.sqlx/query-e03e835c24369349bbec1639b7f5afd42551899edcef89ab4812339e195b4a6b.json new file mode 100644 index 0000000..bb25e6f --- /dev/null +++ b/.sqlx/query-e03e835c24369349bbec1639b7f5afd42551899edcef89ab4812339e195b4a6b.json @@ -0,0 +1,12 @@ +{ + "db_name": "PostgreSQL", + "query": "SET CONSTRAINTS mods_status_id_fkey IMMEDIATE", + "describe": { + "columns": [], + "parameters": { + "Left": [] + }, + "nullable": [] + }, + "hash": "e03e835c24369349bbec1639b7f5afd42551899edcef89ab4812339e195b4a6b" +} diff --git a/.sqlx/query-959d6a60bbb47b24bc86963c0063bdd1268c75d03563eaf78eec41752cc1a491.json b/.sqlx/query-ef332e13a09ed15fc4b02067807ab444d55b6103ab73734a16d586490a147518.json similarity index 68% rename from .sqlx/query-959d6a60bbb47b24bc86963c0063bdd1268c75d03563eaf78eec41752cc1a491.json rename to .sqlx/query-ef332e13a09ed15fc4b02067807ab444d55b6103ab73734a16d586490a147518.json index 6cf71c0..649812a 100644 --- a/.sqlx/query-959d6a60bbb47b24bc86963c0063bdd1268c75d03563eaf78eec41752cc1a491.json +++ b/.sqlx/query-ef332e13a09ed15fc4b02067807ab444d55b6103ab73734a16d586490a147518.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n m.id, m.repository, m.about, m.changelog, m.featured,\n m.download_count, m.created_at, m.updated_at\n FROM mods m\n WHERE id = $1", + "query": "SELECT\n m.id, m.repository, m.about, m.changelog, m.featured,\n m.download_count, m.created_at, m.updated_at,\n ms.status AS \"status: _\", ms.info AS status_info\n FROM mods m\n INNER JOIN mod_statuses ms ON ms.mod_id = m.id\n WHERE m.id = $1", "describe": { "columns": [ { @@ -90,6 +90,39 @@ "name": "updated_at" } } + }, + { + "ordinal": 8, + "name": "status: _", + "type_info": { + "Custom": { + "name": "mod_status", + "kind": { + "Enum": [ + "default", + "archived", + "unlisted" + ] + } + } + }, + "origin": { + "Table": { + "table": "mod_statuses", + "name": "status" + } + } + }, + { + "ordinal": 9, + "name": "status_info", + "type_info": "Text", + "origin": { + "Table": { + "table": "mod_statuses", + "name": "info" + } + } } ], "parameters": { @@ -105,8 +138,10 @@ false, false, false, - false + false, + false, + true ] }, - "hash": "959d6a60bbb47b24bc86963c0063bdd1268c75d03563eaf78eec41752cc1a491" + "hash": "ef332e13a09ed15fc4b02067807ab444d55b6103ab73734a16d586490a147518" } diff --git a/.sqlx/query-fdae277f888f7d8bd3915f146df5a726f91f39a6113306d9a29f77d2cd98186c.json b/.sqlx/query-fdae277f888f7d8bd3915f146df5a726f91f39a6113306d9a29f77d2cd98186c.json new file mode 100644 index 0000000..382d13d --- /dev/null +++ b/.sqlx/query-fdae277f888f7d8bd3915f146df5a726f91f39a6113306d9a29f77d2cd98186c.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO mod_statuses (mod_id) VALUES ($1) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4", + "origin": { + "Table": { + "table": "mod_statuses", + "name": "id" + } + } + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "fdae277f888f7d8bd3915f146df5a726f91f39a6113306d9a29f77d2cd98186c" +} diff --git a/migrations/20260730233906_add_mod_statuses.down.sql b/migrations/20260730233906_add_mod_statuses.down.sql new file mode 100644 index 0000000..774d1d4 --- /dev/null +++ b/migrations/20260730233906_add_mod_statuses.down.sql @@ -0,0 +1,7 @@ +-- Add down migration script here + +drop index mods_status_id_idx; + +alter table mods drop column status_id; +drop table mod_statuses; +drop type mod_status; \ No newline at end of file diff --git a/migrations/20260730233906_add_mod_statuses.up.sql b/migrations/20260730233906_add_mod_statuses.up.sql new file mode 100644 index 0000000..a21a633 --- /dev/null +++ b/migrations/20260730233906_add_mod_statuses.up.sql @@ -0,0 +1,38 @@ +-- Add up migration script here + +create type mod_status as enum('default', 'archived', 'unlisted'); + +create table mod_statuses( + id serial primary key, + status mod_status not null default 'default', + info text, + updated_at timestamptz not null default now(), + mod_id TEXT not null, + actor_id integer, + locked BOOLEAN not null DEFAULT FALSE, + foreign key (mod_id) + references mods(id) + on delete cascade, + foreign key (actor_id) + references developers(id) + on delete set null +); + +create index mod_statuses_actor_id_idx on mod_statuses(actor_id); +create unique index mod_statuses_mod_id_idx on mod_statuses(mod_id); + +alter table mods add column status_id integer; + +insert into mod_statuses (mod_id) SELECT id as mod_id from mods; + +update mods set status_id = ms.id + from mod_statuses ms + where mods.id = ms.mod_id; + +alter table mods alter column status_id set not null; +alter table mods + add foreign key (status_id) + references mod_statuses(id) + deferrable; + +create index mods_status_id_idx on mods(status_id); \ No newline at end of file diff --git a/src/database/repository/mods.rs b/src/database/repository/mods.rs index 5191855..3d16219 100644 --- a/src/database/repository/mods.rs +++ b/src/database/repository/mods.rs @@ -1,6 +1,9 @@ use crate::{ database::DatabaseError, - types::{mod_json::ModJson, models::mod_entity::Mod}, + types::{ + mod_json::ModJson, + models::{mod_entity::Mod, mod_status::ModStatusEnum} + }, }; use chrono::{DateTime, Utc}; use sqlx::PgConnection; @@ -18,6 +21,9 @@ struct ModRecordGetOne { changelog: Option, created_at: DateTime, updated_at: DateTime, + status: ModStatusEnum, + #[sqlx(default)] + status_info: Option, } impl ModRecordGetOne { @@ -35,6 +41,8 @@ impl ModRecordGetOne { about: self.about.clone(), changelog: self.changelog.clone(), links: None, + status: self.status, + status_info: self.status_info, } } } @@ -51,11 +59,13 @@ pub async fn get_one( if include_md { sqlx::query_as!( ModRecordGetOne, - "SELECT + r#"SELECT m.id, m.repository, m.about, m.changelog, m.featured, - m.download_count, m.created_at, m.updated_at + m.download_count, m.created_at, m.updated_at, + ms.status AS "status: _", ms.info AS status_info FROM mods m - WHERE id = $1", + INNER JOIN mod_statuses ms ON ms.mod_id = m.id + WHERE m.id = $1"#, id ) .fetch_optional(conn) @@ -66,11 +76,13 @@ pub async fn get_one( } else { sqlx::query_as!( ModRecordGetOne, - "SELECT + r#"SELECT m.id, m.repository, NULL as about, NULL as changelog, m.featured, - m.download_count, m.created_at, m.updated_at + m.download_count, m.created_at, m.updated_at, + ms.status AS "status: _", NULL AS status_info FROM mods m - WHERE id = $1", + INNER JOIN mod_statuses ms ON ms.mod_id = m.id + WHERE m.id = $1"#, id ) .fetch_optional(conn) @@ -84,15 +96,20 @@ pub async fn get_one( /// Does NOT check if the target mod exists #[tracing::instrument(skip_all, fields(mod_id = %json.id))] pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result { - sqlx::query_as!( - ModRecordGetOne, + sqlx::query!("SET CONSTRAINTS mods_status_id_fkey DEFERRED") + .execute(&mut *conn) + .await + .inspect_err(|e| tracing::error!("{:?}", e))?; + + let record = sqlx::query!( "INSERT INTO mods ( id, repository, changelog, about, - image - ) VALUES ($1, $2, $3, $4, $5) + image, + status_id + ) VALUES ($1, $2, $3, $4, $5, 0) RETURNING id, repository, about, changelog, featured, @@ -104,11 +121,45 @@ pub async fn create(json: &ModJson, conn: &mut PgConnection) -> Result Result<(), D Ok(()) } + +#[tracing::instrument(skip_all, fields(mod_id = %id))] +pub async fn has_status( + id: &str, + status: ModStatusEnum, + pool: &mut PgConnection, +) -> Result { + sqlx::query_scalar!(r#"SELECT EXISTS( + SELECT 1 FROM mods m + INNER JOIN mod_statuses ms ON m.id = ms.mod_id + WHERE m.id = $1 AND ms.status = $2 + ) AS "exists!""#, + id, status as ModStatusEnum + ) + .fetch_one(&mut *pool) + .await + .inspect_err(|e| tracing::error!("{:?}", e)) + .map_err(|e| e.into()) +} + +#[tracing::instrument(skip_all, fields(mod_id = %id))] +pub async fn is_status_locked( + id: &str, + pool: &mut PgConnection, +) -> Result { + sqlx::query_scalar!(r#"SELECT EXISTS( + SELECT 1 FROM mods m + INNER JOIN mod_statuses ms ON m.id = ms.mod_id + WHERE m.id = $1 AND ms.locked = TRUE + ) AS "exists!""#, + id + ) + .fetch_one(&mut *pool) + .await + .inspect_err(|e| tracing::error!("{:?}", e)) + .map_err(|e| e.into()) +} \ No newline at end of file diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 9b1954e..5da9ce5 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -7,6 +7,7 @@ use crate::extractors::auth::Auth; use crate::types::api::{ApiResponse, PaginatedData}; use crate::types::models::audit_actions::{AuditAction, AuditActionRow}; use crate::types::models::developer::Developer; +use crate::types::models::mod_status::ModStatusEnum; use crate::types::models::mod_version_status::ModVersionStatusEnum; use crate::types::models::mod_version_submission::{ CreateCommentPayload, ModVersionSubmission, ModVersionSubmissionAttachment, @@ -80,6 +81,10 @@ async fn check_submission_lock( return Ok(true); } + if mods::has_status(mod_id, ModStatusEnum::Archived, &mut *conn).await? { + return Ok(false); + } + let access_to_mod = developers::has_access_to_mod(dev.id, mod_id, &mut *conn).await?; let active_developer = developers::has_active_mod(dev.id, &mut *conn).await?; diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 3d48b99..83f3d54 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -18,6 +18,7 @@ use crate::events::mod_created::{ use crate::mod_zip::{self, download_mod}; use crate::s3_worker::S3WorkerTask; use crate::types::models; +use crate::types::models::mod_status::ModStatusEnum; use crate::types::models::mod_version_submission::ModVersionSubmissionLock; use crate::webhook::discord::DiscordWebhook; use crate::{ @@ -323,6 +324,10 @@ pub async fn create_version( return Err(ApiError::Authorization); } + if the_mod.status == ModStatusEnum::Archived { + return Err(ApiError::BadRequest("Mod is archived".to_string())); + } + let versions = mod_versions::get_for_mod( &the_mod.id, Some(&[ @@ -462,20 +467,22 @@ pub async fn create_version( tx.commit().await?; if make_accepted { - let owner = developers::get_owner_for_mod(&version.mod_id, &mut pool) - .await? - .ok_or(ApiError::BadRequest("Mod doesn't have an owner".into()))?; + if the_mod.status != ModStatusEnum::Unlisted { + let owner = developers::get_owner_for_mod(&version.mod_id, &mut pool) + .await? + .ok_or(ApiError::BadRequest("Mod doesn't have an owner".into()))?; - NewModVersionAcceptedEvent { - id: version.mod_id.clone(), - name: version.name.clone(), - version: version.version.clone(), - owner, - verified: NewModVersionVerification::VerifiedDev, - base_url: data.app_url().to_string(), + NewModVersionAcceptedEvent { + id: version.mod_id.clone(), + name: version.name.clone(), + version: version.version.clone(), + owner, + verified: NewModVersionVerification::VerifiedDev, + base_url: data.app_url().to_string(), + } + .to_discord_webhook() + .send(data.http_client(), data.webhook_url()); } - .to_discord_webhook() - .send(data.http_client(), data.webhook_url()); data.send_s3_task(S3WorkerTask::UploadMod { data: bytes, @@ -571,6 +578,8 @@ pub async fn update_version( ) .await?; + let mod_status = the_mod.status; + if old_status == ModVersionStatusEnum::Pending && version.status == ModVersionStatusEnum::Accepted { @@ -638,7 +647,7 @@ pub async fn update_version( tx.commit().await?; - if payload.status == ModVersionStatusEnum::Accepted { + if payload.status == ModVersionStatusEnum::Accepted && mod_status != ModStatusEnum::Unlisted { let is_update = approved_count > 0; let owner = developers::get_owner_for_mod(&version.mod_id, &mut pool) diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index b6c622b..c36ac80 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -23,6 +23,7 @@ use crate::types::models::deprecations::Deprecation; use crate::types::models::mod_entity::{Mod, ModUpdate}; use crate::types::models::mod_gd_version::{GDVersionEnum, VerPlatform}; use crate::types::models::mod_link::ModLinks; +use crate::types::models::mod_status::ModStatusEnum; use crate::types::models::mod_version_status::ModVersionStatusEnum; use crate::webhook::discord::DiscordWebhook; use actix_web::{HttpResponse, Responder, get, post, put, web}; @@ -428,7 +429,10 @@ pub async fn get_logo( #[derive(Deserialize, ToSchema)] struct UpdateModPayload { - featured: bool, + featured: Option, + status: Option, + info: Option, + status_locked: Option } /// Update a mod (admin only) @@ -443,7 +447,7 @@ struct UpdateModPayload { responses( (status = 204, description = "Mod updated successfully"), (status = 401, description = "Unauthorized"), - (status = 403, description = "Forbidden - Admin only"), + (status = 403, description = "Forbidden - Fields being updated are admin only"), (status = 404, description = "Mod not found") ), security( @@ -459,40 +463,66 @@ pub async fn update_mod( auth: Auth, ) -> Result { let dev = auth.developer()?; - auth.check_admin()?; let mut pool = data.db().acquire().await?; - let mut tx = pool.begin().await?; let id = path.into_inner(); + let mut tx = pool.begin().await?; if !mods::exists(&id, &mut tx).await? { return Err(ApiError::NotFound("Mod not found".into())); } - let featured = mods::is_featured(&id, &mut tx).await?; + if let Some(payload_featured) = payload.featured { + auth.check_admin()?; - Mod::update_mod(&id, payload.featured, &mut tx).await?; + let featured = mods::is_featured(&id, &mut tx).await?; + + Mod::update_mod(&id, payload_featured, &mut tx).await?; + + if featured != payload_featured { + let item = Mod::get_one(&id, true, &mut tx).await?; + if let Some(item) = item + && let Some(owner) = developers::get_owner_for_mod(&id, &mut tx).await? + && let Some(ver) = item.versions.first() + { + ModFeaturedEvent { + id: item.id, + name: ver.name.clone(), + owner, + admin: dev.clone(), + base_url: data.app_url().to_string(), + featured: payload_featured, + } + .to_discord_webhook() + .send(data.http_client(), data.webhook_url()); + } + } + } - tx.commit().await?; + if let Some(payload_status) = payload.status { + let status_locked = mods::is_status_locked(&id, &mut tx).await?; + let updating_locked_fields = payload.status_locked.is_some() || payload.info.is_some(); - if featured != payload.featured { - let item = Mod::get_one(&id, true, &mut pool).await?; - if let Some(item) = item - && let Some(owner) = developers::get_owner_for_mod(&id, &mut pool).await? - && let Some(ver) = item.versions.first() - { - ModFeaturedEvent { - id: item.id, - name: ver.name.clone(), - owner, - admin: dev, - base_url: data.app_url().to_string(), - featured: payload.featured, - } - .to_discord_webhook() - .send(data.http_client(), data.webhook_url()); + if !dev.admin && (status_locked || updating_locked_fields) { + return Err(ApiError::Authorization); + } + + let dev_of_mod = developers::has_access_to_mod(dev.id, &id, &mut tx).await?; + if !dev.admin && !dev_of_mod { + return Err(ApiError::Authorization); } + + Mod::update_mod_status( + &id, + payload_status, + payload.info.as_deref(), + payload.status_locked.unwrap_or(status_locked), + &dev, + &mut tx, + ).await?; } + tx.commit().await?; + Ok(HttpResponse::NoContent()) } diff --git a/src/types/models/mod.rs b/src/types/models/mod.rs index 1fa103e..b1d9810 100644 --- a/src/types/models/mod.rs +++ b/src/types/models/mod.rs @@ -8,6 +8,7 @@ pub mod github_login_attempt; pub mod incompatibility; pub mod loader_version; pub mod mod_entity; +pub mod mod_status; pub mod mod_gd_version; pub mod mod_link; pub mod mod_version; diff --git a/src/types/models/mod_entity.rs b/src/types/models/mod_entity.rs index 55421ed..3bac14f 100644 --- a/src/types/models/mod_entity.rs +++ b/src/types/models/mod_entity.rs @@ -18,7 +18,12 @@ use crate::{ }, types::{ api::PaginatedData, - models::{mod_version::ModVersion, mod_version_status::ModVersionStatusEnum}, + models::{ + mod_version::ModVersion, + mod_version_status::ModVersionStatusEnum, + developer::Developer, + mod_status::ModStatusEnum, + }, serde::chrono_dt_secs, }, }; @@ -48,6 +53,8 @@ pub struct Mod { #[serde(with = "chrono_dt_secs")] pub updated_at: DateTime, pub links: Option, + pub status: ModStatusEnum, + pub status_info: Option, } #[derive(Serialize, Debug, ToSchema)] @@ -74,6 +81,7 @@ struct ModRecord { changelog: Option, created_at: DateTime, updated_at: DateTime, + status: ModStatusEnum, } #[derive(sqlx::FromRow)] @@ -103,6 +111,8 @@ struct ModRecordGetOne { mod_version_created_at: Option>, mod_version_updated_at: Option>, info: Option, + mod_status: ModStatusEnum, + mod_status_info: Option, } pub struct ModStats { @@ -238,6 +248,7 @@ impl Mod { // joins: only join tables if they are necessary builder.push(" INNER JOIN mod_versions mv ON m.id = mv.mod_id "); builder.push(" INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id "); + builder.push(" INNER JOIN mod_statuses ms ON ms.mod_id = m.id"); if gd.is_some() || platforms.is_some() { builder.push(" INNER JOIN mod_gd_versions mgv ON mgv.mod_id = mv.id "); @@ -270,6 +281,13 @@ impl Mod { } builder.push(" AND mvs.status = ").push_bind(status); + builder.push(" AND ms.status <> 'unlisted'"); + + let direct_search = search_str.as_ref().is_some_and(|x| !x.is_empty()) || developer.is_some(); + if !direct_search { + // hide archived mods from direct search + builder.push(" AND ms.status <> 'archived'"); + } if let Some(rp) = requires_patching { builder.push(" AND mv.requires_patching = ").push_bind(rp); @@ -348,10 +366,10 @@ impl Mod { let mut records_builder = sqlx::QueryBuilder::new( "SELECT q.id, q.repository, q.about, q.changelog, - q.download_count, q.featured, q.created_at, q.updated_at + q.download_count, q.featured, q.created_at, q.updated_at, q.status FROM ( SELECT DISTINCT ON (m.id) m.id, mv.name, m.repository, m.about, m.changelog, - m.download_count, m.featured, m.created_at, m.updated_at ", + m.download_count, m.featured, m.created_at, m.updated_at, ms.status ", ); core_query(&mut records_builder); @@ -434,6 +452,8 @@ impl Mod { about: None, changelog: None, links, + status: x.status, + status_info: None, }) }) .collect(); @@ -481,6 +501,8 @@ impl Mod { about: x.about, changelog: x.changelog, links, + status: x.status, + status_info: None, } }) .collect::>(); @@ -591,10 +613,12 @@ impl Mod { mv.id as version_id, mv.name, mv.description, mv.version, mv.download_link, mv.managed_download_link, mv.download_count as mod_version_download_count, mv.created_at as mod_version_created_at, mv.updated_at as mod_version_updated_at, mv.hash, format_semver(mv.geode_major, mv.geode_minor, mv.geode_patch, mv.geode_meta) as "geode!: _", - mv.early_load, mv.requires_patching, mv.api, mv.mod_id, mvs.status as "status: _", mvs.info + mv.early_load, mv.requires_patching, mv.api, mv.mod_id, mvs.status as "status: _", mvs.info, + ms.status AS "mod_status: _", ms.info AS mod_status_info FROM mods m INNER JOIN mod_versions mv ON m.id = mv.mod_id INNER JOIN mod_version_statuses mvs ON mvs.mod_version_id = mv.id + INNER JOIN mod_statuses ms ON ms.mod_id = m.id WHERE m.id = $1 AND ($2 = false OR mvs.status = 'accepted') ORDER BY mv.id DESC"#, @@ -671,6 +695,8 @@ impl Mod { about: records[0].about.clone(), changelog: records[0].changelog.clone(), links, + status: records[0].mod_status, + status_info: records[0].mod_status_info.clone(), }; Ok(Some(mod_entity)) } @@ -803,4 +829,29 @@ impl Mod { Ok(ret) } + + #[tracing::instrument(skip_all, fields(mod_id = %id, status = ?status))] + pub async fn update_mod_status( + id: &str, + status: ModStatusEnum, + info: Option<&str>, + locked: bool, + updated_by: &Developer, + pool: &mut PgConnection, + ) -> Result<(), DatabaseError> { + sqlx::query!("UPDATE mod_statuses + SET status = $1, + actor_id = $2, + info = $3, + locked = $4, + updated_at = NOW() + WHERE mod_id = $5", + status as ModStatusEnum, updated_by.id, info, locked, id + ) + .execute(&mut *pool) + .await + .inspect_err(|e| tracing::error!("{:?}", e)) + .map_err(|e| e.into()) + .map(|_| ()) + } } diff --git a/src/types/models/mod_status.rs b/src/types/models/mod_status.rs new file mode 100644 index 0000000..f929e64 --- /dev/null +++ b/src/types/models/mod_status.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +#[derive(sqlx::Type, Debug, Deserialize, Serialize, Clone, Copy, Hash, PartialEq, Eq, ToSchema)] +#[serde(rename_all = "lowercase")] +#[sqlx(rename_all = "lowercase", type_name = "mod_status")] +pub enum ModStatusEnum { + Default, + Archived, + Unlisted, +}