add database migration and UserRepository wrapper
- implemented DB migration - added UserRepository for DB interactions - inserted initial test record
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
build/
|
||||||
|
|
||||||
# ---> C++
|
# ---> C++
|
||||||
# Prerequisites
|
# Prerequisites
|
||||||
*.d
|
*.d
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ add_executable(auth_service
|
|||||||
src/http/controllers/health_controller.cpp
|
src/http/controllers/health_controller.cpp
|
||||||
src/db/sqllite_db.cpp
|
src/db/sqllite_db.cpp
|
||||||
src/db/statement.cpp
|
src/db/statement.cpp
|
||||||
|
src/db/transaction.cpp
|
||||||
|
src/db/migrations_runner.cpp
|
||||||
|
src/repo/user_repository.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(auth_service
|
target_include_directories(auth_service
|
||||||
|
|||||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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);
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
#include "application/application.hpp"
|
#include "application/application.hpp"
|
||||||
// #include <nlohmann/json.hpp>
|
// #include <nlohmann/json.hpp>
|
||||||
|
#include "db/migrations_runner.hpp"
|
||||||
#include "db/sqllite_db.hpp"
|
#include "db/sqllite_db.hpp"
|
||||||
#include "http/controllers/health_controller.hpp"
|
#include "http/controllers/health_controller.hpp"
|
||||||
|
#include "repo/user_repository.hpp"
|
||||||
|
|
||||||
#include <drogon/drogon.h>
|
#include <drogon/drogon.h>
|
||||||
#include <format>
|
#include <format>
|
||||||
@@ -27,30 +29,20 @@ int Application::run() const {
|
|||||||
SqlliteDb db(settings_.db_path);
|
SqlliteDb db(settings_.db_path);
|
||||||
db.initialize();
|
db.initialize();
|
||||||
|
|
||||||
// {
|
MigrationsRunner runner(db);
|
||||||
// db.execute("CREATE TABLE IF NOT EXISTS healthcheck_tmp ("
|
runner.run_file("migrations/001_init.sql");
|
||||||
// "id INTEGER PRIMARY KEY AUTOINCREMENT, "
|
|
||||||
// "name TEXT NOT NULL"
|
|
||||||
// ");");
|
|
||||||
|
|
||||||
// {
|
UserRepository user_repository(db);
|
||||||
// auto insert_stmt = db.prepare("INSERT INTO healthcheck_tmp(name) VALUES(?1);");
|
const auto existing_user = user_repository.find_by_email("test@example.com");
|
||||||
// insert_stmt.bind_text(1, "boot");
|
if(!existing_user.has_value()) {
|
||||||
// insert_stmt.execute();
|
const auto created_user = user_repository.create(
|
||||||
// }
|
"test@example.com", "dummy_hash", "2026-04-05T12:00:00Z", "2026-04-05T12:00:00Z");
|
||||||
|
|
||||||
// {
|
std::cout << "Created user: id=" << created_user.id << ", email=" << created_user.email << '\n';
|
||||||
// auto select_stmt =
|
} else {
|
||||||
// db.prepare("SELECT id, name FROM healthcheck_tmp ORDER BY id DESC LIMIT 1;");
|
std::cout << "User already exists: id=" << existing_user->id
|
||||||
|
<< ", email=" << existing_user->email << '\n';
|
||||||
// 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';
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
register_routes();
|
register_routes();
|
||||||
drogon::app().addListener(settings_.host, settings_.port);
|
drogon::app().addListener(settings_.host, settings_.port);
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#include "db/migrations_runner.hpp"
|
||||||
|
|
||||||
|
#include "db/sqllite_db.hpp"
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class SqlliteDb;
|
||||||
|
|
||||||
|
class MigrationsRunner {
|
||||||
|
public:
|
||||||
|
explicit MigrationsRunner(SqlliteDb &db);
|
||||||
|
void run_file(const std::string &path);
|
||||||
|
|
||||||
|
private:
|
||||||
|
SqlliteDb &db_;
|
||||||
|
};
|
||||||
@@ -69,4 +69,12 @@ void SqlliteDb::closeDb() {
|
|||||||
sqlite3_close(db_);
|
sqlite3_close(db_);
|
||||||
db_ = nullptr;
|
db_ = nullptr;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Transaction SqlliteDb::begin_transaction(){
|
||||||
|
return Transaction{*this};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t SqlliteDb::last_insert_rowid() const noexcept{
|
||||||
|
return sqlite3_last_insert_rowid(db_);
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "db/statement.hpp"
|
#include "db/statement.hpp"
|
||||||
|
#include "db/transaction.hpp"
|
||||||
|
|
||||||
#include <sqlite3.h>
|
#include <sqlite3.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -20,9 +21,11 @@ public:
|
|||||||
void execute(std::string_view sql);
|
void execute(std::string_view sql);
|
||||||
void initialize();
|
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:
|
private:
|
||||||
void closeDb();
|
void closeDb();
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
#include "db/transaction.hpp"
|
||||||
|
|
||||||
|
#include "db/sqllite_db.hpp"
|
||||||
|
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct RegisterRequest {
|
||||||
|
std::string mail;
|
||||||
|
std::string username;
|
||||||
|
std::string password;
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
struct UserResponce {
|
||||||
|
std::int64_t id{};
|
||||||
|
std::string email;
|
||||||
|
std::string username;
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#include "repo/user_repository.hpp"
|
||||||
|
|
||||||
|
// #include "db/sqllite_db.hpp"
|
||||||
|
#include "db/statement.hpp"
|
||||||
|
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
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<User> 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<User> 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "db/sqllite_db.hpp"
|
||||||
|
#include "domain/User.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
// class SqliteDb;
|
||||||
|
|
||||||
|
class UserRepository {
|
||||||
|
public:
|
||||||
|
explicit UserRepository(SqlliteDb &db);
|
||||||
|
|
||||||
|
std::optional<User> find_by_email(std::string_view email) const;
|
||||||
|
std::optional<User> 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_;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user