PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
audit_endpoints.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13// IMPORTANT: Include Crow FIRST before any PACS headers to avoid forward
14// declaration conflicts
15#include "crow.h"
16
17// Workaround for Windows: DELETE is defined as a macro in <winnt.h>
18// which conflicts with crow::HTTPMethod::DELETE
19#ifdef DELETE
20#undef DELETE
21#endif
22
29
30#include <chrono>
31#include <iomanip>
32#include <sstream>
33
35
36namespace {
37
41void add_cors_headers(crow::response &res, const rest_server_context &ctx) {
42 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
43 res.add_header("Access-Control-Allow-Origin",
44 ctx.config->cors_allowed_origins);
45 }
46}
47
51std::string format_datetime(
52 const std::chrono::system_clock::time_point &tp) {
53 auto time = std::chrono::system_clock::to_time_t(tp);
54 std::ostringstream oss;
55 oss << std::put_time(std::gmtime(&time), "%Y-%m-%dT%H:%M:%SZ");
56 return oss.str();
57}
58
62std::string audit_record_to_json(const storage::audit_record &record) {
63 std::ostringstream oss;
64 oss << R"({"pk":)" << record.pk
65 << R"(,"event_type":")" << json_escape(record.event_type)
66 << R"(","outcome":")" << json_escape(record.outcome)
67 << R"(","timestamp":")" << format_datetime(record.timestamp)
68 << R"(","user_id":")" << json_escape(record.user_id)
69 << R"(","source_ae":")" << json_escape(record.source_ae)
70 << R"(","target_ae":")" << json_escape(record.target_ae)
71 << R"(","source_ip":")" << json_escape(record.source_ip)
72 << R"(","patient_id":")" << json_escape(record.patient_id)
73 << R"(","study_uid":")" << json_escape(record.study_uid)
74 << R"(","message":")" << json_escape(record.message)
75 << R"(","details":")" << json_escape(record.details)
76 << R"("})";
77 return oss.str();
78}
79
83std::string audit_records_to_json(
84 const std::vector<storage::audit_record> &records,
85 size_t total_count) {
86 std::ostringstream oss;
87 oss << R"({"data":[)";
88 for (size_t i = 0; i < records.size(); ++i) {
89 if (i > 0) {
90 oss << ",";
91 }
92 oss << audit_record_to_json(records[i]);
93 }
94 oss << R"(],"pagination":{"total":)" << total_count
95 << R"(,"count":)" << records.size() << "}}";
96 return oss.str();
97}
98
102std::string audit_records_to_csv(
103 const std::vector<storage::audit_record> &records) {
104 std::ostringstream oss;
105 // Header
106 oss << "pk,event_type,outcome,timestamp,user_id,source_ae,target_ae,"
107 << "source_ip,patient_id,study_uid,message\n";
108
109 for (const auto &record : records) {
110 oss << record.pk << ","
111 << "\"" << record.event_type << "\","
112 << "\"" << record.outcome << "\","
113 << "\"" << format_datetime(record.timestamp) << "\","
114 << "\"" << record.user_id << "\","
115 << "\"" << record.source_ae << "\","
116 << "\"" << record.target_ae << "\","
117 << "\"" << record.source_ip << "\","
118 << "\"" << record.patient_id << "\","
119 << "\"" << record.study_uid << "\","
120 << "\"" << record.message << "\"\n";
121 }
122 return oss.str();
123}
124
128std::pair<size_t, size_t> parse_pagination(const crow::request &req) {
129 size_t limit = 20; // Default limit
130 size_t offset = 0;
131
132 auto limit_param = req.url_params.get("limit");
133 if (limit_param) {
134 try {
135 limit = std::stoul(limit_param);
136 if (limit > 100) {
137 limit = 100; // Cap at 100
138 }
139 } catch (...) {
140 // Use default
141 }
142 }
143
144 auto offset_param = req.url_params.get("offset");
145 if (offset_param) {
146 try {
147 offset = std::stoul(offset_param);
148 } catch (...) {
149 // Use default
150 }
151 }
152
153 return {limit, offset};
154}
155
156} // namespace
157
158// Internal implementation function called from rest_server.cpp
160 crow::SimpleApp &app,
161 std::shared_ptr<rest_server_context> ctx) {
162 // GET /api/v1/audit/logs - List audit log entries (paginated)
163 CROW_ROUTE(app, "/api/v1/audit/logs")
164 .methods(crow::HTTPMethod::GET)([ctx](const crow::request &req) {
165 crow::response res;
166 res.add_header("Content-Type", "application/json");
167 add_cors_headers(res, *ctx);
168
169 if (!ctx->database) {
170 res.code = 503;
171 res.body =
172 make_error_json("DATABASE_UNAVAILABLE", "Database not configured");
173 return res;
174 }
175
176 // Parse pagination
177 auto [limit, offset] = parse_pagination(req);
178
179 // Build query from URL parameters
181 query.limit = limit;
182 query.offset = offset;
183
184 auto event_type = req.url_params.get("event_type");
185 if (event_type) {
186 query.event_type = event_type;
187 }
188
189 auto outcome = req.url_params.get("outcome");
190 if (outcome) {
191 query.outcome = outcome;
192 }
193
194 auto user_id = req.url_params.get("user_id");
195 if (user_id) {
196 query.user_id = user_id;
197 }
198
199 auto source_ae = req.url_params.get("source_ae");
200 if (source_ae) {
201 query.source_ae = source_ae;
202 }
203
204 auto patient_id = req.url_params.get("patient_id");
205 if (patient_id) {
206 query.patient_id = patient_id;
207 }
208
209 auto study_uid = req.url_params.get("study_uid");
210 if (study_uid) {
211 query.study_uid = study_uid;
212 }
213
214 auto date_from = req.url_params.get("date_from");
215 if (date_from) {
216 query.date_from = date_from;
217 }
218
219 auto date_to = req.url_params.get("date_to");
220 if (date_to) {
221 query.date_to = date_to;
222 }
223
224 // Check export format
225 auto format_param = req.url_params.get("format");
226 bool export_csv = format_param && std::string(format_param) == "csv";
227
228 // Get total count (without pagination)
229 storage::audit_query count_query = query;
230 count_query.limit = 0;
231 count_query.offset = 0;
232 auto all_records_result = ctx->database->query_audit_log(count_query);
233 if (!all_records_result.is_ok()) {
234 res.code = 500;
235 res.body = make_error_json("QUERY_ERROR",
236 all_records_result.error().message);
237 return res;
238 }
239 size_t total_count = all_records_result.value().size();
240
241 // Get paginated results
242 auto records_result = ctx->database->query_audit_log(query);
243 if (!records_result.is_ok()) {
244 res.code = 500;
245 res.body = make_error_json("QUERY_ERROR",
246 records_result.error().message);
247 return res;
248 }
249 auto records = std::move(records_result.value());
250
251 if (export_csv) {
252 res.add_header("Content-Type", "text/csv");
253 res.add_header("Content-Disposition",
254 "attachment; filename=\"audit_logs.csv\"");
255 res.code = 200;
256 res.body = audit_records_to_csv(records);
257 } else {
258 res.code = 200;
259 res.body = audit_records_to_json(records, total_count);
260 }
261 return res;
262 });
263
264 // GET /api/v1/audit/logs/:id - Get specific audit log entry
265 CROW_ROUTE(app, "/api/v1/audit/logs/<int>")
266 .methods(crow::HTTPMethod::GET)(
267 [ctx](const crow::request & /*req*/, int pk) {
268 crow::response res;
269 res.add_header("Content-Type", "application/json");
270 add_cors_headers(res, *ctx);
271
272 if (!ctx->database) {
273 res.code = 503;
274 res.body = make_error_json("DATABASE_UNAVAILABLE",
275 "Database not configured");
276 return res;
277 }
278
279 auto record = ctx->database->find_audit_by_pk(pk);
280 if (!record) {
281 res.code = 404;
282 res.body = make_error_json("NOT_FOUND", "Audit log entry not found");
283 return res;
284 }
285
286 res.code = 200;
287 res.body = audit_record_to_json(*record);
288 return res;
289 });
290
291 // GET /api/v1/audit/export - Export audit logs (CSV or JSON)
292 CROW_ROUTE(app, "/api/v1/audit/export")
293 .methods(crow::HTTPMethod::GET)([ctx](const crow::request &req) {
294 crow::response res;
295 add_cors_headers(res, *ctx);
296
297 if (!ctx->database) {
298 res.add_header("Content-Type", "application/json");
299 res.code = 503;
300 res.body =
301 make_error_json("DATABASE_UNAVAILABLE", "Database not configured");
302 return res;
303 }
304
305 // Build query from URL parameters (no pagination for export)
307
308 auto event_type = req.url_params.get("event_type");
309 if (event_type) {
310 query.event_type = event_type;
311 }
312
313 auto outcome = req.url_params.get("outcome");
314 if (outcome) {
315 query.outcome = outcome;
316 }
317
318 auto user_id = req.url_params.get("user_id");
319 if (user_id) {
320 query.user_id = user_id;
321 }
322
323 auto date_from = req.url_params.get("date_from");
324 if (date_from) {
325 query.date_from = date_from;
326 }
327
328 auto date_to = req.url_params.get("date_to");
329 if (date_to) {
330 query.date_to = date_to;
331 }
332
333 auto records_result = ctx->database->query_audit_log(query);
334 if (!records_result.is_ok()) {
335 res.add_header("Content-Type", "application/json");
336 res.code = 500;
337 res.body = make_error_json("QUERY_ERROR",
338 records_result.error().message);
339 return res;
340 }
341 auto records = std::move(records_result.value());
342
343 // Check export format
344 auto format_param = req.url_params.get("format");
345 std::string format = format_param ? format_param : "json";
346
347 if (format == "csv") {
348 res.add_header("Content-Type", "text/csv");
349 res.add_header("Content-Disposition",
350 "attachment; filename=\"audit_logs.csv\"");
351 res.code = 200;
352 res.body = audit_records_to_csv(records);
353 } else {
354 res.add_header("Content-Type", "application/json");
355 res.add_header("Content-Disposition",
356 "attachment; filename=\"audit_logs.json\"");
357 res.code = 200;
358 res.body = audit_records_to_json(records, records.size());
359 }
360 return res;
361 });
362}
363
364} // namespace kcenon::pacs::web::endpoints
Audit log API endpoints for REST server.
Audit log record data structures.
PACS index database for metadata storage and retrieval.
void register_audit_endpoints_impl(crow::SimpleApp &app, std::shared_ptr< rest_server_context > ctx)
std::string make_error_json(std::string_view code, std::string_view message)
Create JSON error response body with details.
Definition rest_types.h:79
std::string json_escape(std::string_view s)
Escape a string for JSON.
Definition rest_types.h:101
Configuration for REST API server.
Common types and utilities for REST API.
Query parameters for audit log search.
size_t limit
Maximum number of results to return (0 = unlimited)
size_t offset
Offset for pagination.
System API endpoints for REST server.