diff --git a/CMakeLists.txt b/CMakeLists.txt index 4929495..d926416 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ add_executable(plural++ src/main.cpp src/database.cpp src/hashing.cpp + src/timestamp.cpp ) # Include directories diff --git a/include/database.hpp b/include/database.hpp index 9da7997..6e85113 100644 --- a/include/database.hpp +++ b/include/database.hpp @@ -1,4 +1,9 @@ +#ifndef DATABASE_HPP +#define DATABASE_HPP #include "types.hpp" +#include "timestamp.hpp" +#include +#include class PostgresDB { private: @@ -7,6 +12,26 @@ private: public: PostgresDB(const std::string& connection) : conn_str(connection) {} - User GetUser(const int uid); + User GetUser(int uid); User GetUser(const std::string& username, const std::string& password); -}; \ No newline at end of file + void GetUser(User& user) { user = GetUser(user.uid);}; + + User CreateUser(const std::string& username, const std::string& password, bool is_system); + void UpdateUserPassowrd(User& user, const std::string& password); + void DeleteUser(const User& user); + + void CreateMember(User& user, const std::string& name, const std::string& pronouns, const std::string& description); + void UpdateMemberDescription(Member& member, const std::string& description); + void UpdateMemberPronouns(Member& member, const std::string& pronouns); + void UpdateMemberName(Member& member, const std::string& name); + void DeleteMember(const Member& member); + + void CreateAccessToken(User& user); + void DeleteAccessToken(const User& user); + + void StartFront(const User& user, const Member& member, const std::string& note = "", Timestamp start_time = std::chrono::system_clock::now()); + void EndFront(const User& user, const Member& member, Timestamp end_time = std::chrono::system_clock::now()); + std::vector GetFronts(const User& user, Timestamp start_time, std::optional end_time = std::nullopt); +}; + +#endif \ No newline at end of file diff --git a/include/hashing.hpp b/include/hashing.hpp index 9fbebf4..0bd0f51 100644 --- a/include/hashing.hpp +++ b/include/hashing.hpp @@ -1,7 +1,12 @@ +#ifndef HASHING_HPP +#define HASHING_HPP #include namespace hashing { std::string GenerateSetting(unsigned long cost = 0); std::string HashPassword(const std::string& password, const std::string& setting); bool VerifyPassword(const std::string& password, const std::string& stored_password); -} \ No newline at end of file + std::string generate_token(size_t bytes = 32); +} + +#endif \ No newline at end of file diff --git a/include/timestamp.hpp b/include/timestamp.hpp new file mode 100644 index 0000000..c365cd4 --- /dev/null +++ b/include/timestamp.hpp @@ -0,0 +1,11 @@ +#ifndef TIMESTAMP_HPP +#define TIMESTAMP_HPP +#include +#include +#include + +typedef std::chrono::system_clock::time_point Timestamp; + +Timestamp ParseTimestamp(const std::string& ts); + +#endif \ No newline at end of file diff --git a/include/types.hpp b/include/types.hpp index 4cb0175..cb0bf37 100644 --- a/include/types.hpp +++ b/include/types.hpp @@ -1,5 +1,8 @@ +#ifndef TYPES_HPP +#define TYPES_HPP #include #include +#include #include "hashing.hpp" struct AcessToken { @@ -52,4 +55,16 @@ struct User { const bool CheckPassword(const std::string& password) const { return hashing::VerifyPassword(password, password_hash); } -}; \ No newline at end of file +}; + +struct Front { + int fid; + int uid; + Member *member; + std::string note; + std::chrono::system_clock::time_point start_time; + bool is_active; + std::chrono::system_clock::time_point end_time; +}; + +#endif \ No newline at end of file diff --git a/src/database.cpp b/src/database.cpp index b46ba4c..9eedb14 100644 --- a/src/database.cpp +++ b/src/database.cpp @@ -1,5 +1,7 @@ #include #include "database.hpp" +#include + User PostgresDB::GetUser(const int uid) { pqxx::connection C(conn_str); @@ -84,4 +86,220 @@ User PostgresDB::GetUser(const std::string& username, const std::string& passwor } return PostgresDB::GetUser(row["uid"].as()); +} + +User PostgresDB::CreateUser(const std::string& username, const std::string& password, bool is_system) { + pqxx::connection C(conn_str); + pqxx::work W(C); + + pqxx::result R = W.exec("SELECT * FROM users WHERE username = $1", pqxx::params{username}); + + if (R.size() != 0) { + W.commit(); + throw std::runtime_error("User already exists"); + } + + W.exec( + "INSERT INTO users (username, password_hash, is_system) VALUES ($1, $2, $3)", + pqxx::params{username, hashing::HashPassword(password, hashing::GenerateSetting()), is_system} + ); + + int uid = W.query_value("SELECT uid FROM users WHERE username = $1", pqxx::params{username}); + + if (is_system) { + W.exec("INSERT INTO systems (uid) VALUES ($1)", pqxx::params{uid}); + } + + W.commit(); + return GetUser(uid); +} + +void PostgresDB::DeleteUser(const User& user) { + pqxx::connection C(conn_str); + pqxx::work W(C); + + W.exec("DELETE FROM users WHERE uid = $1", pqxx::params{user.uid}); + W.commit(); +} + +void PostgresDB::UpdateUserPassowrd(User& user, const std::string& password) { + pqxx::connection C(conn_str); + pqxx::work W(C); + + W.exec( + "UPDATE users SET password_hash = $1 WHERE uid = $2", + pqxx::params{hashing::HashPassword(password, hashing::GenerateSetting()), user.uid} + ); + + W.commit(); + GetUser(user); +} + +void PostgresDB::CreateMember(User& user, const std::string& name, const std::string& pronouns, const std::string& description) { + if (!user.is_system) throw std::runtime_error("User does not have a system"); + + for (const auto& member : user.system->members) { + if (member.name == name) throw std::runtime_error("Member already exists"); + } + + pqxx::connection C(conn_str); + pqxx::work W(C); + + W.exec( + "INSERT INTO members (sid, name, pronouns, description) VALUES ($1, $2, $3, $4)", + pqxx::params{user.system->sid, name, pronouns, description} + ); + W.commit(); + GetUser(user); +} + +void PostgresDB::UpdateMemberDescription(Member& member, const std::string& description) { + pqxx::connection C(conn_str); + pqxx::work W(C); + + W.exec("UPDATE members SET description = $1 WHERE mid = $2", pqxx::params{description, member.mid}); + W.commit(); +} + +void PostgresDB::UpdateMemberPronouns(Member& member, const std::string& pronouns) { + pqxx::connection C(conn_str); + pqxx::work W(C); + + W.exec("UPDATE members SET pronouns = $1 WHERE mid = $2", pqxx::params{pronouns, member.mid}); + W.commit(); +} + +void PostgresDB::UpdateMemberName(Member& member, const std::string& name) { + pqxx::connection C(conn_str); + pqxx::work W(C); + + pqxx::result R = W.exec("SELECT * FROM members WHERE sid = $1", pqxx::params{member.sid}); + + for (auto row : R) { + if (row["name"].as() == name) throw std::runtime_error("Member already exists"); + } + + W.exec("UPDATE members SET name = $1 WHERE mid = $2", pqxx::params{name, member.mid}); + W.commit(); +} + +void PostgresDB::DeleteMember(const Member& member) { + pqxx::connection C(conn_str); + pqxx::work W(C); + + W.exec("DELETE FROM members WHERE mid = $1", pqxx::params{member.mid}); + W.commit(); +} + +void PostgresDB::CreateAccessToken(User& user) { + pqxx::connection C(conn_str); + pqxx::work W(C); + + pqxx::result R = W.exec("SELECT * FROM access_tokens WHERE uid = $1", pqxx::params{user.uid}); + + if (R.size() != 0) throw std::runtime_error("User already has an access token"); + + W.exec( + "INSERT INTO access_tokens (uid, token) VALUES ($1, $2)", + pqxx::params{user.uid, hashing::generate_token()} + ); + W.commit(); + GetUser(user); +} + +void PostgresDB::DeleteAccessToken(const User& user) { + if (user.accesstoken == nullptr) throw std::runtime_error("User has no access token"); + + pqxx::connection C(conn_str); + pqxx::work W(C); + + W.exec("DELETE FROM access_tokens WHERE uid = $1", pqxx::params{user.uid}); + W.commit(); +} + +void PostgresDB::StartFront(const User& user, const Member& member, const std::string& note, Timestamp start_time) { + std::string time_string = std::format("{:%Y-%m-%d %H:%M:%S+00:00}", start_time); + + pqxx::connection C(conn_str); + pqxx::work W(C); + + pqxx::result R = W.exec("SELECT * FROM front_history WHERE mid = $1 AND end_time IS NULL", pqxx::params{member.mid}); + + if (R.size() != 0) throw std::runtime_error("Front already started"); + + W.exec( + "INSERT INTO front_history (uid, mid, start_time, note) VALUES ($1, $2, $3, $4)", + pqxx::params{user.uid, member.mid, time_string, note} + ); + + W.commit(); +} + +void PostgresDB::EndFront(const User& user, const Member& member, Timestamp end_time) { + std::string time_string = std::format("{:%Y-%m-%d %H:%M:%S+00:00}", end_time); + + pqxx::connection C(conn_str); + pqxx::work W(C); + + pqxx::result R = W.exec("SELECT * FROM front_history WHERE mid = $1 AND end_time IS NULL", pqxx::params{member.mid}); + + if (R.size() == 0) throw std::runtime_error("Front not started"); + + int64_t fid = R[0]["fid"].as(); + + W.exec( + "UPDATE front_history SET end_time = $1 WHERE fid = $2", + pqxx::params{time_string, fid} + ); + + W.commit(); +} + +std::vector PostgresDB::GetFronts(const User& user, Timestamp start_time, std::optional end_time) { + std::string start_time_string = std::format("{:%Y-%m-%d %H:%M:%S+00:00}", start_time); + std::vector front_history; + + pqxx::connection C(conn_str); + pqxx::work W(C); + + pqxx::result R; + + if (end_time.has_value()) { + std::string end_time_string = std::format("{:%Y-%m-%d %H:%M:%S+00}", end_time.value()); + R = W.exec( + "SELECT * FROM front_history WHERE uid = $1 AND start_time >= $2 AND end_time <= $3 ORDER BY start_time DESC", + pqxx::params{user.uid, start_time_string, end_time_string} + ); + } else { + R = W.exec( + "SELECT * FROM front_history WHERE uid = $1 AND start_time >= $2 ORDER BY start_time DESC", + pqxx::params{user.uid, start_time_string} + ); + } + + for (auto row : R) { + Front front; + front.fid = row["fid"].as(); + front.uid = row["uid"].as(); + + pqxx::result R2 = W.exec("SELECT * FROM members WHERE mid = $1", pqxx::params{row["mid"].as()}); + front.member = new Member; + front.member->mid = R2[0]["mid"].as(); + front.member->sid = R2[0]["sid"].as(); + front.member->name = R2[0]["name"].as(); + front.member->pronouns = R2[0]["pronouns"].as(); + front.member->description = R2[0]["description"].as(); + + front.is_active = true; + front.start_time = ParseTimestamp(row["start_time"].as()); + if (!row["end_time"].is_null()) { + front.is_active = false; + front.end_time = ParseTimestamp(row["end_time"].as()); + } + + front.note = row["note"].as(); + front_history.push_back(front); + } + + return front_history; } \ No newline at end of file diff --git a/src/hashing.cpp b/src/hashing.cpp index 691b0f5..193940e 100644 --- a/src/hashing.cpp +++ b/src/hashing.cpp @@ -1,6 +1,9 @@ #include #include #include "hashing.hpp" +#include +#include +#include std::string hashing::GenerateSetting(unsigned long cost) { // "$y$" is the yescrypt prefix @@ -24,4 +27,18 @@ bool hashing::VerifyPassword(const std::string& password, const std::string& sto if (!result || result[0] == '*') return false; return stored_hash == result; +} + +std::string hashing::generate_token(size_t bytes) { + std::random_device rd; + std::string token; + token.reserve(bytes * 2); + + std::uniform_int_distribution dist(0, 255); + for (size_t i = 0; i < bytes; ++i) { + std::ostringstream ss; + ss << std::hex << std::setw(2) << std::setfill('0') << (int)dist(rd); + token += ss.str(); + } + return token; } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index e2603f9..dedd29d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -10,4 +10,8 @@ int main() { std::cout << "User: " << user.username << std::endl; std::cout << "System id: " << user.system->sid << std::endl; std::cout << "Member 0 Name: " << user.system->members[0].name << std::endl; + + auto fronts = db.GetFronts(user, ParseTimestamp("2026-03-11 10:30:00")); + + return 0; } \ No newline at end of file diff --git a/src/timestamp.cpp b/src/timestamp.cpp new file mode 100644 index 0000000..5d6a493 --- /dev/null +++ b/src/timestamp.cpp @@ -0,0 +1,8 @@ +#include "timestamp.hpp" + +Timestamp ParseTimestamp(const std::string& ts) { + std::tm tm = {}; + std::istringstream ss(ts); + ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); + return std::chrono::system_clock::from_time_t(std::mktime(&tm)); +} \ No newline at end of file