Add Sesion
implementation util/time
This commit is contained in:
@@ -11,6 +11,7 @@ include(CTest)
|
||||
enable_testing()
|
||||
|
||||
find_package(SQLite3 REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
add_subdirectory(submodules/drogon)
|
||||
add_subdirectory(submodules/json)
|
||||
@@ -25,6 +26,9 @@ add_executable(auth_service
|
||||
src/db/transaction.cpp
|
||||
src/db/migrations_runner.cpp
|
||||
src/repo/user_repository.cpp
|
||||
src/repo/session_repository.cpp
|
||||
src/util/time_utils.cpp
|
||||
src/security/session_token_service.cpp
|
||||
)
|
||||
|
||||
target_include_directories(auth_service
|
||||
@@ -43,6 +47,7 @@ target_link_libraries(auth_service PRIVATE
|
||||
drogon
|
||||
SQLite::SQLite3
|
||||
nlohmann_json::nlohmann_json
|
||||
OpenSSL::Crypto
|
||||
)
|
||||
|
||||
add_executable(auth_service_tests
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -3,7 +3,10 @@
|
||||
#include "db/migrations_runner.hpp"
|
||||
#include "db/sqllite_db.hpp"
|
||||
#include "http/controllers/health_controller.hpp"
|
||||
#include "repo/session_repository.hpp"
|
||||
#include "repo/user_repository.hpp"
|
||||
#include "security/session_token_service.hpp"
|
||||
#include "util/time_utils.hpp"
|
||||
|
||||
#include <drogon/drogon.h>
|
||||
#include <format>
|
||||
@@ -32,16 +35,55 @@ int Application::run() const {
|
||||
MigrationsRunner runner(db);
|
||||
runner.run_file("migrations/001_init.sql");
|
||||
|
||||
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");
|
||||
UserRepository user_repository(db);
|
||||
SessionRepository session_repository(db);
|
||||
|
||||
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';
|
||||
SessionTokenService token_service;
|
||||
|
||||
const auto token_pair = token_service.generate();
|
||||
|
||||
std::cout << "Now UTC: " << now_utc_iso8601() << '\n';
|
||||
std::cout << "Expires at: " << expires_at_from_now(settings_.session_ttl) << '\n';
|
||||
std::cout << "Generated raw token: " << token_pair.raw_token << '\n';
|
||||
std::cout << "Generated token hash: " << token_pair.token_hash << '\n';
|
||||
|
||||
if(false) {
|
||||
session_repository.revoke_by_token_hash("dummy_token_hash", "2026-04-05T13:00:00Z");
|
||||
|
||||
const auto revoked_session = session_repository.find_by_token_hash("dummy_token_hash");
|
||||
|
||||
if(revoked_session.has_value() && revoked_session->revoked_at.has_value()) {
|
||||
std::cout << "Session revoked at: " << *revoked_session->revoked_at << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if(false) {
|
||||
const auto existing_session = session_repository.find_by_token_hash("dummy_token_hash");
|
||||
|
||||
if(!existing_session.has_value()) {
|
||||
const auto created_session = session_repository.create(
|
||||
1, "dummy_token_hash", "2026-04-05T12:00:00Z", "2026-04-06T12:00:00Z");
|
||||
|
||||
std::cout << "Created session: id=" << created_session.id
|
||||
<< ", user_id=" << created_session.user_id << '\n';
|
||||
} else {
|
||||
std::cout << "Session already exists: id=" << existing_session->id
|
||||
<< ", user_id=" << existing_session->user_id << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
if(false) {
|
||||
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");
|
||||
|
||||
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();
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include <string>
|
||||
|
||||
struct Settings {
|
||||
std::string host{ "127.0.0.1" };
|
||||
std::uint16_t port{ 8080 };
|
||||
std::string db_path{ "auth.db" };
|
||||
std::string host{ "127.0.0.1" };
|
||||
std::uint16_t port{ 8080 };
|
||||
std::string db_path{ "auth.db" };
|
||||
std::chrono::seconds session_ttl{ 86400 };
|
||||
bool secure_cookies{ false };
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
struct Session {
|
||||
std::int64_t id{};
|
||||
std::int64_t user_id{};
|
||||
std::string token_hash;
|
||||
std::string created_at;
|
||||
std::string expires_at;
|
||||
std::optional<std::string> revoked_at;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "repo/session_repository.hpp"
|
||||
|
||||
#include "db/statement.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
Session read_session(Statement &stmt) {
|
||||
Session session;
|
||||
session.id = stmt.column_int64(0);
|
||||
session.user_id = stmt.column_int64(1);
|
||||
session.token_hash = stmt.column_text(2);
|
||||
session.created_at = stmt.column_text(3);
|
||||
session.expires_at = stmt.column_text(4);
|
||||
|
||||
if(!stmt.column_is_null(5)) {
|
||||
session.revoked_at = stmt.column_text(5);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SessionRepository::SessionRepository(SqlliteDb &db)
|
||||
: db_(db) {}
|
||||
|
||||
Session SessionRepository::create(std::int64_t user_id, std::string_view token_hash,
|
||||
std::string_view created_at, std::string_view expires_at) const {
|
||||
auto stmt =
|
||||
db_.prepare("INSERT INTO sessions(user_id, token_hash, created_at, expires_at, revoked_at) "
|
||||
"VALUES(?1, ?2, ?3, ?4, NULL);");
|
||||
stmt.bind_int64(1, user_id);
|
||||
stmt.bind_text(2, token_hash);
|
||||
stmt.bind_text(3, created_at);
|
||||
stmt.bind_text(4, expires_at);
|
||||
|
||||
stmt.execute();
|
||||
|
||||
const auto session_id = db_.last_insert_rowid();
|
||||
auto select_stmt =
|
||||
db_.prepare("SELECT id, user_id, token_hash, created_at, expires_at, revoked_at "
|
||||
"FROM sessions "
|
||||
"WHERE id = ?1 "
|
||||
"LIMIT 1;");
|
||||
|
||||
select_stmt.bind_int64(1, session_id);
|
||||
|
||||
if(!select_stmt.step()) {
|
||||
throw std::runtime_error("Failed to load created session");
|
||||
}
|
||||
|
||||
return read_session(select_stmt);
|
||||
}
|
||||
|
||||
std::optional<Session> SessionRepository::find_by_token_hash(std::string_view token_hash) const {
|
||||
auto stmt = db_.prepare("SELECT id, user_id, token_hash, created_at, expires_at, revoked_at "
|
||||
"FROM sessions "
|
||||
"WHERE token_hash = ?1 "
|
||||
"LIMIT 1;");
|
||||
stmt.bind_text(1, token_hash);
|
||||
|
||||
if(!stmt.step()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return read_session(stmt);
|
||||
}
|
||||
|
||||
void SessionRepository::revoke_by_token_hash(std::string_view token_hash,
|
||||
std::string_view revoked_at) const {
|
||||
auto stmt = db_.prepare("UPDATE sessions "
|
||||
"SET revoked_at = ?1 "
|
||||
"WHERE token_hash = ?2 AND revoked_at IS NULL;");
|
||||
stmt.bind_text(1, revoked_at);
|
||||
stmt.bind_text(2, token_hash);
|
||||
|
||||
stmt.execute();
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "domain/Session.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <db/sqllite_db.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
class SessionRepository {
|
||||
public:
|
||||
explicit SessionRepository(SqlliteDb &db);
|
||||
Session create(std::int64_t user_id, std::string_view token_hash, std::string_view created_at,
|
||||
std::string_view expires_at) const;
|
||||
std::optional<Session> find_by_token_hash(std::string_view token_hash) const;
|
||||
void revoke_by_token_hash(std::string_view token_hash, std::string_view revoked_at) const;
|
||||
|
||||
private:
|
||||
SqlliteDb &db_;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "security/session_token_service.hpp"
|
||||
|
||||
#include <iomanip>
|
||||
#include <openssl/rand.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
constexpr int kTokenSizeBytes = 32;
|
||||
std::string bytes_to_hex(const unsigned char *data, std::size_t size) {
|
||||
std::ostringstream out;
|
||||
out << std::hex << std::setfill('0');
|
||||
for(std::size_t i = 0; i < size; ++i) {
|
||||
out << std::setw(2) << static_cast<unsigned int>(data[i]);
|
||||
}
|
||||
|
||||
return out.str();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SessionTokenPair SessionTokenService::generate() const {
|
||||
const auto raw_token = generate_random_token();
|
||||
const auto token_hash = sha256_hex(raw_token);
|
||||
|
||||
return SessionTokenPair{ .raw_token = raw_token, .token_hash = token_hash };
|
||||
}
|
||||
|
||||
std::string SessionTokenService::generate_random_token() {
|
||||
unsigned char buffer[kTokenSizeBytes];
|
||||
if(RAND_bytes(buffer, sizeof(buffer)) != 1) {
|
||||
throw std::runtime_error("Failed to generate secure random session token");
|
||||
}
|
||||
|
||||
return bytes_to_hex(buffer, sizeof(buffer));
|
||||
}
|
||||
|
||||
std::string SessionTokenService::sha256_hex(const std::string &input) {
|
||||
unsigned char hash[SHA256_DIGEST_LENGTH];
|
||||
|
||||
SHA256(reinterpret_cast<const unsigned char *>(input.data()), input.size(), hash);
|
||||
|
||||
return bytes_to_hex(hash, sizeof(hash));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#include <string>
|
||||
|
||||
struct SessionTokenPair {
|
||||
std::string raw_token;
|
||||
std::string token_hash;
|
||||
};
|
||||
|
||||
class SessionTokenService {
|
||||
public:
|
||||
SessionTokenPair generate() const;
|
||||
|
||||
private:
|
||||
static std::string generate_random_token();
|
||||
static std::string sha256_hex(const std::string &input);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "util/time_utils.hpp"
|
||||
|
||||
#include <ctime>
|
||||
#include <iomanip>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
std::string to_iso8601_utc(std::time_t time_value) {
|
||||
std::tm utc_tm{};
|
||||
if(gmtime_r(&time_value, &utc_tm) == nullptr) {
|
||||
throw std::runtime_error("Failed to convert time to UTC");
|
||||
}
|
||||
|
||||
std::ostringstream out;
|
||||
out << std::put_time(&utc_tm, "%Y-%m-%dT%H:%M:%SZ");
|
||||
return out.str();
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::string now_utc_iso8601() {
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto now_time_t = std::chrono::system_clock::to_time_t(now);
|
||||
return to_iso8601_utc(now_time_t);
|
||||
}
|
||||
|
||||
std::string expires_at_from_now(std::chrono::seconds ttl) {
|
||||
const auto expires_at = std::chrono::system_clock::now() + ttl;
|
||||
const auto expires_time_t = std::chrono::system_clock::to_time_t(expires_at);
|
||||
return to_iso8601_utc(expires_time_t);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
std::string now_utc_iso8601();
|
||||
std::string expires_at_from_now(std::chrono::seconds ttl);
|
||||
Reference in New Issue
Block a user