#include "services/AdminService.h" #include "common/Validation.h" #include "repositories/Sql.h" #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; } } // 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')")); return response; } 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