diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index c9e5d12..6cfd36e 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -14,10 +14,13 @@ add_library(csp_core src/app_state.cc src/services/crypto.cc src/services/auth_service.cc + src/domain/enum_strings.cc + src/domain/json.cc ) target_include_directories(csp_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include + /usr/include/jsoncpp ) target_link_libraries(csp_core PUBLIC @@ -60,6 +63,7 @@ add_executable(csp_tests tests/sqlite_db_test.cc tests/auth_service_test.cc tests/auth_http_test.cc + tests/domain_test.cc ) target_include_directories(csp_tests PRIVATE diff --git a/backend/include/csp/domain/entities.h b/backend/include/csp/domain/entities.h new file mode 100644 index 0000000..ee77737 --- /dev/null +++ b/backend/include/csp/domain/entities.h @@ -0,0 +1,84 @@ +#pragma once + +#include +#include +#include + +namespace csp::domain { + +// Notes: +// - All *_at fields use Unix timestamp seconds (int64). +// - id fields match SQLite INTEGER PRIMARY KEY AUTOINCREMENT (int64). + +struct User { + int64_t id = 0; + std::string username; + std::string password_salt; + std::string password_hash; + int32_t rating = 0; + int64_t created_at = 0; +}; + +struct Session { + std::string token; // PK + int64_t user_id = 0; + int64_t expires_at = 0; + int64_t created_at = 0; +}; + +struct Problem { + int64_t id = 0; + std::string slug; + std::string title; + std::string statement_md; + int32_t difficulty = 1; + std::string source; + int64_t created_at = 0; +}; + +struct ProblemTag { + int64_t problem_id = 0; + std::string tag; +}; + +enum class SubmissionStatus { + Pending, + Compiling, + Running, + AC, + WA, + TLE, + MLE, + RE, + CE, + Unknown, +}; + +// MVP: only C++ is supported. +enum class Language { + Cpp, + Unknown, +}; + +struct Submission { + int64_t id = 0; + int64_t user_id = 0; + int64_t problem_id = 0; + Language language = Language::Cpp; + std::string code; + SubmissionStatus status = SubmissionStatus::Pending; + int32_t score = 0; + int32_t time_ms = 0; + int32_t memory_kb = 0; + int64_t created_at = 0; +}; + +struct WrongBookItem { + int64_t user_id = 0; + int64_t problem_id = 0; + std::optional last_submission_id; + std::string note; + int64_t updated_at = 0; +}; + +} // namespace csp::domain diff --git a/backend/include/csp/domain/enum_strings.h b/backend/include/csp/domain/enum_strings.h new file mode 100644 index 0000000..41ceb5a --- /dev/null +++ b/backend/include/csp/domain/enum_strings.h @@ -0,0 +1,17 @@ +#pragma once + +#include "csp/domain/entities.h" + +#include + +namespace csp::domain { + +// String mapping for persistence/API. + +std::string ToString(SubmissionStatus s); +SubmissionStatus SubmissionStatusFromString(const std::string& s); + +std::string ToString(Language l); +Language LanguageFromString(const std::string& s); + +} // namespace csp::domain diff --git a/backend/include/csp/domain/json.h b/backend/include/csp/domain/json.h new file mode 100644 index 0000000..94be3be --- /dev/null +++ b/backend/include/csp/domain/json.h @@ -0,0 +1,18 @@ +#pragma once + +#include "csp/domain/entities.h" +#include "csp/domain/enum_strings.h" + +#include + +namespace csp::domain { + +// These helpers are intended for API responses. +// IMPORTANT: They intentionally exclude sensitive fields (password_salt/password_hash). + +Json::Value ToPublicJson(const User& u); +Json::Value ToJson(const Problem& p); +Json::Value ToJson(const Submission& s); +Json::Value ToJson(const WrongBookItem& w); + +} // namespace csp::domain diff --git a/backend/src/domain/enum_strings.cc b/backend/src/domain/enum_strings.cc new file mode 100644 index 0000000..f98c09a --- /dev/null +++ b/backend/src/domain/enum_strings.cc @@ -0,0 +1,74 @@ +#include "csp/domain/enum_strings.h" + +#include + +namespace csp::domain { + +namespace { + +std::string Upper(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { + return static_cast(std::toupper(c)); + }); + return s; +} + +} // namespace + +std::string ToString(SubmissionStatus s) { + switch (s) { + case SubmissionStatus::Pending: + return "Pending"; + case SubmissionStatus::Compiling: + return "Compiling"; + case SubmissionStatus::Running: + return "Running"; + case SubmissionStatus::AC: + return "AC"; + case SubmissionStatus::WA: + return "WA"; + case SubmissionStatus::TLE: + return "TLE"; + case SubmissionStatus::MLE: + return "MLE"; + case SubmissionStatus::RE: + return "RE"; + case SubmissionStatus::CE: + return "CE"; + case SubmissionStatus::Unknown: + default: + return "Unknown"; + } +} + +SubmissionStatus SubmissionStatusFromString(const std::string& s) { + const auto u = Upper(s); + if (u == "PENDING") return SubmissionStatus::Pending; + if (u == "COMPILING") return SubmissionStatus::Compiling; + if (u == "RUNNING") return SubmissionStatus::Running; + if (u == "AC") return SubmissionStatus::AC; + if (u == "WA") return SubmissionStatus::WA; + if (u == "TLE") return SubmissionStatus::TLE; + if (u == "MLE") return SubmissionStatus::MLE; + if (u == "RE") return SubmissionStatus::RE; + if (u == "CE") return SubmissionStatus::CE; + return SubmissionStatus::Unknown; +} + +std::string ToString(Language l) { + switch (l) { + case Language::Cpp: + return "cpp"; + case Language::Unknown: + default: + return "unknown"; + } +} + +Language LanguageFromString(const std::string& s) { + const auto u = Upper(s); + if (u == "CPP" || u == "C++" || u == "CXX") return Language::Cpp; + return Language::Unknown; +} + +} // namespace csp::domain diff --git a/backend/src/domain/json.cc b/backend/src/domain/json.cc new file mode 100644 index 0000000..69e14ac --- /dev/null +++ b/backend/src/domain/json.cc @@ -0,0 +1,54 @@ +#include "csp/domain/json.h" + +namespace csp::domain { + +Json::Value ToPublicJson(const User& u) { + Json::Value j; + j["id"] = Json::Int64(u.id); + j["username"] = u.username; + j["rating"] = u.rating; + j["created_at"] = Json::Int64(u.created_at); + return j; +} + +Json::Value ToJson(const Problem& p) { + Json::Value j; + j["id"] = Json::Int64(p.id); + j["slug"] = p.slug; + j["title"] = p.title; + j["statement_md"] = p.statement_md; + j["difficulty"] = p.difficulty; + j["source"] = p.source; + j["created_at"] = Json::Int64(p.created_at); + return j; +} + +Json::Value ToJson(const Submission& s) { + Json::Value j; + j["id"] = Json::Int64(s.id); + j["user_id"] = Json::Int64(s.user_id); + j["problem_id"] = Json::Int64(s.problem_id); + j["language"] = ToString(s.language); + j["status"] = ToString(s.status); + j["score"] = s.score; + j["time_ms"] = s.time_ms; + j["memory_kb"] = s.memory_kb; + j["created_at"] = Json::Int64(s.created_at); + return j; +} + +Json::Value ToJson(const WrongBookItem& w) { + Json::Value j; + j["user_id"] = Json::Int64(w.user_id); + j["problem_id"] = Json::Int64(w.problem_id); + if (w.last_submission_id.has_value()) { + j["last_submission_id"] = Json::Int64(*w.last_submission_id); + } else { + j["last_submission_id"] = Json::nullValue; + } + j["note"] = w.note; + j["updated_at"] = Json::Int64(w.updated_at); + return j; +} + +} // namespace csp::domain diff --git a/backend/tests/domain_test.cc b/backend/tests/domain_test.cc new file mode 100644 index 0000000..1932916 --- /dev/null +++ b/backend/tests/domain_test.cc @@ -0,0 +1,29 @@ +#include + +#include "csp/domain/enum_strings.h" +#include "csp/domain/json.h" + +TEST_CASE("enum string mapping") { + REQUIRE(csp::domain::ToString(csp::domain::SubmissionStatus::AC) == "AC"); + REQUIRE(csp::domain::SubmissionStatusFromString("ac") == + csp::domain::SubmissionStatus::AC); + REQUIRE(csp::domain::LanguageFromString("cpp") == csp::domain::Language::Cpp); +} + +TEST_CASE("domain json serialization hides secrets") { + csp::domain::User u; + u.id = 1; + u.username = "alice"; + u.password_salt = "salt"; + u.password_hash = "hash"; + u.rating = 10; + u.created_at = 100; + + auto j = csp::domain::ToPublicJson(u); + REQUIRE(j.isMember("id")); + REQUIRE(j.isMember("username")); + REQUIRE(j.isMember("rating")); + REQUIRE(j.isMember("created_at")); + REQUIRE_FALSE(j.isMember("password_salt")); + REQUIRE_FALSE(j.isMember("password_hash")); +}