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. - 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. - 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 ## Authoritative References
- Local Drogon source: `/home/gabdevele/Projects/snitchitaly/backend/drogon` - Local Drogon source: `/home/gabdevele/Projects/snitchitaly/backend/drogon`
- Official Drogon docs: `https://drogonframework.github.io/drogon-docs/` - 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 error = body.get("error", "").asString();
const auto status = error == "forbidden" ? drogon::k403Forbidden drogon::HttpStatusCode status = drogon::k400BadRequest;
: 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)); callback(visione::http::json(std::move(body), status));
} }
} // namespace } // namespace
+14 -2
View File
@@ -58,8 +58,20 @@ void sendService(Json::Value body,
return; return;
} }
const auto error = body.get("error", "").asString(); const auto error = body.get("error", "").asString();
const auto status = error == "forbidden" ? drogon::k403Forbidden drogon::HttpStatusCode status = drogon::k400BadRequest;
: 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)); 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"); 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"]) for (const auto &role : body["roles"])
{ {
if (!role.isString()) if (!role.isString())
@@ -378,10 +379,15 @@ Json::Value AdminService::updateUserRoles(const auth::UserContext &user,
{ {
return failure("role_not_found"); return failure("role_not_found");
} }
db->execSqlSync( roleIds.push_back(roleRows[0]["id"].as<long long>());
"INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)", }
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, targetUserId,
roleRows[0]["id"].as<long long>()); roleId);
} }
return success(); 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); 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) int boundedPage(const std::optional<std::string> &value)
{ {
if (!value) if (!value)
@@ -257,6 +262,11 @@ void notifyReportOwner(long long reportId,
Json::Value ReportService::createReport(const auth::UserContext &user, Json::Value ReportService::createReport(const auth::UserContext &user,
const Json::Value &body) const Json::Value &body)
{ {
if (!canCreateCitizenReport(user))
{
return failure("forbidden");
}
const auto missing = const auto missing =
validation::missingStrings(body, {"address", "description"}); validation::missingStrings(body, {"address", "description"});
if (!missing.empty() || !validation::hasNumber(body, "category_id") || if (!missing.empty() || !validation::hasNumber(body, "category_id") ||
@@ -318,8 +328,13 @@ Json::Value ReportService::createReport(const auth::UserContext &user,
if (body["media"].isArray()) if (body["media"].isArray())
{ {
size_t mediaCount = 0;
for (const auto &item : body["media"]) for (const auto &item : body["media"])
{ {
if (mediaCount >= 5)
{
break;
}
if (item.isString() && !item.asString().empty()) if (item.isString() && !item.asString().empty())
{ {
db->execSqlSync( db->execSqlSync(
@@ -327,6 +342,7 @@ Json::Value ReportService::createReport(const auth::UserContext &user,
"VALUES (?, ?, 'IMAGE')", "VALUES (?, ?, 'IMAGE')",
reportId, reportId,
item.asString()); item.asString());
++mediaCount;
} }
} }
} }
@@ -713,6 +729,13 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
{ {
return failure("invalid_status_transition"); 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( db->execSqlSync(
"INSERT INTO assignments (report_id, team_id, assigned_by_user_id) " "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, long long reportId,
const Json::Value &body) const Json::Value &body)
{ {
if (!canAccessReport(user, reportId) || if (!validation::hasString(body, "reason"))
!validation::hasString(body, "reason"))
{ {
return failure("invalid_flag"); 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(); auto db = drogon::app().getDbClient();
db->execSqlSync( db->execSqlSync(
"INSERT INTO moderation_flags " "INSERT INTO moderation_flags "
@@ -812,20 +849,27 @@ Json::Value ReportService::flagMessage(const auth::UserContext &user,
long long messageId, long long messageId,
const Json::Value &body) const Json::Value &body)
{ {
if (!canInteractWithReport(user, reportId) || if (!validation::hasString(body, "reason"))
!validation::hasString(body, "reason"))
{ {
return failure("invalid_flag"); return failure("invalid_flag");
} }
if (!canInteractWithReport(user, reportId))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient(); auto db = drogon::app().getDbClient();
const auto message = db->execSqlSync( 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, messageId,
reportId); reportId);
if (message.empty()) if (message.empty())
{ {
return failure("message_not_found"); return failure("message_not_found");
} }
if (message[0]["user_id"].as<long long>() == user.id)
{
return failure("cannot_flag_own_message");
}
db->execSqlSync( db->execSqlSync(
"INSERT INTO moderation_flags " "INSERT INTO moderation_flags "
"(content_type, report_id, message_id, reason, details, created_by_user_id) " "(content_type, report_id, message_id, reason, details, created_by_user_id) "