#include "services/AdminService.h" #include "common/Validation.h" #include "repositories/Sql.h" #include "services/UserService.h" #include #include namespace visione::services { namespace { Json::Value failure(const std::string &code) { Json::Value body; body["ok"] = false; body["error"] = code; return body; } Json::Value success() { Json::Value body; body["ok"] = true; return body; } int boundedPage(const std::optional &value) { if (!value) { return 1; } try { return std::max(1, std::stoi(*value)); } catch (...) { return 1; } } int boundedPerPage(const std::optional &value) { if (!value) { return 50; } try { return std::clamp(std::stoi(*value), 1, 100); } catch (...) { return 50; } } } // namespace Json::Value AdminService::listCategories() { auto response = success(); response["categories"] = repositories::resultToJson( drogon::app().getDbClient()->execSqlSync( "SELECT * FROM report_categories ORDER BY name")); return response; } Json::Value AdminService::createCategory(const auth::UserContext &user, const Json::Value &body) { if (!auth::isAdmin(user) || !validation::hasString(body, "name")) { return failure("forbidden"); } drogon::app().getDbClient()->execSqlSync( "INSERT INTO report_categories (name, description) VALUES (?, ?)", body["name"].asString(), body.get("description", "").asString()); 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()) { return failure("invalid_area"); } Json::StreamWriterBuilder writer; drogon::app().getDbClient()->execSqlSync( "INSERT INTO areas (name, description, boundary_geojson) " "VALUES (?, ?, CAST(? AS JSON))", body["name"].asString(), body.get("description", "").asString(), Json::writeString(writer, body["boundary_geojson"])); 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") || !validation::hasNumber(body, "team_category_id")) { return failure("invalid_team"); } if (body["area_id"].isNumeric()) { drogon::app().getDbClient()->execSqlSync( "INSERT INTO teams (team_category_id, area_id, name, description) " "VALUES (?, ?, ?, ?)", body["team_category_id"].asInt64(), body["area_id"].asInt64(), body["name"].asString(), body.get("description", "").asString()); } else { drogon::app().getDbClient()->execSqlSync( "INSERT INTO teams (team_category_id, name, description) VALUES (?, ?, ?)", body["team_category_id"].asInt64(), body["name"].asString(), body.get("description", "").asString()); } return success(); } Json::Value AdminService::overviewStats(const auth::UserContext &user) { if (!auth::canManageReports(user)) { return failure("forbidden"); } auto db = drogon::app().getDbClient(); auto response = success(); response["reports_by_status"] = repositories::resultToJson(db->execSqlSync( "SELECT status, COUNT(*) AS total FROM reports GROUP BY status")); response["reports_by_area"] = repositories::resultToJson(db->execSqlSync( "SELECT COALESCE(a.name, 'Unassigned') AS area, COUNT(*) AS total " "FROM reports r LEFT JOIN areas a ON a.id = r.area_id " "GROUP BY COALESCE(a.name, 'Unassigned')")); response["avg_resolution_hours"] = repositories::resultToJson(db->execSqlSync( "SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, updated_at)) AS value " "FROM reports WHERE status IN ('RESOLVED', 'CLOSED')")); response["reports_by_category"] = repositories::resultToJson(db->execSqlSync( "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 " "FROM assignments ass JOIN teams t ON t.id = ass.team_id " "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') " "ORDER BY month DESC LIMIT 12")); response["avg_resolution_hours_by_category"] = repositories::resultToJson(db->execSqlSync( "SELECT c.name AS category, " "AVG(TIMESTAMPDIFF(HOUR, r.created_at, r.updated_at)) AS value " "FROM reports r JOIN report_categories c ON c.id = r.category_id " "WHERE r.status IN ('RESOLVED', 'CLOSED') " "GROUP BY c.name ORDER BY c.name")); response["avg_resolution_hours_by_area"] = repositories::resultToJson(db->execSqlSync( "SELECT COALESCE(a.name, 'Unassigned') AS area, " "AVG(TIMESTAMPDIFF(HOUR, r.created_at, r.updated_at)) AS value " "FROM reports r LEFT JOIN areas a ON a.id = r.area_id " "WHERE r.status IN ('RESOLVED', 'CLOSED') " "GROUP BY COALESCE(a.name, 'Unassigned') ORDER BY area")); return response; } Json::Value AdminService::listModeration( const auth::UserContext &user, const std::optional &status, const std::optional &page, const std::optional &perPage) { if (!auth::canManageReports(user)) { return failure("forbidden"); } const auto pageValue = boundedPage(page); const auto perPageValue = boundedPerPage(perPage); const auto offset = (pageValue - 1) * perPageValue; const auto statusValue = status.value_or(""); auto response = success(); response["page"] = pageValue; response["per_page"] = perPageValue; response["flags"] = repositories::resultToJson( drogon::app().getDbClient()->execSqlSync( "SELECT f.*, creator.email AS created_by_email, " "resolver.email AS resolved_by_email " "FROM moderation_flags f " "JOIN users creator ON creator.id = f.created_by_user_id " "LEFT JOIN users resolver ON resolver.id = f.resolved_by_user_id " "WHERE (? = '' OR f.status = ?) " "ORDER BY f.created_at DESC LIMIT ? OFFSET ?", statusValue, statusValue, perPageValue, offset)); return response; } Json::Value AdminService::updateModeration(const auth::UserContext &user, long long flagId, const Json::Value &body) { if (!auth::canManageReports(user)) { return failure("forbidden"); } if (!validation::hasString(body, "status")) { return failure("invalid_moderation_payload"); } const auto status = body["status"].asString(); if (status != "RESOLVED" && status != "DISMISSED") { return failure("invalid_moderation_status"); } auto db = drogon::app().getDbClient(); const auto flag = db->execSqlSync( "SELECT id, report_id FROM moderation_flags WHERE id = ?", flagId); if (flag.empty()) { return failure("moderation_flag_not_found"); } db->execSqlSync( "UPDATE moderation_flags " "SET status = ?, resolved_by_user_id = ?, resolution_note = ?, " "resolved_at = NOW() WHERE id = ?", status, user.id, body.get("resolution_note", "").asString(), flagId); if (body.get("reject_report", false).asBool()) { const auto reportId = flag[0]["report_id"].as(); const auto current = db->execSqlSync( "SELECT status, user_id FROM reports WHERE id = ?", reportId); if (!current.empty() && current[0]["status"].as() != "REJECTED") { const auto oldStatus = current[0]["status"].as(); db->execSqlSync("UPDATE reports SET status = 'REJECTED' WHERE id = ?", reportId); db->execSqlSync( "INSERT INTO report_status_history " "(report_id, old_status, new_status, changed_by_user_id, note) " "VALUES (?, ?, 'REJECTED', ?, ?)", reportId, oldStatus, user.id, body.get("resolution_note", "Rejected after moderation").asString()); UserService::createNotification( current[0]["user_id"].as(), "report_rejected", "Report rejected", "Your report was rejected after moderation review.", reportId); } } return success(); } Json::Value AdminService::listUsers(const auth::UserContext &user) { if (!auth::isAdmin(user)) { return failure("forbidden"); } auto response = success(); response["users"] = repositories::resultToJson( drogon::app().getDbClient()->execSqlSync( "SELECT u.id, u.first_name, u.last_name, u.email, u.fiscal_code, " "u.phone, u.points, u.banned, u.must_change_password, " "GROUP_CONCAT(r.name ORDER BY r.name SEPARATOR ',') AS roles " "FROM users u " "LEFT JOIN user_roles ur ON ur.user_id = u.id " "LEFT JOIN roles r ON r.id = ur.role_id " "GROUP BY u.id, u.first_name, u.last_name, u.email, u.fiscal_code, " "u.phone, u.points, u.banned, u.must_change_password " "ORDER BY u.created_at DESC LIMIT 200")); return response; } Json::Value AdminService::updateUserRoles(const auth::UserContext &user, long long targetUserId, const Json::Value &body) { if (!auth::isAdmin(user) || !body["roles"].isArray()) { return failure("invalid_roles"); } auto db = drogon::app().getDbClient(); const auto target = db->execSqlSync("SELECT id FROM users WHERE id = ?", targetUserId); if (target.empty()) { return failure("user_not_found"); } db->execSqlSync("DELETE FROM user_roles WHERE user_id = ?", targetUserId); for (const auto &role : body["roles"]) { if (!role.isString()) { return failure("invalid_roles"); } const auto roleRows = db->execSqlSync("SELECT id FROM roles WHERE name = ?", role.asString()); if (roleRows.empty()) { return failure("role_not_found"); } db->execSqlSync( "INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)", targetUserId, roleRows[0]["id"].as()); } return success(); } Json::Value AdminService::updateUserBan(const auth::UserContext &user, long long targetUserId, const Json::Value &body) { if (!auth::isAdmin(user) || !body["banned"].isBool()) { return failure("invalid_ban_payload"); } if (targetUserId == user.id) { return failure("cannot_ban_self"); } auto db = drogon::app().getDbClient(); const auto target = db->execSqlSync("SELECT id FROM users WHERE id = ?", targetUserId); if (target.empty()) { return failure("user_not_found"); } db->execSqlSync("UPDATE users SET banned = ? WHERE id = ?", body["banned"].asBool(), targetUserId); if (body["banned"].asBool()) { db->execSqlSync( "UPDATE api_tokens SET revoked_at = NOW() " "WHERE user_id = ? AND revoked_at IS NULL", targetUserId); } return success(); } } // namespace visione::services