28#ifdef PACS_WITH_DATABASE_SYSTEM
50void add_cors_headers(crow::response &res,
const rest_server_context &ctx) {
51 if (ctx.config && !ctx.config->cors_allowed_origins.empty()) {
52 res.add_header(
"Access-Control-Allow-Origin",
53 ctx.config->cors_allowed_origins);
60std::string generate_uuid() {
61 static std::random_device rd;
62 static std::mt19937 gen(rd());
63 static std::uniform_int_distribution<> dis(0, 15);
64 static const char *hex =
"0123456789abcdef";
66 std::string uuid =
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
67 for (
char &c : uuid) {
70 }
else if (c ==
'y') {
71 c = hex[(dis(gen) & 0x3) | 0x8];
80std::string format_timestamp(
81 const std::chrono::system_clock::time_point &tp) {
82 auto time = std::chrono::system_clock::to_time_t(tp);
83 std::ostringstream oss;
84 oss << std::put_time(std::gmtime(&time),
"%Y-%m-%dT%H:%M:%SZ");
91std::string viewer_state_to_json(
const storage::viewer_state_record &state) {
92 std::ostringstream oss;
93 oss << R
"({"state_id":")" << json_escape(state.state_id)
94 << R"(","study_uid":")" << json_escape(state.study_uid)
96 << R"(","state":)" << state.state_json
97 << R"(,"created_at":")" << format_timestamp(state.created_at)
98 << R"(","updated_at":")" << format_timestamp(state.updated_at) << R"("})";
105std::string viewer_states_to_json(
106 const std::vector<storage::viewer_state_record> &states) {
107 std::ostringstream oss;
108 oss << R
"({"data":[)";
109 for (
size_t i = 0; i < states.size(); ++i) {
113 oss << viewer_state_to_json(states[i]);
122std::string recent_study_to_json(
const storage::recent_study_record &record) {
123 std::ostringstream oss;
126 << R"(","accessed_at":")" << format_timestamp(record.accessed_at) << R"("})";
133std::string recent_studies_to_json(
134 const std::vector<storage::recent_study_record> &records,
135 size_t total_count) {
136 std::ostringstream oss;
137 oss << R
"({"data":[)";
138 for (
size_t i = 0; i < records.size(); ++i) {
142 oss << recent_study_to_json(records[i]);
144 oss << R
"(],"total":)" << total_count << "}";
151std::string parse_json_string(
const std::string &json,
const std::string &key) {
152 std::string
search =
"\"" + key +
"\":\"";
153 auto pos = json.find(search);
154 if (pos == std::string::npos) {
158 auto end = json.find(
"\"", pos);
159 if (end == std::string::npos) {
162 return json.substr(pos, end - pos);
168std::string parse_json_object(
const std::string &json,
const std::string &key) {
169 std::string
search =
"\"" + key +
"\":";
170 auto pos = json.find(search);
171 if (pos == std::string::npos) {
175 while (pos < json.size() && (json[pos] ==
' ' || json[pos] ==
'\t')) {
178 if (pos >= json.size() || json[pos] !=
'{') {
184 for (; pos < json.size(); ++pos) {
185 if (json[pos] ==
'{') {
187 }
else if (json[pos] ==
'}') {
190 return json.substr(start, pos - start + 1);
200std::string build_state_json(
const std::string &body) {
202 std::string layout = parse_json_object(body,
"layout");
203 std::string viewports =
"[]";
204 std::string active_viewport =
"";
207 std::string viewports_search =
"\"viewports\":";
208 auto viewports_pos = body.find(viewports_search);
209 if (viewports_pos != std::string::npos) {
210 viewports_pos += viewports_search.length();
211 while (viewports_pos < body.size() &&
212 (body[viewports_pos] ==
' ' || body[viewports_pos] ==
'\t')) {
215 if (viewports_pos < body.size() && body[viewports_pos] ==
'[') {
217 size_t start = viewports_pos;
218 for (; viewports_pos < body.size(); ++viewports_pos) {
219 if (body[viewports_pos] ==
'[') {
221 }
else if (body[viewports_pos] ==
']') {
224 viewports = body.substr(start, viewports_pos - start + 1);
232 active_viewport = parse_json_string(body,
"active_viewport");
234 std::ostringstream oss;
235 oss << R
"({"layout":)" << layout
236 << R"(,"viewports":)" << viewports
237 << R"(,"active_viewport":")" << json_escape(active_viewport) << R"("})";
245 std::shared_ptr<rest_server_context> ctx) {
247 CROW_ROUTE(app,
"/api/v1/viewer-states")
248 .methods(crow::HTTPMethod::POST)([ctx](
const crow::request &req) {
250 res.add_header(
"Content-Type",
"application/json");
251 add_cors_headers(res, *ctx);
253 if (!ctx->database) {
260 std::string body = req.body;
263 res.body =
make_error_json(
"INVALID_REQUEST",
"Request body is empty");
269 state.
study_uid = parse_json_string(body,
"study_uid");
270 state.
user_id = parse_json_string(body,
"user_id");
272 state.
created_at = std::chrono::system_clock::now();
281 #ifdef PACS_WITH_DATABASE_SYSTEM
282 storage::viewer_state_record_repository repo(ctx->database->db_adapter());
283 storage::recent_study_repository recent_repo(ctx->database->db_adapter());
288#ifdef PACS_WITH_DATABASE_SYSTEM
291 repo.save_state(state);
293 if (!save_result.is_ok()) {
302 #ifdef PACS_WITH_DATABASE_SYSTEM
310 std::ostringstream oss;
312 << R"(","created_at":")" << format_timestamp(state.created_at)
314 res.body = oss.str();
319 CROW_ROUTE(app,
"/api/v1/viewer-states")
320 .methods(crow::HTTPMethod::GET)([ctx](
const crow::request &req) {
322 res.add_header(
"Content-Type",
"application/json");
323 add_cors_headers(res, *ctx);
325 if (!ctx->database) {
334 auto study_uid = req.url_params.get(
"study_uid");
336 query.study_uid = study_uid;
338 auto user_id = req.url_params.get(
"user_id");
340 query.user_id = user_id;
342 auto limit_param = req.url_params.get(
"limit");
345 query.limit = std::stoul(limit_param);
346 if (query.limit > 100) {
354 #ifdef PACS_WITH_DATABASE_SYSTEM
355 storage::viewer_state_record_repository repo(ctx->database->db_adapter());
359 #ifdef PACS_WITH_DATABASE_SYSTEM
360 auto states_result = repo.search(query);
361 if (states_result.is_err()) {
367 auto states = std::move(states_result.value());
369 auto states = repo.search_states(query);
373 res.body = viewer_states_to_json(states);
378 CROW_ROUTE(app,
"/api/v1/viewer-states/<string>")
379 .methods(crow::HTTPMethod::GET)(
380 [ctx](
const crow::request & ,
const std::string &state_id) {
382 res.add_header(
"Content-Type",
"application/json");
383 add_cors_headers(res, *ctx);
385 if (!ctx->database) {
388 "Database not configured");
392 #ifdef PACS_WITH_DATABASE_SYSTEM
393 storage::viewer_state_record_repository repo(ctx->database->db_adapter());
397 #ifdef PACS_WITH_DATABASE_SYSTEM
398 auto state_result = repo.find_by_id(state_id);
399 if (state_result.is_err()) {
404 auto state = state_result.value();
406 auto state = repo.find_state_by_id(state_id);
407 if (!state.has_value()) {
415 #ifdef PACS_WITH_DATABASE_SYSTEM
416 res.body = viewer_state_to_json(state);
418 res.body = viewer_state_to_json(state.value());
424 CROW_ROUTE(app,
"/api/v1/viewer-states/<string>")
425 .methods(crow::HTTPMethod::DELETE)(
426 [ctx](
const crow::request & ,
const std::string &state_id) {
428 add_cors_headers(res, *ctx);
430 if (!ctx->database) {
432 res.add_header(
"Content-Type",
"application/json");
434 "Database not configured");
438 #ifdef PACS_WITH_DATABASE_SYSTEM
439 storage::viewer_state_record_repository repo(ctx->database->db_adapter());
443 #ifdef PACS_WITH_DATABASE_SYSTEM
444 auto existing = repo.find_by_id(state_id);
445 if (existing.is_err()) {
447 res.add_header(
"Content-Type",
"application/json");
452 auto existing = repo.find_state_by_id(state_id);
453 if (!existing.has_value()) {
455 res.add_header(
"Content-Type",
"application/json");
462 #ifdef PACS_WITH_DATABASE_SYSTEM
463 repo.remove(state_id);
465 repo.remove_state(state_id);
467 if (!remove_result.is_ok()) {
469 res.add_header(
"Content-Type",
"application/json");
480 CROW_ROUTE(app,
"/api/v1/users/<string>/recent-studies")
481 .methods(crow::HTTPMethod::GET)(
482 [ctx](
const crow::request &req,
const std::string &user_id) {
484 res.add_header(
"Content-Type",
"application/json");
485 add_cors_headers(res, *ctx);
487 if (!ctx->database) {
490 "Database not configured");
495 auto limit_param = req.url_params.get(
"limit");
498 limit = std::stoul(limit_param);
507 #ifdef PACS_WITH_DATABASE_SYSTEM
508 storage::recent_study_repository repo(ctx->database->db_adapter());
512 #ifdef PACS_WITH_DATABASE_SYSTEM
513 auto records_result = repo.find_by_user(user_id, limit);
514 if (records_result.is_err()) {
517 records_result.error().message);
520 auto total_result = repo.count_for_user(user_id);
521 if (total_result.is_err()) {
527 auto records = std::move(records_result.value());
528 size_t total = total_result.value();
530 auto records = repo.get_recent_studies(user_id, limit);
531 size_t total = repo.count_recent_studies(user_id);
535 res.body = recent_studies_to_json(records, total);
Repository for viewer state persistence (legacy SQLite interface)
PACS index database for metadata storage and retrieval.
@ record
RECORD - Treatment record dose.
constexpr std::string_view search
void register_viewer_state_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.
std::string json_escape(std::string_view s)
Escape a string for JSON.
Repository for recent study records using base_repository pattern.
Configuration for REST API server.
Common types and utilities for REST API.
Viewer state record from the database.
std::chrono::system_clock::time_point updated_at
Record last update timestamp.
std::string state_json
Full viewer state as JSON (layout, viewports, settings)
std::string state_id
Unique state identifier (UUID)
std::chrono::system_clock::time_point created_at
Record creation timestamp.
std::string study_uid
Study Instance UID - DICOM tag (0020,000D)
std::string user_id
User who saved the state.
System API endpoints for REST server.
Viewer state API endpoints for REST server.
Viewer state record data structures for database operations.
Repository for viewer state records using base_repository pattern.
Repository for viewer state persistence.