From 63c001ca964827a9258321c668339fd8b4a1c672 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:32:54 -0700 Subject: [PATCH 01/12] 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 0000000..881bc5d --- /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 0000000..1ab2f5c --- /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 70baaf8..e3a6a03 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; @@ -493,3 +493,44 @@ pub async fn has_accepted_mod(id: i32, conn: &mut PgConnection) -> Result, + 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 670fc8b..f46d683 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -10,7 +10,7 @@ use crate::types::models::developer::SelfDeveloper; use crate::{ extractors::auth::Auth, types::models::{ - developer::{Developer, ModDeveloper}, + developer::{Developer, ModDeveloper, DeveloperBan}, mod_entity::Mod, mod_version_status::ModVersionStatusEnum, }, @@ -69,6 +69,11 @@ struct DeveloperIndexQuery { per_page: Option, } +#[derive(Deserialize, ToSchema)] +struct DeveloperBanPayload { + reason: Option, +} + /// List all developers with optional search and pagination #[utoipa::path( get, @@ -483,3 +488,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 f020bc0..15e566f 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -49,6 +49,8 @@ pub enum ApiError { Reqwest(#[from] reqwest::Error), #[error("I/O error: {0}")] IO(#[from] std::io::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 96d8d64..d3047ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -125,6 +125,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 4691b49..18adb92 100644 --- a/src/openapi.rs +++ b/src/openapi.rs @@ -40,6 +40,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 4190e4f..14666d4 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; @@ -43,3 +44,11 @@ impl Developer { } } } + +#[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 1e78e1a0dfdedef7ffafa21324e349b732a47615 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:38:27 -0700 Subject: [PATCH 02/12] 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 3d48b99..36423f5 100644 --- a/src/endpoints/mod_versions.rs +++ b/src/endpoints/mod_versions.rs @@ -313,6 +313,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 mut the_mod = mods::get_one(&id, false, &mut pool) diff --git a/src/endpoints/mods.rs b/src/endpoints/mods.rs index b6c622b..a232f75 100644 --- a/src/endpoints/mods.rs +++ b/src/endpoints/mods.rs @@ -217,6 +217,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( data.check_dns_http_client(), &payload.download_link, From 9116f9c1441b32101077c43d418e108771fd349b Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:39:41 -0700 Subject: [PATCH 03/12] 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 f46d683..fc598b7 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -132,6 +132,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))); } @@ -146,6 +150,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 1e36c6c7b6bdd1c70888f1a95c74740411abc229 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:40:18 -0700 Subject: [PATCH 04/12] 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 0000000..2a484b7 --- /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 0000000..45bac3a --- /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 0000000..f668769 --- /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 2b2ed6aaab7dca5607aff399a923b40a391c865f Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:50:03 -0700 Subject: [PATCH 05/12] 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 881bc5d..72623cb 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 1ab2f5c..6a55c76 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 667dfd7b4a2c31c6220ad1df2c1e528382a1e559 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 17 Apr 2026 18:55:58 -0700 Subject: [PATCH 06/12] 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 fc598b7..f6d886a 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -150,8 +150,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 b861e179ff8fa742b156ed0eadbb3d7482ef0199 Mon Sep 17 00:00:00 2001 From: Chloe Date: Sun, 24 May 2026 21:05:31 -0700 Subject: [PATCH 07/12] 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 2a484b7..82d20ff 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 0000000..51268a5 --- /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 f668769..0000000 --- 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 45bac3a..2be4d74 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 6a55c76..9d5ae07 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 e3a6a03..a619211 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; @@ -498,14 +499,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 @@ -518,7 +520,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 @@ -527,10 +529,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 f6d886a..3bce28f 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -1,6 +1,7 @@ use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; +use chrono::{DateTime, Utc}; use super::ApiError; use crate::config::AppData; @@ -72,6 +73,7 @@ struct DeveloperIndexQuery { #[derive(Deserialize, ToSchema)] struct DeveloperBanPayload { reason: Option, + revoked_at: Option>, } /// List all developers with optional search and pagination @@ -547,6 +549,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 14666d4..2b34593 100644 --- a/src/types/models/developer.rs +++ b/src/types/models/developer.rs @@ -47,8 +47,10 @@ impl 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 18425c71a0b2ddabde81daa9f2cc5e7f1dc0f746 Mon Sep 17 00:00:00 2001 From: Chloe Date: Sun, 24 May 2026 21:13:57 -0700 Subject: [PATCH 08/12] 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 0000000..a9b89a6 --- /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 a619211..0bd70f8 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -528,6 +528,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 3bce28f..00bc27e 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -541,8 +541,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( From 3f5411ff3a4faa4c68b2fa21e9227f30fa012925 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 31 Jul 2026 02:48:55 -0700 Subject: [PATCH 09/12] update api to support multiple bans --- ... => 20260731085957_add_index_ban.down.sql} | 0 ...ql => 20260731085957_add_index_ban.up.sql} | 3 ++ src/database/repository/developers.rs | 21 +++++++++-- src/endpoints/developers.rs | 37 ++++++++++--------- src/endpoints/mod_version_submissions.rs | 4 ++ 5 files changed, 44 insertions(+), 21 deletions(-) rename migrations/{20260418000114_add_index_ban.down.sql => 20260731085957_add_index_ban.down.sql} (100%) rename migrations/{20260418000114_add_index_ban.up.sql => 20260731085957_add_index_ban.up.sql} (74%) diff --git a/migrations/20260418000114_add_index_ban.down.sql b/migrations/20260731085957_add_index_ban.down.sql similarity index 100% rename from migrations/20260418000114_add_index_ban.down.sql rename to migrations/20260731085957_add_index_ban.down.sql diff --git a/migrations/20260418000114_add_index_ban.up.sql b/migrations/20260731085957_add_index_ban.up.sql similarity index 74% rename from migrations/20260418000114_add_index_ban.up.sql rename to migrations/20260731085957_add_index_ban.up.sql index 9d5ae07..bbe0a4e 100644 --- a/migrations/20260418000114_add_index_ban.up.sql +++ b/migrations/20260731085957_add_index_ban.up.sql @@ -7,3 +7,6 @@ CREATE TABLE bans ( created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP NOT NULL, revoked_at TIMESTAMPTZ ); + +CREATE INDEX bans_revoked_at_idx on bans(revoked_at); +CREATE INDEX bans_developer_id_idx on bans(developer_id); diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index 0bd70f8..c39150a 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -511,7 +511,7 @@ pub async fn create_ban( ) .fetch_one(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to insert create developer ban: {e}")) + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -524,7 +524,20 @@ pub async fn check_ban( ) .fetch_optional(&mut *conn) .await - .inspect_err(|e| log::error!("Failed to get developer ban: {e}")) + .inspect_err(|e| tracing::error!("{:?}", e)) + .map_err(|e| e.into()) +} + +pub async fn get_bans( + dev_id: i32, + conn: &mut PgConnection, +) -> Result, DatabaseError> { + sqlx::query_as!(DeveloperBan, + "SELECT developer_id, reason, admin_id, created_at, id, revoked_at FROM bans WHERE developer_id=$1 ORDER BY revoked_at DESC NULLS FIRST, id DESC", dev_id + ) + .fetch_all(&mut *conn) + .await + .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) } @@ -537,7 +550,7 @@ pub async fn update_ban_revoke_time(ban_id: i32, revoked_at: Option Result<(), Data 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}"))?; + .inspect_err(|e| tracing::error!("{:?}", e))?; } Ok(()) diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index 00bc27e..49a5422 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -1,5 +1,6 @@ use actix_web::{HttpResponse, Responder, delete, get, post, put, web}; use serde::{Deserialize, Serialize}; +use sqlx::Acquire; use utoipa::{IntoParams, ToSchema}; use chrono::{DateTime, Utc}; @@ -508,7 +509,7 @@ struct CreateDeveloperBanPath { /// Ban a developer from mod submissions (admin only) #[utoipa::path( post, - path = "/v1/developers/{id}/ban", + path = "/v1/developers/{id}/bans", tag = "developers", params(CreateDeveloperBanPath), request_body = DeveloperBanPayload, @@ -523,7 +524,7 @@ struct CreateDeveloperBanPath { ("bearer_token" = []) ) )] -#[post("v1/developers/ban")] +#[post("/v1/developers/{id}/bans")] pub async fn ban_developer( auth: Auth, data: web::Data, @@ -540,9 +541,11 @@ pub async fn ban_developer( .await? .ok_or(ApiError::NotFound("Developer not found".into()))?; + let mut tx = pool.begin().await?; + // check ban exists - 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) + if let Some(ban) = developers::check_ban(path.id, &mut tx).await? { + let result = developers::update_ban_revoke_time(ban.id, payload.revoked_at, &mut tx) .await? .ok_or(ApiError::InternalError("Ban was deleted between asserting its existence and updating it".into()))?; @@ -557,10 +560,12 @@ pub async fn ban_developer( dev.id, payload.reason.as_deref(), payload.revoked_at, - &mut pool, + &mut tx, ) .await?; + tx.commit().await?; + Ok(web::Json(ApiResponse { error: "".to_string(), payload: result, @@ -572,10 +577,10 @@ struct DeleteDeveloperBanPath { id: i32, } -/// Remove a developer ban (admin only) +/// Revoke a developer's current ban (admin only) #[utoipa::path( delete, - path = "/v1/developers/{id}/ban", + path = "/v1/developers/{id}/bans", tag = "developers", params(DeleteDeveloperBanPath), responses( @@ -588,7 +593,7 @@ struct DeleteDeveloperBanPath { ("bearer_token" = []) ) )] -#[delete("/v1/developers/{id}/ban")] +#[delete("/v1/developers/{id}/bans")] pub async fn unban_developer( auth: Auth, data: web::Data, @@ -612,14 +617,14 @@ struct GetDeveloperBanPath { id: i32, } -/// Check if a developer is banned (admin only) +/// Fetch a list of a developer's bans (admin only) #[utoipa::path( get, - path = "/v1/developers/{id}/ban", + path = "/v1/developers/{id}/bans", tag = "developers", params(GetDeveloperBanPath), responses( - (status = 200, description = "Ban object", body = inline(ApiResponse)), + (status = 200, description = "Ban object", body = inline(ApiResponse>)), (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized"), (status = 403, description = "Forbidden - Admin only"), @@ -629,22 +634,20 @@ struct GetDeveloperBanPath { ("bearer_token" = []) ) )] -#[get("/v1/developers/{id}/ban")] +#[get("/v1/developers/{id}/bans")] pub async fn get_developer_ban( auth: Auth, data: web::Data, - path: web::Path, + path: web::Path, ) -> Result { auth.check_admin()?; let mut pool = data.db().acquire().await?; - let result = developers::check_ban( + let result = developers::get_bans( path.id, &mut pool, - ) - .await? - .ok_or(ApiError::NotFound("Ban was not found".into()))?; + ).await?; Ok(web::Json(ApiResponse { error: "".to_string(), diff --git a/src/endpoints/mod_version_submissions.rs b/src/endpoints/mod_version_submissions.rs index 9b1954e..d9fb700 100644 --- a/src/endpoints/mod_version_submissions.rs +++ b/src/endpoints/mod_version_submissions.rs @@ -80,6 +80,10 @@ async fn check_submission_lock( return Ok(true); } + if developers::check_ban(dev.id, &mut *conn).await?.is_some() { + 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?; From a2a7bcf6a04ebfac50fcb3f8577210f3fc4f3fef Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 31 Jul 2026 02:50:37 -0700 Subject: [PATCH 10/12] i question if this merges well --- ...6784c9d2182f66d79f7abbd318a7dc75b9476.json | 48 ++++++++-- ...f58e210ef0b240b6791e4bf31b2e591bff95c.json | 48 ++++++++-- ...56e84e0884307abe6f9e4c6425f6173f93616.json | 48 ++++++++-- ...e24026a7d06761212823a08cfd2450136c916.json | 88 +++++++++++++++++++ 4 files changed, 214 insertions(+), 18 deletions(-) create mode 100644 .sqlx/query-d170ffeaf2ea1d0deaef4510d63e24026a7d06761212823a08cfd2450136c916.json diff --git a/.sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json b/.sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json index 82d20ff..aeeb463 100644 --- a/.sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json +++ b/.sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json @@ -6,32 +6,68 @@ { "ordinal": 0, "name": "developer_id", - "type_info": "Int4" + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "developer_id" + } + } }, { "ordinal": 1, "name": "reason", - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "bans", + "name": "reason" + } + } }, { "ordinal": 2, "name": "admin_id", - "type_info": "Int4" + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "admin_id" + } + } }, { "ordinal": 3, "name": "created_at", - "type_info": "Timestamptz" + "type_info": "Timestamptz", + "origin": { + "Table": { + "table": "bans", + "name": "created_at" + } + } }, { "ordinal": 4, "name": "id", - "type_info": "Int4" + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "id" + } + } }, { "ordinal": 5, "name": "revoked_at", - "type_info": "Timestamptz" + "type_info": "Timestamptz", + "origin": { + "Table": { + "table": "bans", + "name": "revoked_at" + } + } } ], "parameters": { diff --git a/.sqlx/query-83d6e2d9f2e9cedbc89048baae3f58e210ef0b240b6791e4bf31b2e591bff95c.json b/.sqlx/query-83d6e2d9f2e9cedbc89048baae3f58e210ef0b240b6791e4bf31b2e591bff95c.json index 51268a5..893447f 100644 --- a/.sqlx/query-83d6e2d9f2e9cedbc89048baae3f58e210ef0b240b6791e4bf31b2e591bff95c.json +++ b/.sqlx/query-83d6e2d9f2e9cedbc89048baae3f58e210ef0b240b6791e4bf31b2e591bff95c.json @@ -6,32 +6,68 @@ { "ordinal": 0, "name": "id", - "type_info": "Int4" + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "id" + } + } }, { "ordinal": 1, "name": "developer_id", - "type_info": "Int4" + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "developer_id" + } + } }, { "ordinal": 2, "name": "reason", - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "bans", + "name": "reason" + } + } }, { "ordinal": 3, "name": "admin_id", - "type_info": "Int4" + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "admin_id" + } + } }, { "ordinal": 4, "name": "created_at", - "type_info": "Timestamptz" + "type_info": "Timestamptz", + "origin": { + "Table": { + "table": "bans", + "name": "created_at" + } + } }, { "ordinal": 5, "name": "revoked_at", - "type_info": "Timestamptz" + "type_info": "Timestamptz", + "origin": { + "Table": { + "table": "bans", + "name": "revoked_at" + } + } } ], "parameters": { diff --git a/.sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json b/.sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json index a9b89a6..437ff59 100644 --- a/.sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json +++ b/.sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json @@ -6,32 +6,68 @@ { "ordinal": 0, "name": "id", - "type_info": "Int4" + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "id" + } + } }, { "ordinal": 1, "name": "developer_id", - "type_info": "Int4" + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "developer_id" + } + } }, { "ordinal": 2, "name": "reason", - "type_info": "Text" + "type_info": "Text", + "origin": { + "Table": { + "table": "bans", + "name": "reason" + } + } }, { "ordinal": 3, "name": "admin_id", - "type_info": "Int4" + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "admin_id" + } + } }, { "ordinal": 4, "name": "created_at", - "type_info": "Timestamptz" + "type_info": "Timestamptz", + "origin": { + "Table": { + "table": "bans", + "name": "created_at" + } + } }, { "ordinal": 5, "name": "revoked_at", - "type_info": "Timestamptz" + "type_info": "Timestamptz", + "origin": { + "Table": { + "table": "bans", + "name": "revoked_at" + } + } } ], "parameters": { diff --git a/.sqlx/query-d170ffeaf2ea1d0deaef4510d63e24026a7d06761212823a08cfd2450136c916.json b/.sqlx/query-d170ffeaf2ea1d0deaef4510d63e24026a7d06761212823a08cfd2450136c916.json new file mode 100644 index 0000000..c121ee6 --- /dev/null +++ b/.sqlx/query-d170ffeaf2ea1d0deaef4510d63e24026a7d06761212823a08cfd2450136c916.json @@ -0,0 +1,88 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT developer_id, reason, admin_id, created_at, id, revoked_at FROM bans WHERE developer_id=$1 ORDER BY revoked_at DESC NULLS FIRST, id DESC", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "developer_id", + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "developer_id" + } + } + }, + { + "ordinal": 1, + "name": "reason", + "type_info": "Text", + "origin": { + "Table": { + "table": "bans", + "name": "reason" + } + } + }, + { + "ordinal": 2, + "name": "admin_id", + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "admin_id" + } + } + }, + { + "ordinal": 3, + "name": "created_at", + "type_info": "Timestamptz", + "origin": { + "Table": { + "table": "bans", + "name": "created_at" + } + } + }, + { + "ordinal": 4, + "name": "id", + "type_info": "Int4", + "origin": { + "Table": { + "table": "bans", + "name": "id" + } + } + }, + { + "ordinal": 5, + "name": "revoked_at", + "type_info": "Timestamptz", + "origin": { + "Table": { + "table": "bans", + "name": "revoked_at" + } + } + } + ], + "parameters": { + "Left": [ + "Int4" + ] + }, + "nullable": [ + false, + true, + true, + false, + false, + true + ] + }, + "hash": "d170ffeaf2ea1d0deaef4510d63e24026a7d06761212823a08cfd2450136c916" +} From e35958743e4e7975ea9e2a77483471eb3d501ca7 Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 31 Jul 2026 02:53:12 -0700 Subject: [PATCH 11/12] commit the transaction for ban creation --- src/endpoints/developers.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index 49a5422..b6c5715 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -549,6 +549,8 @@ pub async fn ban_developer( .await? .ok_or(ApiError::InternalError("Ban was deleted between asserting its existence and updating it".into()))?; + tx.commit().await?; + return Ok(web::Json(ApiResponse { error: "".to_string(), payload: result, From fc033a5cfb92c64503bcf43c4d0b7ee9b9c768ba Mon Sep 17 00:00:00 2001 From: Chloe Date: Fri, 31 Jul 2026 04:31:04 -0700 Subject: [PATCH 12/12] have post fully overwrite ban instead of just revoke time --- ...4dceac6934ec8f6b63a3e17aff00b1ec37aa7a6.json} | 4 ++-- ...aac2cc2a6b6b02ca6e89c171e971b49efed1baf.json} | 8 +++++--- src/database/repository/developers.rs | 16 +++++++++++----- src/endpoints/developers.rs | 12 +++++++++--- 4 files changed, 27 insertions(+), 13 deletions(-) rename .sqlx/{query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json => query-4fdaf48f6563fb3e2ceb6f0f14dceac6934ec8f6b63a3e17aff00b1ec37aa7a6.json} (91%) rename .sqlx/{query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json => query-9c66c5da81ded049266cc05ceaac2cc2a6b6b02ca6e89c171e971b49efed1baf.json} (83%) diff --git a/.sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json b/.sqlx/query-4fdaf48f6563fb3e2ceb6f0f14dceac6934ec8f6b63a3e17aff00b1ec37aa7a6.json similarity index 91% rename from .sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json rename to .sqlx/query-4fdaf48f6563fb3e2ceb6f0f14dceac6934ec8f6b63a3e17aff00b1ec37aa7a6.json index aeeb463..33fafe2 100644 --- a/.sqlx/query-4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476.json +++ b/.sqlx/query-4fdaf48f6563fb3e2ceb6f0f14dceac6934ec8f6b63a3e17aff00b1ec37aa7a6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "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", + "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, id DESC LIMIT 1", "describe": { "columns": [ { @@ -84,5 +84,5 @@ true ] }, - "hash": "4ce6aabb164fd260388ee0d4cab6784c9d2182f66d79f7abbd318a7dc75b9476" + "hash": "4fdaf48f6563fb3e2ceb6f0f14dceac6934ec8f6b63a3e17aff00b1ec37aa7a6" } diff --git a/.sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json b/.sqlx/query-9c66c5da81ded049266cc05ceaac2cc2a6b6b02ca6e89c171e971b49efed1baf.json similarity index 83% rename from .sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json rename to .sqlx/query-9c66c5da81ded049266cc05ceaac2cc2a6b6b02ca6e89c171e971b49efed1baf.json index 437ff59..5495109 100644 --- a/.sqlx/query-a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616.json +++ b/.sqlx/query-9c66c5da81ded049266cc05ceaac2cc2a6b6b02ca6e89c171e971b49efed1baf.json @@ -1,6 +1,6 @@ { "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", + "query": "UPDATE bans\n SET revoked_at=$2, reason=$3, admin_id=$4 WHERE id=$1\n RETURNING\n id, developer_id, reason, admin_id, created_at, revoked_at", "describe": { "columns": [ { @@ -73,7 +73,9 @@ "parameters": { "Left": [ "Int4", - "Timestamptz" + "Timestamptz", + "Text", + "Int4" ] }, "nullable": [ @@ -85,5 +87,5 @@ true ] }, - "hash": "a2a271aa89f4ddf8d5e2524276956e84e0884307abe6f9e4c6425f6173f93616" + "hash": "9c66c5da81ded049266cc05ceaac2cc2a6b6b02ca6e89c171e971b49efed1baf" } diff --git a/src/database/repository/developers.rs b/src/database/repository/developers.rs index c39150a..7d66627 100644 --- a/src/database/repository/developers.rs +++ b/src/database/repository/developers.rs @@ -520,7 +520,7 @@ pub async fn check_ban( conn: &mut PgConnection, ) -> Result, DatabaseError> { sqlx::query_as!(DeveloperBan, - "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 + "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, id DESC LIMIT 1", dev_id ) .fetch_optional(&mut *conn) .await @@ -541,14 +541,20 @@ pub async fn get_bans( .map_err(|e| e.into()) } -pub async fn update_ban_revoke_time(ban_id: i32, revoked_at: Option>, conn: &mut PgConnection) -> Result, DatabaseError> { +pub async fn update_ban( + ban_id: i32, + revoked_at: Option>, + reason: Option<&str>, + developer_id: i32, + conn: &mut PgConnection +) -> Result { sqlx::query_as!(DeveloperBan, "UPDATE bans - SET revoked_at=$2 WHERE id=$1 + SET revoked_at=$2, reason=$3, admin_id=$4 WHERE id=$1 RETURNING id, developer_id, reason, admin_id, created_at, revoked_at", - ban_id, revoked_at) - .fetch_optional(&mut *conn) + ban_id, revoked_at, reason, developer_id) + .fetch_one(&mut *conn) .await .inspect_err(|e| tracing::error!("{:?}", e)) .map_err(|e| e.into()) diff --git a/src/endpoints/developers.rs b/src/endpoints/developers.rs index b6c5715..24bf738 100644 --- a/src/endpoints/developers.rs +++ b/src/endpoints/developers.rs @@ -507,6 +507,8 @@ struct CreateDeveloperBanPath { } /// Ban a developer from mod submissions (admin only) +/// +/// If the developer is already banned, this will overwrite that ban. #[utoipa::path( post, path = "/v1/developers/{id}/bans", @@ -545,9 +547,13 @@ pub async fn ban_developer( // check ban exists if let Some(ban) = developers::check_ban(path.id, &mut tx).await? { - let result = developers::update_ban_revoke_time(ban.id, payload.revoked_at, &mut tx) - .await? - .ok_or(ApiError::InternalError("Ban was deleted between asserting its existence and updating it".into()))?; + let result = developers::update_ban( + ban.id, + payload.revoked_at, + payload.reason.as_deref(), + dev.id, + &mut tx + ).await?; tx.commit().await?;