diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e12b232 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +build +.vscode +creds.hpp \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..4929495 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.16) +project(plural++) + +set(CMAKE_CXX_STANDARD 23) + +# Add submodules +add_subdirectory(libs/pqxx) +add_subdirectory(libs/crow) + +# Collect sources +add_executable(plural++ + src/main.cpp + src/database.cpp + src/hashing.cpp +) + +# Include directories +target_include_directories(plural++ PRIVATE + include +) + +# Link libraries +target_link_libraries(plural++ PRIVATE + pqxx + Crow + crypt +) \ No newline at end of file diff --git a/include/database.hpp b/include/database.hpp new file mode 100644 index 0000000..9da7997 --- /dev/null +++ b/include/database.hpp @@ -0,0 +1,12 @@ +#include "types.hpp" + +class PostgresDB { +private: + std::string conn_str; + +public: + PostgresDB(const std::string& connection) : conn_str(connection) {} + + User GetUser(const int uid); + User GetUser(const std::string& username, const std::string& password); +}; \ No newline at end of file diff --git a/include/hashing.hpp b/include/hashing.hpp new file mode 100644 index 0000000..9fbebf4 --- /dev/null +++ b/include/hashing.hpp @@ -0,0 +1,7 @@ +#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 diff --git a/include/types.hpp b/include/types.hpp new file mode 100644 index 0000000..4cb0175 --- /dev/null +++ b/include/types.hpp @@ -0,0 +1,55 @@ +#include +#include +#include "hashing.hpp" + +struct AcessToken { + int uid; + std::string token; + + const bool operator==(const AcessToken& other) const { + return uid == other.uid && token == other.token; + } + + const bool operator==(const std::string& token) const { + return token == this->token; + } +}; + +struct Member { + int mid; + int sid; + std::string name; + std::string pronouns; + std::string description; + + const bool operator==(const Member& other) const { + return mid == other.mid; + } +}; + +struct System { + int sid; + int uid; + std::vector members; + + const bool operator==(const System& other) const { + return sid == other.sid; + } +}; + +struct User { + int uid; + std::string username; + std::string password_hash; + bool is_system; + System *system; + AcessToken *accesstoken; + + const bool operator==(const User& other) const { + return uid == other.uid; + } + + const bool CheckPassword(const std::string& password) const { + return hashing::VerifyPassword(password, password_hash); + } +}; \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..f45b462 --- /dev/null +++ b/readme.md @@ -0,0 +1,3 @@ +# Plural++ + +a wip alternitive to Simply Plural \ No newline at end of file diff --git a/src/database.cpp b/src/database.cpp new file mode 100644 index 0000000..b46ba4c --- /dev/null +++ b/src/database.cpp @@ -0,0 +1,87 @@ +#include +#include "database.hpp" + +User PostgresDB::GetUser(const int uid) { + pqxx::connection C(conn_str); + pqxx::work W(C); + + pqxx::result R = W.exec("SELECT * FROM users WHERE uid = $1", pqxx::params{uid}); + + if (R.empty()) { + W.commit(); + throw std::runtime_error("User not found"); + } + + const pqxx::row& row = R[0]; + User user; + + user.uid = row["uid"].as(); + user.username = row["username"].as(); + user.password_hash = row["password_hash"].as(); + user.is_system = row["is_system"].as(); + + if (!user.is_system) { + W.commit(); + return user; + } + + pqxx::result R2 = W.exec("SELECT * FROM systems WHERE uid = $1", pqxx::params{user.uid}); + + if (R2.empty()) { + W.commit(); + throw std::runtime_error("System not found"); + } + + const pqxx::row& row2 = R2[0]; + user.system = new System; + user.system->sid = row2["sid"].as(); + user.system->uid = row2["uid"].as(); + + pqxx::result R3 = W.exec("SELECT * FROM members WHERE sid = $1", pqxx::params{user.system->sid}); + + for (auto row : R3) { + Member member; + member.mid = row["mid"].as(); + member.sid = row["sid"].as(); + member.name = row["name"].as(); + member.pronouns = row["pronouns"].as(); + member.description = row["description"].as(); + user.system->members.push_back(member); + } + + pqxx::result R4 = W.exec("SELECT * FROM access_tokens WHERE uid = $1", pqxx::params{user.uid}); + + if (!R4.empty()) { + const pqxx::row& row4 = R4[0]; + user.accesstoken = new AcessToken; + user.accesstoken->uid = row4["uid"].as(); + user.accesstoken->token = row4["token"].as(); + } + + W.commit(); + return user; +} + +User PostgresDB::GetUser(const std::string& username, const std::string& password) { + 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 not found"); + } + + const pqxx::row& row = R[0]; + User user; + + user.password_hash = row["password_hash"].as(); + + if (!user.CheckPassword(password)) { + W.commit(); + throw std::runtime_error("Invalid password"); + } + + return PostgresDB::GetUser(row["uid"].as()); +} \ No newline at end of file diff --git a/src/hashing.cpp b/src/hashing.cpp new file mode 100644 index 0000000..691b0f5 --- /dev/null +++ b/src/hashing.cpp @@ -0,0 +1,27 @@ +#include +#include +#include "hashing.hpp" + +std::string hashing::GenerateSetting(unsigned long cost) { + // "$y$" is the yescrypt prefix + const char* setting = crypt_gensalt("$y$", cost, nullptr, 0); + if (!setting) + throw std::runtime_error("crypt_gensalt() failed – yescrypt may not be supported"); + return setting; +} + +std::string hashing::HashPassword(const std::string& password, const std::string& setting) { + crypt_data data{}; + const char* hash = crypt_r(password.c_str(), setting.c_str(), &data); + if (!hash || hash[0] == '*') + throw std::runtime_error("crypt_r() failed"); + return hash; +} + +bool hashing::VerifyPassword(const std::string& password, const std::string& stored_hash) { + crypt_data data{}; + const char* result = crypt_r(password.c_str(), stored_hash.c_str(), &data); + if (!result || result[0] == '*') + return false; + return stored_hash == result; +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..e2603f9 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,13 @@ +#include "database.hpp" +#include "creds.hpp" +#include + +int main() { + PostgresDB db(DB_CREDS); + + User user = db.GetUser("toaster", "password"); + + 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; +} \ No newline at end of file