@localzet/data-connector 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/{mixIdApi.d.ts → api.d.ts} +12 -8
- package/dist/api/api.d.ts.map +1 -0
- package/dist/api/{mixIdApi.js → api.js} +63 -86
- package/dist/api/index.d.ts +1 -1
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -1
- package/dist/api/storage.d.ts +18 -0
- package/dist/api/storage.d.ts.map +1 -0
- package/dist/api/storage.js +53 -0
- package/dist/api/websocket.d.ts.map +1 -1
- package/dist/api/websocket.js +4 -6
- package/dist/components/MixIdConnection.d.ts +3 -3
- package/dist/components/MixIdConnection.d.ts.map +1 -1
- package/dist/components/MixIdConnection.js +20 -23
- package/dist/hooks/useMixIdSession.js +12 -12
- package/dist/hooks/useMixIdStatus.js +5 -5
- package/dist/hooks/useMixIdSync.js +13 -13
- package/dist/hooks/useNotifications.d.ts.map +1 -1
- package/dist/hooks/useNotifications.js +11 -17
- package/package.json +1 -1
- package/dist/api/mixIdApi.d.ts.map +0 -1
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { StorageAdapter } from './storage';
|
|
2
|
+
export interface Config {
|
|
3
|
+
server: string;
|
|
3
4
|
clientId: string;
|
|
4
|
-
clientSecret
|
|
5
|
+
clientSecret?: string;
|
|
5
6
|
accessToken?: string;
|
|
6
7
|
refreshToken?: string;
|
|
7
8
|
}
|
|
8
|
-
declare class
|
|
9
|
+
declare class API {
|
|
9
10
|
private config;
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
private storage;
|
|
12
|
+
constructor(storage?: StorageAdapter);
|
|
13
|
+
setStorageAdapter(adapter: StorageAdapter): void;
|
|
14
|
+
setConfig(config: Config): void;
|
|
15
|
+
getConfig(): Config | null;
|
|
12
16
|
clearConfig(): void;
|
|
13
17
|
private request;
|
|
14
18
|
private refreshAccessToken;
|
|
@@ -71,6 +75,6 @@ declare class MixIdApi {
|
|
|
71
75
|
success: boolean;
|
|
72
76
|
}>;
|
|
73
77
|
}
|
|
74
|
-
export declare const
|
|
78
|
+
export declare const api: API;
|
|
75
79
|
export {};
|
|
76
|
-
//# sourceMappingURL=
|
|
80
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/api/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,cAAc,EAAE,MAAM,WAAW,CAAA;AAIhE,MAAM,WAAW,MAAM;IAErB,MAAM,EAAE,MAAM,CAAA;IAGd,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,CAAC,EAAE,MAAM,CAAA;IAGrB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,cAAM,GAAG;IACP,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,OAAO,CAAgB;gBAEnB,OAAO,CAAC,EAAE,cAAc;IAIpC,iBAAiB,CAAC,OAAO,EAAE,cAAc;IAIzC,SAAS,CAAC,MAAM,EAAE,MAAM;IAexB,SAAS,IAAI,MAAM,GAAG,IAAI;IAe1B,WAAW;YAWG,OAAO;YA+CP,kBAAkB;IA8B1B,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,gBAAgB,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAmBvG,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QACtE,YAAY,EAAE,MAAM,CAAA;QACpB,aAAa,EAAE,MAAM,CAAA;QACrB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,CAAA;KACnB,CAAC;IAoDI,aAAa,IAAI,OAAO,CAAC;QAC7B,YAAY,EAAE,OAAO,CAAA;QACrB,QAAQ,EAAE,OAAO,CAAA;QACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAC1B,CAAC;IAII,qBAAqB,CAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAO9F,cAAc,CAAC,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAO7E,gBAAgB,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,GAAG,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAIlF,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IA8BtF,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAIxF,YAAY,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QAC1E,OAAO,EAAE;YACP,QAAQ,CAAC,EAAE;gBAAE,OAAO,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,CAAA;YACjD,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;gBAAE,SAAS,EAAE,MAAM,CAAA;aAAE,CAAC,CAAA;SAC7C,CAAA;QACD,UAAU,EAAE,OAAO,CAAA;KACpB,CAAC;IAOI,SAAS,CAAC,UAAU,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAO1D,WAAW,IAAI,OAAO,CAAC,KAAK,CAAC;QACjC,EAAE,EAAE,MAAM,CAAA;QACV,UAAU,EAAE,GAAG,CAAA;QACf,cAAc,EAAE,MAAM,CAAA;QACtB,SAAS,EAAE,MAAM,CAAA;KAClB,CAAC,CAAC;IAIG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CAKtE;AAED,eAAO,MAAM,GAAG,KAAY,CAAA"}
|
|
@@ -1,99 +1,87 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
: 'https://data-center.zorin.cloud/api';
|
|
6
|
-
class MixIdApi {
|
|
7
|
-
constructor() {
|
|
1
|
+
import { createDefaultStorage } from './storage';
|
|
2
|
+
const MIX_ID_API_BASE = import.meta?.env?.VITE_MIX_ID_API_BASE ?? 'https://data-center.zorin.cloud/api';
|
|
3
|
+
class API {
|
|
4
|
+
constructor(storage) {
|
|
8
5
|
Object.defineProperty(this, "config", {
|
|
9
6
|
enumerable: true,
|
|
10
7
|
configurable: true,
|
|
11
8
|
writable: true,
|
|
12
9
|
value: null
|
|
13
10
|
});
|
|
11
|
+
Object.defineProperty(this, "storage", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
this.storage = storage || createDefaultStorage();
|
|
18
|
+
}
|
|
19
|
+
setStorageAdapter(adapter) {
|
|
20
|
+
this.storage = adapter;
|
|
14
21
|
}
|
|
15
22
|
setConfig(config) {
|
|
16
23
|
this.config = config;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
localStorage.setItem('mixId_accessToken', config.accessToken);
|
|
23
|
-
}
|
|
24
|
-
if (config.refreshToken) {
|
|
25
|
-
localStorage.setItem('mixId_refreshToken', config.refreshToken);
|
|
26
|
-
}
|
|
27
|
-
// Dispatch custom event for same-tab updates
|
|
28
|
-
window.dispatchEvent(new Event('mixid-config-changed'));
|
|
24
|
+
try {
|
|
25
|
+
this.storage.setItem('mixid_config', JSON.stringify({
|
|
26
|
+
server: config.server,
|
|
27
|
+
clientId: config.clientId,
|
|
28
|
+
}));
|
|
29
29
|
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
console.error('Error saving MIX ID config:', e);
|
|
32
|
+
}
|
|
33
|
+
window?.dispatchEvent(new Event('mixid-config-changed'));
|
|
30
34
|
}
|
|
31
35
|
getConfig() {
|
|
32
36
|
if (!this.config) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const stored = localStorage.getItem('mixId_config');
|
|
38
|
-
if (stored) {
|
|
39
|
-
const parsed = JSON.parse(stored);
|
|
40
|
-
this.config = {
|
|
41
|
-
...parsed,
|
|
42
|
-
accessToken: accessToken || undefined,
|
|
43
|
-
refreshToken: refreshToken || undefined
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
else if (accessToken || refreshToken) {
|
|
47
|
-
// If we have tokens but no config, try to restore from tokens
|
|
48
|
-
console.warn('MIX ID config missing but tokens found. Please reconfigure MIX ID.');
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
catch (error) {
|
|
52
|
-
console.error('Error loading MIX ID config:', error);
|
|
53
|
-
this.config = null;
|
|
37
|
+
try {
|
|
38
|
+
const stored = this.storage.getItem('mixid_config');
|
|
39
|
+
if (stored) {
|
|
40
|
+
this.config = { ...JSON.parse(stored) };
|
|
54
41
|
}
|
|
55
42
|
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('Error loading MIX ID config:', error);
|
|
45
|
+
this.config = null;
|
|
46
|
+
}
|
|
56
47
|
}
|
|
57
48
|
return this.config;
|
|
58
49
|
}
|
|
59
50
|
clearConfig() {
|
|
60
51
|
this.config = null;
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
window.dispatchEvent(new Event('mixid-config-changed'));
|
|
52
|
+
try {
|
|
53
|
+
this.storage.removeItem('mixid_config');
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
console.error('Error clearing MIX ID config:', e);
|
|
67
57
|
}
|
|
58
|
+
window?.dispatchEvent(new Event('mixid-config-changed'));
|
|
68
59
|
}
|
|
69
60
|
async request(endpoint, options = {}) {
|
|
70
61
|
const config = this.getConfig();
|
|
71
62
|
if (!config) {
|
|
72
63
|
throw new Error('MIX ID not configured');
|
|
73
64
|
}
|
|
74
|
-
const token = config.accessToken || (typeof window !== 'undefined' && window.localStorage
|
|
75
|
-
? localStorage.getItem('mixId_accessToken')
|
|
76
|
-
: null);
|
|
77
65
|
const headers = {
|
|
78
66
|
'Content-Type': 'application/json',
|
|
79
67
|
...(options.headers || {}),
|
|
80
68
|
};
|
|
69
|
+
const token = config.accessToken || null;
|
|
81
70
|
if (token) {
|
|
82
71
|
headers['Authorization'] = `Bearer ${token}`;
|
|
83
72
|
}
|
|
84
|
-
const response = await fetch(`${config.
|
|
73
|
+
const response = await fetch(`${config.server || MIX_ID_API_BASE}${endpoint}`, {
|
|
85
74
|
...options,
|
|
86
75
|
headers,
|
|
87
76
|
});
|
|
88
77
|
if (response.status === 401) {
|
|
89
|
-
// Try to refresh token
|
|
90
78
|
const refreshed = await this.refreshAccessToken();
|
|
91
79
|
if (refreshed) {
|
|
92
80
|
const retryHeaders = {
|
|
93
81
|
...headers,
|
|
94
|
-
|
|
82
|
+
Authorization: `Bearer ${refreshed}`,
|
|
95
83
|
};
|
|
96
|
-
const retryResponse = await fetch(`${config.
|
|
84
|
+
const retryResponse = await fetch(`${config.server || MIX_ID_API_BASE}${endpoint}`, {
|
|
97
85
|
...options,
|
|
98
86
|
headers: retryHeaders,
|
|
99
87
|
});
|
|
@@ -111,13 +99,11 @@ class MixIdApi {
|
|
|
111
99
|
}
|
|
112
100
|
async refreshAccessToken() {
|
|
113
101
|
const config = this.getConfig();
|
|
114
|
-
const refreshToken = config?.refreshToken
|
|
115
|
-
? localStorage.getItem('mixId_refreshToken')
|
|
116
|
-
: null);
|
|
102
|
+
const refreshToken = config?.refreshToken;
|
|
117
103
|
if (!refreshToken)
|
|
118
104
|
return null;
|
|
119
105
|
try {
|
|
120
|
-
const response = await fetch(`${config?.
|
|
106
|
+
const response = await fetch(`${config?.server || MIX_ID_API_BASE}/auth/refresh`, {
|
|
121
107
|
method: 'POST',
|
|
122
108
|
headers: { 'Content-Type': 'application/json' },
|
|
123
109
|
body: JSON.stringify({ refreshToken }),
|
|
@@ -126,18 +112,9 @@ class MixIdApi {
|
|
|
126
112
|
return null;
|
|
127
113
|
const data = await response.json();
|
|
128
114
|
if (data.accessToken) {
|
|
129
|
-
if (typeof window !== 'undefined' && window.localStorage) {
|
|
130
|
-
localStorage.setItem('mixId_accessToken', data.accessToken);
|
|
131
|
-
}
|
|
132
115
|
if (this.config) {
|
|
133
116
|
this.config.accessToken = data.accessToken;
|
|
134
|
-
// Save updated config
|
|
135
|
-
const { accessToken: _, refreshToken: __, ...configWithoutTokens } = this.config;
|
|
136
|
-
if (typeof window !== 'undefined' && window.localStorage) {
|
|
137
|
-
localStorage.setItem('mixId_config', JSON.stringify(configWithoutTokens));
|
|
138
|
-
}
|
|
139
117
|
}
|
|
140
|
-
// Dispatch custom event for same-tab updates
|
|
141
118
|
if (typeof window !== 'undefined') {
|
|
142
119
|
window.dispatchEvent(new Event('mixid-config-changed'));
|
|
143
120
|
}
|
|
@@ -149,7 +126,6 @@ class MixIdApi {
|
|
|
149
126
|
}
|
|
150
127
|
return null;
|
|
151
128
|
}
|
|
152
|
-
// OAuth flow
|
|
153
129
|
async initiateOAuth(redirectUri, state) {
|
|
154
130
|
const config = this.getConfig();
|
|
155
131
|
if (!config) {
|
|
@@ -169,26 +145,34 @@ class MixIdApi {
|
|
|
169
145
|
if (!config) {
|
|
170
146
|
throw new Error('MIX ID not configured');
|
|
171
147
|
}
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
const
|
|
148
|
+
// Обмен кода на токен. Не отправляем clientSecret из браузера по умолчанию.
|
|
149
|
+
// Рекомендуется использовать PKCE или выполнять обмен кода на сервере (server-side exchange).
|
|
150
|
+
const body = {
|
|
151
|
+
code,
|
|
152
|
+
clientId: config.clientId,
|
|
153
|
+
redirectUri,
|
|
154
|
+
};
|
|
155
|
+
const allowClientSecretInBrowser = import.meta?.env?.VITE_ALLOW_CLIENT_SECRET_IN_BROWSER === 'true';
|
|
156
|
+
if (config.clientSecret) {
|
|
157
|
+
if (typeof window === 'undefined' || allowClientSecretInBrowser) {
|
|
158
|
+
body.clientSecret = config.clientSecret;
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.warn('clientSecret присутствует, но не отправляется из браузера по соображениям безопасности. Используйте PKCE или server-side exchange.');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const response = await fetch(`${config.server || MIX_ID_API_BASE}/auth/oauth/token`, {
|
|
175
165
|
method: 'POST',
|
|
176
166
|
headers: {
|
|
177
167
|
'Content-Type': 'application/json',
|
|
178
168
|
},
|
|
179
|
-
body: JSON.stringify(
|
|
180
|
-
code,
|
|
181
|
-
clientId: config.clientId,
|
|
182
|
-
clientSecret: config.clientSecret,
|
|
183
|
-
redirectUri,
|
|
184
|
-
}),
|
|
169
|
+
body: JSON.stringify(body),
|
|
185
170
|
});
|
|
186
171
|
if (!response.ok) {
|
|
187
172
|
const error = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
188
173
|
throw new Error(error.error || `HTTP ${response.status}`);
|
|
189
174
|
}
|
|
190
175
|
const data = await response.json();
|
|
191
|
-
// Save tokens
|
|
192
176
|
this.setConfig({
|
|
193
177
|
...config,
|
|
194
178
|
accessToken: data.access_token,
|
|
@@ -196,7 +180,6 @@ class MixIdApi {
|
|
|
196
180
|
});
|
|
197
181
|
return data;
|
|
198
182
|
}
|
|
199
|
-
// Sync
|
|
200
183
|
async getSyncStatus() {
|
|
201
184
|
return this.request('/sync/status');
|
|
202
185
|
}
|
|
@@ -216,17 +199,14 @@ class MixIdApi {
|
|
|
216
199
|
return this.request('/sync/settings');
|
|
217
200
|
}
|
|
218
201
|
async uploadData(dataType, data) {
|
|
219
|
-
|
|
220
|
-
const CHUNK_SIZE = 100; // Number of items per chunk
|
|
202
|
+
const CHUNK_SIZE = 100;
|
|
221
203
|
const dataEntries = Object.entries(data);
|
|
222
204
|
if (dataEntries.length <= CHUNK_SIZE) {
|
|
223
|
-
// Small enough to send in one request
|
|
224
205
|
return this.request('/sync/data', {
|
|
225
206
|
method: 'POST',
|
|
226
207
|
body: JSON.stringify({ dataType, data }),
|
|
227
208
|
});
|
|
228
209
|
}
|
|
229
|
-
// Split into chunks
|
|
230
210
|
const chunks = [];
|
|
231
211
|
for (let i = 0; i < dataEntries.length; i += CHUNK_SIZE) {
|
|
232
212
|
const chunk = {};
|
|
@@ -235,7 +215,6 @@ class MixIdApi {
|
|
|
235
215
|
}
|
|
236
216
|
chunks.push(chunk);
|
|
237
217
|
}
|
|
238
|
-
// Upload chunks sequentially
|
|
239
218
|
for (const chunk of chunks) {
|
|
240
219
|
await this.request('/sync/data', {
|
|
241
220
|
method: 'POST',
|
|
@@ -255,14 +234,12 @@ class MixIdApi {
|
|
|
255
234
|
params.append('dataTypes', dataTypes.join(','));
|
|
256
235
|
return this.request(`/sync/check-updates?${params.toString()}`);
|
|
257
236
|
}
|
|
258
|
-
// Session heartbeat
|
|
259
237
|
async heartbeat(deviceInfo) {
|
|
260
238
|
return this.request('/sessions/heartbeat', {
|
|
261
239
|
method: 'POST',
|
|
262
240
|
body: JSON.stringify({ deviceInfo }),
|
|
263
241
|
});
|
|
264
242
|
}
|
|
265
|
-
// Session management
|
|
266
243
|
async getSessions() {
|
|
267
244
|
return this.request('/sessions');
|
|
268
245
|
}
|
|
@@ -272,4 +249,4 @@ class MixIdApi {
|
|
|
272
249
|
});
|
|
273
250
|
}
|
|
274
251
|
}
|
|
275
|
-
export const
|
|
252
|
+
export const api = new API();
|
package/dist/api/index.d.ts
CHANGED
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,cAAc,OAAO,CAAA;AACrB,cAAc,aAAa,CAAA;AAC3B,cAAc,gBAAgB,CAAA"}
|
package/dist/api/index.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface StorageAdapter {
|
|
2
|
+
getItem(key: string): string | null;
|
|
3
|
+
setItem(key: string, value: string): void;
|
|
4
|
+
removeItem(key: string): void;
|
|
5
|
+
}
|
|
6
|
+
export declare class InMemoryStorage implements StorageAdapter {
|
|
7
|
+
private store;
|
|
8
|
+
getItem(key: string): string | null;
|
|
9
|
+
setItem(key: string, value: string): void;
|
|
10
|
+
removeItem(key: string): void;
|
|
11
|
+
}
|
|
12
|
+
export declare class SessionStorageAdapter implements StorageAdapter {
|
|
13
|
+
getItem(key: string): string | null;
|
|
14
|
+
setItem(key: string, value: string): void;
|
|
15
|
+
removeItem(key: string): void;
|
|
16
|
+
}
|
|
17
|
+
export declare function createDefaultStorage(): StorageAdapter;
|
|
18
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/api/storage.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;IACnC,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACzC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAC9B;AAED,qBAAa,eAAgB,YAAW,cAAc;IACpD,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,GAAG,EAAE,MAAM;IAGnB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAGlC,UAAU,CAAC,GAAG,EAAE,MAAM;CAGvB;AAED,qBAAa,qBAAsB,YAAW,cAAc;IAC1D,OAAO,CAAC,GAAG,EAAE,MAAM;IAInB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAIlC,UAAU,CAAC,GAAG,EAAE,MAAM;CAIvB;AAED,wBAAgB,oBAAoB,IAAI,cAAc,CAerD"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class InMemoryStorage {
|
|
2
|
+
constructor() {
|
|
3
|
+
Object.defineProperty(this, "store", {
|
|
4
|
+
enumerable: true,
|
|
5
|
+
configurable: true,
|
|
6
|
+
writable: true,
|
|
7
|
+
value: new Map()
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
getItem(key) {
|
|
11
|
+
return this.store.has(key) ? this.store.get(key) : null;
|
|
12
|
+
}
|
|
13
|
+
setItem(key, value) {
|
|
14
|
+
this.store.set(key, value);
|
|
15
|
+
}
|
|
16
|
+
removeItem(key) {
|
|
17
|
+
this.store.delete(key);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class SessionStorageAdapter {
|
|
21
|
+
getItem(key) {
|
|
22
|
+
if (typeof window === 'undefined' || !window.sessionStorage)
|
|
23
|
+
return null;
|
|
24
|
+
return window.sessionStorage.getItem(key);
|
|
25
|
+
}
|
|
26
|
+
setItem(key, value) {
|
|
27
|
+
if (typeof window === 'undefined' || !window.sessionStorage)
|
|
28
|
+
return;
|
|
29
|
+
window.sessionStorage.setItem(key, value);
|
|
30
|
+
}
|
|
31
|
+
removeItem(key) {
|
|
32
|
+
if (typeof window === 'undefined' || !window.sessionStorage)
|
|
33
|
+
return;
|
|
34
|
+
window.sessionStorage.removeItem(key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function createDefaultStorage() {
|
|
38
|
+
try {
|
|
39
|
+
if (typeof window !== 'undefined' && window.sessionStorage) {
|
|
40
|
+
// Разрешаем sessionStorage только если явно включено через env (безопасность по умолчанию)
|
|
41
|
+
// В сборках Vite можно установить VITE_ALLOW_PERSISTENT_STORAGE=true для тестов
|
|
42
|
+
// Но лучше использовать безопасное хранилище на бекенде / secure native storage
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
if (import.meta?.env?.VITE_ALLOW_PERSISTENT_STORAGE === 'true') {
|
|
45
|
+
return new SessionStorageAdapter();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
return new InMemoryStorage();
|
|
53
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/api/websocket.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAA;AAEvE,cAAM,eAAe;IACnB,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,aAAa,CAAqD;IAC1E,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAA6D;;IAkB7E,OAAO;
|
|
1
|
+
{"version":3,"file":"websocket.d.ts","sourceRoot":"","sources":["../../src/api/websocket.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;CACnB;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,EAAE,gBAAgB,KAAK,IAAI,CAAA;AAEvE,cAAM,eAAe;IACnB,OAAO,CAAC,EAAE,CAAyB;IACnC,OAAO,CAAC,iBAAiB,CAAI;IAC7B,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,YAAY,CAAQ;IAC5B,OAAO,CAAC,aAAa,CAAqD;IAC1E,OAAO,CAAC,YAAY,CAAyB;IAC7C,OAAO,CAAC,QAAQ,CAA6D;;IAkB7E,OAAO;IAgEP,OAAO,CAAC,gBAAgB;IAqBxB,OAAO,CAAC,aAAa;IAoBrB,IAAI,CAAC,OAAO,EAAE,gBAAgB;IAS9B,OAAO,CAAC,iBAAiB;IASzB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB;IAOpD,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,qBAAqB;IAOrD,UAAU;IAeV,WAAW,IAAI,OAAO;CAGvB;AAED,eAAO,MAAM,QAAQ,iBAAwB,CAAA"}
|
package/dist/api/websocket.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { api } from './api';
|
|
2
2
|
class WebSocketClient {
|
|
3
3
|
constructor() {
|
|
4
4
|
Object.defineProperty(this, "ws", {
|
|
@@ -66,16 +66,14 @@ class WebSocketClient {
|
|
|
66
66
|
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
|
-
const config =
|
|
69
|
+
const config = api.getConfig();
|
|
70
70
|
if (!config || !config.accessToken) {
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
73
|
this.isConnecting = true;
|
|
74
74
|
try {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
: 'https://data-center.zorin.cloud/api';
|
|
78
|
-
const wsUrl = apiBase.replace(/^http/, 'ws').replace(/\/api$/, '') + '/ws';
|
|
75
|
+
const server = config.server || import.meta?.env?.VITE_MIX_ID_API_BASE || 'https://data-center.zorin.cloud/api';
|
|
76
|
+
const wsUrl = server.replace(/^http/, 'ws').replace(/\/api$/, '') + '/ws';
|
|
79
77
|
const ws = new WebSocket(`${wsUrl}?token=${config.accessToken}`);
|
|
80
78
|
ws.onopen = () => {
|
|
81
79
|
this.ws = ws;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export interface MixIdConnectionProps {
|
|
2
|
-
onConnected?: () => void;
|
|
2
|
+
onConnected?: (code?: string) => void;
|
|
3
3
|
onDisconnected?: () => void;
|
|
4
4
|
showSyncSettings?: boolean;
|
|
5
5
|
showSyncData?: boolean;
|
|
6
|
-
|
|
6
|
+
server?: string;
|
|
7
7
|
clientId?: string;
|
|
8
8
|
clientSecret?: string;
|
|
9
9
|
notifications?: {
|
|
@@ -14,5 +14,5 @@ export interface MixIdConnectionProps {
|
|
|
14
14
|
}) => void;
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
export default function MixIdConnection({ onConnected, onDisconnected, showSyncSettings, showSyncData,
|
|
17
|
+
export default function MixIdConnection({ onConnected, onDisconnected, showSyncSettings, showSyncData, server, clientId, clientSecret, notifications, }: MixIdConnectionProps): import("react/jsx-runtime").JSX.Element;
|
|
18
18
|
//# sourceMappingURL=MixIdConnection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MixIdConnection.d.ts","sourceRoot":"","sources":["../../src/components/MixIdConnection.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;
|
|
1
|
+
{"version":3,"file":"MixIdConnection.d.ts","sourceRoot":"","sources":["../../src/components/MixIdConnection.tsx"],"names":[],"mappings":"AAkBA,MAAM,WAAW,oBAAoB;IACnC,WAAW,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,CAAA;IACrC,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,aAAa,CAAC,EAAE;QACd,IAAI,EAAE,CAAC,OAAO,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,KAAK,IAAI,CAAA;KAC5E,CAAA;CACF;AAED,MAAM,CAAC,OAAO,UAAU,eAAe,CAAC,EACtC,WAAW,EACX,cAAc,EACd,gBAAuB,EACvB,YAAmB,EACnB,MAAM,EACN,QAAQ,EACR,YAAY,EACZ,aAAa,GACd,EAAE,oBAAoB,2CAqStB"}
|
|
@@ -3,9 +3,9 @@ import { useState, useEffect } from 'react';
|
|
|
3
3
|
import { Paper, Group, Button, Text, Badge, Modal, Stack, Switch, Alert, Loader, } from '@mantine/core';
|
|
4
4
|
import { IconPlug, IconSettings, IconLogout, IconX } from '@tabler/icons-react';
|
|
5
5
|
import { useDisclosure } from '@mantine/hooks';
|
|
6
|
-
import {
|
|
6
|
+
import { api } from '../api/api';
|
|
7
7
|
import { useMixIdStatus } from '../hooks/useMixIdStatus';
|
|
8
|
-
export default function MixIdConnection({ onConnected, onDisconnected, showSyncSettings = true, showSyncData = true,
|
|
8
|
+
export default function MixIdConnection({ onConnected, onDisconnected, showSyncSettings = true, showSyncData = true, server, clientId, clientSecret, notifications, }) {
|
|
9
9
|
const { isConnected, syncStatus, hasConfig } = useMixIdStatus();
|
|
10
10
|
const [loading, setLoading] = useState(true);
|
|
11
11
|
const [syncStatusData, setSyncStatusData] = useState(null);
|
|
@@ -17,12 +17,12 @@ export default function MixIdConnection({ onConnected, onDisconnected, showSyncS
|
|
|
17
17
|
}, []);
|
|
18
18
|
const checkConnection = async () => {
|
|
19
19
|
try {
|
|
20
|
-
const config =
|
|
20
|
+
const config = api.getConfig();
|
|
21
21
|
if (!config || !config.accessToken) {
|
|
22
22
|
setSyncStatusData(null);
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
|
-
const status = await
|
|
25
|
+
const status = await api.getSyncStatus();
|
|
26
26
|
setSyncStatusData(status);
|
|
27
27
|
setSyncSettings(status.syncSettings);
|
|
28
28
|
setSyncData(status.syncData);
|
|
@@ -37,20 +37,13 @@ export default function MixIdConnection({ onConnected, onDisconnected, showSyncS
|
|
|
37
37
|
const handleConnect = async () => {
|
|
38
38
|
try {
|
|
39
39
|
// Get config from props or environment
|
|
40
|
-
const finalApiBase =
|
|
41
|
-
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_API_BASE)
|
|
42
|
-
? (import.meta.env?.VITE_MIX_ID_API_BASE || 'https://data-center.zorin.cloud/api')
|
|
43
|
-
: 'https://data-center.zorin.cloud/api';
|
|
40
|
+
const finalApiBase = server || import.meta?.env?.VITE_MIX_ID_API_BASE || 'https://data-center.zorin.cloud/api';
|
|
44
41
|
const finalClientId = clientId ||
|
|
45
42
|
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_CLIENT_ID)
|
|
46
43
|
? (import.meta.env?.VITE_MIX_ID_CLIENT_ID || '')
|
|
47
44
|
: '';
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
? (import.meta.env?.VITE_MIX_ID_CLIENT_SECRET || '')
|
|
51
|
-
: '';
|
|
52
|
-
if (!finalClientId || !finalClientSecret) {
|
|
53
|
-
const message = 'MIX ID не настроен. Укажите VITE_MIX_ID_CLIENT_ID и VITE_MIX_ID_CLIENT_SECRET';
|
|
45
|
+
if (!finalClientId) {
|
|
46
|
+
const message = 'MIX ID не настроен. Укажите VITE_MIX_ID_CLIENT_ID (clientId)';
|
|
54
47
|
if (notifications) {
|
|
55
48
|
notifications.show({
|
|
56
49
|
title: 'Ошибка',
|
|
@@ -63,14 +56,17 @@ export default function MixIdConnection({ onConnected, onDisconnected, showSyncS
|
|
|
63
56
|
}
|
|
64
57
|
return;
|
|
65
58
|
}
|
|
66
|
-
|
|
67
|
-
|
|
59
|
+
// Не сохраняем clientSecret в клиентском приложении.
|
|
60
|
+
// Если у вас есть clientSecret, выполняйте обмен кода на сервере (server-side),
|
|
61
|
+
// либо включите PKCE на бэкенде. Для desktop-приложений (Electron) можно
|
|
62
|
+
// передать `clientSecret` через безопасный адаптер хранения (secure native store).
|
|
63
|
+
api.setConfig({
|
|
64
|
+
server: finalApiBase || 'https://data-center.zorin.cloud/api',
|
|
68
65
|
clientId: finalClientId,
|
|
69
|
-
clientSecret: finalClientSecret
|
|
70
66
|
});
|
|
71
67
|
// Initiate OAuth flow
|
|
72
68
|
const redirectUri = typeof window !== 'undefined' ? window.location.origin + '/mixid-callback' : '';
|
|
73
|
-
const { authorizationUrl, code } = await
|
|
69
|
+
const { authorizationUrl, code } = await api.initiateOAuth(redirectUri);
|
|
74
70
|
// Open OAuth window
|
|
75
71
|
if (typeof window === 'undefined')
|
|
76
72
|
return;
|
|
@@ -88,8 +84,10 @@ export default function MixIdConnection({ onConnected, onDisconnected, showSyncS
|
|
|
88
84
|
oauthWindow?.close();
|
|
89
85
|
try {
|
|
90
86
|
const { code: callbackCode } = event.data;
|
|
91
|
-
|
|
92
|
-
//
|
|
87
|
+
// Delegate token exchange to the embedding app/backend for security.
|
|
88
|
+
// Return the authorization code to the parent via callback.
|
|
89
|
+
onConnected?.(callbackCode || code);
|
|
90
|
+
// Trigger status update so parent can check connection state
|
|
93
91
|
window.dispatchEvent(new Event('mixid-config-changed'));
|
|
94
92
|
await checkConnection();
|
|
95
93
|
if (notifications) {
|
|
@@ -99,7 +97,6 @@ export default function MixIdConnection({ onConnected, onDisconnected, showSyncS
|
|
|
99
97
|
color: 'green',
|
|
100
98
|
});
|
|
101
99
|
}
|
|
102
|
-
onConnected?.();
|
|
103
100
|
openSettings();
|
|
104
101
|
}
|
|
105
102
|
catch (error) {
|
|
@@ -145,7 +142,7 @@ export default function MixIdConnection({ onConnected, onDisconnected, showSyncS
|
|
|
145
142
|
return;
|
|
146
143
|
if (!confirm('Вы уверены, что хотите отключить MIX ID?'))
|
|
147
144
|
return;
|
|
148
|
-
|
|
145
|
+
api.clearConfig();
|
|
149
146
|
// Dispatch event to trigger WebSocket disconnection and status update
|
|
150
147
|
window.dispatchEvent(new Event('mixid-config-changed'));
|
|
151
148
|
setSyncStatusData(null);
|
|
@@ -160,7 +157,7 @@ export default function MixIdConnection({ onConnected, onDisconnected, showSyncS
|
|
|
160
157
|
};
|
|
161
158
|
const handleSaveSettings = async () => {
|
|
162
159
|
try {
|
|
163
|
-
await
|
|
160
|
+
await api.updateSyncPreferences(syncSettings, syncData);
|
|
164
161
|
if (notifications) {
|
|
165
162
|
notifications.show({
|
|
166
163
|
title: 'Успешно',
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useCallback, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { api } from '../api/api';
|
|
3
3
|
import { wsClient } from '../api/websocket';
|
|
4
4
|
export function useMixIdSession(options = {}) {
|
|
5
5
|
const { onSessionDeleted, onSessionExpired, onSessionInvalid, heartbeatInterval = 30 * 1000, // 30 seconds
|
|
@@ -7,11 +7,11 @@ export function useMixIdSession(options = {}) {
|
|
|
7
7
|
const [currentSessionId, setCurrentSessionId] = useState(null);
|
|
8
8
|
const sendHeartbeat = useCallback(async () => {
|
|
9
9
|
try {
|
|
10
|
-
const config =
|
|
10
|
+
const config = api.getConfig();
|
|
11
11
|
if (!config || !config.accessToken) {
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
-
const result = await
|
|
14
|
+
const result = await api.heartbeat({
|
|
15
15
|
platform: typeof navigator !== 'undefined' ? navigator.platform : 'unknown',
|
|
16
16
|
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
|
|
17
17
|
timestamp: new Date().toISOString(),
|
|
@@ -26,13 +26,13 @@ export function useMixIdSession(options = {}) {
|
|
|
26
26
|
// If heartbeat fails with 401/403, session might be deleted or expired
|
|
27
27
|
if (error instanceof Error) {
|
|
28
28
|
if (error.message.includes('401') || error.message.includes('403')) {
|
|
29
|
-
|
|
29
|
+
api.clearConfig();
|
|
30
30
|
wsClient.disconnect();
|
|
31
31
|
onSessionExpired?.();
|
|
32
32
|
}
|
|
33
33
|
else if (error.message.includes('404')) {
|
|
34
34
|
// Session not found - might have been deleted
|
|
35
|
-
|
|
35
|
+
api.clearConfig();
|
|
36
36
|
wsClient.disconnect();
|
|
37
37
|
onSessionDeleted?.();
|
|
38
38
|
}
|
|
@@ -44,18 +44,18 @@ export function useMixIdSession(options = {}) {
|
|
|
44
44
|
}, [onSessionDeleted, onSessionExpired, onSessionInvalid]);
|
|
45
45
|
const checkSession = useCallback(async () => {
|
|
46
46
|
try {
|
|
47
|
-
const config =
|
|
47
|
+
const config = api.getConfig();
|
|
48
48
|
if (!config || !config.accessToken) {
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
51
|
// Try to get sessions to verify current session exists
|
|
52
|
-
const sessions = await
|
|
52
|
+
const sessions = await api.getSessions();
|
|
53
53
|
// If we have a current session ID, check if it still exists
|
|
54
54
|
if (currentSessionId) {
|
|
55
55
|
const sessionExists = sessions.some(s => s.id === currentSessionId);
|
|
56
56
|
if (!sessionExists) {
|
|
57
57
|
// Session was deleted
|
|
58
|
-
|
|
58
|
+
api.clearConfig();
|
|
59
59
|
wsClient.disconnect();
|
|
60
60
|
onSessionDeleted?.();
|
|
61
61
|
}
|
|
@@ -64,14 +64,14 @@ export function useMixIdSession(options = {}) {
|
|
|
64
64
|
catch (error) {
|
|
65
65
|
console.error('Failed to check session:', error);
|
|
66
66
|
if (error instanceof Error && (error.message.includes('401') || error.message.includes('403'))) {
|
|
67
|
-
|
|
67
|
+
api.clearConfig();
|
|
68
68
|
wsClient.disconnect();
|
|
69
69
|
onSessionExpired?.();
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
}, [currentSessionId, onSessionDeleted, onSessionExpired]);
|
|
73
73
|
useEffect(() => {
|
|
74
|
-
const config =
|
|
74
|
+
const config = api.getConfig();
|
|
75
75
|
if (!config || !config.accessToken) {
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
@@ -85,7 +85,7 @@ export function useMixIdSession(options = {}) {
|
|
|
85
85
|
if (message.sessionId) {
|
|
86
86
|
if (currentSessionId && message.sessionId === currentSessionId) {
|
|
87
87
|
// Our session was deleted
|
|
88
|
-
|
|
88
|
+
api.clearConfig();
|
|
89
89
|
wsClient.disconnect();
|
|
90
90
|
onSessionDeleted?.();
|
|
91
91
|
}
|
|
@@ -97,7 +97,7 @@ export function useMixIdSession(options = {}) {
|
|
|
97
97
|
}
|
|
98
98
|
};
|
|
99
99
|
const handleSessionExpired = () => {
|
|
100
|
-
|
|
100
|
+
api.clearConfig();
|
|
101
101
|
wsClient.disconnect();
|
|
102
102
|
onSessionExpired?.();
|
|
103
103
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { api } from '../api/api';
|
|
3
3
|
import { wsClient } from '../api/websocket';
|
|
4
4
|
export function useMixIdStatus() {
|
|
5
5
|
const [isConnected, setIsConnected] = useState(false);
|
|
@@ -7,7 +7,7 @@ export function useMixIdStatus() {
|
|
|
7
7
|
const [hasConfig, setHasConfig] = useState(false);
|
|
8
8
|
const checkStatus = useCallback(async () => {
|
|
9
9
|
try {
|
|
10
|
-
const config =
|
|
10
|
+
const config = api.getConfig();
|
|
11
11
|
const hasConfigValue = !!(config && config.accessToken);
|
|
12
12
|
setHasConfig(hasConfigValue);
|
|
13
13
|
if (!hasConfigValue) {
|
|
@@ -24,7 +24,7 @@ export function useMixIdStatus() {
|
|
|
24
24
|
else {
|
|
25
25
|
// Check if we can use REST API (try to get sync status)
|
|
26
26
|
try {
|
|
27
|
-
await
|
|
27
|
+
await api.getSyncStatus();
|
|
28
28
|
setIsConnected(true);
|
|
29
29
|
setSyncStatus('connected-rest');
|
|
30
30
|
}
|
|
@@ -44,9 +44,9 @@ export function useMixIdStatus() {
|
|
|
44
44
|
checkStatus();
|
|
45
45
|
// Check periodically
|
|
46
46
|
const interval = setInterval(checkStatus, 2000); // Check every 2 seconds
|
|
47
|
-
// Listen to storage changes (
|
|
47
|
+
// Listen to storage changes (для cross-tab обновлений, если приложение использует персистентное хранилище)
|
|
48
48
|
const handleStorageChange = (e) => {
|
|
49
|
-
if (e.key === '
|
|
49
|
+
if (e.key === 'mixid_config' || e.key === 'mixId_config') {
|
|
50
50
|
checkStatus();
|
|
51
51
|
}
|
|
52
52
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { api } from '../api/api';
|
|
3
3
|
import { wsClient } from '../api/websocket';
|
|
4
4
|
import { offlineQueue } from '../api/offlineQueue';
|
|
5
5
|
const SYNC_INTERVAL = 5 * 60 * 1000; // 5 minutes (fallback HTTP sync)
|
|
@@ -32,10 +32,10 @@ export function useMixIdSync(options = {}) {
|
|
|
32
32
|
// Upload settings
|
|
33
33
|
const uploadSettings = useCallback(async (settingsToUpload, version) => {
|
|
34
34
|
try {
|
|
35
|
-
const syncStatus = await
|
|
35
|
+
const syncStatus = await api.getSyncStatus();
|
|
36
36
|
if (!syncStatus.syncSettings)
|
|
37
37
|
return;
|
|
38
|
-
const result = await
|
|
38
|
+
const result = await api.uploadSettings(settingsToUpload);
|
|
39
39
|
lastSettingsVersionRef.current = result.version;
|
|
40
40
|
lastSettingsUpdateRef.current = Date.now();
|
|
41
41
|
// Send via WebSocket for real-time sync
|
|
@@ -58,10 +58,10 @@ export function useMixIdSync(options = {}) {
|
|
|
58
58
|
// Upload data
|
|
59
59
|
const uploadData = useCallback(async (dataType, data) => {
|
|
60
60
|
try {
|
|
61
|
-
const syncStatus = await
|
|
61
|
+
const syncStatus = await api.getSyncStatus();
|
|
62
62
|
if (!syncStatus.syncData)
|
|
63
63
|
return;
|
|
64
|
-
await
|
|
64
|
+
await api.uploadData(dataType, data);
|
|
65
65
|
// Send via WebSocket for real-time sync
|
|
66
66
|
if (wsClient.isConnected()) {
|
|
67
67
|
wsClient.send({
|
|
@@ -91,19 +91,19 @@ export function useMixIdSync(options = {}) {
|
|
|
91
91
|
// Perform sync
|
|
92
92
|
const performSync = useCallback(async () => {
|
|
93
93
|
try {
|
|
94
|
-
const config =
|
|
94
|
+
const config = api.getConfig();
|
|
95
95
|
if (!config || !config.accessToken) {
|
|
96
96
|
return;
|
|
97
97
|
}
|
|
98
98
|
// Get sync status
|
|
99
|
-
const syncStatus = await
|
|
99
|
+
const syncStatus = await api.getSyncStatus();
|
|
100
100
|
// Check for updates
|
|
101
|
-
const updates = await
|
|
101
|
+
const updates = await api.checkUpdates(lastSettingsVersionRef.current, syncStatus.syncData && dataTypes.length > 0 ? dataTypes : undefined);
|
|
102
102
|
// Download updates if available
|
|
103
103
|
if (updates.hasUpdates) {
|
|
104
104
|
if (updates.updates.settings && syncStatus.syncSettings && getLocalSettings && saveLocalSettings) {
|
|
105
105
|
try {
|
|
106
|
-
const remoteSettings = await
|
|
106
|
+
const remoteSettings = await api.downloadSettings();
|
|
107
107
|
const localSettings = getLocalSettings();
|
|
108
108
|
const merged = mergeWithConflictResolution(localSettings, remoteSettings.settings, remoteSettings.updatedAt);
|
|
109
109
|
await saveLocalSettings(merged);
|
|
@@ -118,7 +118,7 @@ export function useMixIdSync(options = {}) {
|
|
|
118
118
|
for (const dataType of dataTypes) {
|
|
119
119
|
if (updates.updates.data[dataType] && getLocalData && saveLocalData) {
|
|
120
120
|
try {
|
|
121
|
-
const remoteData = await
|
|
121
|
+
const remoteData = await api.downloadData(dataType);
|
|
122
122
|
const localData = await getLocalData(dataType);
|
|
123
123
|
// Merge with conflict resolution
|
|
124
124
|
const merged = { ...localData, ...remoteData.data };
|
|
@@ -171,7 +171,7 @@ export function useMixIdSync(options = {}) {
|
|
|
171
171
|
]);
|
|
172
172
|
useEffect(() => {
|
|
173
173
|
const setupSync = () => {
|
|
174
|
-
const config =
|
|
174
|
+
const config = api.getConfig();
|
|
175
175
|
if (!config || !config.accessToken) {
|
|
176
176
|
// Disconnect WebSocket if config is cleared
|
|
177
177
|
wsClient.disconnect();
|
|
@@ -188,7 +188,7 @@ export function useMixIdSync(options = {}) {
|
|
|
188
188
|
};
|
|
189
189
|
if (typeof window !== 'undefined') {
|
|
190
190
|
window.addEventListener('mixid-config-changed', handleConfigChange);
|
|
191
|
-
const config =
|
|
191
|
+
const config = api.getConfig();
|
|
192
192
|
if (!config || !config.accessToken) {
|
|
193
193
|
return () => {
|
|
194
194
|
window.removeEventListener('mixid-config-changed', handleConfigChange);
|
|
@@ -225,7 +225,7 @@ export function useMixIdSync(options = {}) {
|
|
|
225
225
|
syncIntervalRef.current = setInterval(performSync, SYNC_INTERVAL);
|
|
226
226
|
// Set up heartbeat
|
|
227
227
|
heartbeatIntervalRef.current = setInterval(() => {
|
|
228
|
-
|
|
228
|
+
api.heartbeat({
|
|
229
229
|
platform: typeof navigator !== 'undefined' ? navigator.platform : 'unknown',
|
|
230
230
|
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
|
|
231
231
|
}).catch(console.error);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useNotifications.d.ts","sourceRoot":"","sources":["../../src/hooks/useNotifications.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB;AAID,wBAAgB,gBAAgB;;;
|
|
1
|
+
{"version":3,"file":"useNotifications.d.ts","sourceRoot":"","sources":["../../src/hooks/useNotifications.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,OAAO,CAAA;IACb,SAAS,EAAE,MAAM,CAAA;CAClB;AAID,wBAAgB,gBAAgB;;;iCA8CL,MAAM;;;EA6GhC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { api } from '../api/api';
|
|
3
3
|
import { wsClient } from '../api/websocket';
|
|
4
4
|
const NOTIFICATIONS_STORAGE_KEY = 'mixId_notifications';
|
|
5
5
|
export function useNotifications() {
|
|
@@ -25,14 +25,12 @@ export function useNotifications() {
|
|
|
25
25
|
}, []);
|
|
26
26
|
const fetchNotifications = useCallback(async () => {
|
|
27
27
|
try {
|
|
28
|
-
const config =
|
|
28
|
+
const config = api.getConfig();
|
|
29
29
|
if (!config || !config.accessToken) {
|
|
30
30
|
return;
|
|
31
31
|
}
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
: 'https://data-center.zorin.cloud/api';
|
|
35
|
-
const response = await fetch(`${apiBase}/notifications`, {
|
|
32
|
+
const server = config.server || import.meta?.env?.VITE_MIX_ID_API_BASE || 'https://data-center.zorin.cloud/api';
|
|
33
|
+
const response = await fetch(`${server}/notifications`, {
|
|
36
34
|
headers: {
|
|
37
35
|
Authorization: `Bearer ${config.accessToken}`,
|
|
38
36
|
},
|
|
@@ -48,7 +46,7 @@ export function useNotifications() {
|
|
|
48
46
|
}, [saveNotifications]);
|
|
49
47
|
const markAsRead = useCallback(async (notificationId) => {
|
|
50
48
|
try {
|
|
51
|
-
const config =
|
|
49
|
+
const config = api.getConfig();
|
|
52
50
|
if (!config || !config.accessToken) {
|
|
53
51
|
return;
|
|
54
52
|
}
|
|
@@ -63,10 +61,8 @@ export function useNotifications() {
|
|
|
63
61
|
});
|
|
64
62
|
}
|
|
65
63
|
// Also send via HTTP as fallback
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
: 'https://data-center.zorin.cloud/api';
|
|
69
|
-
await fetch(`${apiBase}/notifications/${notificationId}/read`, {
|
|
64
|
+
const server = config.server || import.meta?.env?.VITE_MIX_ID_API_BASE || 'https://data-center.zorin.cloud/api';
|
|
65
|
+
await fetch(`${server}/notifications/${notificationId}/read`, {
|
|
70
66
|
method: 'PUT',
|
|
71
67
|
headers: {
|
|
72
68
|
Authorization: `Bearer ${config.accessToken}`,
|
|
@@ -79,7 +75,7 @@ export function useNotifications() {
|
|
|
79
75
|
}, [notifications, saveNotifications]);
|
|
80
76
|
const markAllAsRead = useCallback(async () => {
|
|
81
77
|
try {
|
|
82
|
-
const config =
|
|
78
|
+
const config = api.getConfig();
|
|
83
79
|
if (!config || !config.accessToken) {
|
|
84
80
|
return;
|
|
85
81
|
}
|
|
@@ -87,10 +83,8 @@ export function useNotifications() {
|
|
|
87
83
|
const updated = notifications.map((n) => ({ ...n, read: true }));
|
|
88
84
|
saveNotifications(updated);
|
|
89
85
|
// Send via HTTP
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
: 'https://data-center.zorin.cloud/api';
|
|
93
|
-
await fetch(`${apiBase}/notifications/read-all`, {
|
|
86
|
+
const server = config.server || import.meta?.env?.VITE_MIX_ID_API_BASE || 'https://data-center.zorin.cloud/api';
|
|
87
|
+
await fetch(`${server}/notifications/read-all`, {
|
|
94
88
|
method: 'PUT',
|
|
95
89
|
headers: {
|
|
96
90
|
Authorization: `Bearer ${config.accessToken}`,
|
|
@@ -102,7 +96,7 @@ export function useNotifications() {
|
|
|
102
96
|
}
|
|
103
97
|
}, [notifications, saveNotifications]);
|
|
104
98
|
useEffect(() => {
|
|
105
|
-
const config =
|
|
99
|
+
const config = api.getConfig();
|
|
106
100
|
if (!config || !config.accessToken) {
|
|
107
101
|
// Clear notifications if MIX ID is not connected
|
|
108
102
|
saveNotifications([]);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@localzet/data-connector",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "MIX ID connector library for React/Electron applications with real-time sync, notifications, and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"mixIdApi.d.ts","sourceRoot":"","sources":["../../src/api/mixIdApi.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;IACpB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB;AAED,cAAM,QAAQ;IACZ,OAAO,CAAC,MAAM,CAA2B;IAEzC,SAAS,CAAC,MAAM,EAAE,WAAW;IAmB7B,SAAS,IAAI,WAAW,GAAG,IAAI;IA2B/B,WAAW;YAYG,OAAO;YAkDP,kBAAkB;IA0C1B,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,gBAAgB,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IAmBvG,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QACtE,YAAY,EAAE,MAAM,CAAA;QACpB,aAAa,EAAE,MAAM,CAAA;QACrB,UAAU,EAAE,MAAM,CAAA;QAClB,UAAU,EAAE,MAAM,CAAA;KACnB,CAAC;IA4CI,aAAa,IAAI,OAAO,CAAC;QAC7B,YAAY,EAAE,OAAO,CAAA;QACrB,QAAQ,EAAE,OAAO,CAAA;QACjB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAC1B,CAAC;IAII,qBAAqB,CAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAO9F,cAAc,CAAC,QAAQ,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAO7E,gBAAgB,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,GAAG,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;IAIlF,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAkCtF,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAIxF,YAAY,CAAC,eAAe,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;QAC1E,OAAO,EAAE;YACP,QAAQ,CAAC,EAAE;gBAAE,OAAO,EAAE,MAAM,CAAC;gBAAC,SAAS,EAAE,MAAM,CAAA;aAAE,CAAA;YACjD,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;gBAAE,SAAS,EAAE,MAAM,CAAA;aAAE,CAAC,CAAA;SAC7C,CAAA;QACD,UAAU,EAAE,OAAO,CAAA;KACpB,CAAC;IAQI,SAAS,CAAC,UAAU,CAAC,EAAE,GAAG,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IAQ1D,WAAW,IAAI,OAAO,CAAC,KAAK,CAAC;QACjC,EAAE,EAAE,MAAM,CAAA;QACV,UAAU,EAAE,GAAG,CAAA;QACf,cAAc,EAAE,MAAM,CAAA;QACtB,SAAS,EAAE,MAAM,CAAA;KAClB,CAAC,CAAC;IAIG,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;CAKtE;AAED,eAAO,MAAM,QAAQ,UAAiB,CAAA"}
|