From 4b99829fc5f3b816e42564b75554ed7c8d6ccaf0 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Mon, 8 Sep 2025 09:45:54 +0100 Subject: [PATCH 1/6] feat: start appeal impl --- anon/backend/migrations/0005__appeals.sql | 23 +++++ anon/backend/src/lib.rs | 1 + anon/backend/src/main.rs | 6 +- anon/backend/src/routes/appeals.rs | 101 ++++++++++++++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 anon/backend/migrations/0005__appeals.sql create mode 100644 anon/backend/src/routes/appeals.rs diff --git a/anon/backend/migrations/0005__appeals.sql b/anon/backend/migrations/0005__appeals.sql new file mode 100644 index 00000000..6a55085c --- /dev/null +++ b/anon/backend/migrations/0005__appeals.sql @@ -0,0 +1,23 @@ +CREATE TABLE IF NOT EXISTS appeals ( + id BIGSERIAL PRIMARY KEY, + + actor VARCHAR(20) NOT NULL CHECK (actor IN ('author', 'owner')), + + review_id BIGSERIAL NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, + + reason TEXT NOT NULL, + + status VARCHAR(20) NOT NULL CHECK (status IN ('open', 'accepted', 'rejected')) DEFAULT 'open', + + resolution_note TEXT, + + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_appeals_review_id ON appeals (review_id); + +CREATE INDEX idx_appeals_actor ON appeals (actor); + +CREATE INDEX idx_appeals_status ON appeals (status); + +CREATE INDEX idx_appeals_review_status ON appeals (review_id, status); diff --git a/anon/backend/src/lib.rs b/anon/backend/src/lib.rs index a7bf889b..6782b9fd 100644 --- a/anon/backend/src/lib.rs +++ b/anon/backend/src/lib.rs @@ -15,6 +15,7 @@ pub mod middlewares { } pub mod routes { + pub mod appeals; pub mod generate; pub mod health; pub mod register; diff --git a/anon/backend/src/main.rs b/anon/backend/src/main.rs index 80755f9d..0d5da335 100644 --- a/anon/backend/src/main.rs +++ b/anon/backend/src/main.rs @@ -7,7 +7,7 @@ use axum::{ header::{AUTHORIZATION, CONTENT_TYPE, LOCATION}, }, response::IntoResponse, - routing::{get, post}, + routing::{get, post, put}, }; use tokio::net::TcpListener; use tower_http::{ @@ -51,6 +51,10 @@ async fn main() { .route("/user", get(routes::user::me)) .route("/generate", post(routes::generate::generate_contract)) .route("/reviews", get(routes::reviews::list_reviews)) + .route("/appeals", post(routes::appeals::create_appeal)) + .route("/appeals/{id}", get(routes::appeals::get_appeal)) + // .route("/admin/appeals", get(routes::appeals::get_admin_appeal)) + // .route("/admin/appeals/{id}", put(routes::appeals::update_admin_appeal)) // Swagger UI at /docs and OpenAPI JSON at /api-docs/openapi.json .merge(SwaggerUi::new("/docs").url( "/api-docs/openapi.json", diff --git a/anon/backend/src/routes/appeals.rs b/anon/backend/src/routes/appeals.rs new file mode 100644 index 00000000..036d0a75 --- /dev/null +++ b/anon/backend/src/routes/appeals.rs @@ -0,0 +1,101 @@ +use axum::{ + Json, + extract::{Path, Query, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::{Deserialize, Serialize}; +use sqlx::Arguments; +use utoipa::ToSchema; + +use crate::{ + libs::{db::AppState, error::ApiError}, + middlewares::auth::AuthUser, +}; + +#[derive(Deserialize, ToSchema)] +pub struct CreateAppealRequest { + pub actor: String, + pub review_id: i64, + pub reason: String, + pub status: String, + pub resolution_note: Option, +} + +#[derive(Deserialize, ToSchema)] +pub struct GetAppealResponse { + pub id: i64, + pub actor: String, + pub review_id: i64, + pub reason: String, + pub status: String, + pub resolution_note: Option, + pub created_at: chrono::DateTime, +} + +#[utoipa::path( + post, + path = "/appeals", + tag = "appeals", + request_body = CreateAppealRequest, + responses( + (status = 201, description = "Appeal created successfully", body = CreateAppealRequest), + (status = 400, description = "Invalid request", body = crate::libs::error::ErrorBody), + (status = 404, description = "User not found", body = crate::libs::error::ErrorBody), + (status = 500, description = "Internal error", body = crate::libs::error::ErrorBody) + ) +)] +pub async fn create_appeal( + State(AppState { pool }): State, + AuthUser { wallet }: AuthUser, + Json(req): Json, +) -> Result { + tracing::info!( + "Generating appeal for review_id: {}, reason: {}", + req.review_id, + req.reason + ); + let rec = sqlx::query!( + r#"SELECT u.id, u.wallet, u.created_at, p.referral_code + FROM users u + LEFT JOIN profiles p ON p.user_id = u.id + WHERE u.wallet = $1"#, + wallet + ) + .fetch_optional(&pool) + .await + .map_err(|e| crate::libs::error::map_sqlx_error(&e))?; + + rec.ok_or(ApiError::NotFound("user not found"))?; + + let actor = req.actor; + + if !(actor == String::from("author") || actor == String::from("owner")) { + return Err(ApiError::BadRequest("invalid actor")); + } + + sqlx::query!( + r#"INSERT INTO appeals (actor, review_id, reason, status, resolution_note) + VALUES ($1, $2, $3, $4, $5) + RETURNING id"#, + actor, + req.review_id, + req.reason, + req.status, + req.resolution_note + ) + .fetch_one(&pool) + .await + .map_err(|e| { + tracing::error!("❌ Failed to insert appeal: {:?}", e); + crate::libs::error::map_sqlx_error(&e) + })?; + + Ok((StatusCode::CREATED, Json("Appeal created successfully"))) +} + +pub async fn get_appeal( + State(AppState { pool }): State, + AuthUser { wallet }: AuthUser, + Path(appeal_id): Path, +) -> Result {} From 7dd8616eae35fedff7c9b7136ab73937ffc5ee85 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Tue, 9 Sep 2025 01:53:36 +0100 Subject: [PATCH 2/6] feat: implement create appeal --- ...90a5907a5195552b9f9dc4a971d792e0e1362.json | 12 --- ...45d527d6c3ee503d73906648dd4a94e718427.json | 22 ----- ...ac641b8e560ef7884ac0adbafa5e746b32e6e.json | 58 +++++++++++++ ...3d1c7091d6c9a577990afde278605acf6a849.json | 12 --- ...88d452dd9b6449d02850bd2679b5ec1cf1b3c.json | 82 ------------------- ...edfffd266dfc74473001b257bf99c12fd99b2.json | 12 --- ...c1648b6fef8a4c21cc83de707695ef307b969.json | 26 ++++++ ...f72b32aca5aa9db98af80eb6699523b371fe2.json | 22 ----- ...087521496603e8b1bc10475a864001e795593.json | 12 --- anon/backend/src/main.rs | 2 +- anon/backend/src/routes/appeals.rs | 10 +-- 11 files changed, 90 insertions(+), 180 deletions(-) delete mode 100644 anon/backend/.sqlx/query-613dec2d46124070791f50ad38f90a5907a5195552b9f9dc4a971d792e0e1362.json delete mode 100644 anon/backend/.sqlx/query-6b7ecd5e7c48f4bb8e7af37eca945d527d6c3ee503d73906648dd4a94e718427.json create mode 100644 anon/backend/.sqlx/query-72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e.json delete mode 100644 anon/backend/.sqlx/query-949a44457416ff9332fea3f100f3d1c7091d6c9a577990afde278605acf6a849.json delete mode 100644 anon/backend/.sqlx/query-ac8874076611d27bb4aae32698288d452dd9b6449d02850bd2679b5ec1cf1b3c.json delete mode 100644 anon/backend/.sqlx/query-b707709a22a18789f9a669e08eaedfffd266dfc74473001b257bf99c12fd99b2.json create mode 100644 anon/backend/.sqlx/query-d73d4b90678c6f0f77df4732322c1648b6fef8a4c21cc83de707695ef307b969.json delete mode 100644 anon/backend/.sqlx/query-ed764fe4aaac05e73a8d7510edbf72b32aca5aa9db98af80eb6699523b371fe2.json delete mode 100644 anon/backend/.sqlx/query-f4f8f8c2668ec23ba1f4a315d74087521496603e8b1bc10475a864001e795593.json diff --git a/anon/backend/.sqlx/query-613dec2d46124070791f50ad38f90a5907a5195552b9f9dc4a971d792e0e1362.json b/anon/backend/.sqlx/query-613dec2d46124070791f50ad38f90a5907a5195552b9f9dc4a971d792e0e1362.json deleted file mode 100644 index 6fa29482..00000000 --- a/anon/backend/.sqlx/query-613dec2d46124070791f50ad38f90a5907a5195552b9f9dc4a971d792e0e1362.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM generated_contracts", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "613dec2d46124070791f50ad38f90a5907a5195552b9f9dc4a971d792e0e1362" -} diff --git a/anon/backend/.sqlx/query-6b7ecd5e7c48f4bb8e7af37eca945d527d6c3ee503d73906648dd4a94e718427.json b/anon/backend/.sqlx/query-6b7ecd5e7c48f4bb8e7af37eca945d527d6c3ee503d73906648dd4a94e718427.json deleted file mode 100644 index 1d0d8a38..00000000 --- a/anon/backend/.sqlx/query-6b7ecd5e7c48f4bb8e7af37eca945d527d6c3ee503d73906648dd4a94e718427.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT COUNT(*) FROM generated_contracts WHERE user_id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "count", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - null - ] - }, - "hash": "6b7ecd5e7c48f4bb8e7af37eca945d527d6c3ee503d73906648dd4a94e718427" -} diff --git a/anon/backend/.sqlx/query-72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e.json b/anon/backend/.sqlx/query-72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e.json new file mode 100644 index 00000000..914951b3 --- /dev/null +++ b/anon/backend/.sqlx/query-72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM appeals WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "actor", + "type_info": "Varchar" + }, + { + "ordinal": 2, + "name": "review_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "reason", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "status", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "resolution_note", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + false + ] + }, + "hash": "72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e" +} diff --git a/anon/backend/.sqlx/query-949a44457416ff9332fea3f100f3d1c7091d6c9a577990afde278605acf6a849.json b/anon/backend/.sqlx/query-949a44457416ff9332fea3f100f3d1c7091d6c9a577990afde278605acf6a849.json deleted file mode 100644 index cdc925ac..00000000 --- a/anon/backend/.sqlx/query-949a44457416ff9332fea3f100f3d1c7091d6c9a577990afde278605acf6a849.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM profiles", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "949a44457416ff9332fea3f100f3d1c7091d6c9a577990afde278605acf6a849" -} diff --git a/anon/backend/.sqlx/query-ac8874076611d27bb4aae32698288d452dd9b6449d02850bd2679b5ec1cf1b3c.json b/anon/backend/.sqlx/query-ac8874076611d27bb4aae32698288d452dd9b6449d02850bd2679b5ec1cf1b3c.json deleted file mode 100644 index dbd3f2e5..00000000 --- a/anon/backend/.sqlx/query-ac8874076611d27bb4aae32698288d452dd9b6449d02850bd2679b5ec1cf1b3c.json +++ /dev/null @@ -1,82 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM generated_contracts WHERE id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "user_id", - "type_info": "Int8" - }, - { - "ordinal": 2, - "name": "contract_type", - "type_info": "Text" - }, - { - "ordinal": 3, - "name": "contract_name", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "description", - "type_info": "Text" - }, - { - "ordinal": 5, - "name": "parameters", - "type_info": "Jsonb" - }, - { - "ordinal": 6, - "name": "template_id", - "type_info": "Text" - }, - { - "ordinal": 7, - "name": "generated_code", - "type_info": "Text" - }, - { - "ordinal": 8, - "name": "status", - "type_info": "Text" - }, - { - "ordinal": 9, - "name": "created_at", - "type_info": "Timestamptz" - }, - { - "ordinal": 10, - "name": "updated_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - true, - true, - true, - false, - false, - false, - false - ] - }, - "hash": "ac8874076611d27bb4aae32698288d452dd9b6449d02850bd2679b5ec1cf1b3c" -} diff --git a/anon/backend/.sqlx/query-b707709a22a18789f9a669e08eaedfffd266dfc74473001b257bf99c12fd99b2.json b/anon/backend/.sqlx/query-b707709a22a18789f9a669e08eaedfffd266dfc74473001b257bf99c12fd99b2.json deleted file mode 100644 index f21de1d5..00000000 --- a/anon/backend/.sqlx/query-b707709a22a18789f9a669e08eaedfffd266dfc74473001b257bf99c12fd99b2.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n TRUNCATE TABLE\n generated_contracts,\n profiles,\n users\n RESTART IDENTITY CASCADE\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "b707709a22a18789f9a669e08eaedfffd266dfc74473001b257bf99c12fd99b2" -} diff --git a/anon/backend/.sqlx/query-d73d4b90678c6f0f77df4732322c1648b6fef8a4c21cc83de707695ef307b969.json b/anon/backend/.sqlx/query-d73d4b90678c6f0f77df4732322c1648b6fef8a4c21cc83de707695ef307b969.json new file mode 100644 index 00000000..ae9096c8 --- /dev/null +++ b/anon/backend/.sqlx/query-d73d4b90678c6f0f77df4732322c1648b6fef8a4c21cc83de707695ef307b969.json @@ -0,0 +1,26 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO appeals (actor, review_id, reason, status, resolution_note)\n VALUES ($1, $2, $3, $4, $5)\n RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Varchar", + "Int8", + "Text", + "Varchar", + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "d73d4b90678c6f0f77df4732322c1648b6fef8a4c21cc83de707695ef307b969" +} diff --git a/anon/backend/.sqlx/query-ed764fe4aaac05e73a8d7510edbf72b32aca5aa9db98af80eb6699523b371fe2.json b/anon/backend/.sqlx/query-ed764fe4aaac05e73a8d7510edbf72b32aca5aa9db98af80eb6699523b371fe2.json deleted file mode 100644 index 7e89112f..00000000 --- a/anon/backend/.sqlx/query-ed764fe4aaac05e73a8d7510edbf72b32aca5aa9db98af80eb6699523b371fe2.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO users (wallet) VALUES ($1) RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Text" - ] - }, - "nullable": [ - false - ] - }, - "hash": "ed764fe4aaac05e73a8d7510edbf72b32aca5aa9db98af80eb6699523b371fe2" -} diff --git a/anon/backend/.sqlx/query-f4f8f8c2668ec23ba1f4a315d74087521496603e8b1bc10475a864001e795593.json b/anon/backend/.sqlx/query-f4f8f8c2668ec23ba1f4a315d74087521496603e8b1bc10475a864001e795593.json deleted file mode 100644 index 68dd7533..00000000 --- a/anon/backend/.sqlx/query-f4f8f8c2668ec23ba1f4a315d74087521496603e8b1bc10475a864001e795593.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "DELETE FROM users", - "describe": { - "columns": [], - "parameters": { - "Left": [] - }, - "nullable": [] - }, - "hash": "f4f8f8c2668ec23ba1f4a315d74087521496603e8b1bc10475a864001e795593" -} diff --git a/anon/backend/src/main.rs b/anon/backend/src/main.rs index 0d5da335..be9c6944 100644 --- a/anon/backend/src/main.rs +++ b/anon/backend/src/main.rs @@ -52,7 +52,7 @@ async fn main() { .route("/generate", post(routes::generate::generate_contract)) .route("/reviews", get(routes::reviews::list_reviews)) .route("/appeals", post(routes::appeals::create_appeal)) - .route("/appeals/{id}", get(routes::appeals::get_appeal)) + // .route("/appeals/{id}", get(routes::appeals::get_appeal)) // .route("/admin/appeals", get(routes::appeals::get_admin_appeal)) // .route("/admin/appeals/{id}", put(routes::appeals::update_admin_appeal)) // Swagger UI at /docs and OpenAPI JSON at /api-docs/openapi.json diff --git a/anon/backend/src/routes/appeals.rs b/anon/backend/src/routes/appeals.rs index 036d0a75..e7c8f8a6 100644 --- a/anon/backend/src/routes/appeals.rs +++ b/anon/backend/src/routes/appeals.rs @@ -94,8 +94,8 @@ pub async fn create_appeal( Ok((StatusCode::CREATED, Json("Appeal created successfully"))) } -pub async fn get_appeal( - State(AppState { pool }): State, - AuthUser { wallet }: AuthUser, - Path(appeal_id): Path, -) -> Result {} +// pub async fn get_appeal( +// State(AppState { pool }): State, +// AuthUser { wallet }: AuthUser, +// Path(appeal_id): Path, +// ) -> Result {} From a4cb6ba0565676d76f6ce0c8b9b72899e9531bd7 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Tue, 9 Sep 2025 02:02:23 +0100 Subject: [PATCH 3/6] cargo sqlx prepare --- ...ac641b8e560ef7884ac0adbafa5e746b32e6e.json | 58 ------------------- anon/backend/src/libs/apispec.rs | 11 +++- 2 files changed, 8 insertions(+), 61 deletions(-) delete mode 100644 anon/backend/.sqlx/query-72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e.json diff --git a/anon/backend/.sqlx/query-72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e.json b/anon/backend/.sqlx/query-72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e.json deleted file mode 100644 index 914951b3..00000000 --- a/anon/backend/.sqlx/query-72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT * FROM appeals WHERE id = $1", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - }, - { - "ordinal": 1, - "name": "actor", - "type_info": "Varchar" - }, - { - "ordinal": 2, - "name": "review_id", - "type_info": "Int8" - }, - { - "ordinal": 3, - "name": "reason", - "type_info": "Text" - }, - { - "ordinal": 4, - "name": "status", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "resolution_note", - "type_info": "Text" - }, - { - "ordinal": 6, - "name": "created_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Int8" - ] - }, - "nullable": [ - false, - false, - false, - false, - false, - true, - false - ] - }, - "hash": "72ba1372c58bebcb3d70016baf8ac641b8e560ef7884ac0adbafa5e746b32e6e" -} diff --git a/anon/backend/src/libs/apispec.rs b/anon/backend/src/libs/apispec.rs index 1f7ce183..9f68da0b 100644 --- a/anon/backend/src/libs/apispec.rs +++ b/anon/backend/src/libs/apispec.rs @@ -28,7 +28,8 @@ impl Modify for SecurityAddon { crate::routes::health::healthz, crate::routes::generate::generate_contract, crate::routes::generate::list_generated_contracts, - crate::routes::reviews::list_reviews + crate::routes::reviews::list_reviews, + crate::routes::appeals::create_appeal, ), components( schemas( @@ -45,7 +46,10 @@ impl Modify for SecurityAddon { crate::routes::generate::GeneratedContractsListRes, // Reviews crate::routes::reviews::ReviewItem, - crate::routes::reviews::ReviewsListRes + crate::routes::reviews::ReviewsListRes, + // Apeals + crate::routes::appeals::CreateAppealRequest, + crate::routes::appeals::GetAppealResponse, ) ), modifiers(&SecurityAddon), @@ -53,7 +57,8 @@ impl Modify for SecurityAddon { (name = "health", description = "Health check endpoints"), (name = "auth", description = "Authentication & registration endpoints"), (name = "contracts", description = "Generated contracts endpoints"), - (name = "reviews", description = "Reviews listing endpoints") + (name = "reviews", description = "Reviews listing endpoints"), + (name = "appeals", description = "Adding an appeal endpoint") ) )] pub struct ApiDoc; From 4d76ce667f5e33d10f6b551d66ca9b9596091e67 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Tue, 9 Sep 2025 06:23:11 +0100 Subject: [PATCH 4/6] implement paginated appeal --- ...1407375da88dbf55a0ba7f2982c70e40ad7c9.json | 58 +++++++ ...62da9e39cd9f4a24bd823fdbc9849801cccdd.json | 22 +++ ...c1648b6fef8a4c21cc83de707695ef307b969.json | 2 +- anon/backend/migrations/0005__appeals.sql | 2 +- anon/backend/src/libs/apispec.rs | 2 +- anon/backend/src/main.rs | 12 +- anon/backend/src/routes/appeals.rs | 157 ++++++++++++++++-- 7 files changed, 229 insertions(+), 26 deletions(-) create mode 100644 anon/backend/.sqlx/query-0cc145bb3fb0d46711d52aa7eff1407375da88dbf55a0ba7f2982c70e40ad7c9.json create mode 100644 anon/backend/.sqlx/query-b2fb4c61d3d5fd1f431652603f762da9e39cd9f4a24bd823fdbc9849801cccdd.json diff --git a/anon/backend/.sqlx/query-0cc145bb3fb0d46711d52aa7eff1407375da88dbf55a0ba7f2982c70e40ad7c9.json b/anon/backend/.sqlx/query-0cc145bb3fb0d46711d52aa7eff1407375da88dbf55a0ba7f2982c70e40ad7c9.json new file mode 100644 index 00000000..94302302 --- /dev/null +++ b/anon/backend/.sqlx/query-0cc145bb3fb0d46711d52aa7eff1407375da88dbf55a0ba7f2982c70e40ad7c9.json @@ -0,0 +1,58 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT \n id as appeal_id, actor, review_id, reason, status, resolution_note, created_at::text\n FROM appeals\n WHERE id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "appeal_id", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "actor", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "review_id", + "type_info": "Int8" + }, + { + "ordinal": 3, + "name": "reason", + "type_info": "Text" + }, + { + "ordinal": 4, + "name": "status", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "resolution_note", + "type_info": "Text" + }, + { + "ordinal": 6, + "name": "created_at", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Int8" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + null + ] + }, + "hash": "0cc145bb3fb0d46711d52aa7eff1407375da88dbf55a0ba7f2982c70e40ad7c9" +} diff --git a/anon/backend/.sqlx/query-b2fb4c61d3d5fd1f431652603f762da9e39cd9f4a24bd823fdbc9849801cccdd.json b/anon/backend/.sqlx/query-b2fb4c61d3d5fd1f431652603f762da9e39cd9f4a24bd823fdbc9849801cccdd.json new file mode 100644 index 00000000..a2906eef --- /dev/null +++ b/anon/backend/.sqlx/query-b2fb4c61d3d5fd1f431652603f762da9e39cd9f4a24bd823fdbc9849801cccdd.json @@ -0,0 +1,22 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT wallet\n FROM users\n WHERE wallet = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "wallet", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false + ] + }, + "hash": "b2fb4c61d3d5fd1f431652603f762da9e39cd9f4a24bd823fdbc9849801cccdd" +} diff --git a/anon/backend/.sqlx/query-d73d4b90678c6f0f77df4732322c1648b6fef8a4c21cc83de707695ef307b969.json b/anon/backend/.sqlx/query-d73d4b90678c6f0f77df4732322c1648b6fef8a4c21cc83de707695ef307b969.json index ae9096c8..6e496c30 100644 --- a/anon/backend/.sqlx/query-d73d4b90678c6f0f77df4732322c1648b6fef8a4c21cc83de707695ef307b969.json +++ b/anon/backend/.sqlx/query-d73d4b90678c6f0f77df4732322c1648b6fef8a4c21cc83de707695ef307b969.json @@ -11,7 +11,7 @@ ], "parameters": { "Left": [ - "Varchar", + "Text", "Int8", "Text", "Varchar", diff --git a/anon/backend/migrations/0005__appeals.sql b/anon/backend/migrations/0005__appeals.sql index 6a55085c..adbbcc2c 100644 --- a/anon/backend/migrations/0005__appeals.sql +++ b/anon/backend/migrations/0005__appeals.sql @@ -1,7 +1,7 @@ CREATE TABLE IF NOT EXISTS appeals ( id BIGSERIAL PRIMARY KEY, - actor VARCHAR(20) NOT NULL CHECK (actor IN ('author', 'owner')), + actor TEXT NOT NULL UNIQUE, review_id BIGSERIAL NOT NULL REFERENCES reviews(id) ON DELETE CASCADE, diff --git a/anon/backend/src/libs/apispec.rs b/anon/backend/src/libs/apispec.rs index 9f68da0b..dd8dc625 100644 --- a/anon/backend/src/libs/apispec.rs +++ b/anon/backend/src/libs/apispec.rs @@ -49,7 +49,7 @@ impl Modify for SecurityAddon { crate::routes::reviews::ReviewsListRes, // Apeals crate::routes::appeals::CreateAppealRequest, - crate::routes::appeals::GetAppealResponse, + crate::routes::appeals::GetAppealRes, ) ), modifiers(&SecurityAddon), diff --git a/anon/backend/src/main.rs b/anon/backend/src/main.rs index 5651acbe..66df7b90 100644 --- a/anon/backend/src/main.rs +++ b/anon/backend/src/main.rs @@ -1,13 +1,9 @@ use backend::*; use axum::{ - Router, http::{ - Method, StatusCode, - header::{AUTHORIZATION, CONTENT_TYPE, LOCATION}, - }, - response::IntoResponse, - routing::{get, post, put}, + header::{AUTHORIZATION, CONTENT_TYPE, LOCATION}, Method, StatusCode + }, response::IntoResponse, routing::{get, post, put}, Router }; use tokio::net::TcpListener; use tower_http::{ @@ -57,8 +53,8 @@ async fn main() { ) .route("/reviews", get(routes::reviews::list_reviews)) .route("/appeals", post(routes::appeals::create_appeal)) - // .route("/appeals/{id}", get(routes::appeals::get_appeal)) - // .route("/admin/appeals", get(routes::appeals::get_admin_appeal)) + .route("/appeals/{id}", get(routes::appeals::get_appeal)) + .route("/admin/appeals", get(routes::appeals::get_admin_appeal)) // .route("/admin/appeals/{id}", put(routes::appeals::update_admin_appeal)) // Swagger UI at /docs and OpenAPI JSON at /api-docs/openapi.json .merge(SwaggerUi::new("/docs").url( diff --git a/anon/backend/src/routes/appeals.rs b/anon/backend/src/routes/appeals.rs index e7c8f8a6..46fa3961 100644 --- a/anon/backend/src/routes/appeals.rs +++ b/anon/backend/src/routes/appeals.rs @@ -9,7 +9,7 @@ use sqlx::Arguments; use utoipa::ToSchema; use crate::{ - libs::{db::AppState, error::ApiError}, + libs::{db::AppState, error::ApiError, wallet}, middlewares::auth::AuthUser, }; @@ -22,15 +22,38 @@ pub struct CreateAppealRequest { pub resolution_note: Option, } -#[derive(Deserialize, ToSchema)] -pub struct GetAppealResponse { - pub id: i64, +#[derive(Deserialize, ToSchema, Serialize)] +pub struct GetAppealItems { + pub appeal_id: i64, pub actor: String, pub review_id: i64, pub reason: String, pub status: String, pub resolution_note: Option, - pub created_at: chrono::DateTime, + pub created_at: Option, +} +#[derive(Deserialize, ToSchema, Serialize)] +pub struct AdminAppealRequest { + pub status: Option, + pub reason: Option, + pub cursor: Option, + pub limit: Option, +} + +type AdminAppeal = ( + i64, + String, + i64, + String, + String, + Option, + Option, +); + +#[derive(Deserialize, ToSchema, Serialize)] +pub struct GetAppealRes { + items: Vec, + next_cursor: i64, } #[utoipa::path( @@ -47,7 +70,7 @@ pub struct GetAppealResponse { )] pub async fn create_appeal( State(AppState { pool }): State, - AuthUser { wallet }: AuthUser, + // AuthUser { wallet }: AuthUser, Json(req): Json, ) -> Result { tracing::info!( @@ -70,15 +93,13 @@ pub async fn create_appeal( let actor = req.actor; - if !(actor == String::from("author") || actor == String::from("owner")) { - return Err(ApiError::BadRequest("invalid actor")); - } + let normalized_wallet = wallet::normalize_and_validate(&actor)?; sqlx::query!( r#"INSERT INTO appeals (actor, review_id, reason, status, resolution_note) VALUES ($1, $2, $3, $4, $5) RETURNING id"#, - actor, + normalized_wallet, req.review_id, req.reason, req.status, @@ -94,8 +115,114 @@ pub async fn create_appeal( Ok((StatusCode::CREATED, Json("Appeal created successfully"))) } -// pub async fn get_appeal( -// State(AppState { pool }): State, -// AuthUser { wallet }: AuthUser, -// Path(appeal_id): Path, -// ) -> Result {} +pub async fn get_appeal( + State(AppState { pool }): State, + AuthUser { wallet }: AuthUser, + Path(appeal_id): Path, +) -> Result { + sqlx::query!( + r#"SELECT wallet + FROM users + WHERE wallet = $1"#, + wallet + ) + .fetch_optional(&pool) + .await + .map_err(|e| crate::libs::error::map_sqlx_error(&e))? + .ok_or(ApiError::NotFound("user not found"))?; + + let appeal = sqlx::query_as!( + GetAppealItems, + r#"SELECT + id as appeal_id, actor, review_id, reason, status, resolution_note, created_at::text + FROM appeals + WHERE id = $1"#, + appeal_id + ) + .fetch_optional(&pool) + .await + .map_err(|e| crate::libs::error::map_sqlx_error(&e))? + .ok_or(ApiError::NotFound("appeal not found"))?; + + if wallet == appeal.actor { + Ok(Json(appeal)) + } else { + Err(ApiError::Unauthorized("not allowed to view this appeal")) + } +} + +pub async fn get_admin_appeal( + State(AppState { pool }): State, + Query(params): Query, +) -> Result { + let limit = params.limit.unwrap_or(20).clamp(1, 50); + + let mut dynamic_sql = String::from( + r#"SELECT id as appeal_id, actor, review_id, reason, status, resolution_note, created_at::text FROM appeals WHERE 1 = 1"#, + ); + + let mut args: sqlx::postgres::PgArguments = sqlx::postgres::PgArguments::default(); + let mut i: i32 = 1; + + println!("i before {i}"); + + if let Some(status) = ¶ms.status { + dynamic_sql.push_str(&format!(" AND status = ${i}")); + args.add(status) + .map_err(|_| ApiError::Internal("Failed to add status args"))?; + i += 1; + } + + println!("i after {i}"); + + println!("sql string {dynamic_sql}"); + println!("args {:?}", args); + + if let Some(reason) = ¶ms.reason { + dynamic_sql.push_str(&format!(" AND reason = ${i}")); + args.add(reason) + .map_err(|_| ApiError::Internal("Failed to add reason args"))?; + i += 1; + } + + if let Some(cursor) = ¶ms.cursor { + dynamic_sql.push_str(&format!(" AND id > ${i}")); + args.add(cursor) + .map_err(|_| ApiError::Internal("Failed to add cursor args"))?; + i += 1; + } + + dynamic_sql.push_str(" ORDER BY created_at DESC, id DESC"); + + dynamic_sql.push_str(&format!(" LIMIT ${}", i)); + args.add(&limit) + .map_err(|_| crate::libs::error::ApiError::Internal("Failed to add limit arg"))?; + + let rows: Vec = sqlx::query_as_with(&dynamic_sql, args) + .fetch_all(&pool) + .await + .map_err(|e| crate::libs::error::map_sqlx_error(&e))?; + + println!("\n rows are {:?}", rows); + let items: Vec = rows + .into_iter() + .map( + |(appeal_id, actor, review_id, reason, status, resolution_note, created_at)| { + GetAppealItems { + appeal_id, + actor, + review_id, + reason, + status, + resolution_note, + created_at, + } + }, + ) + .collect(); + + let next_cursor = items.last().unwrap().review_id; + + Ok(Json(GetAppealRes { items, next_cursor })) +} + From 3cea3e5c3576e6c66eb0370f26841c7318a53454 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Wed, 10 Sep 2025 09:46:55 +0100 Subject: [PATCH 5/6] implement the last put function --- ...509d337b751ce408a13735adc53c37deeff5c.json | 17 +++ anon/backend/src/main.rs | 13 ++- anon/backend/src/routes/appeals.rs | 104 +++++++++++++++++- 3 files changed, 128 insertions(+), 6 deletions(-) create mode 100644 anon/backend/.sqlx/query-92172cfcb47356c57914ec94b2a509d337b751ce408a13735adc53c37deeff5c.json diff --git a/anon/backend/.sqlx/query-92172cfcb47356c57914ec94b2a509d337b751ce408a13735adc53c37deeff5c.json b/anon/backend/.sqlx/query-92172cfcb47356c57914ec94b2a509d337b751ce408a13735adc53c37deeff5c.json new file mode 100644 index 00000000..465feefb --- /dev/null +++ b/anon/backend/.sqlx/query-92172cfcb47356c57914ec94b2a509d337b751ce408a13735adc53c37deeff5c.json @@ -0,0 +1,17 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE appeals SET reason = $1, status = $2 , resolution_note = $3 WHERE id = $4 ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Varchar", + "Text", + "Int8" + ] + }, + "nullable": [] + }, + "hash": "92172cfcb47356c57914ec94b2a509d337b751ce408a13735adc53c37deeff5c" +} diff --git a/anon/backend/src/main.rs b/anon/backend/src/main.rs index 66df7b90..2bd7b5b4 100644 --- a/anon/backend/src/main.rs +++ b/anon/backend/src/main.rs @@ -1,9 +1,13 @@ use backend::*; use axum::{ + Router, http::{ - header::{AUTHORIZATION, CONTENT_TYPE, LOCATION}, Method, StatusCode - }, response::IntoResponse, routing::{get, post, put}, Router + Method, StatusCode, + header::{AUTHORIZATION, CONTENT_TYPE, LOCATION}, + }, + response::IntoResponse, + routing::{get, post, put}, }; use tokio::net::TcpListener; use tower_http::{ @@ -55,7 +59,10 @@ async fn main() { .route("/appeals", post(routes::appeals::create_appeal)) .route("/appeals/{id}", get(routes::appeals::get_appeal)) .route("/admin/appeals", get(routes::appeals::get_admin_appeal)) - // .route("/admin/appeals/{id}", put(routes::appeals::update_admin_appeal)) + .route( + "/admin/appeals/{id}", + put(routes::appeals::update_admin_appeal), + ) // Swagger UI at /docs and OpenAPI JSON at /api-docs/openapi.json .merge(SwaggerUi::new("/docs").url( "/api-docs/openapi.json", diff --git a/anon/backend/src/routes/appeals.rs b/anon/backend/src/routes/appeals.rs index 46fa3961..40a75b68 100644 --- a/anon/backend/src/routes/appeals.rs +++ b/anon/backend/src/routes/appeals.rs @@ -22,6 +22,13 @@ pub struct CreateAppealRequest { pub resolution_note: Option, } +#[derive(Deserialize, ToSchema)] +pub struct UpdateAppealRequest { + pub reason: Option, + pub status: Option, + pub resolution_note: Option, +} + #[derive(Deserialize, ToSchema, Serialize)] pub struct GetAppealItems { pub appeal_id: i64, @@ -32,7 +39,7 @@ pub struct GetAppealItems { pub resolution_note: Option, pub created_at: Option, } -#[derive(Deserialize, ToSchema, Serialize)] +#[derive(Deserialize, ToSchema, Serialize, utoipa::IntoParams)] pub struct AdminAppealRequest { pub status: Option, pub reason: Option, @@ -70,7 +77,7 @@ pub struct GetAppealRes { )] pub async fn create_appeal( State(AppState { pool }): State, - // AuthUser { wallet }: AuthUser, + AuthUser { wallet }: AuthUser, Json(req): Json, ) -> Result { tracing::info!( @@ -115,6 +122,17 @@ pub async fn create_appeal( Ok((StatusCode::CREATED, Json("Appeal created successfully"))) } +#[utoipa::path( + get, + path = "/appeals/{id}", + tag = "appeals", + responses( + (status = 201, body = GetAppealItems), + (status = 400, description = "Invalid request", body = crate::libs::error::ErrorBody), + (status = 404, description = "User not found", body = crate::libs::error::ErrorBody), + (status = 500, description = "Internal error", body = crate::libs::error::ErrorBody) + ) +)] pub async fn get_appeal( State(AppState { pool }): State, AuthUser { wallet }: AuthUser, @@ -131,7 +149,7 @@ pub async fn get_appeal( .map_err(|e| crate::libs::error::map_sqlx_error(&e))? .ok_or(ApiError::NotFound("user not found"))?; - let appeal = sqlx::query_as!( + let appeal: GetAppealItems = sqlx::query_as!( GetAppealItems, r#"SELECT id as appeal_id, actor, review_id, reason, status, resolution_note, created_at::text @@ -151,6 +169,18 @@ pub async fn get_appeal( } } +#[utoipa::path( + get, + path = "/admin/appeals", + tag = "appeals", + params(AdminAppealRequest), + responses( + (status = 201, description = "Successfully gets appeals based on params", body = GetAppealRes), + (status = 400, description = "Invalid request", body = crate::libs::error::ErrorBody), + (status = 404, description = "User not found", body = crate::libs::error::ErrorBody), + (status = 500, description = "Internal error", body = crate::libs::error::ErrorBody) + ) +)] pub async fn get_admin_appeal( State(AppState { pool }): State, Query(params): Query, @@ -226,3 +256,71 @@ pub async fn get_admin_appeal( Ok(Json(GetAppealRes { items, next_cursor })) } +#[utoipa::path( + put, + path = "/admin/appeals/{id}", + tag = "appeals", + request_body = UpdateAppealRequest, + responses( + (status = 201, description = "Successfully updates an admin appeal"), + (status = 400, description = "Invalid request", body = crate::libs::error::ErrorBody), + (status = 404, description = "User not found", body = crate::libs::error::ErrorBody), + (status = 500, description = "Internal error", body = crate::libs::error::ErrorBody) + ) +)] +pub async fn update_admin_appeal( + State(AppState { pool }): State, + AuthUser { wallet }: AuthUser, + Path(appeal_id): Path, + Json(req): Json, +) -> Result { + tracing::info!("Updating appeal: {}", appeal_id); + + let rec = sqlx::query!( + r#"SELECT u.id, u.wallet, u.created_at, p.referral_code + FROM users u + LEFT JOIN profiles p ON p.user_id = u.id + WHERE u.wallet = $1"#, + wallet + ) + .fetch_optional(&pool) + .await + .map_err(|e| crate::libs::error::map_sqlx_error(&e))?; + + rec.ok_or(ApiError::NotFound("user not found"))?; + + let appeal = sqlx::query_as!( + GetAppealItems, + r#"SELECT + id as appeal_id, actor, review_id, reason, status, resolution_note, created_at::text + FROM appeals + WHERE id = $1"#, + appeal_id + ) + .fetch_optional(&pool) + .await + .map_err(|e| crate::libs::error::map_sqlx_error(&e))? + .ok_or(ApiError::NotFound("appeal not found"))?; + + let reason = req.reason.unwrap_or(appeal.reason); + let status = req.status.unwrap_or(appeal.status); + let resolution_note = req + .resolution_note + .unwrap_or(appeal.resolution_note.unwrap_or_default()); + println!("details {reason} {status} {resolution_note} {appeal_id}"); + sqlx::query!( + r#"UPDATE appeals SET reason = $1, status = $2 , resolution_note = $3 WHERE id = $4 "#, + reason, + status, + resolution_note, + appeal_id + ) + .execute(&pool) + .await + .map_err(|e| { + tracing::error!("❌ Failed to update appeal: {:?}", e); + crate::libs::error::map_sqlx_error(&e) + })?; + + Ok(Json("APPEAL UPDATED")) +} From b43756855f7a62f0c5288b8e0dfdf481910d3298 Mon Sep 17 00:00:00 2001 From: Akshola00 Date: Wed, 10 Sep 2025 09:51:45 +0100 Subject: [PATCH 6/6] update api docs --- anon/backend/src/libs/apispec.rs | 6 ++++++ anon/backend/src/routes/appeals.rs | 6 ------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/anon/backend/src/libs/apispec.rs b/anon/backend/src/libs/apispec.rs index dd8dc625..10821361 100644 --- a/anon/backend/src/libs/apispec.rs +++ b/anon/backend/src/libs/apispec.rs @@ -30,6 +30,9 @@ impl Modify for SecurityAddon { crate::routes::generate::list_generated_contracts, crate::routes::reviews::list_reviews, crate::routes::appeals::create_appeal, + crate::routes::appeals::get_appeal, + crate::routes::appeals::get_admin_appeal, + crate::routes::appeals::update_admin_appeal, ), components( schemas( @@ -49,6 +52,9 @@ impl Modify for SecurityAddon { crate::routes::reviews::ReviewsListRes, // Apeals crate::routes::appeals::CreateAppealRequest, + crate::routes::appeals::UpdateAppealRequest, + crate::routes::appeals::AdminAppealRequest, + crate::routes::appeals::GetAppealItems, crate::routes::appeals::GetAppealRes, ) ), diff --git a/anon/backend/src/routes/appeals.rs b/anon/backend/src/routes/appeals.rs index 40a75b68..1c113159 100644 --- a/anon/backend/src/routes/appeals.rs +++ b/anon/backend/src/routes/appeals.rs @@ -203,11 +203,6 @@ pub async fn get_admin_appeal( i += 1; } - println!("i after {i}"); - - println!("sql string {dynamic_sql}"); - println!("args {:?}", args); - if let Some(reason) = ¶ms.reason { dynamic_sql.push_str(&format!(" AND reason = ${i}")); args.add(reason) @@ -233,7 +228,6 @@ pub async fn get_admin_appeal( .await .map_err(|e| crate::libs::error::map_sqlx_error(&e))?; - println!("\n rows are {:?}", rows); let items: Vec = rows .into_iter() .map(