feat: enhance Admin and report services with CRUD operations and intervention links

This commit is contained in:
gabdevele
2026-06-15 11:47:44 +02:00 Unverified
parent d684477ac3
commit 170fadbfe0
13 changed files with 1258 additions and 164 deletions
+203 -36
View File
@@ -82,24 +82,24 @@ const JsonRowSchema &adminUserSchema()
{"must_change_password", JsonFieldType::Bool}};
return schema;
}
} // namespace
Json::Value AdminService::listCategories()
bool isActiveField(const Json::Value &body)
{
auto response = success();
response["categories"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync(
"SELECT * FROM report_categories ORDER BY name"));
return response;
return body.isMember("active") && body["active"].isBool();
}
} // namespace
Json::Value AdminService::createCategory(const auth::UserContext &user,
const Json::Value &body)
{
if (!auth::isAdmin(user) || !validation::hasString(body, "name"))
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
if (!validation::hasString(body, "name"))
{
return failure("invalid_category");
}
drogon::app().getDbClient()->execSqlSync(
"INSERT INTO report_categories (name, description) VALUES (?, ?)",
@@ -108,19 +108,14 @@ Json::Value AdminService::createCategory(const auth::UserContext &user,
return success();
}
Json::Value AdminService::listAreas()
{
auto response = success();
response["areas"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync("SELECT * FROM areas ORDER BY name"));
return response;
}
Json::Value AdminService::createArea(const auth::UserContext &user,
const Json::Value &body)
{
if (!auth::isAdmin(user) || !validation::hasString(body, "name") ||
!body["boundary_geojson"].isObject())
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
if (!validation::hasString(body, "name") || !body["boundary_geojson"].isObject())
{
return failure("invalid_area");
}
@@ -135,29 +130,38 @@ Json::Value AdminService::createArea(const auth::UserContext &user,
return success();
}
Json::Value AdminService::listTeams()
{
auto response = success();
response["teams"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync(
"SELECT t.*, tc.name AS category_name, a.name AS area_name "
"FROM teams t JOIN team_categories tc ON tc.id = t.team_category_id "
"LEFT JOIN areas a ON a.id = t.area_id ORDER BY t.name"));
return response;
}
Json::Value AdminService::createTeam(const auth::UserContext &user,
const Json::Value &body)
{
if (!auth::isAdmin(user) || !validation::hasString(body, "name") ||
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
if (!validation::hasString(body, "name") ||
!validation::hasNumber(body, "team_category_id"))
{
return failure("invalid_team");
}
auto db = drogon::app().getDbClient();
const auto teamCategoryRows = db->execSqlSync(
"SELECT id FROM team_categories WHERE id = ?",
body["team_category_id"].asInt64());
if (teamCategoryRows.empty())
{
return failure("invalid_reference");
}
if (body["area_id"].isNumeric())
{
drogon::app().getDbClient()->execSqlSync(
const auto areaRows = db->execSqlSync(
"SELECT id FROM areas WHERE id = ?",
body["area_id"].asInt64());
if (areaRows.empty())
{
return failure("invalid_reference");
}
db->execSqlSync(
"INSERT INTO teams (team_category_id, area_id, name, description) "
"VALUES (?, ?, ?, ?)",
body["team_category_id"].asInt64(),
@@ -167,7 +171,7 @@ Json::Value AdminService::createTeam(const auth::UserContext &user,
}
else
{
drogon::app().getDbClient()->execSqlSync(
db->execSqlSync(
"INSERT INTO teams (team_category_id, name, description) VALUES (?, ?, ?)",
body["team_category_id"].asInt64(),
body["name"].asString(),
@@ -198,10 +202,21 @@ Json::Value AdminService::overviewStats(const auth::UserContext &user)
"SELECT c.name AS category, COUNT(*) AS total "
"FROM reports r JOIN report_categories c ON c.id = r.category_id "
"GROUP BY c.name ORDER BY total DESC"));
response["reports_by_team"] = repositories::resultToJson(db->execSqlSync(
"SELECT t.name AS team, COUNT(*) AS total "
response["assignments_by_team"] = repositories::resultToJson(db->execSqlSync(
"SELECT t.name AS team, COUNT(DISTINCT ass.id) AS total "
"FROM assignments ass JOIN teams t ON t.id = ass.team_id "
"GROUP BY t.name ORDER BY total DESC"));
response["resolved_reports_by_team"] = repositories::resultToJson(db->execSqlSync(
"SELECT t.name AS team, COUNT(DISTINCT ass.report_id) AS total "
"FROM assignments ass "
"JOIN teams t ON t.id = ass.team_id "
"JOIN reports r ON r.id = ass.report_id "
"WHERE r.status IN ('RESOLVED', 'CLOSED') "
"AND NOT EXISTS ("
" SELECT 1 FROM assignments ass2 "
" WHERE ass2.report_id = ass.report_id AND ass2.assigned_at > ass.assigned_at"
") "
"GROUP BY t.name ORDER BY total DESC"));
response["monthly_reports"] = repositories::resultToJson(db->execSqlSync(
"SELECT DATE_FORMAT(created_at, '%Y-%m') AS month, COUNT(*) AS total "
"FROM reports GROUP BY DATE_FORMAT(created_at, '%Y-%m') "
@@ -229,7 +244,7 @@ Json::Value AdminService::listModeration(
const std::optional<std::string> &page,
const std::optional<std::string> &perPage)
{
if (!auth::canManageReports(user))
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
@@ -262,7 +277,7 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user,
long long flagId,
const Json::Value &body)
{
if (!auth::canManageReports(user))
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
@@ -324,6 +339,158 @@ Json::Value AdminService::updateModeration(const auth::UserContext &user,
return success();
}
Json::Value AdminService::updateCategory(const auth::UserContext &user,
long long categoryId,
const Json::Value &body)
{
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient();
const auto rows = db->execSqlSync(
"SELECT id FROM report_categories WHERE id = ?",
categoryId);
if (rows.empty())
{
return failure("category_not_found");
}
if (validation::hasString(body, "name"))
{
db->execSqlSync("UPDATE report_categories SET name = ? WHERE id = ?",
body["name"].asString(),
categoryId);
}
if (body.isMember("description") && body["description"].isString())
{
db->execSqlSync("UPDATE report_categories SET description = ? WHERE id = ?",
body["description"].asString(),
categoryId);
}
if (isActiveField(body))
{
db->execSqlSync("UPDATE report_categories SET active = ? WHERE id = ?",
body["active"].asBool(),
categoryId);
}
return success();
}
Json::Value AdminService::updateArea(const auth::UserContext &user,
long long areaId,
const Json::Value &body)
{
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient();
const auto rows = db->execSqlSync("SELECT id FROM areas WHERE id = ?", areaId);
if (rows.empty())
{
return failure("area_not_found");
}
if (validation::hasString(body, "name"))
{
db->execSqlSync("UPDATE areas SET name = ? WHERE id = ?",
body["name"].asString(),
areaId);
}
if (body.isMember("description") && body["description"].isString())
{
db->execSqlSync("UPDATE areas SET description = ? WHERE id = ?",
body["description"].asString(),
areaId);
}
if (isActiveField(body))
{
db->execSqlSync("UPDATE areas SET active = ? WHERE id = ?",
body["active"].asBool(),
areaId);
}
if (body["boundary_geojson"].isObject())
{
Json::StreamWriterBuilder writer;
db->execSqlSync(
"UPDATE areas SET boundary_geojson = CAST(? AS JSON) WHERE id = ?",
Json::writeString(writer, body["boundary_geojson"]),
areaId);
}
return success();
}
Json::Value AdminService::updateTeam(const auth::UserContext &user,
long long teamId,
const Json::Value &body)
{
if (!auth::isAdmin(user))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient();
const auto rows = db->execSqlSync("SELECT id FROM teams WHERE id = ?", teamId);
if (rows.empty())
{
return failure("team_not_found");
}
if (validation::hasString(body, "name"))
{
db->execSqlSync("UPDATE teams SET name = ? WHERE id = ?",
body["name"].asString(),
teamId);
}
if (body.isMember("description") && body["description"].isString())
{
db->execSqlSync("UPDATE teams SET description = ? WHERE id = ?",
body["description"].asString(),
teamId);
}
if (validation::hasNumber(body, "team_category_id"))
{
const auto categoryRows = db->execSqlSync(
"SELECT id FROM team_categories WHERE id = ?",
body["team_category_id"].asInt64());
if (categoryRows.empty())
{
return failure("invalid_reference");
}
db->execSqlSync("UPDATE teams SET team_category_id = ? WHERE id = ?",
body["team_category_id"].asInt64(),
teamId);
}
if (body.isMember("area_id"))
{
if (body["area_id"].isNull())
{
db->execSqlSync("UPDATE teams SET area_id = NULL WHERE id = ?", teamId);
}
else if (body["area_id"].isNumeric())
{
const auto areaRows =
db->execSqlSync("SELECT id FROM areas WHERE id = ?",
body["area_id"].asInt64());
if (areaRows.empty())
{
return failure("invalid_reference");
}
db->execSqlSync("UPDATE teams SET area_id = ? WHERE id = ?",
body["area_id"].asInt64(),
teamId);
}
else
{
return failure("invalid_reference");
}
}
if (isActiveField(body))
{
db->execSqlSync("UPDATE teams SET active = ? WHERE id = ?",
body["active"].asBool(),
teamId);
}
return success();
}
Json::Value AdminService::listUsers(const auth::UserContext &user)
{
if (!auth::isAdmin(user))
+9 -3
View File
@@ -12,11 +12,8 @@ namespace visione::services
class AdminService
{
public:
Json::Value listCategories();
Json::Value createCategory(const auth::UserContext &user, const Json::Value &body);
Json::Value listAreas();
Json::Value createArea(const auth::UserContext &user, const Json::Value &body);
Json::Value listTeams();
Json::Value createTeam(const auth::UserContext &user, const Json::Value &body);
Json::Value overviewStats(const auth::UserContext &user);
Json::Value listUsers(const auth::UserContext &user);
@@ -27,6 +24,15 @@ class AdminService
Json::Value updateModeration(const auth::UserContext &user,
long long flagId,
const Json::Value &body);
Json::Value updateCategory(const auth::UserContext &user,
long long categoryId,
const Json::Value &body);
Json::Value updateArea(const auth::UserContext &user,
long long areaId,
const Json::Value &body);
Json::Value updateTeam(const auth::UserContext &user,
long long teamId,
const Json::Value &body);
Json::Value updateUserRoles(const auth::UserContext &user,
long long targetUserId,
const Json::Value &body);
+312 -13
View File
@@ -1,5 +1,6 @@
#include "services/ReportService.h"
#include "common/Crypto.h"
#include "common/GeoJson.h"
#include "common/Validation.h"
#include "repositories/Sql.h"
@@ -11,6 +12,7 @@
#include <set>
#include <sstream>
#include <algorithm>
#include <cctype>
namespace visione::services
{
@@ -34,6 +36,14 @@ Json::Value success()
return body;
}
Json::Value successWithLink(const std::string &url, const std::string &expiresAt)
{
auto body = success();
body["url"] = url;
body["expires_at"] = expiresAt;
return body;
}
bool isAllowedStatus(const std::string &status)
{
static const std::set<std::string> statuses = {
@@ -56,6 +66,15 @@ bool canTransition(const std::string &from, const std::string &to)
return it != transitions.end() && it->second.contains(to);
}
std::string maskedLastName(const std::string &lastName)
{
if (lastName.empty())
{
return {};
}
return std::string(1, static_cast<char>(std::toupper(lastName.front()))) + ".";
}
std::optional<long long> findArea(double latitude, double longitude)
{
const auto result = drogon::app().getDbClient()->execSqlSync(
@@ -109,6 +128,27 @@ bool canModifyReport(const auth::UserContext &user, long long reportId)
return !result.empty();
}
bool canOwnOrModifyReport(const auth::UserContext &user, long long reportId)
{
if (auth::isAdmin(user))
{
return true;
}
if (!auth::canOperateReports(user))
{
return false;
}
const auto result = drogon::app().getDbClient()->execSqlSync(
"SELECT assigned_operator_id FROM reports WHERE id = ?",
reportId);
if (result.empty())
{
return false;
}
return result[0]["assigned_operator_id"].isNull() ||
result[0]["assigned_operator_id"].as<long long>() == user.id;
}
bool canFlagReportContent(const auth::UserContext &user, long long reportId)
{
if (!auth::canOperateReports(user))
@@ -260,6 +300,27 @@ std::optional<long long> assignedOperator(long long reportId)
return rows[0]["assigned_operator_id"].as<long long>();
}
std::optional<long long> activeAssignmentId(long long reportId)
{
const auto rows = drogon::app().getDbClient()->execSqlSync(
"SELECT id FROM assignments WHERE report_id = ? AND ended_at IS NULL "
"ORDER BY assigned_at DESC LIMIT 1",
reportId);
if (rows.empty())
{
return std::nullopt;
}
return rows[0]["id"].as<long long>();
}
void revokeActiveLinksForReport(long long reportId)
{
drogon::app().getDbClient()->execSqlSync(
"UPDATE intervention_access_links SET revoked_at = NOW() "
"WHERE report_id = ? AND revoked_at IS NULL AND expires_at > NOW()",
reportId);
}
void notifyReportOwner(long long reportId,
const std::string &type,
const std::string &title,
@@ -271,6 +332,50 @@ void notifyReportOwner(long long reportId,
UserService::createNotification(*owner, type, title, body, reportId);
}
}
Json::Value buildInterventionSummaryRow(const drogon::orm::Row &row,
const drogon::orm::Result &mediaRows,
const drogon::orm::Result &messageRows,
bool publicView)
{
auto response = success();
Json::Value summary = repositories::rowToJson(row, reportSchema());
summary["category_name"] = row["category_name"].as<std::string>();
if (!row["area_name"].isNull())
{
summary["area_name"] = row["area_name"].as<std::string>();
}
if (!row["assigned_team_name"].isNull())
{
summary["assigned_team_name"] = row["assigned_team_name"].as<std::string>();
}
if (publicView)
{
summary["citizen_name"] = row["first_name"].as<std::string>() + " " +
maskedLastName(row["last_name"].as<std::string>());
if (!row["phone"].isNull())
{
summary["phone"] = row["phone"].as<std::string>();
}
response["summary"] = std::move(summary);
response["media"] = repositories::resultToJson(mediaRows, reportMediaSchema());
response["latest_messages"] =
repositories::resultToJson(messageRows, messageSchema());
return response;
}
summary["first_name"] = row["first_name"].as<std::string>();
summary["last_name"] = row["last_name"].as<std::string>();
if (!row["phone"].isNull())
{
summary["phone"] = row["phone"].as<std::string>();
}
summary["email"] = row["email"].as<std::string>();
response["summary"] = std::move(summary);
response["media"] = repositories::resultToJson(mediaRows, reportMediaSchema());
response["latest_messages"] =
repositories::resultToJson(messageRows, messageSchema());
return response;
}
} // namespace
Json::Value ReportService::createReport(const auth::UserContext &user,
@@ -493,6 +598,44 @@ Json::Value ReportService::otherOperatorReports(
return pagedReportResponse(result, pageValue, perPageValue);
}
Json::Value ReportService::listCategories(const auth::UserContext &user)
{
if (user.id == 0)
{
return failure("forbidden");
}
auto response = success();
response["categories"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync(
"SELECT * FROM report_categories WHERE active = TRUE ORDER BY name"));
return response;
}
Json::Value ReportService::listAreas()
{
auto response = success();
response["areas"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync(
"SELECT * FROM areas WHERE active = TRUE ORDER BY name"));
return response;
}
Json::Value ReportService::listOperatorTeams(const auth::UserContext &user)
{
if (!auth::canManageReports(user))
{
return failure("forbidden");
}
auto response = success();
response["teams"] = repositories::resultToJson(
drogon::app().getDbClient()->execSqlSync(
"SELECT t.*, tc.name AS category_name, a.name AS area_name "
"FROM teams t JOIN team_categories tc ON tc.id = t.team_category_id "
"LEFT JOIN areas a ON a.id = t.area_id "
"WHERE t.active = TRUE ORDER BY t.name"));
return response;
}
Json::Value ReportService::reportDetail(const auth::UserContext &user,
long long reportId)
{
@@ -549,19 +692,64 @@ Json::Value ReportService::interventionSummary(const auth::UserContext &user,
{
return failure("report_not_found");
}
auto response = success();
response["summary"] = repositories::rowToJson(report[0], reportSchema());
response["media"] = repositories::resultToJson(db->execSqlSync(
return buildInterventionSummaryRow(report[0],
db->execSqlSync(
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
reportId),
reportMediaSchema());
response["latest_messages"] = repositories::resultToJson(db->execSqlSync(
db->execSqlSync(
"SELECT m.id, m.user_id, u.first_name, u.last_name, m.body, m.created_at "
"FROM messages m JOIN users u ON u.id = m.user_id "
"WHERE m.report_id = ? ORDER BY m.created_at DESC LIMIT 5",
reportId),
messageSchema());
return response;
false);
}
Json::Value ReportService::publicInterventionSummary(const std::string &token)
{
auto db = drogon::app().getDbClient();
const auto rows = db->execSqlSync(
"SELECT l.report_id "
"FROM intervention_access_links l "
"WHERE l.token_hash = ? AND l.revoked_at IS NULL AND l.expires_at > NOW() "
"LIMIT 1",
crypto::hashSecret(token));
if (rows.empty())
{
return failure("intervention_link_not_found");
}
const auto reportId = rows[0]["report_id"].as<long long>();
const auto report = db->execSqlSync(
"SELECT r.*, c.name AS category_name, a.name AS area_name, "
"u.first_name, u.last_name, u.phone, "
"t.name AS assigned_team_name "
"FROM reports r "
"JOIN users u ON u.id = r.user_id "
"JOIN report_categories c ON c.id = r.category_id "
"LEFT JOIN areas a ON a.id = r.area_id "
"LEFT JOIN assignments ass ON ass.report_id = r.id AND ass.ended_at IS NULL "
"LEFT JOIN teams t ON t.id = ass.team_id "
"WHERE r.id = ?",
reportId);
if (report.empty())
{
return failure("intervention_link_not_found");
}
db->execSqlSync(
"UPDATE intervention_access_links SET last_accessed_at = NOW() "
"WHERE token_hash = ?",
crypto::hashSecret(token));
return buildInterventionSummaryRow(report[0],
db->execSqlSync(
"SELECT * FROM report_photos WHERE report_id = ? ORDER BY id",
reportId),
db->execSqlSync(
"SELECT m.id, m.user_id, u.first_name, u.last_name, m.body, m.created_at "
"FROM messages m JOIN users u ON u.id = m.user_id "
"WHERE m.report_id = ? AND u.id <> ? "
"ORDER BY m.created_at DESC LIMIT 5",
reportId,
report[0]["user_id"].as<long long>()),
true);
}
Json::Value ReportService::updateStatus(const auth::UserContext &user,
@@ -725,31 +913,57 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
long long reportId,
const Json::Value &body)
{
if (!canModifyReport(user, reportId) || !validation::hasNumber(body, "team_id"))
if (!canOwnOrModifyReport(user, reportId))
{
return failure("forbidden");
}
if (!validation::hasNumber(body, "team_id"))
{
return failure("invalid_team");
}
auto db = drogon::app().getDbClient();
const auto current = db->execSqlSync(
"SELECT status FROM reports WHERE id = ?",
"SELECT status, assigned_operator_id FROM reports WHERE id = ?",
reportId);
if (current.empty())
{
return failure("report_not_found");
}
if (!auth::isAdmin(user) && !current[0]["assigned_operator_id"].isNull() &&
current[0]["assigned_operator_id"].as<long long>() != user.id)
{
return failure("report_owned_by_other_operator");
}
const auto oldStatus = current[0]["status"].as<std::string>();
if (!canTransition(oldStatus, "ASSIGNED"))
if (oldStatus != "OPEN" && !canTransition(oldStatus, "ASSIGNED"))
{
return failure("invalid_status_transition");
}
const auto team = db->execSqlSync(
"SELECT id FROM teams WHERE id = ? AND active = TRUE",
"SELECT id, active FROM teams WHERE id = ?",
body["team_id"].asInt64());
if (team.empty())
{
return failure("team_not_found");
}
if (!team[0]["active"].as<bool>())
{
return failure("invalid_reference");
}
if (oldStatus == "OPEN")
{
db->execSqlSync(
"UPDATE reports SET assigned_operator_id = ?, status = 'ASSIGNED' WHERE id = ?",
user.id,
reportId);
}
else
{
db->execSqlSync("UPDATE reports SET status = 'ASSIGNED' WHERE id = ?",
reportId);
}
db->execSqlSync(
"UPDATE assignments SET ended_at = NOW() "
@@ -761,8 +975,6 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
reportId,
body["team_id"].asInt64(),
user.id);
db->execSqlSync("UPDATE reports SET status = 'ASSIGNED' WHERE id = ?",
reportId);
db->execSqlSync(
"INSERT INTO report_status_history "
"(report_id, old_status, new_status, changed_by_user_id, note) "
@@ -770,6 +982,7 @@ Json::Value ReportService::assignTeam(const auth::UserContext &user,
reportId,
oldStatus,
user.id);
revokeActiveLinksForReport(reportId);
notifyReportOwner(reportId,
"team_assigned",
"Team assigned",
@@ -895,6 +1108,92 @@ Json::Value ReportService::flagMessage(const auth::UserContext &user,
return response;
}
Json::Value ReportService::teamSuggestions(const auth::UserContext &user,
long long reportId)
{
if (!auth::canManageReports(user))
{
return failure("forbidden");
}
auto db = drogon::app().getDbClient();
const auto reportRows = db->execSqlSync(
"SELECT id, category_id, area_id FROM reports WHERE id = ?",
reportId);
if (reportRows.empty())
{
return failure("report_not_found");
}
const auto result = db->execSqlSync(
"SELECT t.*, tc.name AS category_name, a.name AS area_name, "
"CASE "
" WHEN t.area_id = r.area_id THEN 'area_and_category' "
" WHEN t.area_id IS NULL THEN 'category_only' "
" ELSE 'fallback_global' "
"END AS match_reason, "
"CASE WHEN t.area_id = r.area_id THEN 0 "
" WHEN t.area_id IS NULL THEN 1 "
" ELSE 2 END AS rank_value "
"FROM reports r "
"JOIN report_category_team_categories map ON map.report_category_id = r.category_id "
"JOIN teams t ON t.team_category_id = map.team_category_id AND t.active = TRUE "
"JOIN team_categories tc ON tc.id = t.team_category_id "
"LEFT JOIN areas a ON a.id = t.area_id "
"WHERE r.id = ? AND (t.area_id = r.area_id OR t.area_id IS NULL) "
"ORDER BY rank_value, t.name",
reportId);
auto response = success();
response["suggestions"] = Json::arrayValue;
for (size_t i = 0; i < result.size(); ++i)
{
auto item = repositories::rowToJson(result[i]);
item["is_recommended"] = (i == 0);
response["suggestions"].append(std::move(item));
}
return response;
}
Json::Value ReportService::createInterventionLink(const auth::UserContext &user,
long long reportId)
{
if (!canModifyReport(user, reportId))
{
return failure("forbidden");
}
const auto assignmentId = activeAssignmentId(reportId);
if (!assignmentId)
{
return failure("invalid_status_transition");
}
auto token = crypto::randomToken();
auto db = drogon::app().getDbClient();
revokeActiveLinksForReport(reportId);
db->execSqlSync(
"INSERT INTO intervention_access_links "
"(report_id, assignment_id, token_hash, created_by_user_id, expires_at) "
"VALUES (?, ?, ?, ?, DATE_ADD(NOW(), INTERVAL 7 DAY))",
reportId,
*assignmentId,
crypto::hashSecret(token),
user.id);
const auto expiresRows = db->execSqlSync(
"SELECT expires_at FROM intervention_access_links WHERE token_hash = ?",
crypto::hashSecret(token));
return successWithLink("/api/v1/interventions/" + token,
expiresRows[0]["expires_at"].as<std::string>());
}
Json::Value ReportService::revokeInterventionLink(const auth::UserContext &user,
long long reportId)
{
if (!canModifyReport(user, reportId))
{
return failure("forbidden");
}
revokeActiveLinksForReport(reportId);
return success();
}
Json::Value ReportService::openDataReports(const std::optional<std::string> &page,
const std::optional<std::string> &perPage)
{
+7
View File
@@ -19,14 +19,18 @@ class ReportService
Json::Value assignedReports(const auth::UserContext &user,
const std::optional<std::string> &page,
const std::optional<std::string> &perPage);
Json::Value listAreas();
Json::Value openReports(const auth::UserContext &user,
const std::optional<std::string> &page,
const std::optional<std::string> &perPage);
Json::Value otherOperatorReports(const auth::UserContext &user,
const std::optional<std::string> &page,
const std::optional<std::string> &perPage);
Json::Value listCategories(const auth::UserContext &user);
Json::Value listOperatorTeams(const auth::UserContext &user);
Json::Value reportDetail(const auth::UserContext &user, long long reportId);
Json::Value interventionSummary(const auth::UserContext &user, long long reportId);
Json::Value publicInterventionSummary(const std::string &token);
Json::Value updateStatus(const auth::UserContext &user,
long long reportId,
const Json::Value &body);
@@ -48,6 +52,9 @@ class ReportService
long long reportId,
long long messageId,
const Json::Value &body);
Json::Value teamSuggestions(const auth::UserContext &user, long long reportId);
Json::Value createInterventionLink(const auth::UserContext &user, long long reportId);
Json::Value revokeInterventionLink(const auth::UserContext &user, long long reportId);
Json::Value openDataReports(const std::optional<std::string> &page,
const std::optional<std::string> &perPage);
Json::Value canAddMedia(const auth::UserContext &user, long long reportId);