From ca09e7b6c87a34e46d6d4cda2438467511393cc4 Mon Sep 17 00:00:00 2001 From: gabdevele Date: Sun, 14 Jun 2026 21:37:12 +0200 Subject: [PATCH] feat: enhance error handling and permissions in Admin and Report services --- AGENTS.md | 6 ++++ controllers/AdminController.cc | 15 +++++++-- controllers/ReportController.cc | 16 ++++++++-- services/AdminService.cc | 16 +++++++--- services/ReportService.cc | 54 ++++++++++++++++++++++++++++++--- 5 files changed, 93 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bccfa40..71128ac 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,6 +6,12 @@ - User-facing product labels may be Italian when they represent UI/business vocabulary. - Docker is for local external services only: MySQL, Mailpit and future Redis-like dependencies. +ENVIRONMENT: +You are running in a docker container, so it's normal if something doesn't work as expected. +You are allowed to install new tools, libraries or dependencies if you think they are necessary to complete your task. +Since you are running in a docker container, you could have some build issues sometimes, do not waste tokens over-trying to fix them, just report the issue and move on to the next task if you believe the issue is not related to your task. +Prefer using commands to read files, search for patterns ecc... since I have installed a tool that helps reduce token usage for those operations. + ## Authoritative References - Local Drogon source: `/home/gabdevele/Projects/snitchitaly/backend/drogon` - Official Drogon docs: `https://drogonframework.github.io/drogon-docs/` diff --git a/controllers/AdminController.cc b/controllers/AdminController.cc index b979100..b5fa34d 100644 --- a/controllers/AdminController.cc +++ b/controllers/AdminController.cc @@ -56,8 +56,19 @@ void sendAdmin(Json::Value body, } const auto error = body.get("error", "").asString(); - const auto status = error == "forbidden" ? drogon::k403Forbidden - : drogon::k400BadRequest; + drogon::HttpStatusCode status = drogon::k400BadRequest; + if (error == "forbidden" || error.rfind("cannot_", 0) == 0) + { + status = drogon::k403Forbidden; + } + else if (error.ends_with("_not_found")) + { + status = drogon::k404NotFound; + } + else if (error == "invalid_status_transition") + { + status = drogon::k409Conflict; + } callback(visione::http::json(std::move(body), status)); } } // namespace diff --git a/controllers/ReportController.cc b/controllers/ReportController.cc index 3f0c04b..6d36e65 100644 --- a/controllers/ReportController.cc +++ b/controllers/ReportController.cc @@ -58,8 +58,20 @@ void sendService(Json::Value body, return; } const auto error = body.get("error", "").asString(); - const auto status = error == "forbidden" ? drogon::k403Forbidden - : drogon::k400BadRequest; + drogon::HttpStatusCode status = drogon::k400BadRequest; + if (error == "forbidden" || error.rfind("cannot_", 0) == 0) + { + status = drogon::k403Forbidden; + } + else if (error.ends_with("_not_found")) + { + status = drogon::k404NotFound; + } + else if (error == "invalid_status_transition" || + error == "media_limit_reached") + { + status = drogon::k409Conflict; + } callback(visione::http::json(std::move(body), status)); } diff --git a/services/AdminService.cc b/services/AdminService.cc index 737225a..24cc6af 100644 --- a/services/AdminService.cc +++ b/services/AdminService.cc @@ -364,7 +364,8 @@ Json::Value AdminService::updateUserRoles(const auth::UserContext &user, return failure("user_not_found"); } - db->execSqlSync("DELETE FROM user_roles WHERE user_id = ?", targetUserId); + std::vector roleIds; + roleIds.reserve(body["roles"].size()); for (const auto &role : body["roles"]) { if (!role.isString()) @@ -378,10 +379,15 @@ Json::Value AdminService::updateUserRoles(const auth::UserContext &user, { return failure("role_not_found"); } - db->execSqlSync( - "INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)", - targetUserId, - roleRows[0]["id"].as()); + roleIds.push_back(roleRows[0]["id"].as()); + } + + db->execSqlSync("DELETE FROM user_roles WHERE user_id = ?", targetUserId); + for (const auto roleId : roleIds) + { + db->execSqlSync("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)", + targetUserId, + roleId); } return success(); } diff --git a/services/ReportService.cc b/services/ReportService.cc index 757085b..44e833a 100644 --- a/services/ReportService.cc +++ b/services/ReportService.cc @@ -123,6 +123,11 @@ bool canInteractWithReport(const auth::UserContext &user, long long reportId) return ownsReport(user, reportId) || canModifyReport(user, reportId); } +bool canCreateCitizenReport(const auth::UserContext &user) +{ + return !auth::canManageReports(user); +} + int boundedPage(const std::optional &value) { if (!value) @@ -257,6 +262,11 @@ void notifyReportOwner(long long reportId, Json::Value ReportService::createReport(const auth::UserContext &user, const Json::Value &body) { + if (!canCreateCitizenReport(user)) + { + return failure("forbidden"); + } + const auto missing = validation::missingStrings(body, {"address", "description"}); if (!missing.empty() || !validation::hasNumber(body, "category_id") || @@ -318,8 +328,13 @@ Json::Value ReportService::createReport(const auth::UserContext &user, if (body["media"].isArray()) { + size_t mediaCount = 0; for (const auto &item : body["media"]) { + if (mediaCount >= 5) + { + break; + } if (item.isString() && !item.asString().empty()) { db->execSqlSync( @@ -327,6 +342,7 @@ Json::Value ReportService::createReport(const auth::UserContext &user, "VALUES (?, ?, 'IMAGE')", reportId, item.asString()); + ++mediaCount; } } } @@ -713,6 +729,13 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user, { return failure("invalid_status_transition"); } + const auto team = db->execSqlSync( + "SELECT id FROM teams WHERE id = ? AND active = TRUE", + body["team_id"].asInt64()); + if (team.empty()) + { + return failure("team_not_found"); + } db->execSqlSync( "INSERT INTO assignments (report_id, team_id, assigned_by_user_id) " @@ -786,11 +809,25 @@ Json::Value ReportService::flagReport(const auth::UserContext &user, long long reportId, const Json::Value &body) { - if (!canAccessReport(user, reportId) || - !validation::hasString(body, "reason")) + if (!validation::hasString(body, "reason")) { return failure("invalid_flag"); } + + const auto owner = reportOwner(reportId); + if (!owner) + { + return failure("report_not_found"); + } + if (*owner == user.id) + { + return failure("cannot_flag_own_report"); + } + if (!canAccessReport(user, reportId)) + { + return failure("forbidden"); + } + auto db = drogon::app().getDbClient(); db->execSqlSync( "INSERT INTO moderation_flags " @@ -812,20 +849,27 @@ Json::Value ReportService::flagMessage(const auth::UserContext &user, long long messageId, const Json::Value &body) { - if (!canInteractWithReport(user, reportId) || - !validation::hasString(body, "reason")) + if (!validation::hasString(body, "reason")) { return failure("invalid_flag"); } + if (!canInteractWithReport(user, reportId)) + { + return failure("forbidden"); + } auto db = drogon::app().getDbClient(); const auto message = db->execSqlSync( - "SELECT id FROM messages WHERE id = ? AND report_id = ?", + "SELECT id, user_id FROM messages WHERE id = ? AND report_id = ?", messageId, reportId); if (message.empty()) { return failure("message_not_found"); } + if (message[0]["user_id"].as() == user.id) + { + return failure("cannot_flag_own_message"); + } db->execSqlSync( "INSERT INTO moderation_flags " "(content_type, report_id, message_id, reason, details, created_by_user_id) "