diff --git a/controllers/AdminController.cc b/controllers/AdminController.cc index 9cedba7..3acf93d 100644 --- a/controllers/AdminController.cc +++ b/controllers/AdminController.cc @@ -138,6 +138,20 @@ void AdminController::updateCategory( std::move(callback)); } +void AdminController::deleteCategory( + const drogon::HttpRequestPtr &req, + std::function &&callback, + long long categoryId) +{ + auto user = requireUser(req, callback); + if (!user || blockPasswordChangeRequired(*user, callback)) + { + return; + } + visione::services::AdminService service; + sendAdmin(service.deleteCategory(*user, categoryId), std::move(callback)); +} + void AdminController::updateArea( const drogon::HttpRequestPtr &req, std::function &&callback, @@ -153,6 +167,20 @@ void AdminController::updateArea( std::move(callback)); } +void AdminController::deleteArea( + const drogon::HttpRequestPtr &req, + std::function &&callback, + long long areaId) +{ + auto user = requireUser(req, callback); + if (!user || blockPasswordChangeRequired(*user, callback)) + { + return; + } + visione::services::AdminService service; + sendAdmin(service.deleteArea(*user, areaId), std::move(callback)); +} + void AdminController::updateTeam( const drogon::HttpRequestPtr &req, std::function &&callback, @@ -168,6 +196,20 @@ void AdminController::updateTeam( std::move(callback)); } +void AdminController::deleteTeam( + const drogon::HttpRequestPtr &req, + std::function &&callback, + long long teamId) +{ + auto user = requireUser(req, callback); + if (!user || blockPasswordChangeRequired(*user, callback)) + { + return; + } + visione::services::AdminService service; + sendAdmin(service.deleteTeam(*user, teamId), std::move(callback)); +} + void AdminController::overviewStats( const drogon::HttpRequestPtr &req, std::function &&callback) diff --git a/controllers/AdminController.h b/controllers/AdminController.h index 98ccdda..552a4cb 100644 --- a/controllers/AdminController.h +++ b/controllers/AdminController.h @@ -14,12 +14,21 @@ class AdminController : public drogon::HttpController ADD_METHOD_TO(AdminController::updateCategory, "/api/v1/admin/categories/{1}", drogon::Patch); + ADD_METHOD_TO(AdminController::deleteCategory, + "/api/v1/admin/categories/{1}", + drogon::Delete); ADD_METHOD_TO(AdminController::updateArea, "/api/v1/admin/areas/{1}", drogon::Patch); + ADD_METHOD_TO(AdminController::deleteArea, + "/api/v1/admin/areas/{1}", + drogon::Delete); ADD_METHOD_TO(AdminController::updateTeam, "/api/v1/admin/teams/{1}", drogon::Patch); + ADD_METHOD_TO(AdminController::deleteTeam, + "/api/v1/admin/teams/{1}", + drogon::Delete); ADD_METHOD_TO(AdminController::overviewStats, "/api/v1/admin/stats/overview", drogon::Get); @@ -51,14 +60,26 @@ class AdminController : public drogon::HttpController const drogon::HttpRequestPtr &req, std::function &&callback, long long categoryId); + void deleteCategory( + const drogon::HttpRequestPtr &req, + std::function &&callback, + long long categoryId); void updateArea( const drogon::HttpRequestPtr &req, std::function &&callback, long long areaId); + void deleteArea( + const drogon::HttpRequestPtr &req, + std::function &&callback, + long long areaId); void updateTeam( const drogon::HttpRequestPtr &req, std::function &&callback, long long teamId); + void deleteTeam( + const drogon::HttpRequestPtr &req, + std::function &&callback, + long long teamId); void overviewStats( const drogon::HttpRequestPtr &req, std::function &&callback); diff --git a/docs/API_SPEC.md b/docs/API_SPEC.md index 6d3e727..daa02de 100644 --- a/docs/API_SPEC.md +++ b/docs/API_SPEC.md @@ -111,11 +111,14 @@ Authorization: Bearer ## Admin Reference Data - `POST /admin/categories` - `PATCH /admin/categories/{id}` +- `DELETE /admin/categories/{id}` - `POST /admin/areas` - `PATCH /admin/areas/{id}` +- `DELETE /admin/areas/{id}` area polygon must stay inside allowed Bari municipal boundary - `POST /admin/teams` - `PATCH /admin/teams/{id}` +- `DELETE /admin/teams/{id}` - `GET /admin/users` - `PATCH /admin/users/{id}/roles` - `PATCH /admin/users/{id}/ban` diff --git a/docs/DATABASE_SCHEMA.md b/docs/DATABASE_SCHEMA.md index abfdfd9..0ab5a32 100644 --- a/docs/DATABASE_SCHEMA.md +++ b/docs/DATABASE_SCHEMA.md @@ -92,6 +92,7 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB - `reward_events(report_id, reason)` unique - `api_tokens.token_hash` unique - `intervention_access_links.token_hash` unique +- category/team/area delete behavior is soft delete via `active = FALSE` ## Current Query Model - No generated ORM models diff --git a/docs/PROJECT_SPEC.md b/docs/PROJECT_SPEC.md index b52ae42..2d87084 100644 --- a/docs/PROJECT_SPEC.md +++ b/docs/PROJECT_SPEC.md @@ -79,9 +79,12 @@ moderate abuse, and inspect operational stats. ### Admin - create/update categories +- soft-delete categories - create/update areas area polygons must stay inside Bari municipal boundary +- soft-delete areas - create/update teams +- soft-delete teams - list users, update roles, ban/unban - global stats including area/category/team resolution aggregates diff --git a/docs/openapi/visione.openapi.yaml b/docs/openapi/visione.openapi.yaml index 4c9537f..2f9fe76 100644 --- a/docs/openapi/visione.openapi.yaml +++ b/docs/openapi/visione.openapi.yaml @@ -1712,6 +1712,51 @@ } }, "/api/v1/admin/areas/{id}": { + "delete": { + "description": "Soft-deletes an existing municipal area by setting active to false.", + "operationId": "AdminController.deleteArea", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResponse" + } + } + }, + "description": "Deleted area result." + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + }, + "security": [ + { + "bearerAuth": [ ] + } + ], + "summary": "Delete area", + "tags": [ + "Admin" + ] + }, "patch": { "description": "Updates an existing municipal area.", "operationId": "AdminController.updateArea", @@ -1816,6 +1861,51 @@ } }, "/api/v1/admin/categories/{id}": { + "delete": { + "description": "Soft-deletes an existing report category by setting active to false.", + "operationId": "AdminController.deleteCategory", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResponse" + } + } + }, + "description": "Deleted category result." + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + }, + "security": [ + { + "bearerAuth": [ ] + } + ], + "summary": "Delete category", + "tags": [ + "Admin" + ] + }, "patch": { "description": "Updates an existing report category.", "operationId": "AdminController.updateCategory", @@ -2075,6 +2165,51 @@ } }, "/api/v1/admin/teams/{id}": { + "delete": { + "description": "Soft-deletes an existing intervention team by setting active to false.", + "operationId": "AdminController.deleteTeam", + "parameters": [ + { + "in": "path", + "name": "id", + "required": true, + "schema": { + "format": "int64", + "type": "integer" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/OkResponse" + } + } + }, + "description": "Deleted team result." + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + } + }, + "security": [ + { + "bearerAuth": [ ] + } + ], + "summary": "Delete team", + "tags": [ + "Admin" + ] + }, "patch": { "description": "Updates an existing intervention team.", "operationId": "AdminController.updateTeam", diff --git a/services/AdminService.cc b/services/AdminService.cc index de41d2c..413a31e 100644 --- a/services/AdminService.cc +++ b/services/AdminService.cc @@ -374,6 +374,26 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user, return success(); } +Json::Value AdminService::deleteCategory(const auth::UserContext &user, + long long categoryId) +{ + if (!auth::isAdmin(user)) + { + return failure("forbidden"); + } + auto db = drogon::app().getDbClient(); + const auto rows = db->execSqlSync( + "SELECT id FROM report_categories WHERE id = ?", + categoryId); + if (rows.empty()) + { + return failure("category_not_found"); + } + db->execSqlSync("UPDATE report_categories SET active = FALSE WHERE id = ?", + categoryId); + return success(); +} + Json::Value AdminService::updateCategory(const auth::UserContext &user, long long categoryId, const Json::Value &body) @@ -411,6 +431,23 @@ Json::Value AdminService::updateCategory(const auth::UserContext &user, return success(); } +Json::Value AdminService::deleteArea(const auth::UserContext &user, + long long areaId) +{ + if (!auth::isAdmin(user)) + { + return failure("forbidden"); + } + auto db = drogon::app().getDbClient(); + const auto rows = db->execSqlSync("SELECT id FROM areas WHERE id = ?", areaId); + if (rows.empty()) + { + return failure("area_not_found"); + } + db->execSqlSync("UPDATE areas SET active = FALSE WHERE id = ?", areaId); + return success(); +} + Json::Value AdminService::updateArea(const auth::UserContext &user, long long areaId, const Json::Value &body) @@ -458,6 +495,23 @@ Json::Value AdminService::updateArea(const auth::UserContext &user, return success(); } +Json::Value AdminService::deleteTeam(const auth::UserContext &user, + long long teamId) +{ + if (!auth::isAdmin(user)) + { + return failure("forbidden"); + } + auto db = drogon::app().getDbClient(); + const auto rows = db->execSqlSync("SELECT id FROM teams WHERE id = ?", teamId); + if (rows.empty()) + { + return failure("team_not_found"); + } + db->execSqlSync("UPDATE teams SET active = FALSE WHERE id = ?", teamId); + return success(); +} + Json::Value AdminService::updateTeam(const auth::UserContext &user, long long teamId, const Json::Value &body) diff --git a/services/AdminService.h b/services/AdminService.h index 26f995a..e49f616 100644 --- a/services/AdminService.h +++ b/services/AdminService.h @@ -24,6 +24,9 @@ class AdminService Json::Value updateModeration(const auth::UserContext &user, long long flagId, const Json::Value &body); + Json::Value deleteCategory(const auth::UserContext &user, long long categoryId); + Json::Value deleteArea(const auth::UserContext &user, long long areaId); + Json::Value deleteTeam(const auth::UserContext &user, long long teamId); Json::Value updateCategory(const auth::UserContext &user, long long categoryId, const Json::Value &body);