22#ifdef PACS_WITH_DATABASE_SYSTEM
24#include <database/query_builder.h>
28using kcenon::common::make_error;
29using kcenon::common::ok;
33auto get_string(
const database_row& row,
const std::string& key) -> std::string {
34 auto it = row.find(key);
35 return (it != row.end()) ? it->second : std::string{};
38auto get_int64(
const database_row& row,
const std::string& key) -> int64_t {
39 auto it = row.find(key);
40 if (it == row.end() || it->second.empty()) {
45 return std::stoll(it->second);
54 : base_repository(std::
move(db),
"audit_log",
"audit_pk") {}
56auto audit_repository::to_like_pattern(std::string_view pattern)
59 result.reserve(pattern.size());
61 for (
char c : pattern) {
64 }
else if (c ==
'?') {
66 }
else if (c ==
'%' || c ==
'_') {
77auto audit_repository::parse_timestamp(
const std::string& str)
const
78 -> std::chrono::system_clock::time_point {
84 if (std::sscanf(str.c_str(),
"%d-%d-%d %d:%d:%d", &tm.tm_year, &tm.tm_mon,
85 &tm.tm_mday, &tm.tm_hour, &tm.tm_min, &tm.tm_sec) != 6) {
91 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
94auto audit_repository::format_timestamp(
95 std::chrono::system_clock::time_point tp)
const -> std::string {
96 if (tp == std::chrono::system_clock::time_point{}) {
100 auto time = std::chrono::system_clock::to_time_t(tp);
103 gmtime_s(&tm, &time);
105 gmtime_r(&time, &tm);
109 std::strftime(buf,
sizeof(buf),
"%Y-%m-%d %H:%M:%S", &tm);
113auto audit_repository::add_audit_log(
const audit_record& record)
115 if (!db() || !db()->is_connected()) {
116 return make_error<int64_t>(-1,
"Database not connected",
"storage");
119 std::map<std::string, database::core::database_value> insert_data{
120 {
"event_type",
record.event_type},
121 {
"user_id",
record.user_id},
122 {
"source_ae",
record.source_ae},
123 {
"target_ae",
record.target_ae},
124 {
"source_ip",
record.source_ip},
125 {
"patient_id",
record.patient_id},
126 {
"study_uid",
record.study_uid},
127 {
"message",
record.message},
128 {
"details",
record.details},
129 {
"outcome",
record.outcome.empty() ? std::string(
"SUCCESS")
131 if (
record.timestamp != std::chrono::system_clock::time_point{}) {
132 insert_data[
"timestamp"] = format_timestamp(
record.timestamp);
135 auto builder = query_builder();
136 builder.insert_into(table_name()).values(insert_data);
138 auto insert_result = db()->insert(builder.build());
139 if (insert_result.is_err()) {
140 return make_error<int64_t>(
142 kcenon::pacs::compat::format(
"Failed to insert audit log: {}",
143 insert_result.error().message),
147 return db()->last_insert_rowid();
150auto audit_repository::query_audit_log(
const audit_query& query)
151 -> Result<std::vector<audit_record>> {
152 if (!db() || !db()->is_connected()) {
153 return make_error<std::vector<audit_record>>(-1,
154 "Database not connected",
158 auto builder = query_builder();
159 builder.select(select_columns()).from(table_name());
161 if (
query.event_type.has_value()) {
162 builder.where(
"event_type",
"=", *
query.event_type);
164 if (
query.outcome.has_value()) {
165 builder.where(
"outcome",
"=", *
query.outcome);
167 if (
query.user_id.has_value()) {
168 builder.where(
"user_id",
"LIKE", to_like_pattern(*
query.user_id));
170 if (
query.source_ae.has_value()) {
171 builder.where(
"source_ae",
"=", *
query.source_ae);
173 if (
query.patient_id.has_value()) {
174 builder.where(
"patient_id",
"=", *
query.patient_id);
176 if (
query.study_uid.has_value()) {
177 builder.where(
"study_uid",
"=", *
query.study_uid);
179 if (
query.date_from.has_value()) {
180 builder.where(
"date(timestamp)",
">=", *
query.date_from);
182 if (
query.date_to.has_value()) {
183 builder.where(
"date(timestamp)",
"<=", *
query.date_to);
186 builder.order_by(
"timestamp", database::sort_order::desc);
188 if (
query.limit > 0) {
189 builder.limit(
query.limit);
191 if (
query.offset > 0) {
192 builder.offset(
query.offset);
195 auto result = db()->select(builder.build());
196 if (result.is_err()) {
197 return Result<std::vector<audit_record>>::err(result.error());
200 std::vector<audit_record> items;
201 items.reserve(result.value().size());
202 for (
const auto& row : result.value()) {
203 items.push_back(map_row_to_entity(row));
206 return ok(std::move(items));
209auto audit_repository::find_audit_by_pk(int64_t pk)
210 -> std::optional<audit_record> {
211 auto result = find_by_id(pk);
212 if (result.is_err()) {
215 return result.value();
218auto audit_repository::audit_count() -> Result<size_t> {
222auto audit_repository::cleanup_old_audit_logs(std::chrono::hours age)
224 auto cutoff = std::chrono::system_clock::now() - age;
225 auto cutoff_time = std::chrono::system_clock::to_time_t(cutoff);
229 std::ostringstream oss;
230 oss << std::put_time(&tm,
"%Y-%m-%d %H:%M:%S");
231 auto cutoff_str = oss.str();
233 if (!db() || !db()->is_connected()) {
234 return make_error<size_t>(-1,
"Database not connected",
"storage");
237 auto builder = query_builder();
238 builder.delete_from(table_name())
239 .where(
"timestamp",
"<", cutoff_str);
241 auto result = db()->remove(builder.build());
242 if (result.is_err()) {
243 return make_error<size_t>(
245 kcenon::pacs::compat::format(
"Failed to cleanup old audit logs: {}",
246 result.error().message),
250 return ok(
static_cast<size_t>(result.value()));
253auto audit_repository::map_row_to_entity(
const database_row& row)
const
256 record.pk = get_int64(row,
"audit_pk");
257 record.event_type = get_string(row,
"event_type");
258 record.outcome = get_string(row,
"outcome");
260 auto timestamp = get_string(row,
"timestamp");
261 if (!timestamp.empty()) {
262 record.timestamp = parse_timestamp(timestamp);
265 record.user_id = get_string(row,
"user_id");
266 record.source_ae = get_string(row,
"source_ae");
267 record.target_ae = get_string(row,
"target_ae");
268 record.source_ip = get_string(row,
"source_ip");
269 record.patient_id = get_string(row,
"patient_id");
270 record.study_uid = get_string(row,
"study_uid");
271 record.message = get_string(row,
"message");
272 record.details = get_string(row,
"details");
276auto audit_repository::entity_to_row(
const audit_record& entity)
const
277 -> std::map<std::string, database_value> {
279 {
"event_type", entity.event_type},
280 {
"outcome", entity.outcome},
281 {
"timestamp", format_timestamp(entity.timestamp)},
282 {
"user_id", entity.user_id},
283 {
"source_ae", entity.source_ae},
284 {
"target_ae", entity.target_ae},
285 {
"source_ip", entity.source_ip},
286 {
"patient_id", entity.patient_id},
287 {
"study_uid", entity.study_uid},
288 {
"message", entity.message},
289 {
"details", entity.details},
293auto audit_repository::get_pk(
const audit_record& entity)
const -> int64_t {
297auto audit_repository::has_pk(
const audit_record& entity)
const ->
bool {
298 return entity.pk > 0;
301auto audit_repository::select_columns() const -> std::vector<std::
string> {
302 return {
"audit_pk",
"event_type",
"outcome",
"timestamp",
303 "user_id",
"source_ae",
"target_ae",
"source_ip",
304 "patient_id",
"study_uid",
"message",
"details"};
315using kcenon::common::make_error;
316using kcenon::common::ok;
320auto parse_datetime(
const char* str) -> std::chrono::system_clock::time_point {
321 if (!str || *str ==
'\0') {
322 return std::chrono::system_clock::time_point{};
326 std::istringstream ss(str);
327 ss >> std::get_time(&tm,
"%Y-%m-%d %H:%M:%S");
329 return std::chrono::system_clock::time_point{};
332 return std::chrono::system_clock::from_time_t(std::mktime(&tm));
335auto get_text(sqlite3_stmt* stmt,
int col) -> std::string {
337 reinterpret_cast<const char*
>(sqlite3_column_text(stmt, col));
338 return text ? std::string(
text) : std::string{};
354 if (
this != &other) {
364 result.reserve(pattern.size());
366 for (
char c : pattern) {
369 }
else if (c ==
'?') {
371 }
else if (c ==
'%' || c ==
'_') {
383 -> std::chrono::system_clock::time_point {
384 return parse_datetime(str.c_str());
388 auto* stmt =
static_cast<sqlite3_stmt*
>(stmt_ptr);
391 record.pk = sqlite3_column_int64(stmt, 0);
392 record.event_type = get_text(stmt, 1);
393 record.outcome = get_text(stmt, 2);
394 record.timestamp = parse_datetime(get_text(stmt, 3).c_str());
395 record.user_id = get_text(stmt, 4);
396 record.source_ae = get_text(stmt, 5);
397 record.target_ae = get_text(stmt, 6);
398 record.source_ip = get_text(stmt, 7);
399 record.patient_id = get_text(stmt, 8);
400 record.study_uid = get_text(stmt, 9);
401 record.message = get_text(stmt, 10);
402 record.details = get_text(stmt, 11);
408 const char* sql = R
"(
409 INSERT INTO audit_log (
410 event_type, outcome, user_id, source_ae, target_ae,
411 source_ip, patient_id, study_uid, message, details
412 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
415 sqlite3_stmt* stmt = nullptr;
416 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
417 if (rc != SQLITE_OK) {
418 return make_error<int64_t>(
420 kcenon::pacs::compat::format(
"Failed to prepare audit insert: {}",
421 sqlite3_errmsg(db_)),
425 sqlite3_bind_text(stmt, 1, record.event_type.c_str(), -1, SQLITE_TRANSIENT);
426 sqlite3_bind_text(stmt, 2, record.outcome.c_str(), -1, SQLITE_TRANSIENT);
427 sqlite3_bind_text(stmt, 3, record.user_id.c_str(), -1, SQLITE_TRANSIENT);
428 sqlite3_bind_text(stmt, 4, record.source_ae.c_str(), -1, SQLITE_TRANSIENT);
429 sqlite3_bind_text(stmt, 5, record.target_ae.c_str(), -1, SQLITE_TRANSIENT);
430 sqlite3_bind_text(stmt, 6, record.source_ip.c_str(), -1, SQLITE_TRANSIENT);
431 sqlite3_bind_text(stmt, 7, record.patient_id.c_str(), -1, SQLITE_TRANSIENT);
432 sqlite3_bind_text(stmt, 8, record.study_uid.c_str(), -1, SQLITE_TRANSIENT);
433 sqlite3_bind_text(stmt, 9, record.message.c_str(), -1, SQLITE_TRANSIENT);
434 sqlite3_bind_text(stmt, 10, record.details.c_str(), -1, SQLITE_TRANSIENT);
436 rc = sqlite3_step(stmt);
437 sqlite3_finalize(stmt);
439 if (rc != SQLITE_DONE) {
440 return make_error<int64_t>(
442 kcenon::pacs::compat::format(
"Failed to insert audit log: {}",
443 sqlite3_errmsg(db_)),
447 return sqlite3_last_insert_rowid(db_);
452 std::vector<audit_record> results;
453 std::ostringstream sql;
454 sql <<
"SELECT audit_pk, event_type, outcome, timestamp, user_id, "
455 <<
"source_ae, target_ae, source_ip, patient_id, study_uid, "
456 <<
"message, details FROM audit_log WHERE 1=1";
458 std::vector<std::string> params;
460 if (query.event_type) {
461 sql <<
" AND event_type = ?";
462 params.push_back(*query.event_type);
465 sql <<
" AND outcome = ?";
466 params.push_back(*query.outcome);
469 sql <<
" AND user_id LIKE ?";
470 params.push_back(to_like_pattern(*query.user_id));
472 if (query.source_ae) {
473 sql <<
" AND source_ae = ?";
474 params.push_back(*query.source_ae);
476 if (query.patient_id) {
477 sql <<
" AND patient_id = ?";
478 params.push_back(*query.patient_id);
480 if (query.study_uid) {
481 sql <<
" AND study_uid = ?";
482 params.push_back(*query.study_uid);
484 if (query.date_from) {
485 sql <<
" AND date(timestamp) >= date(?)";
486 params.push_back(*query.date_from);
489 sql <<
" AND date(timestamp) <= date(?)";
490 params.push_back(*query.date_to);
493 sql <<
" ORDER BY timestamp DESC";
495 if (query.limit > 0) {
496 sql <<
" LIMIT " << query.limit;
498 if (query.offset > 0) {
499 sql <<
" OFFSET " << query.offset;
503 sqlite3_stmt* stmt =
nullptr;
504 auto rc = sqlite3_prepare_v2(db_, sql.str().c_str(), -1, &stmt,
nullptr);
505 if (rc != SQLITE_OK) {
506 return make_error<std::vector<audit_record>>(
508 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
509 sqlite3_errmsg(db_)),
513 for (
size_t i = 0; i < params.size(); ++i) {
514 sqlite3_bind_text(stmt,
static_cast<int>(i + 1), params[i].c_str(), -1,
518 while (sqlite3_step(stmt) == SQLITE_ROW) {
519 results.push_back(parse_audit_row(stmt));
522 sqlite3_finalize(stmt);
523 return ok(std::move(results));
527 -> std::optional<audit_record> {
529 "SELECT audit_pk, event_type, outcome, timestamp, user_id, "
530 "source_ae, target_ae, source_ip, patient_id, study_uid, "
531 "message, details FROM audit_log WHERE audit_pk = ?;";
533 sqlite3_stmt* stmt =
nullptr;
534 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
535 if (rc != SQLITE_OK) {
539 sqlite3_bind_int64(stmt, 1, pk);
541 std::optional<audit_record> result;
542 if (sqlite3_step(stmt) == SQLITE_ROW) {
543 result = parse_audit_row(stmt);
546 sqlite3_finalize(stmt);
551 const char* sql =
"SELECT COUNT(*) FROM audit_log;";
552 sqlite3_stmt* stmt =
nullptr;
553 auto rc = sqlite3_prepare_v2(
db_, sql, -1, &stmt,
nullptr);
554 if (rc != SQLITE_OK) {
555 return make_error<size_t>(
557 kcenon::pacs::compat::format(
"Failed to prepare query: {}",
558 sqlite3_errmsg(
db_)),
563 if (sqlite3_step(stmt) == SQLITE_ROW) {
564 count =
static_cast<size_t>(sqlite3_column_int64(stmt, 0));
566 sqlite3_finalize(stmt);
572 auto cutoff = std::chrono::system_clock::now() - age;
573 auto cutoff_time = std::chrono::system_clock::to_time_t(cutoff);
574 std::ostringstream oss;
575 oss << std::put_time(std::gmtime(&cutoff_time),
"%Y-%m-%d %H:%M:%S");
576 auto cutoff_str = oss.str();
578 const char* sql =
"DELETE FROM audit_log WHERE timestamp < ?;";
579 sqlite3_stmt* stmt =
nullptr;
580 auto rc = sqlite3_prepare_v2(db_, sql, -1, &stmt,
nullptr);
581 if (rc != SQLITE_OK) {
582 return make_error<size_t>(
584 kcenon::pacs::compat::format(
"Failed to prepare audit cleanup: {}",
585 sqlite3_errmsg(db_)),
589 sqlite3_bind_text(stmt, 1, cutoff_str.c_str(), -1, SQLITE_TRANSIENT);
590 rc = sqlite3_step(stmt);
591 sqlite3_finalize(stmt);
593 if (rc != SQLITE_DONE) {
594 return make_error<size_t>(
596 kcenon::pacs::compat::format(
"Failed to cleanup old audit logs: {}",
597 sqlite3_errmsg(db_)),
601 return static_cast<size_t>(sqlite3_changes(db_));
Repository for audit log persistence.
auto audit_count() const -> Result< size_t >
auto query_audit_log(const audit_query &query) const -> Result< std::vector< audit_record > >
static auto to_like_pattern(std::string_view pattern) -> std::string
auto add_audit_log(const audit_record &record) -> Result< int64_t >
auto operator=(const audit_repository &) -> audit_repository &=delete
auto cleanup_old_audit_logs(std::chrono::hours age) -> Result< size_t >
static auto parse_timestamp(const std::string &str) -> std::chrono::system_clock::time_point
auto parse_audit_row(void *stmt) const -> audit_record
audit_repository(sqlite3 *db)
auto find_audit_by_pk(int64_t pk) const -> std::optional< audit_record >
std::tm * gmtime_safe(const std::time_t *time, std::tm *result)
Cross-platform thread-safe UTC time conversion.
@ move
C-MOVE move request/response.
const atna_coded_value query
Query (110112)
@ record
RECORD - Treatment record dose.
Query parameters for audit log search.
Audit log record from the database.
Compatibility header for cross-platform time functions.