feat: add canOperateReports and canFlagReportContent functions for report management

This commit is contained in:
gabdevele
2026-06-14 21:49:53 +02:00 Unverified
parent ca09e7b6c8
commit cb922baa46
7 changed files with 38 additions and 20 deletions
+5
View File
@@ -113,6 +113,11 @@ bool canManageReports(const UserContext &user)
return isAdmin(user) || hasPermission(user, "reports.manage");
}
bool canOperateReports(const UserContext &user)
{
return !isAdmin(user) && canManageReports(user);
}
bool isAdmin(const UserContext &user)
{
return hasRole(user, "admin") || hasPermission(user, "admin.manage");
+1
View File
@@ -23,6 +23,7 @@ std::optional<UserContext> authenticate(const drogon::HttpRequestPtr &request);
bool hasRole(const UserContext &user, const std::string &role);
bool hasPermission(const UserContext &user, const std::string &permission);
bool canManageReports(const UserContext &user);
bool canOperateReports(const UserContext &user);
bool isAdmin(const UserContext &user);
bool mustChangePassword(const UserContext &user);
} // namespace visione::auth
+4 -3
View File
@@ -32,13 +32,14 @@ Authorization: Bearer <token>
- `GET /reports/{id}/intervention-summary`: operator-facing report summary with
citizen contact data, media and latest messages.
- `PATCH /reports/{id}/status`: update status.
- `POST /reports/{id}/take`: operator/admin takes an open report.
- `POST /reports/{id}/take`: assigned-queue operator takes an open report.
- `POST /reports/{id}/messages`: write a report message.
- `GET /reports/{id}/messages`: list report messages.
- `POST /reports/{id}/messages/{message_id}/flags`: flag a message for moderation.
- `POST /reports/{id}/messages/{message_id}/flags`: assigned operator flags a
report message for moderation.
- `POST /reports/{id}/assignments`: assign a team.
- `POST /reports/{id}/feedback`: add citizen feedback.
- `POST /reports/{id}/flags`: flag a report for moderation.
- `POST /reports/{id}/flags`: assigned operator flags a report for moderation.
- `POST /reports/{id}/media`: upload up to five multipart media files. Supported
types are JPEG, PNG, WebP, MP4, WebM and QuickTime.
+3 -2
View File
@@ -8,7 +8,8 @@ assign and resolve interventions while preserving history and basic analytics.
- `citizen`: registers, requests OTP login, creates reports, follows own reports,
sends messages and leaves feedback after resolution.
- `operator`: views reports, takes open reports, assigns teams, updates managed
reports and messages citizens.
reports, messages citizens and flags abusive/fake citizen content on managed
reports.
- `admin`: manages users, roles, reference data, teams, areas and all reports.
The system admin seed must change password before using protected operational
@@ -25,7 +26,7 @@ or administrative APIs.
Valid milestone 2 transitions:
- `OPEN -> UNDER_REVIEW` when an operator/admin takes the report.
- `OPEN -> UNDER_REVIEW` when an operator takes the report.
- `UNDER_REVIEW -> ASSIGNED` when a team is assigned.
- `ASSIGNED -> IN_PROGRESS`.
- `IN_PROGRESS -> RESOLVED`.
+3 -3
View File
@@ -3056,7 +3056,7 @@
},
"/api/v1/reports/{id}/flags": {
"post": {
"description": "Creates a moderation flag for a report.",
"description": "Creates a moderation flag for a report by assigned operator only.",
"operationId": "ReportController.flagReport",
"parameters": [
{
@@ -3313,7 +3313,7 @@
},
"/api/v1/reports/{id}/messages/{message_id}/flags": {
"post": {
"description": "Creates a moderation flag for a report message.",
"description": "Creates a moderation flag for a report message by assigned operator only.",
"operationId": "ReportController.flagMessage",
"parameters": [
{
@@ -3436,7 +3436,7 @@
},
"/api/v1/reports/{id}/take": {
"post": {
"description": "Assigns an open report to the current operator.",
"description": "Assigns an open report to current non-admin operator.",
"operationId": "ReportController.takeReport",
"parameters": [
{
+1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <drogon/orm/Field.h>
#include <drogon/orm/Result.h>
#include <json/json.h>
+21 -12
View File
@@ -109,6 +109,20 @@ bool canModifyReport(const auth::UserContext &user, long long reportId)
return !result.empty();
}
bool canFlagReportContent(const auth::UserContext &user, long long reportId)
{
if (!auth::canOperateReports(user))
{
return false;
}
const auto result = drogon::app().getDbClient()->execSqlSync(
"SELECT id FROM reports WHERE id = ? AND assigned_operator_id = ?",
reportId,
user.id);
return !result.empty();
}
bool ownsReport(const auth::UserContext &user, long long reportId)
{
const auto result = drogon::app().getDbClient()->execSqlSync(
@@ -616,7 +630,7 @@ Json::Value ReportService::updateStatus(const auth::UserContext &user,
Json::Value ReportService::takeReport(const auth::UserContext &user,
long long reportId)
{
if (!auth::canManageReports(user))
if (!auth::canOperateReports(user))
{
return failure("forbidden");
}
@@ -737,6 +751,10 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
return failure("team_not_found");
}
db->execSqlSync(
"UPDATE assignments SET ended_at = NOW() "
"WHERE report_id = ? AND ended_at IS NULL",
reportId);
db->execSqlSync(
"INSERT INTO assignments (report_id, team_id, assigned_by_user_id) "
"VALUES (?, ?, ?)",
@@ -814,16 +832,7 @@ Json::Value ReportService::flagReport(const auth::UserContext &user,
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))
if (!canFlagReportContent(user, reportId))
{
return failure("forbidden");
}
@@ -853,7 +862,7 @@ Json::Value ReportService::flagMessage(const auth::UserContext &user,
{
return failure("invalid_flag");
}
if (!canInteractWithReport(user, reportId))
if (!canFlagReportContent(user, reportId))
{
return failure("forbidden");
}