feat: add soft-delete functionality for categories, areas, and teams in Admin service

This commit is contained in:
gabdevele
2026-06-15 15:10:52 +02:00 Unverified
parent aab7420706
commit bf85168914
8 changed files with 262 additions and 0 deletions
+42
View File
@@ -138,6 +138,20 @@ void AdminController::updateCategory(
std::move(callback)); std::move(callback));
} }
void AdminController::deleteCategory(
const drogon::HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&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( void AdminController::updateArea(
const drogon::HttpRequestPtr &req, const drogon::HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback, std::function<void(const HttpResponsePtr &)> &&callback,
@@ -153,6 +167,20 @@ void AdminController::updateArea(
std::move(callback)); std::move(callback));
} }
void AdminController::deleteArea(
const drogon::HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&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( void AdminController::updateTeam(
const drogon::HttpRequestPtr &req, const drogon::HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback, std::function<void(const HttpResponsePtr &)> &&callback,
@@ -168,6 +196,20 @@ void AdminController::updateTeam(
std::move(callback)); std::move(callback));
} }
void AdminController::deleteTeam(
const drogon::HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&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( void AdminController::overviewStats(
const drogon::HttpRequestPtr &req, const drogon::HttpRequestPtr &req,
std::function<void(const HttpResponsePtr &)> &&callback) std::function<void(const HttpResponsePtr &)> &&callback)
+21
View File
@@ -14,12 +14,21 @@ class AdminController : public drogon::HttpController<AdminController>
ADD_METHOD_TO(AdminController::updateCategory, ADD_METHOD_TO(AdminController::updateCategory,
"/api/v1/admin/categories/{1}", "/api/v1/admin/categories/{1}",
drogon::Patch); drogon::Patch);
ADD_METHOD_TO(AdminController::deleteCategory,
"/api/v1/admin/categories/{1}",
drogon::Delete);
ADD_METHOD_TO(AdminController::updateArea, ADD_METHOD_TO(AdminController::updateArea,
"/api/v1/admin/areas/{1}", "/api/v1/admin/areas/{1}",
drogon::Patch); drogon::Patch);
ADD_METHOD_TO(AdminController::deleteArea,
"/api/v1/admin/areas/{1}",
drogon::Delete);
ADD_METHOD_TO(AdminController::updateTeam, ADD_METHOD_TO(AdminController::updateTeam,
"/api/v1/admin/teams/{1}", "/api/v1/admin/teams/{1}",
drogon::Patch); drogon::Patch);
ADD_METHOD_TO(AdminController::deleteTeam,
"/api/v1/admin/teams/{1}",
drogon::Delete);
ADD_METHOD_TO(AdminController::overviewStats, ADD_METHOD_TO(AdminController::overviewStats,
"/api/v1/admin/stats/overview", "/api/v1/admin/stats/overview",
drogon::Get); drogon::Get);
@@ -51,14 +60,26 @@ class AdminController : public drogon::HttpController<AdminController>
const drogon::HttpRequestPtr &req, const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback, std::function<void(const drogon::HttpResponsePtr &)> &&callback,
long long categoryId); long long categoryId);
void deleteCategory(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
long long categoryId);
void updateArea( void updateArea(
const drogon::HttpRequestPtr &req, const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback, std::function<void(const drogon::HttpResponsePtr &)> &&callback,
long long areaId); long long areaId);
void deleteArea(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
long long areaId);
void updateTeam( void updateTeam(
const drogon::HttpRequestPtr &req, const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback, std::function<void(const drogon::HttpResponsePtr &)> &&callback,
long long teamId); long long teamId);
void deleteTeam(
const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback,
long long teamId);
void overviewStats( void overviewStats(
const drogon::HttpRequestPtr &req, const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback); std::function<void(const drogon::HttpResponsePtr &)> &&callback);
+3
View File
@@ -111,11 +111,14 @@ Authorization: Bearer <token>
## Admin Reference Data ## Admin Reference Data
- `POST /admin/categories` - `POST /admin/categories`
- `PATCH /admin/categories/{id}` - `PATCH /admin/categories/{id}`
- `DELETE /admin/categories/{id}`
- `POST /admin/areas` - `POST /admin/areas`
- `PATCH /admin/areas/{id}` - `PATCH /admin/areas/{id}`
- `DELETE /admin/areas/{id}`
area polygon must stay inside allowed Bari municipal boundary area polygon must stay inside allowed Bari municipal boundary
- `POST /admin/teams` - `POST /admin/teams`
- `PATCH /admin/teams/{id}` - `PATCH /admin/teams/{id}`
- `DELETE /admin/teams/{id}`
- `GET /admin/users` - `GET /admin/users`
- `PATCH /admin/users/{id}/roles` - `PATCH /admin/users/{id}/roles`
- `PATCH /admin/users/{id}/ban` - `PATCH /admin/users/{id}/ban`
+1
View File
@@ -92,6 +92,7 @@ Schema truth is `migrations/*.sql`. This file is compact human map of current DB
- `reward_events(report_id, reason)` unique - `reward_events(report_id, reason)` unique
- `api_tokens.token_hash` unique - `api_tokens.token_hash` unique
- `intervention_access_links.token_hash` unique - `intervention_access_links.token_hash` unique
- category/team/area delete behavior is soft delete via `active = FALSE`
## Current Query Model ## Current Query Model
- No generated ORM models - No generated ORM models
+3
View File
@@ -79,9 +79,12 @@ moderate abuse, and inspect operational stats.
### Admin ### Admin
- create/update categories - create/update categories
- soft-delete categories
- create/update areas - create/update areas
area polygons must stay inside Bari municipal boundary area polygons must stay inside Bari municipal boundary
- soft-delete areas
- create/update teams - create/update teams
- soft-delete teams
- list users, update roles, ban/unban - list users, update roles, ban/unban
- global stats including area/category/team resolution aggregates - global stats including area/category/team resolution aggregates
+135
View File
@@ -1712,6 +1712,51 @@
} }
}, },
"/api/v1/admin/areas/{id}": { "/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": { "patch": {
"description": "Updates an existing municipal area.", "description": "Updates an existing municipal area.",
"operationId": "AdminController.updateArea", "operationId": "AdminController.updateArea",
@@ -1816,6 +1861,51 @@
} }
}, },
"/api/v1/admin/categories/{id}": { "/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": { "patch": {
"description": "Updates an existing report category.", "description": "Updates an existing report category.",
"operationId": "AdminController.updateCategory", "operationId": "AdminController.updateCategory",
@@ -2075,6 +2165,51 @@
} }
}, },
"/api/v1/admin/teams/{id}": { "/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": { "patch": {
"description": "Updates an existing intervention team.", "description": "Updates an existing intervention team.",
"operationId": "AdminController.updateTeam", "operationId": "AdminController.updateTeam",
+54
View File
@@ -374,6 +374,26 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user,
return success(); 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, Json::Value AdminService::updateCategory(const auth::UserContext &user,
long long categoryId, long long categoryId,
const Json::Value &body) const Json::Value &body)
@@ -411,6 +431,23 @@ Json::Value AdminService::updateCategory(const auth::UserContext &user,
return success(); 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, Json::Value AdminService::updateArea(const auth::UserContext &user,
long long areaId, long long areaId,
const Json::Value &body) const Json::Value &body)
@@ -458,6 +495,23 @@ Json::Value AdminService::updateArea(const auth::UserContext &user,
return success(); 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, Json::Value AdminService::updateTeam(const auth::UserContext &user,
long long teamId, long long teamId,
const Json::Value &body) const Json::Value &body)
+3
View File
@@ -24,6 +24,9 @@ class AdminService
Json::Value updateModeration(const auth::UserContext &user, Json::Value updateModeration(const auth::UserContext &user,
long long flagId, long long flagId,
const Json::Value &body); 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, Json::Value updateCategory(const auth::UserContext &user,
long long categoryId, long long categoryId,
const Json::Value &body); const Json::Value &body);