From 03aa03caa6d6f86fc61c3fe9838b8a6d179bd20c Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:32:54 -0700 Subject: [PATCH 1/8] implement ban endpoints --- .../20260418000114_add_index_ban.down.sql | 3 + .../20260418000114_add_index_ban.up.sql | 9 ++ src/database/repository/developers.rs | 43 ++++- src/endpoints/developers.rs | 152 +++++++++++++++++- src/endpoints/mod.rs | 2 + src/main.rs | 3 + src/openapi.rs | 3 + src/types/models/developer.rs | 9 ++ 8 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 migrations/20260418000114_add_index_ban.down.sql create mode 100644 migrations/20260418000114_add_index_ban.up.sql diff --git a/migrations/20260418000114_add_index_ban.down.sql b/migrations/20260418000114_add_index_ban.down.sql new file mode 100644 index 00000000..881bc5dc --- /dev/null +++ b/migrations/20260418000114_add_index_ban.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here +ALTER TABLE developers DROP COLUMN IF EXISTS note; +DROP TABLE IF EXISTS bans; \ No newline at end of file diff --git a/migrations/20260418000114_add_index_ban.up.sql b/migrations/20260418000114_add_index_ban.up.sql new file mode 100644 index 00000000..1ab2f5cc --- /dev/null +++ b/migrations/20260418000114_add_index_ban.up.sql @@ -0,0 +1,9 @@ +-- Add up migration script here +ALTER TABLE developers ADD COLUMN note TEXT; + +CREATE TABLE bans ( + developer_id INTEGER PRIMARY KEY NOT NULL REFERENCES developers(id) ON DELETE CASCADE, + reason TEXT, + admin_id INTEGER REFERENCES developers(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL +); diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index 18eae63b..4d534f0b 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -1,6 +1,6 @@ use crate::database::DatabaseError; use crate::types::api::PaginatedData; -use crate::types::models::developer::{Developer, ModDeveloper}; +use crate::types::models::developer::{Developer, DeveloperBan, ModDeveloper}; use sqlx::PgConnection; use std::collections::HashMap; use uuid::Uuid; @@ -427,3 +427,44 @@ pub async fn find_by_token( .inspect_err(|e| log::error!("{}", e)) .map_err(|e| e.into()) } + +pub async fn create_ban( + dev_id: i32, + admin_id: i32, + reason: Option<&str>, + conn: &mut PgConnection, +) -> Result { + sqlx::query_as!(DeveloperBan, + "INSERT INTO bans (developer_id, reason, admin_id) + VALUES ($1, $2, $3) + RETURNING + developer_id, reason, admin_id, created_at", + dev_id, reason, admin_id + ) + .fetch_one(&mut *conn) + .await + .inspect_err(|e| log::error!("Failed to insert create developer ban: {e}")) + .map_err(|e| e.into()) +} + +pub async fn check_ban( + dev_id: i32, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query_as!(DeveloperBan, + "SELECT developer_id, reason, admin_id, created_at FROM bans WHERE developer_id=$1", dev_id + ) + .fetch_optional(&mut *conn) + .await + .inspect_err(|e| log::error!("Failed to get developer ban: {e}")) + .map_err(|e| e.into()) +} + +pub async fn delete_ban(dev_id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { + sqlx::query!("DELETE FROM bans WHERE developer_id = $1", dev_id) + .execute(conn) + .await + .inspect_err(|e| log::error!("Failed to delete developer ban: {e}"))?; + + Ok(()) +} diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index da80cabb..2ad67fb9 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -10,7 +10,7 @@ use crate::{ extractors::auth::Auth, types::{ models::{ - developer::{ModDeveloper, Developer}, + developer::{ModDeveloper, Developer, DeveloperBan}, mod_entity::Mod, mod_version_status::ModVersionStatusEnum, }, @@ -70,6 +70,11 @@ struct DeveloperIndexQuery { per_page: Option, } +#[derive(Deserialize, ToSchema)] +struct DeveloperBanPayload { + reason: Option, +} + /// List all developers with optional search and pagination #[utoipa::path( get, @@ -470,3 +475,148 @@ pub async fn update_developer( payload: result, })) } + + +#[derive(Deserialize, IntoParams)] +struct CreateDeveloperBanPath { + id: i32, +} + +/// Ban a developer from mod submissions (admin only) +#[utoipa::path( + post, + path = "/v1/developers/{id}/ban", + tag = "developers", + params(CreateDeveloperBanPath), + request_body = DeveloperBanPayload, + responses( + (status = 200, description = "Developer banned", body = inline(ApiResponse)), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden - Admin only"), + (status = 404, description = "Developer not found") + ), + security( + ("bearer_token" = []) + ) +)] +#[post("v1/developers/ban")] +pub async fn ban_developer( + auth: Auth, + data: web::Data, + path: web::Path, + payload: web::Json, +) -> Result { + let dev = auth.developer()?; + auth.check_admin()?; + + let mut pool = data.db().acquire().await?; + + // check dev exists (we don't need the result) + developers::get_one(path.id, &mut pool) + .await? + .ok_or(ApiError::NotFound("Developer not found".into()))?; + + // check ban exists + if let None = developers::check_ban(path.id, &mut pool).await? { + return Err(ApiError::BadRequest("This developer is already banned".into())); + } + + let result = developers::create_ban( + path.id, + dev.id, + payload.reason.as_deref(), + &mut pool, + ) + .await?; + + Ok(web::Json(ApiResponse { + error: "".to_string(), + payload: result, + })) +} + +#[derive(Deserialize, IntoParams)] +struct DeleteDeveloperBanPath { + id: i32, +} + +/// Remove a developer ban (admin only) +#[utoipa::path( + delete, + path = "/v1/developers/{id}/ban", + tag = "developers", + params(DeleteDeveloperBanPath), + responses( + (status = 204, description = "Ban deleted"), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden - Admin only"), + ), + security( + ("bearer_token" = []) + ) +)] +#[delete("/v1/developers/{id}/ban")] +pub async fn unban_developer( + auth: Auth, + data: web::Data, + path: web::Path, +) -> Result { + auth.check_admin()?; + + let mut pool = data.db().acquire().await?; + + developers::delete_ban( + path.id, + &mut pool, + ) + .await?; + + Ok(HttpResponse::NoContent()) +} + +#[derive(Deserialize, IntoParams)] +struct GetDeveloperBanPath { + id: i32, +} + +/// Check if a developer is banned (admin only) +#[utoipa::path( + get, + path = "/v1/developers/{id}/ban", + tag = "developers", + params(GetDeveloperBanPath), + responses( + (status = 200, description = "Ban object", body = inline(ApiResponse)), + (status = 400, description = "Bad request"), + (status = 401, description = "Unauthorized"), + (status = 403, description = "Forbidden - Admin only"), + (status = 404, description = "Ban not found") + ), + security( + ("bearer_token" = []) + ) +)] +#[get("/v1/developers/{id}/ban")] +pub async fn get_developer_ban( + auth: Auth, + data: web::Data, + path: web::Path, +) -> Result { + auth.check_admin()?; + + let mut pool = data.db().acquire().await?; + + let result = developers::check_ban( + path.id, + &mut pool, + ) + .await? + .ok_or(ApiError::NotFound("Ban was not found".into()))?; + + Ok(web::Json(ApiResponse { + error: "".to_string(), + payload: result, + })) +} diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index b519f9ed..551f40bf 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -43,6 +43,8 @@ pub enum ApiError { Zip(#[from] zip::result::ZipError), #[error("Failed to contact external resource: {0}")] Reqwest(#[from] reqwest::Error), + #[error("You are banned from accessing this resource: {}", .0.as_deref().unwrap_or("No reason provided"))] + Banned(Option), } impl ApiError { diff --git a/src/main.rs b/src/main.rs index d1bff29f..81bfda6c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -93,6 +93,9 @@ async fn main() -> anyhow::Result<()> { .service(endpoints::developers::get_own_mods) .service(endpoints::developers::get_me) .service(endpoints::developers::update_developer) + .service(endpoints::developers::ban_developer) + .service(endpoints::developers::unban_developer) + .service(endpoints::developers::get_developer_ban) .service(endpoints::tags::index) .service(endpoints::tags::detailed_index) .service(endpoints::stats::get_stats) diff --git a/src/openapi.rs b/src/openapi.rs index 11e824f3..7aad85a3 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -31,6 +31,9 @@ use crate::{endpoints, types}; endpoints::developers::get_own_mods, endpoints::developers::get_me, endpoints::developers::update_developer, + endpoints::developers::ban_developer, + endpoints::developers::unban_developer, + endpoints::developers::get_developer_ban, endpoints::tags::index, endpoints::tags::detailed_index, endpoints::stats::get_stats, diff --git a/src/types/models/developer.rs b/src/types/models/developer.rs index 29ac26af..64084f40 100644 --- a/src/types/models/developer.rs +++ b/src/types/models/developer.rs @@ -1,3 +1,4 @@ +use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; @@ -18,3 +19,11 @@ pub struct Developer { pub admin: bool, pub github_id: i64, } + +#[derive(sqlx::FromRow, Serialize, Clone, Debug, ToSchema)] +pub struct DeveloperBan { + pub developer_id: i32, + pub reason: Option, + pub admin_id: Option, + pub created_at: DateTime, +} From db9ea8e7ae86c97705f8814ea1a791bc2760ed06 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:38:27 -0700 Subject: [PATCH 2/8] check ban when uploading mods --- src/endpoints/mod_versions.rs | 4 ++++ src/endpoints/mods.rs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/endpoints/mod_versions.rs b/src/endpoints/mod_versions.rs index 487fcc4e..8d042188 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -300,6 +300,10 @@ pub async fn create_version( let dev = auth.developer()?; let mut pool = data.db().acquire().await?; + if let Some(ban) = developers::check_ban(dev.id, &mut pool).await? { + return Err(ApiError::Banned(ban.reason)); + } + let id = path.into_inner(); let the_mod = mods::get_one(&id, false, &mut pool) diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index 5e579fd3..c3fd3592 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -208,6 +208,11 @@ pub async fn create( ) -> Result { let dev = auth.developer()?; let mut pool = data.db().acquire().await?; + + if let Some(ban) = developers::check_ban(dev.id, &mut pool).await? { + return Err(ApiError::Banned(ban.reason)); + } + let bytes = mod_zip::download_mod(&payload.download_link, data.max_download_mb()).await?; let json = ModJson::from_zip(bytes, &payload.download_link, false)?; json.validate()?; From 7cc2ff1b3db46b31f802a6e9f917926c92639890 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:39:41 -0700 Subject: [PATCH 3/8] disallow banned developers from editing their mod developers --- src/endpoints/developers.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index 2ad67fb9..df66a25a 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -131,6 +131,10 @@ pub async fn add_developer_to_mod( let dev = auth.developer()?; let mut pool = data.db().acquire().await?; + if let Some(ban) = developers::check_ban(dev.id, &mut pool).await? { + return Err(ApiError::Banned(ban.reason)); + } + if !mods::exists(&path.id, &mut pool).await? { return Err(ApiError::NotFound(format!("Mod id {} not found", path.id))); } @@ -145,6 +149,10 @@ pub async fn add_developer_to_mod( json.username )))?; + if let Some(ban) = developers::check_ban(target.id, &mut pool).await? { + return Err(ApiError::Banned(ban.reason)); + } + mods::assign_developer(&path.id, target.id, false, &mut pool).await?; Ok(HttpResponse::NoContent()) From 93a3678dd6dc8260d15161bfc74645215654d3c0 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:40:18 -0700 Subject: [PATCH 4/8] perform prepare --- ...56107d8dd1b534606af6d7e2e58b3d49e4e05.json | 42 +++++++++++++++++++ ...27bd755d270c20e8c4372b2232caa87db596b.json | 14 +++++++ ...19a2504652f51dc4bce82029831fddbb3f9d2.json | 40 ++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 .sqlx/query-6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05.json create mode 100644 .sqlx/query-bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b.json create mode 100644 .sqlx/query-e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2.json diff --git a/.sqlx/query-6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05.json b/.sqlx/query-6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05.json new file mode 100644 index 00000000..2a484b7c --- /dev/null +++ b/.sqlx/query-6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05.json @@ -0,0 +1,42 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO bans (developer_id, reason, admin_id)\n VALUES ($1, $2, $3)\n RETURNING\n developer_id, reason, admin_id, created_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "developer_id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "reason", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "admin_id", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int4", + "Text", + "Int4" + ] + }, + "nullable": [ + false, + true, + true, + false + ] + }, + "hash": "6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05" +} diff --git a/.sqlx/query-bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b.json b/.sqlx/query-bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b.json new file mode 100644 index 00000000..45bac3a5 --- /dev/null +++ b/.sqlx/query-bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "DELETE FROM bans WHERE developer_id = $1", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [] + }, + "hash": "bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b" +} diff --git a/.sqlx/query-e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2.json b/.sqlx/query-e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2.json new file mode 100644 index 00000000..f6687699 --- /dev/null +++ b/.sqlx/query-e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT developer_id, reason, admin_id, created_at FROM bans WHERE developer_id=$1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "developer_id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "reason", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "admin_id", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + true, + true, + false + ] + }, + "hash": "e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2" +} From 42de228d7053126d1c8b75fe26da34a2d0c46241 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:50:03 -0700 Subject: [PATCH 5/8] remove leftover --- migrations/20260418000114_add_index_ban.down.sql | 1 - migrations/20260418000114_add_index_ban.up.sql | 2 -- 2 files changed, 3 deletions(-) diff --git a/migrations/20260418000114_add_index_ban.down.sql b/migrations/20260418000114_add_index_ban.down.sql index 881bc5dc..72623cb3 100644 --- a/migrations/20260418000114_add_index_ban.down.sql +++ b/migrations/20260418000114_add_index_ban.down.sql @@ -1,3 +1,2 @@ -- Add down migration script here -ALTER TABLE developers DROP COLUMN IF EXISTS note; DROP TABLE IF EXISTS bans; \ No newline at end of file diff --git a/migrations/20260418000114_add_index_ban.up.sql b/migrations/20260418000114_add_index_ban.up.sql index 1ab2f5cc..6a55c76e 100644 --- a/migrations/20260418000114_add_index_ban.up.sql +++ b/migrations/20260418000114_add_index_ban.up.sql @@ -1,6 +1,4 @@ -- Add up migration script here -ALTER TABLE developers ADD COLUMN note TEXT; - CREATE TABLE bans ( developer_id INTEGER PRIMARY KEY NOT NULL REFERENCES developers(id) ON DELETE CASCADE, reason TEXT, From b8bae188d07e0643be73048d316dba125d4221a2 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:55:58 -0700 Subject: [PATCH 6/8] hide other user ban reason when adding developer to mod --- src/endpoints/developers.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index df66a25a..100dff58 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -149,8 +149,8 @@ pub async fn add_developer_to_mod( json.username )))?; - if let Some(ban) = developers::check_ban(target.id, &mut pool).await? { - return Err(ApiError::Banned(ban.reason)); + if let Some(_) = developers::check_ban(target.id, &mut pool).await? { + return Err(ApiError::Banned(Some("The developer being added is banned".into()))); } mods::assign_developer(&path.id, target.id, false, &mut pool).await?; From 66cb5a07bbd4e7849d43ff7b013fa7cd6199fbfa Mon Sep 17 00:00:00 2001 From: Chloe Date: Sun, 24 May 2026 21:05:31 -0700 Subject: [PATCH 7/8] update bans to support revocation --- ...784c9d2182f66d79f7abbd318a7dc75b9476.json} | 20 +++++-- ...f58e210ef0b240b6791e4bf31b2e591bff95c.json | 55 +++++++++++++++++++ ...19a2504652f51dc4bce82029831fddbb3f9d2.json | 40 -------------- ...8c05ceda6bf3c590b473a0e1a021905ba569.json} | 4 +- .../20260418000114_add_index_ban.up.sql | 6 +- src/database/repository/developers.rs | 22 +++++--- src/endpoints/developers.rs | 3 + src/types/models/developer.rs | 2 + 8 files changed, 94 insertions(+), 58 deletions(-) rename .sqlx/{query-6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05.json => query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json} (53%) create mode 100644 .sqlx/query-83d6e2d9f2e9cedbc89048baae3f58e210ef0b240b6791e4bf31b2e591bff95c.json delete mode 100644 .sqlx/query-e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2.json rename .sqlx/{query-bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b.json => query-f6e4bad631ddc06e2b5a3f951e668c05ceda6bf3c590b473a0e1a021905ba569.json} (53%) diff --git a/.sqlx/query-6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05.json b/.sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json similarity index 53% rename from .sqlx/query-6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05.json rename to .sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json index 2a484b7c..82d20ff2 100644 --- a/.sqlx/query-6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05.json +++ b/.sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO bans (developer_id, reason, admin_id)\n VALUES ($1, $2, $3)\n RETURNING\n developer_id, reason, admin_id, created_at", + "query": "SELECT developer_id, reason, admin_id, created_at, id, revoked_at FROM bans WHERE developer_id=$1 AND revoked_at > NOW() or revoked_at IS NULL ORDER BY revoked_at DESC NULLS FIRST LIMIT 1", "describe": { "columns": [ { @@ -22,12 +22,20 @@ "ordinal": 3, "name": "created_at", "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "revoked_at", + "type_info": "Timestamptz" } ], "parameters": { "Left": [ - "Int4", - "Text", "Int4" ] }, @@ -35,8 +43,10 @@ false, true, true, - false + false, + false, + true ] }, - "hash": "6f3aa0d7e709dccacaf9e96c45356107d8dd1b534606af6d7e2e58b3d49e4e05" + "hash": "4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476" } diff --git a/.sqlx/query-83d6e2d9f2e9cedbc89048baae3f58e210ef0b240b6791e4bf31b2e591bff95c.json b/.sqlx/query-83d6e2d9f2e9cedbc89048baae3f58e210ef0b240b6791e4bf31b2e591bff95c.json new file mode 100644 index 00000000..51268a56 --- /dev/null +++ b/.sqlx/query-83d6e2d9f2e9cedbc89048baae3f58e210ef0b240b6791e4bf31b2e591bff95c.json @@ -0,0 +1,55 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO bans (developer_id, reason, admin_id, revoked_at)\n VALUES ($1, $2, $3, $4)\n RETURNING\n id, developer_id, reason, admin_id, created_at, revoked_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "developer_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "reason", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "admin_id", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "revoked_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int4", + "Text", + "Int4", + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + true + ] + }, + "hash": "83d6e2d9f2e9cedbc89048baae3f58e210ef0b240b6791e4bf31b2e591bff95c" +} diff --git a/.sqlx/query-e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2.json b/.sqlx/query-e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2.json deleted file mode 100644 index f6687699..00000000 --- a/.sqlx/query-e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT developer_id, reason, admin_id, created_at FROM bans WHERE developer_id=$1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "developer_id", - "type_info": "Int4" - }, - { - "ordinal": 1, - "name": "reason", - "type_info": "Text" - }, - { - "ordinal": 2, - "name": "admin_id", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "created_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int4" - ] - }, - "nullable": [ - false, - true, - true, - false - ] - }, - "hash": "e6eb166e1e17a20fca226f01c5719a2504652f51dc4bce82029831fddbb3f9d2" -} diff --git a/.sqlx/query-bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b.json b/.sqlx/query-f6e4bad631ddc06e2b5a3f951e668c05ceda6bf3c590b473a0e1a021905ba569.json similarity index 53% rename from .sqlx/query-bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b.json rename to .sqlx/query-f6e4bad631ddc06e2b5a3f951e668c05ceda6bf3c590b473a0e1a021905ba569.json index 45bac3a5..2be4d746 100644 --- a/.sqlx/query-bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b.json +++ b/.sqlx/query-f6e4bad631ddc06e2b5a3f951e668c05ceda6bf3c590b473a0e1a021905ba569.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "DELETE FROM bans WHERE developer_id = $1", + "query": "UPDATE bans SET revoked_at=NOW() WHERE id=$1", "describe": { "columns": [], "parameters": { @@ -10,5 +10,5 @@ }, "nullable": [] }, - "hash": "bc6a0417daca187acff85f7b2a327bd755d270c20e8c4372b2232caa87db596b" + "hash": "f6e4bad631ddc06e2b5a3f951e668c05ceda6bf3c590b473a0e1a021905ba569" } diff --git a/migrations/20260418000114_add_index_ban.up.sql b/migrations/20260418000114_add_index_ban.up.sql index 6a55c76e..9d5ae071 100644 --- a/migrations/20260418000114_add_index_ban.up.sql +++ b/migrations/20260418000114_add_index_ban.up.sql @@ -1,7 +1,9 @@ -- Add up migration script here CREATE TABLE bans ( - developer_id INTEGER PRIMARY KEY NOT NULL REFERENCES developers(id) ON DELETE CASCADE, + id SERIAL PRIMARY KEY NOT NULL, + developer_id INTEGER NOT NULL REFERENCES developers(id) ON DELETE CASCADE, reason TEXT, admin_id INTEGER REFERENCES developers(id) ON DELETE SET NULL, - created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL + created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, + revoked_at TIMESTAMPTZ ); diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index 4d534f0b..91ff4687 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -2,6 +2,7 @@ use crate::database::DatabaseError; use crate::types::api::PaginatedData; use crate::types::models::developer::{Developer, DeveloperBan, ModDeveloper}; use sqlx::PgConnection; +use sqlx::types::chrono::{DateTime, Utc}; use std::collections::HashMap; use uuid::Uuid; @@ -432,14 +433,15 @@ pub async fn create_ban( dev_id: i32, admin_id: i32, reason: Option<&str>, + revoked_at: Option>, conn: &mut PgConnection, ) -> Result { sqlx::query_as!(DeveloperBan, - "INSERT INTO bans (developer_id, reason, admin_id) - VALUES ($1, $2, $3) + "INSERT INTO bans (developer_id, reason, admin_id, revoked_at) + VALUES ($1, $2, $3, $4) RETURNING - developer_id, reason, admin_id, created_at", - dev_id, reason, admin_id + id, developer_id, reason, admin_id, created_at, revoked_at", + dev_id, reason, admin_id, revoked_at ) .fetch_one(&mut *conn) .await @@ -452,7 +454,7 @@ pub async fn check_ban( conn: &mut PgConnection, ) -> Result, DatabaseError> { sqlx::query_as!(DeveloperBan, - "SELECT developer_id, reason, admin_id, created_at FROM bans WHERE developer_id=$1", dev_id + "SELECT developer_id, reason, admin_id, created_at, id, revoked_at FROM bans WHERE developer_id=$1 AND revoked_at > NOW() or revoked_at IS NULL ORDER BY revoked_at DESC NULLS FIRST LIMIT 1", dev_id ) .fetch_optional(&mut *conn) .await @@ -461,10 +463,12 @@ pub async fn check_ban( } pub async fn delete_ban(dev_id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { - sqlx::query!("DELETE FROM bans WHERE developer_id = $1", dev_id) - .execute(conn) - .await - .inspect_err(|e| log::error!("Failed to delete developer ban: {e}"))?; + if let Some(current_ban) = check_ban(dev_id, conn).await? { + sqlx::query!("UPDATE bans SET revoked_at=NOW() WHERE id=$1", current_ban.id) + .execute(conn) + .await + .inspect_err(|e| log::error!("Failed to revoke developer ban: {e}"))?; + } Ok(()) } diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index 100dff58..1f4ecb4e 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -1,6 +1,7 @@ use actix_web::{delete, get, post, put, web, HttpResponse, Responder}; use serde::{Deserialize, Serialize}; use utoipa::{ToSchema, IntoParams}; +use chrono::{DateTime, Utc}; use super::ApiError; use crate::config::AppData; @@ -73,6 +74,7 @@ struct DeveloperIndexQuery { #[derive(Deserialize, ToSchema)] struct DeveloperBanPayload { reason: Option, + revoked_at: Option>, } /// List all developers with optional search and pagination @@ -534,6 +536,7 @@ pub async fn ban_developer( path.id, dev.id, payload.reason.as_deref(), + payload.revoked_at, &mut pool, ) .await?; diff --git a/src/types/models/developer.rs b/src/types/models/developer.rs index 64084f40..da814d54 100644 --- a/src/types/models/developer.rs +++ b/src/types/models/developer.rs @@ -22,8 +22,10 @@ pub struct Developer { #[derive(sqlx::FromRow, Serialize, Clone, Debug, ToSchema)] pub struct DeveloperBan { + pub id: i32, pub developer_id: i32, pub reason: Option, pub admin_id: Option, pub created_at: DateTime, + pub revoked_at: Option>, } From 11582b7c1f003cc30f6f765484afa00abf369448 Mon Sep 17 00:00:00 2001 From: Chloe Date: Sun, 24 May 2026 21:13:57 -0700 Subject: [PATCH 8/8] have reposting a ban update the most recent one --- ...56e84e0884307abe6f9e4c6425f6173f93616.json | 53 +++++++++++++++++++ src/database/repository/developers.rs | 13 +++++ src/endpoints/developers.rs | 11 +++- 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 .sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json diff --git a/.sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json b/.sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json new file mode 100644 index 00000000..a9b89a60 --- /dev/null +++ b/.sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json @@ -0,0 +1,53 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE bans\n SET revoked_at=$2 WHERE id=$1\n RETURNING\n id, developer_id, reason, admin_id, created_at, revoked_at", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int4" + }, + { + "ordinal": 1, + "name": "developer_id", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "reason", + "type_info": "Text" + }, + { + "ordinal": 3, + "name": "admin_id", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 5, + "name": "revoked_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int4", + "Timestamptz" + ] + }, + "nullable": [ + false, + false, + true, + true, + false, + true + ] + }, + "hash": "a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616" +} diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index 91ff4687..5e37677e 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -462,6 +462,19 @@ pub async fn check_ban( .map_err(|e| e.into()) } +pub async fn update_ban_revoke_time(ban_id: i32, revoked_at: Option>, conn: &mut PgConnection) -> Result, DatabaseError> { + sqlx::query_as!(DeveloperBan, + "UPDATE bans + SET revoked_at=$2 WHERE id=$1 + RETURNING + id, developer_id, reason, admin_id, created_at, revoked_at", + ban_id, revoked_at) + .fetch_optional(&mut *conn) + .await + .inspect_err(|e| log::error!("Failed to update developer ban: {e}")) + .map_err(|e| e.into()) +} + pub async fn delete_ban(dev_id: i32, conn: &mut PgConnection) -> Result<(), DatabaseError> { if let Some(current_ban) = check_ban(dev_id, conn).await? { sqlx::query!("UPDATE bans SET revoked_at=NOW() WHERE id=$1", current_ban.id) diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index 1f4ecb4e..74149ecf 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -528,8 +528,15 @@ pub async fn ban_developer( .ok_or(ApiError::NotFound("Developer not found".into()))?; // check ban exists - if let None = developers::check_ban(path.id, &mut pool).await? { - return Err(ApiError::BadRequest("This developer is already banned".into())); + if let Some(ban) = developers::check_ban(path.id, &mut pool).await? { + let result = developers::update_ban_revoke_time(ban.id, payload.revoked_at, &mut pool) + .await? + .ok_or(ApiError::InternalError("Ban was deleted between asserting its existence and updating it".into()))?; + + return Ok(web::Json(ApiResponse { + error: "".to_string(), + payload: result, + })) } let result = developers::create_ban(