diff --git a/.gitignore b/.gitignore index ee4d4c8..89385f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +build/ + # ---> C++ # Prerequisites *.d diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bcab0a..dc41378 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,6 +22,9 @@ add_executable(auth_service src/http/controllers/health_controller.cpp src/db/sqllite_db.cpp src/db/statement.cpp + src/db/transaction.cpp + src/db/migrations_runner.cpp + src/repo/user_repository.cpp ) target_include_directories(auth_service diff --git a/auth.db b/auth.db new file mode 100644 index 0000000..6381f6b Binary files /dev/null and b/auth.db differ diff --git a/auth.db-shm b/auth.db-shm new file mode 100644 index 0000000..164ad7c Binary files /dev/null and b/auth.db-shm differ diff --git a/auth.db-wal b/auth.db-wal new file mode 100644 index 0000000..b498427 Binary files /dev/null and b/auth.db-wal differ diff --git a/migrations/001_init.sql b/migrations/001_init.sql new file mode 100644 index 0000000..d68b119 --- /dev/null +++ b/migrations/001_init.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL, + password_hash TEXT NOT NULL, + is_active INTEGER NOT NULL DEFAULT 1, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email +ON users(email); + +CREATE TABLE IF NOT EXISTS sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + token_hash TEXT NOT NULL, + created_at TEXT NOT NULL, + expires_at TEXT NOT NULL, + revoked_at TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) +); + +CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_token_hash +ON sessions(token_hash); + +CREATE INDEX IF NOT EXISTS idx_sessions_user_id +ON sessions(user_id); + +CREATE INDEX IF NOT EXISTS idx_sessions_expires_at +ON sessions(expires_at); \ No newline at end of file diff --git a/src/application/application.cpp b/src/application/application.cpp index c6ba632..f5857a2 100644 --- a/src/application/application.cpp +++ b/src/application/application.cpp @@ -1,7 +1,9 @@ #include "application/application.hpp" // #include +#include "db/migrations_runner.hpp" #include "db/sqllite_db.hpp" #include "http/controllers/health_controller.hpp" +#include "repo/user_repository.hpp" #include #include @@ -27,30 +29,20 @@ int Application::run() const { SqlliteDb db(settings_.db_path); db.initialize(); - // { - // db.execute("CREATE TABLE IF NOT EXISTS healthcheck_tmp (" - // "id INTEGER PRIMARY KEY AUTOINCREMENT, " - // "name TEXT NOT NULL" - // ");"); + MigrationsRunner runner(db); + runner.run_file("migrations/001_init.sql"); - // { - // auto insert_stmt = db.prepare("INSERT INTO healthcheck_tmp(name) VALUES(?1);"); - // insert_stmt.bind_text(1, "boot"); - // insert_stmt.execute(); - // } + UserRepository user_repository(db); + const auto existing_user = user_repository.find_by_email("test@example.com"); + if(!existing_user.has_value()) { + const auto created_user = user_repository.create( + "test@example.com", "dummy_hash", "2026-04-05T12:00:00Z", "2026-04-05T12:00:00Z"); - // { - // auto select_stmt = - // db.prepare("SELECT id, name FROM healthcheck_tmp ORDER BY id DESC LIMIT 1;"); - - // if(select_stmt.step()) { - // const auto id = select_stmt.column_int64(0); - // const auto name = select_stmt.column_text(1); - - // std::cout << "id=" << id << ", value=" << name << '\n'; - // } - // } - // } + std::cout << "Created user: id=" << created_user.id << ", email=" << created_user.email << '\n'; + } else { + std::cout << "User already exists: id=" << existing_user->id + << ", email=" << existing_user->email << '\n'; + } register_routes(); drogon::app().addListener(settings_.host, settings_.port); diff --git a/src/db/migrations_runner.cpp b/src/db/migrations_runner.cpp new file mode 100644 index 0000000..8143807 --- /dev/null +++ b/src/db/migrations_runner.cpp @@ -0,0 +1,22 @@ +#include "db/migrations_runner.hpp" + +#include "db/sqllite_db.hpp" + +#include +#include +#include + +MigrationsRunner::MigrationsRunner(SqlliteDb &db) + : db_(db) {} + +void MigrationsRunner::run_file(const std::string &path) { + std::ifstream file(path); + if(!file) { + throw std::runtime_error("Failed open maigration file: " + path); + } + + std::stringstream buff; + buff << file.rdbuf(); + + db_.execute(buff.str()); +} \ No newline at end of file diff --git a/src/db/migrations_runner.hpp b/src/db/migrations_runner.hpp new file mode 100644 index 0000000..78b5b0c --- /dev/null +++ b/src/db/migrations_runner.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +class SqlliteDb; + +class MigrationsRunner { +public: + explicit MigrationsRunner(SqlliteDb &db); + void run_file(const std::string &path); + +private: + SqlliteDb &db_; +}; \ No newline at end of file diff --git a/src/db/sqllite_db.cpp b/src/db/sqllite_db.cpp index d9e5d17..99182ad 100644 --- a/src/db/sqllite_db.cpp +++ b/src/db/sqllite_db.cpp @@ -69,4 +69,12 @@ void SqlliteDb::closeDb() { sqlite3_close(db_); db_ = nullptr; } +} + +Transaction SqlliteDb::begin_transaction(){ + return Transaction{*this}; +} + +std::int64_t SqlliteDb::last_insert_rowid() const noexcept{ + return sqlite3_last_insert_rowid(db_); } \ No newline at end of file diff --git a/src/db/sqllite_db.hpp b/src/db/sqllite_db.hpp index dae2d41..a60a102 100644 --- a/src/db/sqllite_db.hpp +++ b/src/db/sqllite_db.hpp @@ -1,6 +1,7 @@ #pragma once #include "db/statement.hpp" +#include "db/transaction.hpp" #include #include @@ -20,9 +21,11 @@ public: void execute(std::string_view sql); void initialize(); - Statement prepare(std::string_view sql) const; + Statement prepare(std::string_view sql) const; + Transaction begin_transaction(); - sqlite3 *native_handle() const noexcept; + sqlite3 *native_handle() const noexcept; + std::int64_t last_insert_rowid() const noexcept; private: void closeDb(); diff --git a/src/db/transaction.cpp b/src/db/transaction.cpp new file mode 100644 index 0000000..abee603 --- /dev/null +++ b/src/db/transaction.cpp @@ -0,0 +1,47 @@ +#include "db/transaction.hpp" + +#include "db/sqllite_db.hpp" + +#include + +Transaction::Transaction(SqlliteDb &db) + : db_(&db) { + db_->execute("BEGIN TRANSACTION;"); +} + +Transaction::~Transaction() { + if(db_ != nullptr && not isCommited_) { + try { + db_->execute("ROLLBACK;"); + } catch(...) { + // TODO + } + } +} + +Transaction::Transaction(Transaction &&other) noexcept + : db_(std::exchange(other.db_, nullptr)) + , isCommited_(std::exchange(other.isCommited_, false)) {} + +Transaction &Transaction::operator=(Transaction &&other) noexcept { + if(this != &other) { + if(db_ != nullptr && not isCommited_) { + try { + db_->execute("ROLLBACK;"); + } catch(...) { + // TODO some log + } + } + db_ = std::exchange(other.db_, nullptr); + isCommited_ = std::exchange(other.isCommited_, false); + } + return *this; +} + +void Transaction::commit() { + if(db_ == nullptr || isCommited_) { + return; + } + db_->execute("COMMIT;"); + isCommited_ = true; +} \ No newline at end of file diff --git a/src/db/transaction.hpp b/src/db/transaction.hpp new file mode 100644 index 0000000..50f326a --- /dev/null +++ b/src/db/transaction.hpp @@ -0,0 +1,21 @@ +#pragma once + +class SqlliteDb; + +class Transaction { +public: + explicit Transaction(SqlliteDb &db); + ~Transaction(); + + Transaction(const Transaction &) = delete; + Transaction &operator=(const Transaction &) = delete; + + Transaction(Transaction &&other) noexcept; + Transaction &operator=(Transaction &&other) noexcept; + + void commit(); + +private: + SqlliteDb *db_{ nullptr }; + bool isCommited_{ false }; +}; \ No newline at end of file diff --git a/src/domain/User.h b/src/domain/User.h new file mode 100644 index 0000000..1703c6a --- /dev/null +++ b/src/domain/User.h @@ -0,0 +1,13 @@ +#pragma once + +#include +#include + +struct User { + std::int64_t id{}; + std::string email; + std::string password_hash; + bool is_active{ true }; + std::string created_at; + std::string updated_at; +}; diff --git a/src/modeles/RegisterRequest.h b/src/modeles/RegisterRequest.h new file mode 100644 index 0000000..1e23125 --- /dev/null +++ b/src/modeles/RegisterRequest.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +struct RegisterRequest { + std::string mail; + std::string username; + std::string password; +}; \ No newline at end of file diff --git a/src/modeles/UserResponse.h b/src/modeles/UserResponse.h new file mode 100644 index 0000000..60a67d6 --- /dev/null +++ b/src/modeles/UserResponse.h @@ -0,0 +1,10 @@ +#pragma once + +#include +#include + +struct UserResponce { + std::int64_t id{}; + std::string email; + std::string username; +}; \ No newline at end of file diff --git a/src/repo/user_repository.cpp b/src/repo/user_repository.cpp new file mode 100644 index 0000000..47fcb10 --- /dev/null +++ b/src/repo/user_repository.cpp @@ -0,0 +1,72 @@ +#include "repo/user_repository.hpp" + +// #include "db/sqllite_db.hpp" +#include "db/statement.hpp" + +#include + +namespace { +User read_user(Statement &stmt) { + User user; + user.id = stmt.column_int64(0); + user.email = stmt.column_text(1); + user.password_hash = stmt.column_text(2); + user.is_active = stmt.column_int64(3) != 0; + user.created_at = stmt.column_text(4); + user.updated_at = stmt.column_text(5); + return user; +} +} // namespace + +UserRepository::UserRepository(SqlliteDb &db) + : db_(db) {} + +std::optional UserRepository::find_by_email(std::string_view email) const { + auto stmt = db_.prepare("SELECT id, email, password_hash, is_active, created_at, updated_at " + "FROM users " + "WHERE email = ?1 " + "LIMIT 1;"); + stmt.bind_text(1, email); + + if(!stmt.step()) { + return std::nullopt; + } + + return read_user(stmt); +} + +std::optional UserRepository::find_by_id(std::int64_t id) const { + auto stmt = db_.prepare("SELECT id, email, password_hash, is_active, created_at, updated_at " + "FROM users " + "WHERE id = ?1 " + "LIMIT 1;"); + stmt.bind_int64(1, id); + + if(!stmt.step()) { + return std::nullopt; + } + return read_user(stmt); +} + +User UserRepository::create(std::string_view email, std::string_view password_hash, + std::string_view created_at, std::string_view updated_at) const { + auto stmt = + db_.prepare("INSERT INTO users(email, password_hash, is_active, created_at, updated_at) " + "VALUES(?1, ?2, 1, ?3, ?4);"); + + stmt.bind_text(1, email); + stmt.bind_text(2, password_hash); + stmt.bind_text(3, created_at); + stmt.bind_text(4, updated_at); + + stmt.execute(); + + const auto user_id = db_.last_insert_rowid(); + const auto user = find_by_id(user_id); + + if(!user.has_value()) { + throw std::runtime_error("Failed to load created user"); + } + + return *user; +} \ No newline at end of file diff --git a/src/repo/user_repository.hpp b/src/repo/user_repository.hpp new file mode 100644 index 0000000..767fc01 --- /dev/null +++ b/src/repo/user_repository.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "db/sqllite_db.hpp" +#include "domain/User.h" + +#include +#include +#include + +// class SqliteDb; + +class UserRepository { +public: + explicit UserRepository(SqlliteDb &db); + + std::optional find_by_email(std::string_view email) const; + std::optional find_by_id(std::int64_t id) const; + + User create(std::string_view email, std::string_view password_hash, std::string_view created_at, + std::string_view updated_at) const; + +private: + SqlliteDb &db_; +}; \ No newline at end of file