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"); return isAdmin(user) || hasPermission(user, "reports.manage");
} }
bool canOperateReports(const UserContext &user)
{
return !isAdmin(user) && canManageReports(user);
}
bool isAdmin(const UserContext &user) bool isAdmin(const UserContext &user)
{ {
return hasRole(user, "admin") || hasPermission(user, "admin.manage"); 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 hasRole(const UserContext &user, const std::string &role);
bool hasPermission(const UserContext &user, const std::string &permission); bool hasPermission(const UserContext &user, const std::string &permission);
bool canManageReports(const UserContext &user); bool canManageReports(const UserContext &user);
bool canOperateReports(const UserContext &user);
bool isAdmin(const UserContext &user); bool isAdmin(const UserContext &user);
bool mustChangePassword(const UserContext &user); bool mustChangePassword(const UserContext &user);
} // namespace visione::auth } // namespace visione::auth
+4 -3
View File
@@ -32,13 +32,14 @@ Authorization: Bearer <token>
- `GET /reports/{id}/intervention-summary`: operator-facing report summary with - `GET /reports/{id}/intervention-summary`: operator-facing report summary with
citizen contact data, media and latest messages. citizen contact data, media and latest messages.
- `PATCH /reports/{id}/status`: update status. - `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. - `POST /reports/{id}/messages`: write a report message.
- `GET /reports/{id}/messages`: list report messages. - `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}/assignments`: assign a team.
- `POST /reports/{id}/feedback`: add citizen feedback. - `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 - `POST /reports/{id}/media`: upload up to five multipart media files. Supported
types are JPEG, PNG, WebP, MP4, WebM and QuickTime. 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, - `citizen`: registers, requests OTP login, creates reports, follows own reports,
sends messages and leaves feedback after resolution. sends messages and leaves feedback after resolution.
- `operator`: views reports, takes open reports, assigns teams, updates managed - `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. - `admin`: manages users, roles, reference data, teams, areas and all reports.
The system admin seed must change password before using protected operational The system admin seed must change password before using protected operational
@@ -25,7 +26,7 @@ or administrative APIs.
Valid milestone 2 transitions: 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. - `UNDER_REVIEW -> ASSIGNED` when a team is assigned.
- `ASSIGNED -> IN_PROGRESS`. - `ASSIGNED -> IN_PROGRESS`.
- `IN_PROGRESS -> RESOLVED`. - `IN_PROGRESS -> RESOLVED`.
+3 -3
View File
@@ -3056,7 +3056,7 @@
}, },
"/api/v1/reports/{id}/flags": { "/api/v1/reports/{id}/flags": {
"post": { "post": {
"description": "Creates a moderation flag for a report.", "description": "Creates a moderation flag for a report by assigned operator only.",
"operationId": "ReportController.flagReport", "operationId": "ReportController.flagReport",
"parameters": [ "parameters": [
{ {
@@ -3313,7 +3313,7 @@
}, },
"/api/v1/reports/{id}/messages/{message_id}/flags": { "/api/v1/reports/{id}/messages/{message_id}/flags": {
"post": { "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", "operationId": "ReportController.flagMessage",
"parameters": [ "parameters": [
{ {
@@ -3436,7 +3436,7 @@
}, },
"/api/v1/reports/{id}/take": { "/api/v1/reports/{id}/take": {
"post": { "post": {
"description": "Assigns an open report to the current operator.", "description": "Assigns an open report to current non-admin operator.",
"operationId": "ReportController.takeReport", "operationId": "ReportController.takeReport",
"parameters": [ "parameters": [
{ {
+1
View File
@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <drogon/orm/Field.h>
#include <drogon/orm/Result.h> #include <drogon/orm/Result.h>
#include <json/json.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(); 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) bool ownsReport(const auth::UserContext &user, long long reportId)
{ {
const auto result = drogon::app().getDbClient()->execSqlSync( 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, Json::Value ReportService::takeReport(const auth::UserContext &user,
long long reportId) long long reportId)
{ {
if (!auth::canManageReports(user)) if (!auth::canOperateReports(user))
{ {
return failure("forbidden"); return failure("forbidden");
} }
@@ -737,6 +751,10 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
return failure("team_not_found"); return failure("team_not_found");
} }
db->execSqlSync(
"UPDATE assignments SET ended_at = NOW() "
"WHERE report_id = ? AND ended_at IS NULL",
reportId);
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) "
"VALUES (?, ?, ?)", "VALUES (?, ?, ?)",
@@ -814,16 +832,7 @@ Json::Value ReportService::flagReport(const auth::UserContext &user,
return failure("invalid_flag"); return failure("invalid_flag");
} }
const auto owner = reportOwner(reportId); if (!canFlagReportContent(user, 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"); return failure("forbidden");
} }
@@ -853,7 +862,7 @@ Json::Value ReportService::flagMessage(const auth::UserContext &user,
{ {
return failure("invalid_flag"); return failure("invalid_flag");
} }
if (!canInteractWithReport(user, reportId)) if (!canFlagReportContent(user, reportId))
{ {
return failure("forbidden"); return failure("forbidden");
} }