- Einheitlicher WebserverWorker in IBKRTrader.Hosting auf Kestrel-Basis - Token-Authentifizierung (AuthService) mit zeitkonstantem Vergleich und HMAC-Session-Tokens - OrderManager im Core fuer Bestands- und Reservierungsabgleich offener Broker-Orders - SettingsSchema fuer reflexionsbasiertes Auslesen und sichere Aktualisierung ohne Klartext-Passwoerter - SSE-Streaming (LogStreamService) fuer Echtzeit-Logs ueber isolierte Channels - Statische Single-Page-Application in wwwroot (Avalonia Fluent / Polytrader Theme) fuer alle 9 Ansichten - Unit-Tests fuer OrderManager, AuthService, SettingsSchema, LogStreamService und WebserverWorker (498 Tests gruen)
78 lines
1.8 KiB
JavaScript
78 lines
1.8 KiB
JavaScript
// ─── API Client mit Authentifizierungs-Interceptor ───
|
|
|
|
export class ApiClient {
|
|
constructor() {
|
|
this.token = sessionStorage.getItem('ibkr_token') || '';
|
|
this.onUnauthorized = null;
|
|
}
|
|
|
|
setToken(token) {
|
|
this.token = token;
|
|
if (token) {
|
|
sessionStorage.setItem('ibkr_token', token);
|
|
} else {
|
|
sessionStorage.removeItem('ibkr_token');
|
|
}
|
|
}
|
|
|
|
hasToken() {
|
|
return !!this.token;
|
|
}
|
|
|
|
async request(url, options = {}) {
|
|
const headers = {
|
|
'Accept': 'application/json',
|
|
...options.headers
|
|
};
|
|
|
|
if (this.token) {
|
|
headers['Authorization'] = `Bearer ${this.token}`;
|
|
}
|
|
|
|
if (options.body && typeof options.body === 'object' && !(options.body instanceof FormData)) {
|
|
headers['Content-Type'] = 'application/json';
|
|
options.body = JSON.stringify(options.body);
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(url, { ...options, headers });
|
|
|
|
if (response.status === 401) {
|
|
if (this.onUnauthorized) {
|
|
this.onUnauthorized();
|
|
}
|
|
throw new Error('Authentifizierung erforderlich (401)');
|
|
}
|
|
|
|
if (!response.ok) {
|
|
let errMessage = `HTTP ${response.status}`;
|
|
try {
|
|
const errData = await response.json();
|
|
if (errData.error || errData.message) {
|
|
errMessage = errData.error || errData.message;
|
|
}
|
|
} catch (_) {}
|
|
throw new Error(errMessage);
|
|
}
|
|
|
|
const contentType = response.headers.get('content-type') || '';
|
|
if (contentType.includes('application/json')) {
|
|
return await response.json();
|
|
}
|
|
return response;
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
get(url) {
|
|
return this.request(url, { method: 'GET' });
|
|
}
|
|
|
|
post(url, body) {
|
|
return this.request(url, { method: 'POST', body });
|
|
}
|
|
}
|
|
|
|
export const api = new ApiClient();
|