@localzet/data-connector 1.0.0
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/LICENSE +674 -0
- package/README.md +52 -0
- package/dist/api/index.d.ts +4 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +3 -0
- package/dist/api/mixIdApi.d.ts +76 -0
- package/dist/api/mixIdApi.d.ts.map +1 -0
- package/dist/api/mixIdApi.js +275 -0
- package/dist/api/offlineQueue.d.ts +24 -0
- package/dist/api/offlineQueue.d.ts.map +1 -0
- package/dist/api/offlineQueue.js +137 -0
- package/dist/api/websocket.d.ts +28 -0
- package/dist/api/websocket.d.ts.map +1 -0
- package/dist/api/websocket.js +201 -0
- package/dist/components/MixIdCallbackPage.d.ts +6 -0
- package/dist/components/MixIdCallbackPage.d.ts.map +1 -0
- package/dist/components/MixIdCallbackPage.js +38 -0
- package/dist/components/MixIdConnection.d.ts +18 -0
- package/dist/components/MixIdConnection.d.ts.map +1 -0
- package/dist/components/MixIdConnection.js +197 -0
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +2 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/useMixIdSession.d.ts +19 -0
- package/dist/hooks/useMixIdSession.d.ts.map +1 -0
- package/dist/hooks/useMixIdSession.js +124 -0
- package/dist/hooks/useMixIdStatus.d.ts +9 -0
- package/dist/hooks/useMixIdStatus.d.ts.map +1 -0
- package/dist/hooks/useMixIdStatus.js +81 -0
- package/dist/hooks/useMixIdSync.d.ts +16 -0
- package/dist/hooks/useMixIdSync.d.ts.map +1 -0
- package/dist/hooks/useMixIdSync.js +263 -0
- package/dist/hooks/useNotifications.d.ts +17 -0
- package/dist/hooks/useNotifications.d.ts.map +1 -0
- package/dist/hooks/useNotifications.js +144 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/ui/Button.d.ts +5 -0
- package/dist/ui/Button.d.ts.map +1 -0
- package/dist/ui/Button.js +7 -0
- package/dist/ui/Card.d.ts +5 -0
- package/dist/ui/Card.d.ts.map +1 -0
- package/dist/ui/Card.js +7 -0
- package/dist/ui/index.d.ts +3 -0
- package/dist/ui/index.d.ts.map +1 -0
- package/dist/ui/index.js +2 -0
- package/package.json +69 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { mixIdApi } from './mixIdApi';
|
|
2
|
+
class WebSocketClient {
|
|
3
|
+
constructor() {
|
|
4
|
+
Object.defineProperty(this, "ws", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: null
|
|
9
|
+
});
|
|
10
|
+
Object.defineProperty(this, "reconnectAttempts", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: 0
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "maxReconnectAttempts", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: 10
|
|
21
|
+
});
|
|
22
|
+
Object.defineProperty(this, "reconnectDelay", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: 1000
|
|
27
|
+
});
|
|
28
|
+
Object.defineProperty(this, "isConnecting", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: false
|
|
33
|
+
});
|
|
34
|
+
Object.defineProperty(this, "eventHandlers", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
configurable: true,
|
|
37
|
+
writable: true,
|
|
38
|
+
value: new Map()
|
|
39
|
+
});
|
|
40
|
+
Object.defineProperty(this, "messageQueue", {
|
|
41
|
+
enumerable: true,
|
|
42
|
+
configurable: true,
|
|
43
|
+
writable: true,
|
|
44
|
+
value: []
|
|
45
|
+
});
|
|
46
|
+
Object.defineProperty(this, "isOnline", {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: true,
|
|
50
|
+
value: typeof navigator !== 'undefined' ? navigator.onLine : true
|
|
51
|
+
});
|
|
52
|
+
if (typeof window !== 'undefined') {
|
|
53
|
+
// Listen to online/offline events
|
|
54
|
+
window.addEventListener('online', () => {
|
|
55
|
+
this.isOnline = true;
|
|
56
|
+
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
|
57
|
+
this.connect();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
window.addEventListener('offline', () => {
|
|
61
|
+
this.isOnline = false;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
connect() {
|
|
66
|
+
if (this.isConnecting || (this.ws && this.ws.readyState === WebSocket.OPEN)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const config = mixIdApi.getConfig();
|
|
70
|
+
if (!config || !config.accessToken) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.isConnecting = true;
|
|
74
|
+
try {
|
|
75
|
+
const apiBase = config.apiBase || (typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_API_BASE)
|
|
76
|
+
? (import.meta.env?.VITE_MIX_ID_API_BASE || 'http://localhost:3000/api')
|
|
77
|
+
: 'http://localhost:3000/api';
|
|
78
|
+
const wsUrl = apiBase.replace(/^http/, 'ws').replace(/\/api$/, '') + '/ws';
|
|
79
|
+
const ws = new WebSocket(`${wsUrl}?token=${config.accessToken}`);
|
|
80
|
+
ws.onopen = () => {
|
|
81
|
+
this.ws = ws;
|
|
82
|
+
this.isConnecting = false;
|
|
83
|
+
this.reconnectAttempts = 0;
|
|
84
|
+
console.log('WebSocket connected');
|
|
85
|
+
// Dispatch custom event for connection status update
|
|
86
|
+
if (typeof window !== 'undefined') {
|
|
87
|
+
window.dispatchEvent(new Event('mixid-ws-status-changed'));
|
|
88
|
+
}
|
|
89
|
+
// Send queued messages
|
|
90
|
+
this.flushMessageQueue();
|
|
91
|
+
};
|
|
92
|
+
ws.onmessage = (event) => {
|
|
93
|
+
try {
|
|
94
|
+
const message = JSON.parse(event.data);
|
|
95
|
+
this.handleMessage(message);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
console.error('Error parsing WebSocket message:', error);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
ws.onerror = (error) => {
|
|
102
|
+
console.error('WebSocket error:', error);
|
|
103
|
+
this.isConnecting = false;
|
|
104
|
+
};
|
|
105
|
+
ws.onclose = () => {
|
|
106
|
+
this.ws = null;
|
|
107
|
+
this.isConnecting = false;
|
|
108
|
+
// Dispatch custom event for connection status update
|
|
109
|
+
if (typeof window !== 'undefined') {
|
|
110
|
+
window.dispatchEvent(new Event('mixid-ws-status-changed'));
|
|
111
|
+
}
|
|
112
|
+
this.attemptReconnect();
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
console.error('WebSocket connection error:', error);
|
|
117
|
+
this.isConnecting = false;
|
|
118
|
+
this.attemptReconnect();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
attemptReconnect() {
|
|
122
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
123
|
+
console.log('Max reconnection attempts reached');
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (!this.isOnline) {
|
|
127
|
+
// Wait for online event
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
this.reconnectAttempts++;
|
|
131
|
+
const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1), 30000);
|
|
132
|
+
setTimeout(() => {
|
|
133
|
+
if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
|
|
134
|
+
this.connect();
|
|
135
|
+
}
|
|
136
|
+
}, delay);
|
|
137
|
+
}
|
|
138
|
+
handleMessage(message) {
|
|
139
|
+
// Handle ping/pong
|
|
140
|
+
if (message.type === 'ping') {
|
|
141
|
+
this.send({ type: 'pong' });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
// Call registered event handlers
|
|
145
|
+
const handlers = this.eventHandlers.get(message.type);
|
|
146
|
+
if (handlers) {
|
|
147
|
+
handlers.forEach((handler) => handler(message));
|
|
148
|
+
}
|
|
149
|
+
// Also call wildcard handlers
|
|
150
|
+
const wildcardHandlers = this.eventHandlers.get('*');
|
|
151
|
+
if (wildcardHandlers) {
|
|
152
|
+
wildcardHandlers.forEach((handler) => handler(message));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
send(message) {
|
|
156
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
157
|
+
this.ws.send(JSON.stringify(message));
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
// Queue message for later
|
|
161
|
+
this.messageQueue.push(message);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
flushMessageQueue() {
|
|
165
|
+
while (this.messageQueue.length > 0) {
|
|
166
|
+
const message = this.messageQueue.shift();
|
|
167
|
+
if (message) {
|
|
168
|
+
this.send(message);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
on(eventType, handler) {
|
|
173
|
+
if (!this.eventHandlers.has(eventType)) {
|
|
174
|
+
this.eventHandlers.set(eventType, new Set());
|
|
175
|
+
}
|
|
176
|
+
this.eventHandlers.get(eventType).add(handler);
|
|
177
|
+
}
|
|
178
|
+
off(eventType, handler) {
|
|
179
|
+
const handlers = this.eventHandlers.get(eventType);
|
|
180
|
+
if (handlers) {
|
|
181
|
+
handlers.delete(handler);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
disconnect() {
|
|
185
|
+
if (this.ws) {
|
|
186
|
+
this.ws.close();
|
|
187
|
+
this.ws = null;
|
|
188
|
+
}
|
|
189
|
+
this.eventHandlers.clear();
|
|
190
|
+
this.messageQueue = [];
|
|
191
|
+
this.reconnectAttempts = 0;
|
|
192
|
+
// Dispatch custom event for connection status update
|
|
193
|
+
if (typeof window !== 'undefined') {
|
|
194
|
+
window.dispatchEvent(new Event('mixid-ws-status-changed'));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
isConnected() {
|
|
198
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export const wsClient = new WebSocketClient();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface MixIdCallbackPageProps {
|
|
2
|
+
onCallback?: (code: string, state: string | null) => void;
|
|
3
|
+
redirectTo?: string;
|
|
4
|
+
}
|
|
5
|
+
export default function MixIdCallbackPage({ onCallback, redirectTo }?: MixIdCallbackPageProps): import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
//# sourceMappingURL=MixIdCallbackPage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MixIdCallbackPage.d.ts","sourceRoot":"","sources":["../../src/components/MixIdCallbackPage.tsx"],"names":[],"mappings":"AAGA,MAAM,WAAW,sBAAsB;IACrC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAA;IACzD,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,UAAU,EAAE,UAAwB,EAAE,GAAE,sBAA2B,2CA2C9G"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect } from 'react';
|
|
3
|
+
import { Center, Loader, Text, Stack } from '@mantine/core';
|
|
4
|
+
export default function MixIdCallbackPage({ onCallback, redirectTo = '/settings' } = {}) {
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (typeof window === 'undefined')
|
|
7
|
+
return;
|
|
8
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
9
|
+
const code = urlParams.get('code');
|
|
10
|
+
const state = urlParams.get('state');
|
|
11
|
+
if (code) {
|
|
12
|
+
// Send message to parent window
|
|
13
|
+
if (window.opener) {
|
|
14
|
+
window.opener.postMessage({
|
|
15
|
+
type: 'mixid-oauth-callback',
|
|
16
|
+
code,
|
|
17
|
+
state,
|
|
18
|
+
}, window.location.origin);
|
|
19
|
+
window.close();
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
// If no opener, call callback or redirect
|
|
23
|
+
if (onCallback) {
|
|
24
|
+
onCallback(code, state);
|
|
25
|
+
}
|
|
26
|
+
else if (redirectTo && typeof window !== 'undefined') {
|
|
27
|
+
window.location.href = redirectTo;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
if (redirectTo && typeof window !== 'undefined') {
|
|
33
|
+
window.location.href = redirectTo;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}, [onCallback, redirectTo]);
|
|
37
|
+
return (_jsx(Center, { h: "100vh", children: _jsxs(Stack, { align: "center", gap: "md", children: [_jsx(Loader, {}), _jsx(Text, { children: "\u041E\u0431\u0440\u0430\u0431\u043E\u0442\u043A\u0430 \u0430\u0432\u0442\u043E\u0440\u0438\u0437\u0430\u0446\u0438\u0438 MIX ID..." })] }) }));
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface MixIdConnectionProps {
|
|
2
|
+
onConnected?: () => void;
|
|
3
|
+
onDisconnected?: () => void;
|
|
4
|
+
showSyncSettings?: boolean;
|
|
5
|
+
showSyncData?: boolean;
|
|
6
|
+
apiBase?: string;
|
|
7
|
+
clientId?: string;
|
|
8
|
+
clientSecret?: string;
|
|
9
|
+
notifications?: {
|
|
10
|
+
show: (options: {
|
|
11
|
+
title: string;
|
|
12
|
+
message: string;
|
|
13
|
+
color?: string;
|
|
14
|
+
}) => void;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export default function MixIdConnection({ onConnected, onDisconnected, showSyncSettings, showSyncData, apiBase, clientId, clientSecret, notifications, }: MixIdConnectionProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=MixIdConnection.d.ts.map
|
|
@@ -0,0 +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;IACxB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAA;IAC3B,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,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,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,aAAa,GACd,EAAE,oBAAoB,2CAwStB"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useEffect } from 'react';
|
|
3
|
+
import { Paper, Group, Button, Text, Badge, Modal, Stack, Switch, Alert, Loader, } from '@mantine/core';
|
|
4
|
+
import { IconPlug, IconSettings, IconLogout, IconX } from '@tabler/icons-react';
|
|
5
|
+
import { useDisclosure } from '@mantine/hooks';
|
|
6
|
+
import { mixIdApi } from '../api/mixIdApi';
|
|
7
|
+
import { useMixIdStatus } from '../hooks/useMixIdStatus';
|
|
8
|
+
export default function MixIdConnection({ onConnected, onDisconnected, showSyncSettings = true, showSyncData = true, apiBase, clientId, clientSecret, notifications, }) {
|
|
9
|
+
const { isConnected, syncStatus, hasConfig } = useMixIdStatus();
|
|
10
|
+
const [loading, setLoading] = useState(true);
|
|
11
|
+
const [syncStatusData, setSyncStatusData] = useState(null);
|
|
12
|
+
const [settingsModalOpened, { open: openSettings, close: closeSettings }] = useDisclosure(false);
|
|
13
|
+
const [syncSettings, setSyncSettings] = useState(false);
|
|
14
|
+
const [syncData, setSyncData] = useState(false);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
checkConnection();
|
|
17
|
+
}, []);
|
|
18
|
+
const checkConnection = async () => {
|
|
19
|
+
try {
|
|
20
|
+
const config = mixIdApi.getConfig();
|
|
21
|
+
if (!config || !config.accessToken) {
|
|
22
|
+
setSyncStatusData(null);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const status = await mixIdApi.getSyncStatus();
|
|
26
|
+
setSyncStatusData(status);
|
|
27
|
+
setSyncSettings(status.syncSettings);
|
|
28
|
+
setSyncData(status.syncData);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
setSyncStatusData(null);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
setLoading(false);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const handleConnect = async () => {
|
|
38
|
+
try {
|
|
39
|
+
// Get config from props or environment
|
|
40
|
+
const finalApiBase = apiBase ||
|
|
41
|
+
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_API_BASE)
|
|
42
|
+
? (import.meta.env?.VITE_MIX_ID_API_BASE || 'http://localhost:3000/api')
|
|
43
|
+
: 'http://localhost:3000/api';
|
|
44
|
+
const finalClientId = clientId ||
|
|
45
|
+
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_CLIENT_ID)
|
|
46
|
+
? (import.meta.env?.VITE_MIX_ID_CLIENT_ID || '')
|
|
47
|
+
: '';
|
|
48
|
+
const finalClientSecret = clientSecret ||
|
|
49
|
+
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_MIX_ID_CLIENT_SECRET)
|
|
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';
|
|
54
|
+
if (notifications) {
|
|
55
|
+
notifications.show({
|
|
56
|
+
title: 'Ошибка',
|
|
57
|
+
message,
|
|
58
|
+
color: 'red',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
alert(message);
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
mixIdApi.setConfig({
|
|
67
|
+
apiBase: finalApiBase || 'http://localhost:3000/api',
|
|
68
|
+
clientId: finalClientId,
|
|
69
|
+
clientSecret: finalClientSecret
|
|
70
|
+
});
|
|
71
|
+
// Initiate OAuth flow
|
|
72
|
+
const redirectUri = typeof window !== 'undefined' ? window.location.origin + '/mixid-callback' : '';
|
|
73
|
+
const { authorizationUrl, code } = await mixIdApi.initiateOAuth(redirectUri);
|
|
74
|
+
// Open OAuth window
|
|
75
|
+
if (typeof window === 'undefined')
|
|
76
|
+
return;
|
|
77
|
+
const width = 600;
|
|
78
|
+
const height = 700;
|
|
79
|
+
const left = window.screenX + (window.outerWidth - width) / 2;
|
|
80
|
+
const top = window.screenY + (window.outerHeight - height) / 2;
|
|
81
|
+
const oauthWindow = window.open(authorizationUrl, 'MIX ID Authorization', `width=${width},height=${height},left=${left},top=${top}`);
|
|
82
|
+
// Listen for OAuth callback
|
|
83
|
+
const handleMessage = async (event) => {
|
|
84
|
+
if (event.origin !== window.location.origin)
|
|
85
|
+
return;
|
|
86
|
+
if (event.data.type === 'mixid-oauth-callback') {
|
|
87
|
+
window.removeEventListener('message', handleMessage);
|
|
88
|
+
oauthWindow?.close();
|
|
89
|
+
try {
|
|
90
|
+
const { code: callbackCode } = event.data;
|
|
91
|
+
await mixIdApi.exchangeCodeForToken(callbackCode || code, redirectUri);
|
|
92
|
+
// Dispatch event to trigger WebSocket connection and status update
|
|
93
|
+
window.dispatchEvent(new Event('mixid-config-changed'));
|
|
94
|
+
await checkConnection();
|
|
95
|
+
if (notifications) {
|
|
96
|
+
notifications.show({
|
|
97
|
+
title: 'Успешно',
|
|
98
|
+
message: 'MIX ID подключен',
|
|
99
|
+
color: 'green',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
onConnected?.();
|
|
103
|
+
openSettings();
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
const message = error instanceof Error ? error.message : 'Не удалось подключить MIX ID';
|
|
107
|
+
if (notifications) {
|
|
108
|
+
notifications.show({
|
|
109
|
+
title: 'Ошибка',
|
|
110
|
+
message,
|
|
111
|
+
color: 'red',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
alert(message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
window.addEventListener('message', handleMessage);
|
|
121
|
+
// Fallback: check if window was closed manually
|
|
122
|
+
const checkClosed = setInterval(() => {
|
|
123
|
+
if (oauthWindow?.closed) {
|
|
124
|
+
clearInterval(checkClosed);
|
|
125
|
+
window.removeEventListener('message', handleMessage);
|
|
126
|
+
}
|
|
127
|
+
}, 1000);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : 'Не удалось инициировать подключение';
|
|
131
|
+
if (notifications) {
|
|
132
|
+
notifications.show({
|
|
133
|
+
title: 'Ошибка',
|
|
134
|
+
message,
|
|
135
|
+
color: 'red',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
alert(message);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
const handleDisconnect = async () => {
|
|
144
|
+
if (typeof window === 'undefined')
|
|
145
|
+
return;
|
|
146
|
+
if (!confirm('Вы уверены, что хотите отключить MIX ID?'))
|
|
147
|
+
return;
|
|
148
|
+
mixIdApi.clearConfig();
|
|
149
|
+
// Dispatch event to trigger WebSocket disconnection and status update
|
|
150
|
+
window.dispatchEvent(new Event('mixid-config-changed'));
|
|
151
|
+
setSyncStatusData(null);
|
|
152
|
+
if (notifications) {
|
|
153
|
+
notifications.show({
|
|
154
|
+
title: 'Успешно',
|
|
155
|
+
message: 'MIX ID отключен',
|
|
156
|
+
color: 'blue',
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
onDisconnected?.();
|
|
160
|
+
};
|
|
161
|
+
const handleSaveSettings = async () => {
|
|
162
|
+
try {
|
|
163
|
+
await mixIdApi.updateSyncPreferences(syncSettings, syncData);
|
|
164
|
+
if (notifications) {
|
|
165
|
+
notifications.show({
|
|
166
|
+
title: 'Успешно',
|
|
167
|
+
message: 'Настройки синхронизации сохранены',
|
|
168
|
+
color: 'green',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
closeSettings();
|
|
172
|
+
await checkConnection();
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
const message = error instanceof Error ? error.message : 'Не удалось сохранить настройки';
|
|
176
|
+
if (notifications) {
|
|
177
|
+
notifications.show({
|
|
178
|
+
title: 'Ошибка',
|
|
179
|
+
message,
|
|
180
|
+
color: 'red',
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
alert(message);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
if (loading) {
|
|
189
|
+
return (_jsx(Paper, { p: "md", withBorder: true, children: _jsx(Loader, { size: "sm" }) }));
|
|
190
|
+
}
|
|
191
|
+
return (_jsxs(_Fragment, { children: [_jsx(Paper, { p: "md", withBorder: true, children: _jsxs(Group, { justify: "space-between", children: [_jsxs(Group, { children: [_jsx(IconPlug, { size: 24 }), _jsxs("div", { children: [_jsx(Text, { fw: 500, children: "MIX ID" }), _jsx(Text, { size: "sm", c: "dimmed", children: "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u044F \u0434\u0430\u043D\u043D\u044B\u0445 \u0447\u0435\u0440\u0435\u0437 Zorin Projects" })] })] }), isConnected ? (_jsxs(Group, { children: [syncStatusData && (_jsxs(Group, { gap: "xs", children: [showSyncSettings && (_jsx(Badge, { color: syncStatusData.syncSettings ? 'green' : 'gray', children: "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438" })), showSyncData && (_jsx(Badge, { color: syncStatusData.syncData ? 'green' : 'gray', children: "\u0414\u0430\u043D\u043D\u044B\u0435" }))] })), _jsx(Button, { leftSection: _jsx(IconSettings, { size: 16 }), variant: "light", onClick: openSettings, children: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B" }), _jsx(Button, { leftSection: _jsx(IconLogout, { size: 16 }), variant: "subtle", onClick: handleDisconnect, children: "\u0412\u044B\u0439\u0442\u0438" })] })) : (_jsx(Button, { leftSection: _jsx(IconPlug, { size: 16 }), onClick: handleConnect, children: "\u041F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C" }))] }) }), _jsx(Modal, { opened: settingsModalOpened, onClose: closeSettings, title: "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u0438 MIX ID", children: _jsxs(Stack, { gap: "md", children: [_jsx(Alert, { children: _jsx(Text, { size: "sm", children: "MIX ID \u043F\u043E\u0437\u0432\u043E\u043B\u044F\u0435\u0442 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0432\u0430\u0448\u0438 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u0438 \u0434\u0430\u043D\u043D\u044B\u0435 \u043C\u0435\u0436\u0434\u0443 \u0443\u0441\u0442\u0440\u043E\u0439\u0441\u0442\u0432\u0430\u043C\u0438. \u0412\u044B \u043C\u043E\u0436\u0435\u0442\u0435 \u0432\u044B\u0431\u0440\u0430\u0442\u044C, \u0447\u0442\u043E \u0438\u043C\u0435\u043D\u043D\u043E \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C." }) }), showSyncSettings && (_jsx(Switch, { label: "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438", description: "\u041D\u0430\u0441\u0442\u0440\u043E\u0439\u043A\u0438 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0431\u0443\u0434\u0443\u0442 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C", checked: syncSettings, onChange: (e) => {
|
|
192
|
+
setSyncSettings(e.currentTarget.checked);
|
|
193
|
+
if (!e.currentTarget.checked) {
|
|
194
|
+
setSyncData(false);
|
|
195
|
+
}
|
|
196
|
+
} })), showSyncData && (_jsx(Switch, { label: "\u0421\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435", description: "\u0414\u0430\u043D\u043D\u044B\u0435 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F \u0431\u0443\u0434\u0443\u0442 \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0438\u0440\u043E\u0432\u0430\u0442\u044C\u0441\u044F \u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u043E\u043C", checked: syncData, onChange: (e) => setSyncData(e.currentTarget.checked), disabled: showSyncSettings && !syncSettings })), showSyncSettings && !syncSettings && (_jsx(Alert, { color: "yellow", icon: _jsx(IconX, { size: 16 }), children: "\u0414\u043B\u044F \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u0438 \u0434\u0430\u043D\u043D\u044B\u0445 \u043D\u0435\u043E\u0431\u0445\u043E\u0434\u0438\u043C\u043E \u0432\u043A\u043B\u044E\u0447\u0438\u0442\u044C \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u044E \u043D\u0430\u0441\u0442\u0440\u043E\u0435\u043A" })), syncStatusData?.lastSyncAt && (_jsxs(Text, { size: "sm", c: "dimmed", children: ["\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u0441\u0438\u043D\u0445\u0440\u043E\u043D\u0438\u0437\u0430\u0446\u0438\u044F: ", new Date(syncStatusData.lastSyncAt).toLocaleString('ru-RU')] })), _jsxs(Group, { justify: "flex-end", children: [_jsx(Button, { variant: "subtle", onClick: closeSettings, children: "\u041E\u0442\u043C\u0435\u043D\u0430" }), _jsx(Button, { onClick: handleSaveSettings, children: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C" })] })] }) })] }));
|
|
197
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { default as MixIdConnection } from './MixIdConnection';
|
|
2
|
+
export { default as MixIdCallbackPage } from './MixIdCallbackPage';
|
|
3
|
+
export type { MixIdConnectionProps } from './MixIdConnection';
|
|
4
|
+
export type { MixIdCallbackPageProps } from './MixIdCallbackPage';
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAC9D,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAA;AAClE,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAC7D,YAAY,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAA;AAChC,cAAc,gBAAgB,CAAA;AAC9B,cAAc,oBAAoB,CAAA;AAClC,cAAc,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Session {
|
|
2
|
+
id: string;
|
|
3
|
+
deviceInfo: any;
|
|
4
|
+
lastActivityAt: string;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
}
|
|
7
|
+
export interface UseMixIdSessionOptions {
|
|
8
|
+
onSessionDeleted?: () => void;
|
|
9
|
+
onSessionExpired?: () => void;
|
|
10
|
+
onSessionInvalid?: () => void;
|
|
11
|
+
heartbeatInterval?: number;
|
|
12
|
+
checkSessionOnMount?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function useMixIdSession(options?: UseMixIdSessionOptions): {
|
|
15
|
+
sendHeartbeat: () => Promise<void>;
|
|
16
|
+
checkSession: () => Promise<void>;
|
|
17
|
+
currentSessionId: string | null;
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=useMixIdSession.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMixIdSession.d.ts","sourceRoot":"","sources":["../../src/hooks/useMixIdSession.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,OAAO;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,UAAU,EAAE,GAAG,CAAA;IACf,cAAc,EAAE,MAAM,CAAA;IACtB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,sBAAsB;IACrC,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAA;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,mBAAmB,CAAC,EAAE,OAAO,CAAA;CAC9B;AAED,wBAAgB,eAAe,CAAC,OAAO,GAAE,sBAA2B;;;;EAwInE"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useEffect, useCallback, useState } from 'react';
|
|
2
|
+
import { mixIdApi } from '../api/mixIdApi';
|
|
3
|
+
import { wsClient } from '../api/websocket';
|
|
4
|
+
export function useMixIdSession(options = {}) {
|
|
5
|
+
const { onSessionDeleted, onSessionExpired, onSessionInvalid, heartbeatInterval = 30 * 1000, // 30 seconds
|
|
6
|
+
checkSessionOnMount = true, } = options;
|
|
7
|
+
const [currentSessionId, setCurrentSessionId] = useState(null);
|
|
8
|
+
const sendHeartbeat = useCallback(async () => {
|
|
9
|
+
try {
|
|
10
|
+
const config = mixIdApi.getConfig();
|
|
11
|
+
if (!config || !config.accessToken) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const result = await mixIdApi.heartbeat({
|
|
15
|
+
platform: typeof navigator !== 'undefined' ? navigator.platform : 'unknown',
|
|
16
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
|
|
17
|
+
timestamp: new Date().toISOString(),
|
|
18
|
+
});
|
|
19
|
+
// Store session ID if returned
|
|
20
|
+
if (result && result.sessionId) {
|
|
21
|
+
setCurrentSessionId(result.sessionId);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error('Heartbeat failed:', error);
|
|
26
|
+
// If heartbeat fails with 401/403, session might be deleted or expired
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
if (error.message.includes('401') || error.message.includes('403')) {
|
|
29
|
+
mixIdApi.clearConfig();
|
|
30
|
+
wsClient.disconnect();
|
|
31
|
+
onSessionExpired?.();
|
|
32
|
+
}
|
|
33
|
+
else if (error.message.includes('404')) {
|
|
34
|
+
// Session not found - might have been deleted
|
|
35
|
+
mixIdApi.clearConfig();
|
|
36
|
+
wsClient.disconnect();
|
|
37
|
+
onSessionDeleted?.();
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
onSessionInvalid?.();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}, [onSessionDeleted, onSessionExpired, onSessionInvalid]);
|
|
45
|
+
const checkSession = useCallback(async () => {
|
|
46
|
+
try {
|
|
47
|
+
const config = mixIdApi.getConfig();
|
|
48
|
+
if (!config || !config.accessToken) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// Try to get sessions to verify current session exists
|
|
52
|
+
const sessions = await mixIdApi.getSessions();
|
|
53
|
+
// If we have a current session ID, check if it still exists
|
|
54
|
+
if (currentSessionId) {
|
|
55
|
+
const sessionExists = sessions.some(s => s.id === currentSessionId);
|
|
56
|
+
if (!sessionExists) {
|
|
57
|
+
// Session was deleted
|
|
58
|
+
mixIdApi.clearConfig();
|
|
59
|
+
wsClient.disconnect();
|
|
60
|
+
onSessionDeleted?.();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
console.error('Failed to check session:', error);
|
|
66
|
+
if (error instanceof Error && (error.message.includes('401') || error.message.includes('403'))) {
|
|
67
|
+
mixIdApi.clearConfig();
|
|
68
|
+
wsClient.disconnect();
|
|
69
|
+
onSessionExpired?.();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}, [currentSessionId, onSessionDeleted, onSessionExpired]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
const config = mixIdApi.getConfig();
|
|
75
|
+
if (!config || !config.accessToken) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
// Check session on mount if enabled
|
|
79
|
+
if (checkSessionOnMount) {
|
|
80
|
+
checkSession();
|
|
81
|
+
}
|
|
82
|
+
// Set up WebSocket handlers for session events
|
|
83
|
+
const handleSessionDeleted = (message) => {
|
|
84
|
+
// If message contains sessionId, check if it's our session
|
|
85
|
+
if (message.sessionId) {
|
|
86
|
+
if (currentSessionId && message.sessionId === currentSessionId) {
|
|
87
|
+
// Our session was deleted
|
|
88
|
+
mixIdApi.clearConfig();
|
|
89
|
+
wsClient.disconnect();
|
|
90
|
+
onSessionDeleted?.();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Session deletion event without specific ID - might be ours
|
|
95
|
+
// Check by making a request
|
|
96
|
+
checkSession();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const handleSessionExpired = () => {
|
|
100
|
+
mixIdApi.clearConfig();
|
|
101
|
+
wsClient.disconnect();
|
|
102
|
+
onSessionExpired?.();
|
|
103
|
+
};
|
|
104
|
+
wsClient.on('session:deleted', handleSessionDeleted);
|
|
105
|
+
wsClient.on('session:expired', handleSessionExpired);
|
|
106
|
+
// Start heartbeat
|
|
107
|
+
const heartbeatIntervalId = setInterval(sendHeartbeat, heartbeatInterval);
|
|
108
|
+
// Send initial heartbeat
|
|
109
|
+
sendHeartbeat();
|
|
110
|
+
// Check session periodically (every 5 minutes)
|
|
111
|
+
const sessionCheckInterval = setInterval(checkSession, 5 * 60 * 1000);
|
|
112
|
+
return () => {
|
|
113
|
+
wsClient.off('session:deleted', handleSessionDeleted);
|
|
114
|
+
wsClient.off('session:expired', handleSessionExpired);
|
|
115
|
+
clearInterval(heartbeatIntervalId);
|
|
116
|
+
clearInterval(sessionCheckInterval);
|
|
117
|
+
};
|
|
118
|
+
}, [sendHeartbeat, checkSession, onSessionDeleted, onSessionExpired, heartbeatInterval, checkSessionOnMount, currentSessionId]);
|
|
119
|
+
return {
|
|
120
|
+
sendHeartbeat,
|
|
121
|
+
checkSession,
|
|
122
|
+
currentSessionId,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type MixIdSyncStatus = 'connected-ws' | 'connected-rest' | 'disconnected' | 'checking';
|
|
2
|
+
export interface UseMixIdStatusReturn {
|
|
3
|
+
isConnected: boolean;
|
|
4
|
+
syncStatus: MixIdSyncStatus;
|
|
5
|
+
hasConfig: boolean;
|
|
6
|
+
refresh: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function useMixIdStatus(): UseMixIdStatusReturn;
|
|
9
|
+
//# sourceMappingURL=useMixIdStatus.d.ts.map
|