ORE Studio 0.0.4
Loading...
Searching...
No Matches
ClientManager.hpp
1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
2 *
3 * Copyright (C) 2025 Marco Craveiro <marco.craveiro@gmail.com>
4 *
5 * This program is free software; you can redistribute it and/or modify it under
6 * the terms of the GNU General Public License as published by the Free Software
7 * Foundation; either version 3 of the License, or (at your option) any later
8 * version.
9 *
10 * This program is distributed in the hope that it will be useful, but WITHOUT
11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 * details.
14 *
15 * You should have received a copy of the GNU General Public License along with
16 * this program; if not, write to the Free Software Foundation, Inc., 51
17 * Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 *
19 */
20#ifndef ORES_QT_CLIENT_MANAGER_HPP
21#define ORES_QT_CLIENT_MANAGER_HPP
22
23#include <atomic>
24#include <chrono>
25#include <concepts>
26#include <expected>
27#include <memory>
28#include <optional>
29#include <string>
30#include <filesystem>
31#include <boost/uuid/uuid.hpp>
32#include <boost/uuid/uuid_io.hpp>
33#include <boost/uuid/random_generator.hpp>
34#include <rfl/json.hpp>
35#include <rfl/AddTagsToVariants.hpp>
36#include <QObject>
37#include <QTimer>
38#include "ores.utility/rfl/reflectors.hpp"
39#include <QDateTime>
40#include "ores.nats/service/nats_client.hpp"
41#include "ores.nats/service/nats_connect_error.hpp"
42#include "ores.nats/service/session_expired_error.hpp"
43#include "ores.nats/service/jetstream_admin.hpp"
44#include "ores.nats/service/subscription.hpp"
45#include "ores.eventing/service/event_bus.hpp"
46#include "ores.logging/make_logger.hpp"
47#include "ores.iam.api/domain/session.hpp"
48#include "ores.iam.api/messaging/session_samples_protocol.hpp"
49#include "ores.trading.api/messaging/trade_protocol.hpp"
50#include "ores.qt/export.hpp"
51
52namespace ores::qt {
53
59template <typename T>
60concept nats_request = requires {
61 { T::nats_subject } -> std::convertible_to<std::string_view>;
62 typename T::response_type;
63};
64
68struct PartyInfo {
69 boost::uuids::uuid id;
70 QString name;
71 QString party_category; // "System" or "Operational"
72 QString business_center_code; // FpML code, e.g. "GBLO", "USNY"
73
74 bool is_system() const { return party_category == "System"; }
75};
76
81 bool success = false;
82 QString error_message;
83 bool password_reset_required = false;
84 bool bootstrap_mode = false;
85 bool tenant_bootstrap_mode = false;
86 bool party_setup_required = false;
87 boost::uuids::uuid selected_party_id;
88 std::vector<PartyInfo> available_parties;
89};
90
95 bool success = false;
96 QString error_message;
97 QString username;
98};
99
104 std::vector<iam::domain::session> sessions;
105 std::uint32_t total_count = 0;
106};
107
112 std::vector<trading::domain::trade> trades;
113 std::uint32_t total_count = 0;
114};
115
123class ORES_QT_API ClientManager : public QObject {
124 Q_OBJECT
125
126private:
127 inline static std::string_view logger_name =
128 "ores.qt.client_manager";
129
130 [[nodiscard]] static auto& lg() {
131 using namespace ores::logging;
132 static auto instance = make_logger(logger_name);
133 return instance;
134 }
135
136public:
137 // Standard timeout for quick NATS round-trips (queries, lookups).
138 static constexpr std::chrono::seconds fast_timeout{30};
139 // Extended timeout for slow server-side operations (provisioning, imports).
140 static constexpr std::chrono::seconds slow_timeout{120};
141
142 explicit ClientManager(std::shared_ptr<eventing::service::event_bus> event_bus,
143 QObject* parent = nullptr);
144 ~ClientManager() override;
145
152 void setSubjectPrefix(const std::string& prefix) { subject_prefix_ = prefix; }
153
157 const std::string& subjectPrefix() const { return subject_prefix_; }
158
162 LoginResult connect(const std::string& host, std::uint16_t port);
163
167 LoginResult login(const std::string& username, const std::string& password);
168
172 LoginResult connectAndLogin(
173 const std::string& host,
174 std::uint16_t port,
175 const std::string& username,
176 const std::string& password);
177
181 LoginResult testConnection(
182 const std::string& host,
183 std::uint16_t port,
184 const std::string& username,
185 const std::string& password);
186
190 SignupResult signup(
191 const std::string& host,
192 std::uint16_t port,
193 const std::string& username,
194 const std::string& email,
195 const std::string& password);
196
200 void disconnect();
201
205 bool logout();
206
210 bool isConnected() const;
211
215 [[deprecated("Permission checks are now server-side via RBAC")]]
216 bool isAdmin() const { return false; }
217
221 bool isLoggedIn() const { return session_.is_logged_in(); }
222
228 [[nodiscard]] nats::service::jetstream_admin admin();
229
233 std::string currentUsername() const {
234 if (!session_.is_logged_in()) return {};
235 return session_.auth().username;
236 }
237
241 std::string currentEmail() const { return current_email_; }
242
246 void setCurrentEmail(const std::string& email) { current_email_ = email; }
247
251 std::optional<boost::uuids::uuid> accountId() const { return current_account_id_; }
252
256 std::string serverAddress() const {
257 if (!isConnected()) return "";
258 return connected_host_ + ":" + std::to_string(connected_port_);
259 }
260
264 std::string connectedHost() const {
265 if (!isConnected()) return "";
266 return connected_host_;
267 }
268
272 std::uint16_t connectedPort() const {
273 if (!isConnected()) return 0;
274 return connected_port_;
275 }
276
280 std::string storedUsername() const { return stored_username_; }
281
285 std::string storedPassword() const { return stored_password_; }
286
290 boost::uuids::uuid currentPartyId() const { return current_party_id_; }
291
295 QString currentPartyName() const { return current_party_name_; }
296
300 QString currentPartyCategory() const { return current_party_category_; }
301
305 bool isSystemParty() const { return current_party_category_ == "System"; }
306
310 bool lastPartySetupRequired() const { return last_party_setup_required_; }
311
318 const std::string& httpBaseUrl() const { return http_base_url_; }
319
323 bool selectParty(const boost::uuids::uuid& party_id, const QString& party_name);
324
335 template <nats_request RequestType>
336 auto process_request(RequestType request)
337 -> std::expected<typename RequestType::response_type, std::string> {
338 using ResponseType = typename RequestType::response_type;
339 try {
340 const auto json_body = rfl::json::write(request);
341 auto msg = session_.request(RequestType::nats_subject, json_body);
342 const std::string_view data(
343 reinterpret_cast<const char*>(msg.data.data()),
344 msg.data.size());
345 auto result = rfl::json::read<ResponseType, rfl::AddTagsToVariants>(data);
346 if (!result) {
347 return std::unexpected(
348 std::string("Failed to deserialize response: ") +
349 result.error().what());
350 }
351 return std::move(*result);
353 throw; // Propagate so connect() can map to a user-visible message
354 } catch (const std::exception& e) {
355 return std::unexpected(std::string(e.what()));
356 }
357 }
358
369 template <nats_request RequestType>
370 auto process_authenticated_request(RequestType request,
371 std::chrono::milliseconds timeout = std::chrono::seconds(30))
372 -> std::expected<typename RequestType::response_type, std::string> {
373 using ResponseType = typename RequestType::response_type;
374 if (!session_.is_logged_in()) {
375 return std::unexpected(std::string("Not logged in"));
376 }
377 try {
378 const auto cid = boost::uuids::to_string(
379 boost::uuids::random_generator()());
380 const auto json_body = rfl::json::write(request);
381 auto scoped = session_
382 .with_correlation_id(cid)
383 .with_session_id(session_id_);
384 auto msg = scoped.authenticated_request(
385 RequestType::nats_subject, json_body, timeout);
386 const std::string_view data(
387 reinterpret_cast<const char*>(msg.data.data()),
388 msg.data.size());
389 auto result = rfl::json::read<ResponseType, rfl::AddTagsToVariants>(data);
390 if (!result) {
391 return std::unexpected(
392 std::string("Failed to deserialize response: ") +
393 result.error().what());
394 }
395 return std::move(*result);
397 throw; // Propagate so connect() can map to a user-visible message
399 using namespace ores::logging;
400 BOOST_LOG_SEV(lg(), warn) << "Session expired: " << e.what();
401 QMetaObject::invokeMethod(this, "sessionExpired", Qt::QueuedConnection);
402 return std::unexpected(std::string(e.what()));
403 } catch (const std::exception& e) {
404 return std::unexpected(std::string(e.what()));
405 }
406 }
407
411 std::optional<SessionListResult> listSessions(
412 const boost::uuids::uuid& accountId,
413 std::uint32_t limit = 100,
414 std::uint32_t offset = 0);
415
423 std::optional<TradeListResult> listTrades(
424 std::optional<boost::uuids::uuid> node_id = std::nullopt,
425 std::uint32_t offset = 0,
426 std::uint32_t limit = 100);
427
434 std::optional<trading::messaging::trade_export_item>
435 getTradeDetail(const std::string& trade_id);
436
440 std::optional<std::vector<iam::domain::session>> getActiveSessions();
441
445 std::optional<std::vector<iam::messaging::session_sample_dto>>
446 getSessionSamples(const boost::uuids::uuid& sessionId);
447
448 // =========================================================================
449 // Connection Status Accessors (stubbed for NATS - not all available)
450 // =========================================================================
451
452 std::uint64_t bytesSent() const { return 0; }
453 std::uint64_t bytesReceived() const { return 0; }
454 std::uint64_t lastRttMs() const { return 0; }
455
456 std::optional<std::chrono::steady_clock::time_point> disconnectedSince() const {
457 return disconnected_since_;
458 }
459
460 // =========================================================================
461 // Session Recording (stubbed - not applicable for NATS)
462 // =========================================================================
463
464 bool enableRecording(const std::filesystem::path&) { return false; }
465 void disableRecording() {}
466 bool isRecording() const { return false; }
467 std::filesystem::path recordingFilePath() const { return {}; }
468 void setRecordingDirectory(const std::filesystem::path& dir) {
469 recording_directory_ = dir;
470 }
471 std::filesystem::path recordingDirectory() const {
472 return recording_directory_;
473 }
474
475 // =========================================================================
476 // Event Subscriptions
477 // =========================================================================
478
479 void subscribeToEvent(const std::string& subject);
480 void unsubscribeFromEvent(const std::string& subject);
481
482signals:
483 void connected();
484 void loggedIn();
485 void disconnected();
486 void reconnecting();
487 void reconnected();
488 void connectionError(const QString& message);
489
498
499 void notificationReceived(const QString& eventType, const QDateTime& timestamp,
500 const QStringList& entityIds, const QString& tenantId,
501 int payloadType, const QByteArray& payload);
502
503 void recordingStarted(const QString& filePath);
504 void recordingStopped();
505 void streamingStarted();
506 void streamingStopped();
507
508private:
509 // Fraction of token lifetime at which the proactive refresh fires
510 static constexpr double refresh_lifetime_ratio = 0.8;
511
519 void arm_refresh_timer(int lifetime_s);
520
527 void onRefreshTimer();
528
529 // NATS session for connection and authentication
531
532 // Subject prefix applied to all outbound NATS messages
533 std::string subject_prefix_;
534
535 // Event bus for publishing connection events
536 std::shared_ptr<eventing::service::event_bus> event_bus_;
537
538 // Connection details
539 std::string connected_host_;
540 std::uint16_t connected_port_{0};
541
542 // Session recording directory
543 std::filesystem::path recording_directory_;
544
545 // Time point when the connection was lost
546 std::optional<std::chrono::steady_clock::time_point> disconnected_since_;
547
548 // Stored credentials for display/reconnect
549 std::string stored_username_;
550 std::string stored_password_;
551
552 // Current account info (from login)
553 std::optional<boost::uuids::uuid> current_account_id_;
554 std::string current_email_;
555
556 // Session trace ID generated on login, forwarded on every NATS request
557 // as Nats-Session-Id to group all calls from this login session in logs.
558 std::string session_id_;
559
560 // Currently selected party context
561 boost::uuids::uuid current_party_id_;
562 QString current_party_name_;
563 QString current_party_category_;
564 bool last_party_setup_required_ = false;
565
566 // HTTP server base URL discovered post-login via NATS.
567 std::string http_base_url_;
568
569 // Active NATS event subscriptions keyed by subject
570 std::unordered_map<std::string, nats::service::subscription> nats_subscriptions_;
571
572 // Proactive token refresh timer (fires before token expires)
573 QTimer* refresh_timer_ = nullptr;
574};
575
576}
577
578#endif
Implements logging infrastructure for ORE Studio.
Definition boost_severity.hpp:28
Qt-based graphical user interface for ORE Studio.
Definition AccountController.hpp:32
JetStream management API.
Definition jetstream_admin.hpp:53
Authenticated NATS client for both interactive and service-to-service use.
Definition nats_client.hpp:72
Thrown when a NATS connection or initial service check fails.
Definition nats_connect_error.hpp:56
Thrown when a session reaches its maximum allowed duration.
Definition session_expired_error.hpp:34
Summary of a party the user can select during login.
Definition ClientManager.hpp:68
Result of a login attempt.
Definition ClientManager.hpp:80
Result of a signup attempt.
Definition ClientManager.hpp:94
Result of a session list request.
Definition ClientManager.hpp:103
Result of a trade list request (metadata only, no instruments).
Definition ClientManager.hpp:111
Manages the lifecycle of the NATS client and login state.
Definition ClientManager.hpp:123
QString currentPartyName() const
Get the name of the currently selected party.
Definition ClientManager.hpp:295
std::string serverAddress() const
Get the server address string.
Definition ClientManager.hpp:256
std::string currentUsername() const
Get the current logged-in user's username.
Definition ClientManager.hpp:233
void sessionExpired()
Emitted when the session can no longer be refreshed.
bool isSystemParty() const
Returns true if the currently selected party is the system party.
Definition ClientManager.hpp:305
boost::uuids::uuid currentPartyId() const
Get the UUID of the currently selected party.
Definition ClientManager.hpp:290
std::uint16_t connectedPort() const
Get the connected server port.
Definition ClientManager.hpp:272
const std::string & httpBaseUrl() const
HTTP base URL discovered during login via NATS service discovery.
Definition ClientManager.hpp:318
bool lastPartySetupRequired() const
Whether the last selectParty call indicated the party needs setup.
Definition ClientManager.hpp:310
std::string storedUsername() const
Get the stored username used for the current session.
Definition ClientManager.hpp:280
std::string connectedHost() const
Get the connected server hostname.
Definition ClientManager.hpp:264
bool isLoggedIn() const
Check if currently logged in.
Definition ClientManager.hpp:221
std::string currentEmail() const
Get the current logged-in user's email.
Definition ClientManager.hpp:241
void setCurrentEmail(const std::string &email)
Set the current logged-in user's email.
Definition ClientManager.hpp:246
auto process_request(RequestType request) -> std::expected< typename RequestType::response_type, std::string >
Process a request that does not require authentication.
Definition ClientManager.hpp:336
void setSubjectPrefix(const std::string &prefix)
Set the NATS subject prefix used for all outbound messages.
Definition ClientManager.hpp:152
std::optional< boost::uuids::uuid > accountId() const
Get the account ID if logged in.
Definition ClientManager.hpp:251
std::string storedPassword() const
Get the stored password used for the current session.
Definition ClientManager.hpp:285
bool isAdmin() const
Definition ClientManager.hpp:216
QString currentPartyCategory() const
Get the category of the currently selected party.
Definition ClientManager.hpp:300
const std::string & subjectPrefix() const
Get the current NATS subject prefix.
Definition ClientManager.hpp:157
auto process_authenticated_request(RequestType request, std::chrono::milliseconds timeout=std::chrono::seconds(30)) -> std::expected< typename RequestType::response_type, std::string >
Process a request that requires authentication.
Definition ClientManager.hpp:370
Concept for NATS-aware request types.
Definition ClientManager.hpp:60