feat: enhance error handling and permissions in Admin and Report services
This commit is contained in:
@@ -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/`
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) "
|
||||||
|
|||||||
Reference in New Issue
Block a user