From cb922baa4657067298613e28edfdb811ca35c17e Mon Sep 17 00:00:00 2001 From: gabdevele Date: Sun, 14 Jun 2026 21:49:53 +0200 Subject: [PATCH] feat: add canOperateReports and canFlagReportContent functions for report management --- common/Auth.cc | 5 +++++ common/Auth.h | 1 + docs/API_SPEC.md | 7 ++++--- docs/PROJECT_SPEC.md | 5 +++-- docs/openapi/visione.openapi.yaml | 6 +++--- repositories/Sql.h | 1 + services/ReportService.cc | 33 ++++++++++++++++++++----------- 7 files changed, 38 insertions(+), 20 deletions(-) diff --git a/common/Auth.cc b/common/Auth.cc index 0a44276..272ffa2 100644 --- a/common/Auth.cc +++ b/common/Auth.cc @@ -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"); diff --git a/common/Auth.h b/common/Auth.h index df4fe9a..dcfb530 100644 --- a/common/Auth.h +++ b/common/Auth.h @@ -23,6 +23,7 @@ std::optional 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 diff --git a/docs/API_SPEC.md b/docs/API_SPEC.md index 53d6d44..f557f3a 100644 --- a/docs/API_SPEC.md +++ b/docs/API_SPEC.md @@ -32,13 +32,14 @@ Authorization: Bearer - `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. diff --git a/docs/PROJECT_SPEC.md b/docs/PROJECT_SPEC.md index bee95fb..c763d62 100644 --- a/docs/PROJECT_SPEC.md +++ b/docs/PROJECT_SPEC.md @@ -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`. diff --git a/docs/openapi/visione.openapi.yaml b/docs/openapi/visione.openapi.yaml index c9a432d..5f47d94 100644 --- a/docs/openapi/visione.openapi.yaml +++ b/docs/openapi/visione.openapi.yaml @@ -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": [ { diff --git a/repositories/Sql.h b/repositories/Sql.h index 66e862d..9034154 100644 --- a/repositories/Sql.h +++ b/repositories/Sql.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include diff --git a/services/ReportService.cc b/services/ReportService.cc index 44e833a..490481b 100644 --- a/services/ReportService.cc +++ b/services/ReportService.cc @@ -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"); }