689 lines
22 KiB
C++
689 lines
22 KiB
C++
#include "services/AdminService.h"
|
|
|
|
#include "common/GeoJson.h"
|
|
#include "common/Validation.h"
|
|
#include "repositories/Sql.h"
|
|
#include "services/UserService.h"
|
|
|
|
#include <drogon/drogon.h>
|
|
|
|
#include <algorithm>
|
|
#include <sstream>
|
|
|
|
namespace visione::services
|
|
{
|
|
namespace
|
|
{
|
|
using repositories::JsonFieldType;
|
|
using repositories::JsonRowSchema;
|
|
|
|
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<std::string> &value)
|
|
{
|
|
if (!value)
|
|
{
|
|
return 1;
|
|
}
|
|
try
|
|
{
|
|
return std::max(1, std::stoi(*value));
|
|
}
|
|
catch (...)
|
|
{
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
int boundedPerPage(const std::optional<std::string> &value)
|
|
{
|
|
if (!value)
|
|
{
|
|
return 50;
|
|
}
|
|
try
|
|
{
|
|
return std::clamp(std::stoi(*value), 1, 100);
|
|
}
|
|
catch (...)
|
|
{
|
|
return 50;
|
|
}
|
|
}
|
|
|
|
const JsonRowSchema &moderationFlagSchema()
|
|
{
|
|
static const JsonRowSchema schema{
|
|
{"id", JsonFieldType::UInt64},
|
|
{"report_id", JsonFieldType::UInt64},
|
|
{"message_id", JsonFieldType::UInt64},
|
|
{"created_by_user_id", JsonFieldType::UInt64},
|
|
{"resolved_by_user_id", JsonFieldType::UInt64}};
|
|
return schema;
|
|
}
|
|
|
|
const JsonRowSchema &adminUserSchema()
|
|
{
|
|
static const JsonRowSchema schema{
|
|
{"id", JsonFieldType::UInt64},
|
|
{"points", JsonFieldType::Int64},
|
|
{"banned", JsonFieldType::Bool},
|
|
{"must_change_password", JsonFieldType::Bool}};
|
|
return schema;
|
|
}
|
|
|
|
bool isActiveField(const Json::Value &body)
|
|
{
|
|
return body.isMember("active") && body["active"].isBool();
|
|
}
|
|
|
|
std::optional<Json::Value> loadAllowedBoundary(const std::string &code)
|
|
{
|
|
const auto rows = drogon::app().getDbClient()->execSqlSync(
|
|
"SELECT JSON_UNQUOTE(JSON_EXTRACT(boundary_geojson, '$')) AS geojson "
|
|
"FROM allowed_boundaries WHERE code = ? AND active = TRUE LIMIT 1",
|
|
code);
|
|
if (rows.empty())
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
|
|
Json::CharReaderBuilder builder;
|
|
Json::Value geojson;
|
|
std::string errors;
|
|
std::istringstream stream(rows[0]["geojson"].as<std::string>());
|
|
if (!Json::parseFromStream(builder, stream, &geojson, &errors))
|
|
{
|
|
return std::nullopt;
|
|
}
|
|
return geojson;
|
|
}
|
|
|
|
bool isAreaInsideAllowedBoundary(const Json::Value &boundaryGeojson)
|
|
{
|
|
const auto allowed = loadAllowedBoundary("BARI");
|
|
return allowed &&
|
|
geojson::polygonInsideGeometry(boundaryGeojson, *allowed);
|
|
}
|
|
} // namespace
|
|
|
|
Json::Value AdminService::createCategory(const auth::UserContext &user,
|
|
const Json::Value &body)
|
|
{
|
|
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 (?, ?)",
|
|
body["name"].asString(),
|
|
body.get("description", "").asString());
|
|
return success();
|
|
}
|
|
|
|
Json::Value AdminService::createArea(const auth::UserContext &user,
|
|
const Json::Value &body)
|
|
{
|
|
if (!auth::isAdmin(user))
|
|
{
|
|
return failure("forbidden");
|
|
}
|
|
if (!validation::hasString(body, "name") || !body["boundary_geojson"].isObject())
|
|
{
|
|
return failure("invalid_area");
|
|
}
|
|
if (!isAreaInsideAllowedBoundary(body["boundary_geojson"]))
|
|
{
|
|
return failure("area_outside_allowed_boundary");
|
|
}
|
|
|
|
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::createTeam(const auth::UserContext &user,
|
|
const Json::Value &body)
|
|
{
|
|
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())
|
|
{
|
|
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(),
|
|
body["area_id"].asInt64(),
|
|
body["name"].asString(),
|
|
body.get("description", "").asString());
|
|
}
|
|
else
|
|
{
|
|
db->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["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') "
|
|
"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<std::string> &status,
|
|
const std::optional<std::string> &page,
|
|
const std::optional<std::string> &perPage)
|
|
{
|
|
if (!auth::isAdmin(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),
|
|
moderationFlagSchema());
|
|
return response;
|
|
}
|
|
|
|
Json::Value AdminService::updateModeration(const auth::UserContext &user,
|
|
long long flagId,
|
|
const Json::Value &body)
|
|
{
|
|
if (!auth::isAdmin(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<long long>();
|
|
const auto current = db->execSqlSync(
|
|
"SELECT status, user_id FROM reports WHERE id = ?",
|
|
reportId);
|
|
if (!current.empty() && current[0]["status"].as<std::string>() != "REJECTED")
|
|
{
|
|
const auto oldStatus = current[0]["status"].as<std::string>();
|
|
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<long long>(),
|
|
"report_rejected",
|
|
"Report rejected",
|
|
"Your report was rejected after moderation review.",
|
|
reportId);
|
|
}
|
|
}
|
|
return success();
|
|
}
|
|
|
|
Json::Value AdminService::deleteCategory(const auth::UserContext &user,
|
|
long long categoryId)
|
|
{
|
|
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");
|
|
}
|
|
db->execSqlSync("UPDATE report_categories SET active = FALSE WHERE id = ?",
|
|
categoryId);
|
|
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::deleteArea(const auth::UserContext &user,
|
|
long long areaId)
|
|
{
|
|
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");
|
|
}
|
|
db->execSqlSync("UPDATE areas SET active = FALSE WHERE id = ?", areaId);
|
|
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())
|
|
{
|
|
if (!isAreaInsideAllowedBoundary(body["boundary_geojson"]))
|
|
{
|
|
return failure("area_outside_allowed_boundary");
|
|
}
|
|
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::deleteTeam(const auth::UserContext &user,
|
|
long long teamId)
|
|
{
|
|
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");
|
|
}
|
|
db->execSqlSync("UPDATE teams SET active = FALSE WHERE id = ?", teamId);
|
|
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))
|
|
{
|
|
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"),
|
|
adminUserSchema());
|
|
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");
|
|
}
|
|
|
|
std::vector<long long> roleIds;
|
|
roleIds.reserve(body["roles"].size());
|
|
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");
|
|
}
|
|
roleIds.push_back(roleRows[0]["id"].as<long long>());
|
|
}
|
|
|
|
db->execSqlSync("DELETE FROM user_roles WHERE user_id = ?", targetUserId);
|
|
for (const auto roleId : roleIds)
|
|
{
|
|
db->execSqlSync("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)",
|
|
targetUserId,
|
|
roleId);
|
|
}
|
|
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
|