236 lines
7.3 KiB
C++
236 lines
7.3 KiB
C++
#include "services/AdminService.h"
|
|
|
|
#include "common/Validation.h"
|
|
#include "repositories/Sql.h"
|
|
|
|
#include <drogon/drogon.h>
|
|
|
|
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<long long>());
|
|
}
|
|
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
|