feat: enhance error handling and permissions in Admin and Report services

This commit is contained in:
gabdevele
2026-06-14 21:37:12 +02:00 Unverified
parent 002f89bd73
commit ca09e7b6c8
5 changed files with 93 additions and 14 deletions
+6
View File
@@ -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/`
+13 -2
View File
@@ -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
+14 -2
View File
@@ -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));
}
+10 -4
View File
@@ -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<long long> 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 (?, ?)",
roleIds.push_back(roleRows[0]["id"].as<long long>());
}
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,
roleRows[0]["id"].as<long long>());
roleId);
}
return success();
}
+49 -5
View File
@@ -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<std::string> &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<long long>() == 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) "