diff --git a/.gitignore b/.gitignore index 45cc774..e748619 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ cmake-build-debug/ build/ .idea/ +.vscode/ # .env file backend/.env @@ -60,6 +61,5 @@ backend/config/mysqlconfig.json # Ignore .env file .env - -#ignore .vscode settings -.vscode/ \ No newline at end of file +#ignore root dir /CMakeLists.txt; its setup for testing only +CMakeLists.txt \ No newline at end of file diff --git a/backend/CMakeLists.txt b/backend/CMakeLists.txt index e39209c..c53201f 100644 --- a/backend/CMakeLists.txt +++ b/backend/CMakeLists.txt @@ -1,52 +1,52 @@ #------------ Application SetUp ---------------- -cmake_minimum_required(VERSION 3.10) + cmake_minimum_required(VERSION 3.10) -# Set the project name -project(StudentManagementSystem CXX) + # Set the project name + project(StudentManagementSystem CXX) -# C++ standard -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) + # C++ standard + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) -# Drogon framework -find_package(Drogon REQUIRED) + # Drogon framework + find_package(Drogon REQUIRED) -# libsodium (for password hashing) -find_package(PkgConfig REQUIRED) -pkg_check_modules(SODIUM REQUIRED libsodium) # libsodium + # libsodium (for password hashing) + find_package(PkgConfig REQUIRED) + pkg_check_modules(SODIUM REQUIRED libsodium) # libsodium -# Source files -file(GLOB_RECURSE SOURCES - main.cpp - src/*.cpp - controllers/*.cpp - controllers/auth/*.cpp -) + # Source files + file(GLOB_RECURSE SOURCES + main.cpp + src/*.cpp + controllers/*.cpp + controllers/auth/*.cpp + ) -# Executable -add_executable(${PROJECT_NAME} - ${SOURCES} + # Executable + add_executable(${PROJECT_NAME} + ${SOURCES} -) + ) -# Link libraries -target_link_libraries(${PROJECT_NAME} - PRIVATE - Drogon::Drogon - ${SODIUM_LIBRARIES} -) + # Link libraries + target_link_libraries(${PROJECT_NAME} + PRIVATE + Drogon::Drogon + ${SODIUM_LIBRARIES} + ) -# Use C++20 features -target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20) + # Use C++20 features + target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20) -target_include_directories(${PROJECT_NAME} PUBLIC - controllers/auth - ${SODIUM_INCLUDE_DIRS} -) -# Include directories -target_include_directories(${PROJECT_NAME} PRIVATE .) + target_include_directories(${PROJECT_NAME} PUBLIC + controllers/auth + ${SODIUM_INCLUDE_DIRS} + ) + # Include directories + target_include_directories(${PROJECT_NAME} PRIVATE .) #------------------------------------------------------------------- @@ -54,64 +54,96 @@ target_include_directories(${PROJECT_NAME} PRIVATE .) -# # ------------ Unit-Test SetUp ---------------- -# cmake_minimum_required(VERSION 3.10) - -# # Set the project name -# project(StudentManagementSystem CXX) -# set(CMAKE_CXX_STANDARD 20) -# set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# # Include directories (add the path to your CxxTest installation) -# include_directories(/opt/homebrew/opt/cxxtest/include) - -# # Set the path to the cxxtestgen script -# set(CXXTEST_TESTGEN_EXECUTABLE /opt/homebrew/opt/cxxtest/bin/cxxtestgen) - - -# # ---- This MUST Stay before find_package for it to work ---- -# set(CXXTEST_TESTGEN_EXTRA_ARGS "--runner=VerbosePrinter") - -# # Find CxxTest package -# find_package(CxxTest REQUIRED) -# # Source files -# file(GLOB_RECURSE SOURCES -# controllers/auth/*.cpp -# ) -# # Enable testing -# enable_testing() +# ========== UNCOMMENT BELOW CODE WHEN RUNNING UNIT TEST =========== -# # ---- libsodium (via pkg-config; simplest on macOS/Linux) ---- -# find_package(PkgConfig REQUIRED) -# pkg_check_modules(SODIUM REQUIRED libsodium) # finds include/lib flags - - -# # Create the test target using the generated runner.cpp -# CXXTEST_ADD_TEST(unittest # Name of the test target -# runner.cpp # Generated by cxxtestgen -# ${CMAKE_CURRENT_SOURCE_DIR}/tests/newCxxTest.h # Test file -# ${CMAKE_CURRENT_SOURCE_DIR}/controllers/auth/Hasher.h # Source files to be tested -# ) - -# target_sources(unittest PRIVATE -# ${CMAKE_CURRENT_SOURCE_DIR}/controllers/auth/Hasher.cpp -# ) - -# # UPDATED: Use SODIUM_LINK_LIBRARIES for full paths (fixes linker 'not found') -# target_include_directories(unittest PRIVATE ${SODIUM_INCLUDE_DIRS}) -# target_link_libraries(unittest PRIVATE ${SODIUM_LINK_LIBRARIES}) - -# # Add the executable for the StudentManagementSystem project -# add_executable(${PROJECT_NAME} ${SOURCES} -# runner.cpp # Generated by cxxtestgen -# ) - -# # UPDATED: Same for main executable -# target_include_directories(${PROJECT_NAME} PRIVATE ${SODIUM_INCLUDE_DIRS}) -# target_link_libraries(${PROJECT_NAME} PRIVATE ${SODIUM_LINK_LIBRARIES}) +# # ------------ Unit-Test SetUp ---------------- +#cmake_minimum_required(VERSION 3.10) +# +## Set the project name +#project(StudentManagementSystem CXX) +#set(CMAKE_CXX_STANDARD 20) +#set(CMAKE_CXX_STANDARD_REQUIRED ON) +# +## Include directories (add the path to your CxxTest installation) +#include_directories(/opt/homebrew/opt/cxxtest/include) +# +## Set the path to the cxxtestgen script +#set(CXXTEST_TESTGEN_EXECUTABLE /opt/homebrew/opt/cxxtest/bin/cxxtestgen) +# +## Set the path to Jsoncpp +#include_directories(/opt/homebrew/Cellar/json-c/0.18/include) +# +## Set the path to drogon +#include_directories(/opt/homebrew/Cellar/drogon/1.9.11/include) +# +# +## ---- This MUST Stay before find_package for it to work ---- +#set(CXXTEST_TESTGEN_EXTRA_ARGS "--runner=VerbosePrinter") +# +## Find CxxTest package +#find_package(CxxTest REQUIRED) +# +# +# +## Source files +#file(GLOB_RECURSE SOURCES +# controllers/auth/*.cpp +# controllers/admin/*.cpp +#) +# +## Enable testing +#enable_testing() +# +## ---- libsodium (via pkg-config; simplest on macOS/Linux) ---- +#find_package(PkgConfig REQUIRED) +#pkg_check_modules(SODIUM REQUIRED libsodium) # finds include/lib flags +#find_package(Drogon REQUIRED) +## Create the test target using the generated runner.cpp +#CXXTEST_ADD_TEST(unittest # Name of the test target +# runner.cpp # Generated by cxxtestgen +# ${CMAKE_CURRENT_SOURCE_DIR}/tests/newCxxTest.h # Test file +# ${CMAKE_CURRENT_SOURCE_DIR}/controllers/auth/Hasher.h # Source files to be tested +# ${CMAKE_CURRENT_SOURCE_DIR}/controllers/auth/StoreCredential.h # Source files to be tested +# ${CMAKE_CURRENT_SOURCE_DIR}/controllers/admin/StudentController.h +#) +# +#target_sources(unittest PRIVATE +# ${CMAKE_CURRENT_SOURCE_DIR}/controllers/auth/Hasher.cpp +# ${CMAKE_CURRENT_SOURCE_DIR}/controllers/auth/StoreCredential.cpp +# ${CMAKE_CURRENT_SOURCE_DIR}/controllers/admin/StudentController.cpp +#) +# +## UPDATED: Use SODIUM_LINK_LIBRARIES for full paths (fixes linker 'not found') +#target_include_directories(unittest +# PRIVATE +# ${SODIUM_INCLUDE_DIRS} +# ${Drogon_INCLUDE_DIRS} +# ) +#target_link_libraries(unittest +# PRIVATE +# Drogon::Drogon +# ${SODIUM_LINK_LIBRARIES} +# ) +# +## Add the executable for the StudentManagementSystem project +#add_executable(${PROJECT_NAME} ${SOURCES} +# runner.cpp # Generated by cxxtestgen +#) +# +## UPDATED: Same for main executable +#target_include_directories(${PROJECT_NAME} +# PRIVATE +# ${Drogon_INCLUDE_DIRS} +# ${SODIUM_INCLUDE_DIRS} +# ) +#target_link_libraries(${PROJECT_NAME} +# PRIVATE +# Drogon::Drogon +# ${SODIUM_LINK_LIBRARIES} +#) # ------------------------------------------------------------------- \ No newline at end of file diff --git a/backend/controllers/GateController.cpp b/backend/controllers/GateController.cpp index 514dad0..39d6f56 100644 --- a/backend/controllers/GateController.cpp +++ b/backend/controllers/GateController.cpp @@ -1,23 +1,23 @@ #include "GateController.h" + #include -#include -#include #include +#include +#include #include #include #include -#include + #include +#include using namespace drogon; using namespace drogon::orm; -void GateController::handleLogin(const HttpRequestPtr &req, - std::function &&callback) -{ +void GateController::handleLogin(const HttpRequestPtr& req, + std::function&& callback) { auto json = req->getJsonObject(); - if (!json || !json->isMember("username") || !json->isMember("password")) - { + if (!json || !json->isMember("username") || !json->isMember("password")) { auto resp = HttpResponse::newHttpJsonResponse(Json::Value("Missing credentials")); resp->setStatusCode(k400BadRequest); callback(resp); @@ -28,8 +28,7 @@ void GateController::handleLogin(const HttpRequestPtr &req, std::string password = (*json)["password"].asString(); // ✅ Basic validation - if (username.empty() || password.empty()) - { + if (username.empty() || password.empty()) { Json::Value result; result["message"] = "Username and password required"; auto resp = HttpResponse::newHttpJsonResponse(result); @@ -38,11 +37,9 @@ void GateController::handleLogin(const HttpRequestPtr &req, return; } - try - { + try { auto clientPtr = drogon::app().getDbClient(); - if (!clientPtr) - { + if (!clientPtr) { throw std::runtime_error("Database client not available"); } @@ -51,18 +48,14 @@ void GateController::handleLogin(const HttpRequestPtr &req, clientPtr->execSqlAsync( sql, - [callback, req](const Result &r) - { - if (r.empty()) - { + [callback, req](const Result& r) { + if (r.empty()) { Json::Value result; result["message"] = "Incorrect username or password"; auto resp = HttpResponse::newHttpJsonResponse(result); resp->setStatusCode(k401Unauthorized); callback(resp); - } - else - { + } else { std::string username = r[0]["username"].as(); std::string role = r[0]["role"].as(); Json::Value result; @@ -71,8 +64,7 @@ void GateController::handleLogin(const HttpRequestPtr &req, // ✅ Correct session handling for newer Drogon versions auto session = req->session(); - if (session) - { + if (session) { session->insert("role", role); session->insert("username", username); } @@ -91,8 +83,7 @@ void GateController::handleLogin(const HttpRequestPtr &req, callback(resp); } }, - [callback](const DrogonDbException &e) - { + [callback](const DrogonDbException& e) { LOG_ERROR << "Database error: " << e.base().what(); Json::Value result; result["message"] = "Database error"; @@ -100,11 +91,8 @@ void GateController::handleLogin(const HttpRequestPtr &req, resp->setStatusCode(k500InternalServerError); callback(resp); }, - username, - password); - } - catch (const std::exception &e) - { + username, password); + } catch (const std::exception& e) { LOG_ERROR << "Exception: " << e.what(); auto resp = HttpResponse::newHttpJsonResponse(Json::Value("Server error")); resp->setStatusCode(k500InternalServerError); @@ -112,25 +100,21 @@ void GateController::handleLogin(const HttpRequestPtr &req, } } -void GateController::serveProtectedFile(const HttpRequestPtr &req, - std::function &&callback, - const std::string &role, - const std::string &path) -{ +void GateController::serveProtectedFile(const HttpRequestPtr& req, + std::function&& callback, + const std::string& role, const std::string& path) { // ✅ Check cookie instead of session for simplicity auto cookies = req->getCookies(); auto roleCookie = req->getCookie("user_role"); - if (roleCookie.empty()) - { + if (roleCookie.empty()) { // No role cookie, redirect to login auto resp = HttpResponse::newRedirectionResponse("/index.html"); callback(resp); return; } - if (roleCookie != role) - { + if (roleCookie != role) { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k403Forbidden); resp->setBody("Access forbidden"); @@ -140,8 +124,7 @@ void GateController::serveProtectedFile(const HttpRequestPtr &req, // ✅ Path sanitization std::string cleanPath = path.empty() ? "index.html" : path; - if (cleanPath.find("..") != std::string::npos || cleanPath.find("/") == 0) - { + if (cleanPath.find("..") != std::string::npos || cleanPath.find("/") == 0) { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k400BadRequest); resp->setBody("Invalid path"); @@ -152,15 +135,12 @@ void GateController::serveProtectedFile(const HttpRequestPtr &req, std::string fullPath = drogon::app().getDocumentRoot() + "/" + role + "/" + cleanPath; // Check if file exists - if (!std::filesystem::exists(fullPath)) - { + if (!std::filesystem::exists(fullPath)) { // Try with index.html if it's a directory - if (std::filesystem::is_directory(fullPath)) - { + if (std::filesystem::is_directory(fullPath)) { fullPath += "/index.html"; } - if (!std::filesystem::exists(fullPath)) - { + if (!std::filesystem::exists(fullPath)) { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k404NotFound); callback(resp); diff --git a/backend/controllers/admin/StudentController.cpp b/backend/controllers/admin/StudentController.cpp index 55a6828..bec307b 100644 --- a/backend/controllers/admin/StudentController.cpp +++ b/backend/controllers/admin/StudentController.cpp @@ -1,106 +1,77 @@ #include "StudentController.h" -#include -#include using namespace drogon; -void StudentController::createStudent(const HttpRequestPtr &req, - std::function &&callback) -{ +void StudentController::createStudent(const HttpRequestPtr& req, + std::function&& callback) { auto json = req->getJsonObject(); - if (!json) - { + if (!json) { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k400BadRequest); resp->setBody("Invalid JSON"); callback(resp); return; } + // for debugging + LOG_INFO << "Received JSON: " << json->toStyledString(); // Validate required fields - if (!json->isMember("name") || !json->isMember("email") || + // !important- TODO: Needs improvement! Doesn't handle the multi data model + if (!json->isMember("firstName") || !json->isMember("lastName") || !json->isMember("email") || !json->isMember("studentId") || !json->isMember("phone") || - !json->isMember("dateofbirth") || !json->isMember("address") || - !json->isMember("sex")) - { + !json->isMember("dateofbirth") || !json->isMember("address") || !json->isMember("sex")) { auto resp = HttpResponse::newHttpResponse(); resp->setStatusCode(k400BadRequest); resp->setBody("Missing required fields"); callback(resp); return; } + // Otherwise response with 200 OK + auto resp = HttpResponse::newHttpResponse(); + resp->setStatusCode(k200OK); + resp->setBody("Request received successfully"); + callback(resp); - auto client = app().getDbClient("default"); + // Extract and validate fields + std::string firstName = json->get("firstName", "").asString(); + std::string lastName = json->get("lastName", "").asString(); + std::string email = json->get("email", "").asString(); + std::string studentId = json->get("studentId", "").asString(); + std::string phone = json->get("phone", "").asString(); + std::string dateOfBirth = json->get("dateofbirth", "").asString(); + std::string address = json->get("address", "").asString(); + std::string sex = json->get("sex", "").asString(); + std::string username = json->get("username", "").asString(); + //std::string password = json->get("password", "").asString(); - // Fixed: Proper Drogon execSqlAsync syntax with parameters after callbacks - client->execSqlAsync( - "INSERT INTO Students (name, phone, email, dateofbirth, address, sex, studentId) VALUES (?,?,?,?,?,?,?)", - [callback](const drogon::orm::Result &) - { - auto resp = HttpResponse::newHttpResponse(); - resp->setStatusCode(k200OK); - resp->setBody("Student created successfully"); - callback(resp); - }, - [callback](const drogon::orm::DrogonDbException &e) - { - auto resp = HttpResponse::newHttpResponse(); - resp->setStatusCode(k500InternalServerError); - resp->setBody("Database error: " + std::string(e.base().what())); - callback(resp); - }, - (*json)["name"].asString(), - (*json)["phone"].asString(), - (*json)["email"].asString(), - (*json)["dateofbirth"].asString(), - (*json)["address"].asString(), - (*json)["sex"].asString(), - (*json)["studentId"].asString()); -} - -// Handles GET request to retrieve all student records from the database -void StudentController::getAllStudents(const HttpRequestPtr &req, - std::function &&callback) -{ - // Get the default database client instance - auto client = app().getDbClient("default"); - - // Execute SQL query asynchronously to fetch student data - client->execSqlAsync( - "SELECT id, name, phone, email, dateofbirth, address, sex, studentId FROM Students", - - // Success callback: transform DB result into JSON array - [callback](const drogon::orm::Result &result) - { - Json::Value jsonResponse(Json::arrayValue); + // Basic validation for empty fields + if (firstName.empty() || lastName.empty() || email.empty() || studentId.empty() || + phone.empty() || dateOfBirth.empty() || address.empty() || sex.empty() || (json->get("password", "").asString()).empty()) { + auto resp = HttpResponse::newHttpResponse(); + resp->setStatusCode(k400BadRequest); + resp->setBody("Fields cannot be empty"); + callback(resp); + return; + } + try { + // Hash password here as soon as received from client-side + //Hasher hasher(HashConfig{1, crypto_pwhash_MEMLIMIT_MIN}); + Hasher hasher(HashConfig{4, 1ull * 1024 * 1024 * 1024}); + std::string hashedPassword = hasher.hash(json->get("password", "").asString()); + if (hashedPassword.empty()) { + throw std::runtime_error("Password hashing failed"); + } - // Iterate through each row and build a JSON object for each student - for (const auto &row : result) - { - Json::Value student; - student["studentId"] = row["studentId"].as(); - student["name"] = row["name"].as(); - student["dateofbirth"] = row["dateofbirth"].as(); - student["sex"] = row["sex"].as(); - student["phone"] = row["phone"].as(); - student["email"] = row["email"].as(); - student["address"] = row["address"].as(); - //student["enrollmentStatus"] = row["enrollmentStatus"].as(); // May be null - jsonResponse.append(student); - } + // Call the StoreCredential class to construct object and store in DB + StoreCredential storeCredential(firstName, lastName, dateOfBirth, email, phone, address, + sex, studentId, username, hashedPassword); - // Return JSON response with HTTP 200 OK - auto resp = HttpResponse::newHttpJsonResponse(jsonResponse); - resp->setStatusCode(k200OK); - callback(resp); - }, + storeCredential.storeToDB(); // call storeToDB method to store info to database - // Error callback: return HTTP 500 with error message - [callback](const drogon::orm::DrogonDbException &e) - { - auto resp = HttpResponse::newHttpResponse(); - resp->setStatusCode(k500InternalServerError); - resp->setBody("Database error: " + std::string(e.base().what())); - callback(resp); - }); + } catch (const std::exception& e) { + auto resp = HttpResponse::newHttpResponse(); + resp->setStatusCode(k500InternalServerError); + resp->setBody("Error: " + std::string(e.what())); + callback(resp); + } } diff --git a/backend/controllers/admin/StudentController.h b/backend/controllers/admin/StudentController.h index 219baeb..ae590e7 100644 --- a/backend/controllers/admin/StudentController.h +++ b/backend/controllers/admin/StudentController.h @@ -1,19 +1,33 @@ #pragma once #include +#include +#include +#include +#include +#include + +#include "../auth/Hasher.h" +#include "../auth/StoreCredential.h" class StudentController : public drogon::HttpController { -public: + public: METHOD_LIST_BEGIN // Admin-only endpoint to create a new student ADD_METHOD_TO(StudentController::createStudent, "/api/admin/students", drogon::Post); - // Route: GET /api/admin/students → calls getAllStudents - ADD_METHOD_TO(StudentController::getAllStudents, "/api/admin/students", drogon::Get); METHOD_LIST_END // Handler - void createStudent(const drogon::HttpRequestPtr &req, - std::function &&callback); - // Handles GET request to return all students as JSON - void getAllStudents(const drogon::HttpRequestPtr &req, - std::function &&callback); + void createStudent(const drogon::HttpRequestPtr& req, + std::function&& callback); + + + // TODO: In the next story - We may need to add another function that checks for data duplicates before inserting data into DB. + // Maybe Batch processing method. + // + /* List of this that can be duplicate: + * name + * Dob + * Phone + * address + * */ }; diff --git a/backend/controllers/auth/Hasher.cpp b/backend/controllers/auth/Hasher.cpp index 5f99990..9606b4f 100644 --- a/backend/controllers/auth/Hasher.cpp +++ b/backend/controllers/auth/Hasher.cpp @@ -17,44 +17,45 @@ std::string Hasher::hash(const std::string& password) const { ensureSodiumReady(); // Ensure libsodium is initialized and must be called before any other // libsodium function. - // Libsodium outputs a PHC-like encoded string into fixed-size buffer. - std::array encoded{}; - const int rc = crypto_pwhash_str(encoded.data(), // output buffer - password.c_str(), // password input - password.size(), // password length - cfg_.opslimit, // opslimit (time) - cfg_.memlimit_bytes // memlimit (memory) - ); - if (rc != 0) { - // typically means memlimit too high for the machine/container + if (password.empty()) { + throw std::invalid_argument("Password cannot be empty"); + } + //TODO: Remove std::cerr log to avoid security issue + std::cerr << "Hashing with opslimit=" << cfg_.opslimit << ", memlimit=" << cfg_.memlimit_bytes + << " bytes\n"; + std::string encoded(crypto_pwhash_STRBYTES, '\0'); + if (crypto_pwhash_str(encoded.data(), password.c_str(), password.size(), cfg_.opslimit, + cfg_.memlimit_bytes) != 0) { throw std::runtime_error("crypto_pwhash_str failed (insufficient memory resources?)"); } - return std::string(encoded.data()); // PHC string is NULL-terminated + encoded.resize(std::strlen(encoded.c_str())); + return encoded; } bool Hasher::verify(const std::string& password, const std::string& encoded) const { ensureSodiumReady(); // Ensure libsodium is initialized and must be called before any other // libsodium function. - - // Verify password against the stored hash - const int rc = crypto_pwhash_str_verify(encoded.c_str(), // stored hash (PHC string) - password.c_str(), // password input - password.size() // password length - ); - return rc == 0; // 0 = success; -1 = failure libsodium uses constant-time comparison internally + if (password.empty() || encoded.empty()) { + return false; + } + return crypto_pwhash_str_verify(encoded.c_str(), password.c_str(), password.size()) == + 0; // 0 = match, -1 = no match } bool Hasher::needsRehash(const std::string& encoded) const { ensureSodiumReady(); - - const int rc = crypto_pwhash_str_needs_rehash(encoded.c_str(), // stored hash (PHC string) - cfg_.opslimit, // current opslimit - cfg_.memlimit_bytes // current memlimit - ); - - if (rc < 0) { - // Malformed/unknown string -> treat as needs rehash ( or reject at higher layer ) + if (encoded.empty()) { + return true; + } + // Rehash if params are weaker + size_t mem_pos = encoded.find("m="); + size_t ops_pos = encoded.find("t="); + if (mem_pos == std::string::npos || ops_pos == std::string::npos) { return true; } - return rc == 1; // 1 = needs rehash; 0 = ok + size_t mem_end = encoded.find(',', mem_pos); + size_t ops_end = encoded.find(',', ops_pos); + unsigned long long stored_mem = std::stoull(encoded.substr(mem_pos + 2, mem_end - mem_pos - 2)); + unsigned long long stored_ops = std::stoull(encoded.substr(ops_pos + 2, ops_end - ops_pos - 2)); + return (stored_mem < cfg_.memlimit_bytes) || (stored_ops < cfg_.opslimit); } \ No newline at end of file diff --git a/backend/controllers/auth/Hasher.h b/backend/controllers/auth/Hasher.h index 177f3b7..aa0e737 100644 --- a/backend/controllers/auth/Hasher.h +++ b/backend/controllers/auth/Hasher.h @@ -4,15 +4,14 @@ #include #include +#include #include #include #include struct HashConfig { - // libsodium “opslimit” (time) and “memlimit” (memory) knobs - // Start here; you can raise them after benchmarking your server. - unsigned long long opslimit = 3; // crypto_pwhash_OPSLIMIT_MODERATE-ish - size_t memlimit_bytes = 64ull * 1024 * 1024; // 64 MB + unsigned long long opslimit = 4; // Minimal time cost + size_t memlimit_bytes = 1ull * 1024 * 1024 * 1024; // 8 KB, absolute minimum }; class Hasher { diff --git a/backend/controllers/auth/StoreCredential.cpp b/backend/controllers/auth/StoreCredential.cpp new file mode 100644 index 0000000..b185b9d --- /dev/null +++ b/backend/controllers/auth/StoreCredential.cpp @@ -0,0 +1,57 @@ +#include "StoreCredential.h" + +#include +#include +#include + +#include +#include +#include + +// Non-default constructor +// TODO: Initialize all data from add-student form to this constructor +StoreCredential::StoreCredential(const std::string& firstname, const std::string& lastname, + const std::string& DOB, const std::string& email, + const std::string& phone_no, const std::string& address, + const std::string& sex, const std::string& studentID, + const std::string& username, const std::string& hashed_pass) + : firstname(firstname), + lastname(lastname), + DOB(DOB), + email(email), + phone(phone_no), + address(address), + sex(sex), + studentID(studentID), + username(username), + hashed_password(hashed_pass) {} + +const std::string& StoreCredential::getFirstName() const { return firstname; } +const std::string& StoreCredential::getLastName() const { return lastname; } +const std::string& StoreCredential::getDOB() const { return DOB; } +const std::string& StoreCredential::getEmail() const { return email; } +const std::string& StoreCredential::getPhone() const { return phone; } +const std::string& StoreCredential::getAddress() const { return address; } +const std::string& StoreCredential::getSex() const { return sex; } + +const std::string& StoreCredential::getStudentID() const { return studentID; } + +const std::string& StoreCredential::getUsername() const { return username; } +const std::string& StoreCredential::getHashedPassword() const { return hashed_password; } + +// stores Info to Database +void StoreCredential::storeToDB() const { + auto client = drogon::app().getDbClient(); + // Use async exec; throw on sync for simplicity, or handle async + try { + //TODO: add timestamp + auto result = client->execSqlSync( + "INSERT INTO Students (first_name, last_name, dob, email, phone, address, sex, " + "student_id, username, password_hash) " + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + firstname, lastname, DOB, email, phone, address, sex, studentID, username, + hashed_password); + } catch (const drogon::orm::DrogonDbException& e) { + throw std::runtime_error("DB storage failed: " + std::string(e.base().what())); + } +} \ No newline at end of file diff --git a/backend/controllers/auth/StoreCredential.h b/backend/controllers/auth/StoreCredential.h new file mode 100644 index 0000000..a78a51c --- /dev/null +++ b/backend/controllers/auth/StoreCredential.h @@ -0,0 +1,38 @@ +#ifndef STORE_CREDENTIAL_H +#define STORE_CREDENTIAL_H + +#include +#include + +#include "Hasher.h" + +class StoreCredential { + private: + std::string firstname, lastname, DOB, email, phone, address, sex, studentID, username, + hashed_password; + + public: + // non-default constructor + StoreCredential(const std::string& firstname, const std::string& lastname, + const std::string& DOB, const std::string& email, const std::string& phone_no, + const std::string& address, const std::string& sex, + const std::string& studentID, const std::string& username, + const std::string& password); + + // Getter & Setter for unit-testing + const std::string& getFirstName() const; + const std::string& getLastName() const; + const std::string& getDOB() const; + const std::string& getEmail() const; + const std::string& getPhone() const; + const std::string& getAddress() const; + const std::string& getSex() const; + const std::string& getStudentID() const; + const std::string& getUsername() const; + const std::string& getHashedPassword() const; + + // store username and hashed password into database + void storeToDB() const; +}; + +#endif // STORE_CREDENTIAL_H diff --git a/backend/db/init/add_student.sql b/backend/db/init/add_student.sql index 4d94434..178e8b9 100644 --- a/backend/db/init/add_student.sql +++ b/backend/db/init/add_student.sql @@ -1,13 +1,15 @@ USE studentmanagementsystem; CREATE TABLE IF NOT EXISTS Students ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(100) NOT NULL, - phone VARCHAR(20) NOT NULL, - email VARCHAR(100) NOT NULL UNIQUE, - dateofbirth DATE NOT NULL, - address TEXT NOT NULL, - sex VARCHAR(10) NOT NULL, - studentId VARCHAR(50) NOT NULL UNIQUE, - timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) + id INT AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + dob VARCHAR(255), + email VARCHAR(255) NOT NULL, + phone VARCHAR(50), + address VARCHAR(255), + sex VARCHAR(10), + student_id VARCHAR(50) NOT NULL, + username VARCHAR(255), + password_hash VARCHAR(255) NOT NULL + ); diff --git a/backend/db/init/createdb.sql b/backend/db/init/createdb.sql index 8427068..7791ddc 100644 --- a/backend/db/init/createdb.sql +++ b/backend/db/init/createdb.sql @@ -10,3 +10,12 @@ CREATE TABLE IF NOT EXISTS Users ( password VARCHAR(100) NOT NULL, role ENUM('admin', 'student') NOT NULL ); +-- ✅ Demo admin account (plaintext for now — hash later) +INSERT INTO Users (username, password, role) +VALUES ('admin01', 'Admin123!', 'admin') +ON DUPLICATE KEY UPDATE password='Admin123!', role='admin'; + +-- ✅ Add a demo student account for testing +INSERT INTO Users (username, password, role) +VALUES ('student01', 'Student123!', 'student') +ON DUPLICATE KEY UPDATE password='Student123!', role='student'; \ No newline at end of file diff --git a/backend/db/init/login.sql b/backend/db/init/login.sql deleted file mode 100644 index 93da835..0000000 --- a/backend/db/init/login.sql +++ /dev/null @@ -1,8 +0,0 @@ -USE studentmanagementsystem; - -CREATE TABLE IF NOT EXISTS Login ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(20), - passwd VARCHAR(20), - timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP -) \ No newline at end of file diff --git a/backend/tests/newCxxTest.h b/backend/tests/newCxxTest.h index 01ad45c..9bf53c4 100644 --- a/backend/tests/newCxxTest.h +++ b/backend/tests/newCxxTest.h @@ -3,8 +3,6 @@ #ifndef NEWCXXTEST_H #define NEWCXXTEST_H -/* include this path when using vscode: /opt/homebrew/opt/cxxtest/include */ - #include #include @@ -13,119 +11,116 @@ #include #include "../controllers/auth/Hasher.h" - -// Use modest defaults so tests run quickly. -// You can bump mem/ops for a heavier test build later. -static HashConfig testCfg() { - HashConfig cfg; - cfg.opslimit = 2; // lighter for CI - cfg.memlimit_bytes = 32ull * 1024 * 1024; // 32 MB - return cfg; -} - +#include "../controllers/auth/StoreCredential.h" class newCxxTest : public CxxTest::TestSuite { public: // test - void testHashAndVerifyOk() { - Hasher hasher(testCfg()); - const std::string pwd = "example-password"; - const std::string enc = hasher.hash(pwd); - - // For debugging - std::cout << std::endl; - std::cout << "=============================================================================" - "=============================================================================" - << std::endl; - std::ostringstream oss1; - oss1 << "Original Password: " << pwd; - std::cout << oss1.str() << std::endl; - std::ostringstream oss2; - oss2 << "Encoded Password: " << enc; - std::cout << oss2.str() << std::endl; - - TS_ASSERT(!enc.empty()); - TS_ASSERT(hasher.verify(pwd, enc)); - } - - void testVerifyWrongPasswordFails() { - Hasher hasher(testCfg()); - const std::string enc = hasher.hash("secret-123"); - - // For debugging - std::cout << "=============================================================================" - "============================================================================" - << std::endl; - std::ostringstream oss1; - oss1 << "Encoded Password: " << enc; - std::cout << oss1.str() << std::endl; - - TS_ASSERT(!hasher.verify("wrong", enc)); - } - - void testDifferentSaltsDifferentHashes() { - Hasher hasher(testCfg()); - const std::string pwd = "same-password"; - const std::string a = hasher.hash(pwd); - const std::string b = hasher.hash(pwd); - - std::cout << "=============================================================================" - "============================================================================" - << std::endl; - std::ostringstream oss1; - oss1 << "Original Password: " << pwd; - std::cout << oss1.str() << std::endl; - - std::ostringstream oss2; - oss2 << "Encoded A: " << a; - - std::cout << oss2.str() << std::endl; - std::ostringstream oss3; - - oss3 << "Encoded B: " << b; - std::cout << oss3.str() << std::endl; - - TS_ASSERT_DIFFERS(a, b); // probabilistically true because fresh salt - } - - void testNeedsRehashWhenParamsIncrease() { - // Create a hash with lower parameters... - Hasher weak({.opslimit = 1, .memlimit_bytes = 16ull * 1024 * 1024}); - std::string enc = weak.hash("pw-abc"); - - // For debugging - std::cout << "=============================================================================" - "============================================================================" - << std::endl; - std::ostringstream oss1; - - oss1 << "Encoded with weak policy: " << enc; - std::cout << oss1.str() << std::endl; - - // ...then check with stronger policy. - Hasher strong({.opslimit = 3, .memlimit_bytes = 64ull * 1024 * 1024}); - TS_ASSERT(strong.needsRehash(enc)); - } - - void testEncodedLooksLikeArgon2id() { - Hasher hasher(testCfg()); - const std::string enc = hasher.hash("pw"); - - // For debugging - std::cout << "=============================================================================" - "============================================================================" - << std::endl; - std::ostringstream oss1; - oss1 << "Encoded Password: " << enc; - - std::cout << oss1.str() << std::endl; - - // libsodium returns PHC-like strings starting with "$argon2id$" - TS_ASSERT(enc.rfind("$argon2id$", 0) == 0); - std::cout - << "=========================================================================== End" - " ==========================================================================" - << std::endl; - } + void testHashAndVerifyOk() { + Hasher hasher(HashConfig{1, crypto_pwhash_MEMLIMIT_MIN}); + const std::string pwd = "example-password"; + const std::string enc = hasher.hash(pwd); + + // For debugging + std::cout << std::endl; + std::cout << + "=============================================================================" + "=============================================================================" + << std::endl; + std::ostringstream oss1; + oss1 << "Original Password: " << pwd; + std::cout << oss1.str() << std::endl; + std::ostringstream oss2; + oss2 << "Encoded Password: " << enc; + std::cout << oss2.str() << std::endl; + + TS_ASSERT(!enc.empty()); + TS_ASSERT(hasher.verify(pwd, enc)); + } + + void testVerifyWrongPasswordFails() { + Hasher hasher(HashConfig{1, crypto_pwhash_MEMLIMIT_MIN}); + const std::string enc = hasher.hash("secret-123"); + + // For debugging + std::cout << + "=============================================================================" + "============================================================================" + << std::endl; + std::ostringstream oss1; + oss1 << "Encoded Password: " << enc; + std::cout << oss1.str() << std::endl; + + TS_ASSERT(!hasher.verify("wrong", enc)); + } + + void testDifferentSaltsDifferentHashes() { + Hasher hasher(HashConfig{1, crypto_pwhash_MEMLIMIT_MIN}); + const std::string pwd = "same-password"; + const std::string a = hasher.hash(pwd); + const std::string b = hasher.hash(pwd); + + std::cout << + "=============================================================================" + "============================================================================" + << std::endl; + std::ostringstream oss1; + oss1 << "Original Password: " << pwd; + std::cout << oss1.str() << std::endl; + + std::ostringstream oss2; + oss2 << "Encoded A: " << a; + + std::cout << oss2.str() << std::endl; + std::ostringstream oss3; + + oss3 << "Encoded B: " << b; + std::cout << oss3.str() << std::endl; + + TS_ASSERT_DIFFERS(a, b); // probabilistically true because fresh salt + } + + void testNeedsRehashWhenParamsIncrease() { + // Create a hash with lower parameters... + Hasher weak({.opslimit = 1, .memlimit_bytes = 16ull * 1024 * 1024}); + std::string enc = weak.hash("pw-abc"); + + // For debugging + std::cout << + "=============================================================================" + "============================================================================" + << std::endl; + std::ostringstream oss1; + + oss1 << "Encoded with weak policy: " << enc; + std::cout << oss1.str() << std::endl; + + // ...then check with stronger policy. + Hasher strong({.opslimit = 3, .memlimit_bytes = 64ull * 1024 * 1024}); + TS_ASSERT(strong.needsRehash(enc)); + } + + void testEncodedLooksLikeArgon2id() { + Hasher hasher(HashConfig{1, crypto_pwhash_MEMLIMIT_MIN}); + const std::string enc = hasher.hash("pw"); + + // For debugging + std::cout << + "=============================================================================" + "============================================================================" + << std::endl; + std::ostringstream oss1; + oss1 << "Encoded Password: " << enc; + + std::cout << oss1.str() << std::endl; + + // libsodium returns PHC-like strings starting with "$argon2id$" + TS_ASSERT(enc.rfind("$argon2id$", 0) == 0); + std::cout + << "=========================================================================== End" + " ==========================================================================" + << std::endl; + } + }; #endif /* NEWCXXTEST_H */ diff --git a/docs/admin/assets/sidebar.css b/docs/admin/assets/sidebar.css deleted file mode 100644 index 09dc01a..0000000 --- a/docs/admin/assets/sidebar.css +++ /dev/null @@ -1,53 +0,0 @@ -/* Base layout setup */ -body { - margin: 0; - display: flex; /* Enables sidebar + main layout */ - height: 100vh; - font-family: "Segoe UI", sans-serif; -} - -/* Sidebar styling */ -.sidebar { - width: 220px; - background-color: #1e1e2f; - color: #fff; - display: flex; - flex-direction: column; - padding: 20px; - height: 100vh; /* Fill full vertical space */ - position: fixed; /* Stay fixed on the left */ - top: 0; - left: 0; -} - -/* Sidebar title */ -.sidebar h2 { - font-size: 1.5rem; - margin-bottom: 30px; - text-align: center; -} - -/* Sidebar links */ -.sidebar a { - color: #cfd1d7; - text-decoration: none; - padding: 10px 15px; - border-radius: 6px; - margin-bottom: 10px; - transition: background 0.2s ease; -} - -/* Hover and active link styles */ -.sidebar a:hover, -.sidebar a.active { - background-color: #34344a; - color: #fff; -} - -/* Main content area */ -.main { - flex-grow: 1; /* Take remaining space */ - background-color: #f8f9fa; - padding: 40px; - margin-left: 220px; /* Offset for fixed sidebar */ -} diff --git a/docs/admin/assets/sidebar.js b/docs/admin/assets/sidebar.js deleted file mode 100644 index f1ea5d6..0000000 --- a/docs/admin/assets/sidebar.js +++ /dev/null @@ -1,25 +0,0 @@ -// Load sidebar HTML and highlight the active link -document.addEventListener("DOMContentLoaded", () => { - const sidebarContainer = document.getElementById("sidebar-container"); - - if (sidebarContainer) { - // Inject sidebar.html into the container - fetch("sidebar.html") - .then(res => res.text()) - .then(html => { - sidebarContainer.innerHTML = html; - - // Add 'active' class to the link matching current page - const links = sidebarContainer.querySelectorAll("a"); - const currentPage = window.location.pathname; - links.forEach(link => { - if (link.getAttribute("href") === currentPage) { - link.classList.add("active"); - } - }); - }) - .catch(err => { - console.error("Error loading sidebar:", err); - }); - } -}); diff --git a/docs/admin/assets/student_table.css b/docs/admin/assets/student_table.css deleted file mode 100644 index f521ac8..0000000 --- a/docs/admin/assets/student_table.css +++ /dev/null @@ -1,55 +0,0 @@ -/* =============================== - Table layout and formatting - =============================== */ -table { - width: 100%; - border-collapse: collapse; - margin-top: 20px; -} - -thead th { - text-align: center; - background-color: #f0f0f0; - color: #333; -} - -th, -td { - padding: 10px; - border: 1px solid #ddd; - text-align: left; - white-space: nowrap; -} - -/* First column: center-align and fixed width */ -td:first-child, -th:first-child { - text-align: center; - width: 60px; -} - -/* =============================== - Confirm button styling - =============================== */ - -/* Container aligns button to bottom-right of table */ -.confirm-container { - display: flex; - justify-content: flex-end; - margin-top: 20px; -} - -/* Button appearance and hover effect */ -.confirm-button { - padding: 10px 20px; - background-color: #28a745; /* success green */ - color: white; - border: none; - border-radius: 5px; - font-size: 16px; - cursor: pointer; -} - -.confirm-button:hover { - background-color: #218838; -} diff --git a/docs/admin/assets/student_table.js b/docs/admin/assets/student_table.js deleted file mode 100644 index e1102cb..0000000 --- a/docs/admin/assets/student_table.js +++ /dev/null @@ -1,29 +0,0 @@ -// =============================== -// Fetch and render student data -// =============================== - -// Send GET request to backend API -fetch("/api/admin/students") - .then(res => res.json()) - .then(students => { - const tableBody = document.querySelector("#studentTableBody"); - tableBody.innerHTML = ""; // Clear any existing rows - - // Loop through each student and build a table row - students.forEach((student, index) => { - const row = ` - - - ${student.studentId} - ${student.name} - ${student.dateofbirth} - ${student.sex} - ${student.phone} - ${student.email} - ${student.address} - ${student.enrollmentStatus || "Active"} - - `; - tableBody.innerHTML += row; - }); - }); diff --git a/docs/admin/dashboard.html b/docs/admin/dashboard.html index 83a80f2..813bb05 100644 --- a/docs/admin/dashboard.html +++ b/docs/admin/dashboard.html @@ -4,19 +4,55 @@ Admin Dashboard - + - - - +

Welcome, Admin!

This is your admin dashboard.

- - - diff --git a/docs/admin/list_of_students.html b/docs/admin/list_of_students.html deleted file mode 100644 index 14b569e..0000000 --- a/docs/admin/list_of_students.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - List of Students - - - - - - - - - - - - - - - - -
-

Registered Students

- - - - - - - - - - - - - - - - - - - -
Student IDFull NameDOBSexPhoneEmailAddressEnrollment Status
- - -
- -
-
- - - - - - - - - diff --git a/docs/admin/new_student.html b/docs/admin/new_student.html index f26fdb2..5fb9942 100644 --- a/docs/admin/new_student.html +++ b/docs/admin/new_student.html @@ -1,130 +1,164 @@ - -New Student Registration - - - + + New Student Registration + + - - - - -
-

Register New Student

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- - - -
- -

- -