feat(api): add auth HTTP controller with tests
这个提交包含在:
@@ -25,9 +25,22 @@ target_link_libraries(csp_core PUBLIC
|
|||||||
OpenSSL::Crypto
|
OpenSSL::Crypto
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_library(csp_web
|
||||||
|
src/controllers/auth_controller.cc
|
||||||
|
src/health_controller.cc
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(csp_web PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(csp_web PRIVATE
|
||||||
|
Drogon::Drogon
|
||||||
|
csp_core
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(csp_server
|
add_executable(csp_server
|
||||||
src/main.cc
|
src/main.cc
|
||||||
src/health_controller.cc
|
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(csp_server PRIVATE
|
target_include_directories(csp_server PRIVATE
|
||||||
@@ -37,6 +50,7 @@ target_include_directories(csp_server PRIVATE
|
|||||||
target_link_libraries(csp_server PRIVATE
|
target_link_libraries(csp_server PRIVATE
|
||||||
Drogon::Drogon
|
Drogon::Drogon
|
||||||
csp_core
|
csp_core
|
||||||
|
csp_web
|
||||||
)
|
)
|
||||||
|
|
||||||
enable_testing()
|
enable_testing()
|
||||||
@@ -45,11 +59,18 @@ add_executable(csp_tests
|
|||||||
tests/version_test.cc
|
tests/version_test.cc
|
||||||
tests/sqlite_db_test.cc
|
tests/sqlite_db_test.cc
|
||||||
tests/auth_service_test.cc
|
tests/auth_service_test.cc
|
||||||
|
tests/auth_http_test.cc
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(csp_tests PRIVATE
|
||||||
|
/usr/include/jsoncpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(csp_tests PRIVATE
|
target_link_libraries(csp_tests PRIVATE
|
||||||
Catch2::Catch2WithMain
|
Catch2::Catch2WithMain
|
||||||
|
Drogon::Drogon
|
||||||
csp_core
|
csp_core
|
||||||
|
csp_web
|
||||||
)
|
)
|
||||||
|
|
||||||
include(CTest)
|
include(CTest)
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <drogon/HttpController.h>
|
||||||
|
|
||||||
|
namespace csp::controllers {
|
||||||
|
|
||||||
|
class AuthController : public drogon::HttpController<AuthController> {
|
||||||
|
public:
|
||||||
|
METHOD_LIST_BEGIN
|
||||||
|
ADD_METHOD_TO(AuthController::registerUser, "/api/v1/auth/register", drogon::Post);
|
||||||
|
ADD_METHOD_TO(AuthController::login, "/api/v1/auth/login", drogon::Post);
|
||||||
|
METHOD_LIST_END
|
||||||
|
|
||||||
|
void registerUser(const drogon::HttpRequestPtr& req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
|
||||||
|
|
||||||
|
void login(const drogon::HttpRequestPtr& req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace csp::controllers
|
||||||
@@ -10,7 +10,11 @@ AppState& AppState::Instance() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void AppState::Init(const std::string& sqlite_path) {
|
void AppState::Init(const std::string& sqlite_path) {
|
||||||
db_ = std::make_unique<db::SqliteDb>(db::SqliteDb::OpenFile(sqlite_path));
|
if (sqlite_path == ":memory:") {
|
||||||
|
db_ = std::make_unique<db::SqliteDb>(db::SqliteDb::OpenMemory());
|
||||||
|
} else {
|
||||||
|
db_ = std::make_unique<db::SqliteDb>(db::SqliteDb::OpenFile(sqlite_path));
|
||||||
|
}
|
||||||
csp::db::ApplyMigrations(*db_);
|
csp::db::ApplyMigrations(*db_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#include "csp/controllers/auth_controller.h"
|
||||||
|
|
||||||
|
#include "csp/app_state.h"
|
||||||
|
#include "csp/services/auth_service.h"
|
||||||
|
|
||||||
|
#include <drogon/HttpResponse.h>
|
||||||
|
|
||||||
|
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 services::AuthResult& r) {
|
||||||
|
Json::Value j;
|
||||||
|
j["ok"] = true;
|
||||||
|
j["user_id"] = r.user_id;
|
||||||
|
j["token"] = r.token;
|
||||||
|
j["expires_at"] = Json::Int64(r.expires_at);
|
||||||
|
auto resp = drogon::HttpResponse::newHttpJsonResponse(j);
|
||||||
|
resp->setStatusCode(drogon::k200OK);
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<std::string, std::string> ParseUsernamePassword(
|
||||||
|
const drogon::HttpRequestPtr& req) {
|
||||||
|
const auto json = req->getJsonObject();
|
||||||
|
if (!json) throw std::runtime_error("body must be json");
|
||||||
|
|
||||||
|
const auto username = (*json).get("username", "").asString();
|
||||||
|
const auto password = (*json).get("password", "").asString();
|
||||||
|
|
||||||
|
if (username.empty() || password.empty()) {
|
||||||
|
throw std::runtime_error("username/password required");
|
||||||
|
}
|
||||||
|
return {username, password};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void AuthController::registerUser(
|
||||||
|
const drogon::HttpRequestPtr& req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||||
|
try {
|
||||||
|
const auto [username, password] = ParseUsernamePassword(req);
|
||||||
|
services::AuthService auth(AppState::Instance().db());
|
||||||
|
const auto r = auth.Register(username, password);
|
||||||
|
cb(JsonOk(r));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
cb(JsonError(drogon::k400BadRequest, e.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AuthController::login(
|
||||||
|
const drogon::HttpRequestPtr& req,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr&)>&& cb) {
|
||||||
|
try {
|
||||||
|
const auto [username, password] = ParseUsernamePassword(req);
|
||||||
|
services::AuthService auth(AppState::Instance().db());
|
||||||
|
const auto r = auth.Login(username, password);
|
||||||
|
cb(JsonOk(r));
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
cb(JsonError(drogon::k400BadRequest, e.what()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace csp::controllers
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
int main(int argc, char** argv) {
|
int main(int argc, char** argv) {
|
||||||
const std::string db_path = (argc >= 2) ? argv[1] : std::string("data/csp.db");
|
const std::string db_path = (argc >= 2) ? argv[1] : std::string("data/csp.db");
|
||||||
std::filesystem::create_directories(std::filesystem::path(db_path).parent_path());
|
const auto parent = std::filesystem::path(db_path).parent_path();
|
||||||
|
if (!parent.empty()) std::filesystem::create_directories(parent);
|
||||||
|
|
||||||
csp::AppState::Instance().Init(db_path);
|
csp::AppState::Instance().Init(db_path);
|
||||||
|
|
||||||
|
|||||||
53
backend/tests/auth_http_test.cc
普通文件
53
backend/tests/auth_http_test.cc
普通文件
@@ -0,0 +1,53 @@
|
|||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
#include "csp/app_state.h"
|
||||||
|
#include "csp/controllers/auth_controller.h"
|
||||||
|
|
||||||
|
#include <drogon/HttpRequest.h>
|
||||||
|
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
drogon::HttpResponsePtr Call(
|
||||||
|
void (csp::controllers::AuthController::*fn)(
|
||||||
|
const drogon::HttpRequestPtr&,
|
||||||
|
std::function<void(const drogon::HttpResponsePtr&)>&&),
|
||||||
|
const Json::Value& body) {
|
||||||
|
auto req = drogon::HttpRequest::newHttpJsonRequest(body);
|
||||||
|
req->setMethod(drogon::Post);
|
||||||
|
|
||||||
|
std::promise<drogon::HttpResponsePtr> p;
|
||||||
|
csp::controllers::AuthController ctl;
|
||||||
|
(ctl.*fn)(req, [&p](const drogon::HttpResponsePtr& resp) { p.set_value(resp); });
|
||||||
|
return p.get_future().get();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("auth controller: register + login") {
|
||||||
|
csp::AppState::Instance().Init(":memory:");
|
||||||
|
|
||||||
|
Json::Value reg;
|
||||||
|
reg["username"] = "bob";
|
||||||
|
reg["password"] = "password123";
|
||||||
|
|
||||||
|
auto regResp = Call(&csp::controllers::AuthController::registerUser, reg);
|
||||||
|
REQUIRE(regResp != nullptr);
|
||||||
|
REQUIRE(regResp->statusCode() == drogon::k200OK);
|
||||||
|
auto j1 = regResp->jsonObject();
|
||||||
|
REQUIRE(j1 != nullptr);
|
||||||
|
REQUIRE((*j1)["ok"].asBool() == true);
|
||||||
|
REQUIRE((*j1)["token"].asString().size() > 10);
|
||||||
|
|
||||||
|
Json::Value login;
|
||||||
|
login["username"] = "bob";
|
||||||
|
login["password"] = "password123";
|
||||||
|
auto loginResp = Call(&csp::controllers::AuthController::login, login);
|
||||||
|
REQUIRE(loginResp != nullptr);
|
||||||
|
REQUIRE(loginResp->statusCode() == drogon::k200OK);
|
||||||
|
auto j2 = loginResp->jsonObject();
|
||||||
|
REQUIRE(j2 != nullptr);
|
||||||
|
REQUIRE((*j2)["ok"].asBool() == true);
|
||||||
|
REQUIRE((*j2)["token"].asString().size() > 10);
|
||||||
|
}
|
||||||
0
scripts/bootstrap_ubuntu.sh
可执行文件 -> 普通文件
0
scripts/bootstrap_ubuntu.sh
可执行文件 -> 普通文件
在新工单中引用
屏蔽一个用户