Add Sesion

implementation util/time
This commit is contained in:
2026-04-18 22:51:08 +03:00
parent fe9fb77a02
commit 875c692180
13 changed files with 268 additions and 12 deletions
+5
View File
@@ -11,6 +11,7 @@ include(CTest)
enable_testing() enable_testing()
find_package(SQLite3 REQUIRED) find_package(SQLite3 REQUIRED)
find_package(OpenSSL REQUIRED)
add_subdirectory(submodules/drogon) add_subdirectory(submodules/drogon)
add_subdirectory(submodules/json) add_subdirectory(submodules/json)
@@ -25,6 +26,9 @@ add_executable(auth_service
src/db/transaction.cpp src/db/transaction.cpp
src/db/migrations_runner.cpp src/db/migrations_runner.cpp
src/repo/user_repository.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 target_include_directories(auth_service
@@ -43,6 +47,7 @@ target_link_libraries(auth_service PRIVATE
drogon drogon
SQLite::SQLite3 SQLite::SQLite3
nlohmann_json::nlohmann_json nlohmann_json::nlohmann_json
OpenSSL::Crypto
) )
add_executable(auth_service_tests add_executable(auth_service_tests
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+43 -1
View File
@@ -3,7 +3,10 @@
#include "db/migrations_runner.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/session_repository.hpp"
#include "repo/user_repository.hpp" #include "repo/user_repository.hpp"
#include "security/session_token_service.hpp"
#include "util/time_utils.hpp"
#include <drogon/drogon.h> #include <drogon/drogon.h>
#include <format> #include <format>
@@ -33,16 +36,55 @@ int Application::run() const {
runner.run_file("migrations/001_init.sql"); runner.run_file("migrations/001_init.sql");
UserRepository user_repository(db); UserRepository user_repository(db);
SessionRepository session_repository(db);
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"); const auto existing_user = user_repository.find_by_email("test@example.com");
if(!existing_user.has_value()) { if(!existing_user.has_value()) {
const auto created_user = user_repository.create( const auto created_user = user_repository.create(
"test@example.com", "dummy_hash", "2026-04-05T12:00:00Z", "2026-04-05T12:00:00Z"); "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'; std::cout << "Created user: id=" << created_user.id << ", email=" << created_user.email
<< '\n';
} else { } else {
std::cout << "User already exists: id=" << existing_user->id std::cout << "User already exists: id=" << existing_user->id
<< ", email=" << existing_user->email << '\n'; << ", email=" << existing_user->email << '\n';
} }
}
register_routes(); register_routes();
drogon::app().addListener(settings_.host, settings_.port); drogon::app().addListener(settings_.host, settings_.port);
+2
View File
@@ -7,4 +7,6 @@ struct Settings {
std::string host{ "127.0.0.1" }; std::string host{ "127.0.0.1" };
std::uint16_t port{ 8080 }; std::uint16_t port{ 8080 };
std::string db_path{ "auth.db" }; std::string db_path{ "auth.db" };
std::chrono::seconds session_ttl{ 86400 };
bool secure_cookies{ false };
}; };
+13
View File
@@ -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;
};
+77
View File
@@ -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();
}
+20
View File
@@ -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_;
};
+44
View File
@@ -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));
}
+15
View File
@@ -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);
};
+31
View File
@@ -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);
}
+7
View File
@@ -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);