#include "csp/controllers/admin_controller.h" #include "csp/app_state.h" #include "csp/services/redeem_service.h" #include "csp/services/user_service.h" #include "http_auth.h" #include #include #include #include #include #include namespace csp::controllers { namespace { drogon::HttpResponsePtr JsonError(drogon::HttpStatusCode code, const std::string& msg) { Json::Value j; j["ok"] = false; j["error"] = msg; auto resp = drogon::HttpResponse::newHttpJsonResponse(j); resp->setStatusCode(code); return resp; } drogon::HttpResponsePtr JsonOk(const Json::Value& data) { Json::Value j; j["ok"] = true; j["data"] = data; auto resp = drogon::HttpResponse::newHttpJsonResponse(j); resp->setStatusCode(drogon::k200OK); return resp; } int ParseClampedInt(const std::string& s, int default_value, int min_value, int max_value) { if (s.empty()) return default_value; const int v = std::stoi(s); return std::max(min_value, std::min(max_value, v)); } bool ParseBoolLike(const std::string& raw, bool default_value) { if (raw.empty()) return default_value; std::string v = raw; std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); if (v == "1" || v == "true" || v == "yes" || v == "on") return true; if (v == "0" || v == "false" || v == "no" || v == "off") return false; return default_value; } std::optional ParseOptionalInt64(const std::string& raw) { if (raw.empty()) return std::nullopt; return std::stoll(raw); } services::RedeemItemWrite ParseRedeemItemWrite(const Json::Value& json) { services::RedeemItemWrite write; write.name = json.get("name", "").asString(); write.description = json.get("description", "").asString(); write.unit_label = json.get("unit_label", "小时").asString(); write.holiday_cost = json.get("holiday_cost", 5).asInt(); write.studyday_cost = json.get("studyday_cost", 25).asInt(); write.is_active = json.get("is_active", true).asBool(); write.is_global = json.get("is_global", true).asBool(); return write; } Json::Value ToJson(const services::RedeemItem& item) { Json::Value j; j["id"] = Json::Int64(item.id); j["name"] = item.name; j["description"] = item.description; j["unit_label"] = item.unit_label; j["holiday_cost"] = item.holiday_cost; j["studyday_cost"] = item.studyday_cost; j["is_active"] = item.is_active; j["is_global"] = item.is_global; j["created_by"] = Json::Int64(item.created_by); j["created_at"] = Json::Int64(item.created_at); j["updated_at"] = Json::Int64(item.updated_at); return j; } std::optional RequireAdminUserId( const drogon::HttpRequestPtr& req, std::function& cb) { std::string auth_error; const auto user_id = GetAuthedUserId(req, auth_error); if (!user_id.has_value()) { cb(JsonError(drogon::k401Unauthorized, auth_error)); return std::nullopt; } services::UserService users(csp::AppState::Instance().db()); const auto user = users.GetById(*user_id); if (!user.has_value() || user->username != "admin") { cb(JsonError(drogon::k403Forbidden, "admin only")); return std::nullopt; } return user_id; } } // namespace void AdminController::listUsers( const drogon::HttpRequestPtr& req, std::function&& cb) { try { if (!RequireAdminUserId(req, cb).has_value()) return; const int page = ParseClampedInt(req->getParameter("page"), 1, 1, 100000); const int page_size = ParseClampedInt(req->getParameter("page_size"), 50, 1, 200); services::UserService users(csp::AppState::Instance().db()); const auto result = users.ListUsers(page, page_size); Json::Value arr(Json::arrayValue); for (const auto& item : result.items) { Json::Value one; one["id"] = Json::Int64(item.user_id); one["username"] = item.username; one["rating"] = item.rating; one["created_at"] = Json::Int64(item.created_at); arr.append(one); } Json::Value payload; payload["items"] = arr; payload["total_count"] = result.total_count; payload["page"] = page; payload["page_size"] = page_size; cb(JsonOk(payload)); } catch (const std::invalid_argument&) { cb(JsonError(drogon::k400BadRequest, "invalid query parameter")); } catch (const std::out_of_range&) { cb(JsonError(drogon::k400BadRequest, "query parameter out of range")); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void AdminController::updateUserRating( const drogon::HttpRequestPtr& req, std::function&& cb, int64_t user_id) { try { if (!RequireAdminUserId(req, cb).has_value()) return; const auto json = req->getJsonObject(); if (!json) { cb(JsonError(drogon::k400BadRequest, "body must be json")); return; } if (!(*json).isMember("rating")) { cb(JsonError(drogon::k400BadRequest, "rating is required")); return; } const int rating = (*json)["rating"].asInt(); if (rating < 0) { cb(JsonError(drogon::k400BadRequest, "rating must be >= 0")); return; } services::UserService users(csp::AppState::Instance().db()); users.SetRating(user_id, rating); const auto updated = users.GetById(user_id); if (!updated.has_value()) { cb(JsonError(drogon::k404NotFound, "user not found")); return; } Json::Value payload; payload["id"] = Json::Int64(updated->id); payload["username"] = updated->username; payload["rating"] = updated->rating; payload["updated"] = true; cb(JsonOk(payload)); } catch (const std::invalid_argument&) { cb(JsonError(drogon::k400BadRequest, "invalid rating")); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void AdminController::deleteUser( const drogon::HttpRequestPtr& req, std::function&& cb, int64_t user_id) { try { const auto admin_user_id = RequireAdminUserId(req, cb); if (!admin_user_id.has_value()) return; if (*admin_user_id == user_id) { cb(JsonError(drogon::k400BadRequest, "cannot delete current admin user")); return; } services::UserService users(csp::AppState::Instance().db()); const auto target = users.GetById(user_id); if (!target.has_value()) { cb(JsonError(drogon::k404NotFound, "user not found")); return; } if (target->username == "admin") { cb(JsonError(drogon::k400BadRequest, "cannot delete reserved admin user")); return; } users.DeleteUser(user_id); Json::Value payload; payload["id"] = Json::Int64(user_id); payload["username"] = target->username; payload["deleted"] = true; cb(JsonOk(payload)); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void AdminController::listRedeemItems( const drogon::HttpRequestPtr& req, std::function&& cb) { try { if (!RequireAdminUserId(req, cb).has_value()) return; const bool include_inactive = ParseBoolLike(req->getParameter("include_inactive"), true); services::RedeemService redeem(csp::AppState::Instance().db()); const auto items = redeem.ListItems(include_inactive); Json::Value arr(Json::arrayValue); for (const auto& item : items) arr.append(ToJson(item)); cb(JsonOk(arr)); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void AdminController::createRedeemItem( const drogon::HttpRequestPtr& req, std::function&& cb) { try { const auto admin_user_id = RequireAdminUserId(req, cb); if (!admin_user_id.has_value()) return; const auto json = req->getJsonObject(); if (!json) { cb(JsonError(drogon::k400BadRequest, "body must be json")); return; } const auto input = ParseRedeemItemWrite(*json); services::RedeemService redeem(csp::AppState::Instance().db()); const auto item = redeem.CreateItem(*admin_user_id, input); cb(JsonOk(ToJson(item))); } catch (const std::runtime_error& e) { cb(JsonError(drogon::k400BadRequest, e.what())); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void AdminController::updateRedeemItem( const drogon::HttpRequestPtr& req, std::function&& cb, int64_t item_id) { try { if (!RequireAdminUserId(req, cb).has_value()) return; const auto json = req->getJsonObject(); if (!json) { cb(JsonError(drogon::k400BadRequest, "body must be json")); return; } const auto input = ParseRedeemItemWrite(*json); services::RedeemService redeem(csp::AppState::Instance().db()); const auto item = redeem.UpdateItem(item_id, input); cb(JsonOk(ToJson(item))); } catch (const std::runtime_error& e) { cb(JsonError(drogon::k400BadRequest, e.what())); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void AdminController::deleteRedeemItem( const drogon::HttpRequestPtr& req, std::function&& cb, int64_t item_id) { try { if (!RequireAdminUserId(req, cb).has_value()) return; services::RedeemService redeem(csp::AppState::Instance().db()); redeem.DeactivateItem(item_id); Json::Value payload; payload["id"] = Json::Int64(item_id); payload["deleted"] = true; cb(JsonOk(payload)); } catch (const std::runtime_error& e) { cb(JsonError(drogon::k400BadRequest, e.what())); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } void AdminController::listRedeemRecords( const drogon::HttpRequestPtr& req, std::function&& cb) { try { if (!RequireAdminUserId(req, cb).has_value()) return; const auto user_id = ParseOptionalInt64(req->getParameter("user_id")); const int limit = ParseClampedInt(req->getParameter("limit"), 200, 1, 500); services::RedeemService redeem(csp::AppState::Instance().db()); const auto rows = redeem.ListRecordsAll(user_id, limit); Json::Value arr(Json::arrayValue); for (const auto& row : rows) { Json::Value j; j["id"] = Json::Int64(row.id); j["user_id"] = Json::Int64(row.user_id); j["username"] = row.username; j["item_id"] = Json::Int64(row.item_id); j["item_name"] = row.item_name; j["quantity"] = row.quantity; j["day_type"] = row.day_type; j["unit_cost"] = row.unit_cost; j["total_cost"] = row.total_cost; j["note"] = row.note; j["created_at"] = Json::Int64(row.created_at); arr.append(j); } cb(JsonOk(arr)); } catch (const std::invalid_argument&) { cb(JsonError(drogon::k400BadRequest, "invalid query parameter")); } catch (const std::out_of_range&) { cb(JsonError(drogon::k400BadRequest, "query parameter out of range")); } catch (const std::exception& e) { cb(JsonError(drogon::k500InternalServerError, e.what())); } } } // namespace csp::controllers