@oxyhq/services 5.13.15 → 5.13.16
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/README.md +10 -0
- package/lib/commonjs/core/OxyServices.base.js +271 -0
- package/lib/commonjs/core/OxyServices.base.js.map +1 -0
- package/lib/commonjs/core/OxyServices.errors.js +26 -0
- package/lib/commonjs/core/OxyServices.errors.js.map +1 -0
- package/lib/commonjs/core/OxyServices.js +58 -2168
- package/lib/commonjs/core/OxyServices.js.map +1 -1
- package/lib/commonjs/core/mixins/OxyServices.analytics.js +60 -0
- package/lib/commonjs/core/mixins/OxyServices.analytics.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.assets.js +406 -0
- package/lib/commonjs/core/mixins/OxyServices.assets.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.auth.js +303 -0
- package/lib/commonjs/core/mixins/OxyServices.auth.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.developer.js +115 -0
- package/lib/commonjs/core/mixins/OxyServices.developer.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.devices.js +119 -0
- package/lib/commonjs/core/mixins/OxyServices.devices.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.karma.js +117 -0
- package/lib/commonjs/core/mixins/OxyServices.karma.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.language.js +124 -0
- package/lib/commonjs/core/mixins/OxyServices.language.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.location.js +55 -0
- package/lib/commonjs/core/mixins/OxyServices.location.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.payment.js +66 -0
- package/lib/commonjs/core/mixins/OxyServices.payment.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.privacy.js +174 -0
- package/lib/commonjs/core/mixins/OxyServices.privacy.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.totp.js +53 -0
- package/lib/commonjs/core/mixins/OxyServices.totp.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.user.js +389 -0
- package/lib/commonjs/core/mixins/OxyServices.user.js.map +1 -0
- package/lib/commonjs/core/mixins/OxyServices.utility.js +161 -0
- package/lib/commonjs/core/mixins/OxyServices.utility.js.map +1 -0
- package/lib/commonjs/core/mixins/index.js +39 -0
- package/lib/commonjs/core/mixins/index.js.map +1 -0
- package/lib/commonjs/core/mixins/mixinHelpers.js +62 -0
- package/lib/commonjs/core/mixins/mixinHelpers.js.map +1 -0
- package/lib/commonjs/ui/context/OxyContext.js +27 -2
- package/lib/commonjs/ui/context/OxyContext.js.map +1 -1
- package/lib/module/core/OxyServices.base.js +265 -0
- package/lib/module/core/OxyServices.base.js.map +1 -0
- package/lib/module/core/OxyServices.errors.js +20 -0
- package/lib/module/core/OxyServices.errors.js.map +1 -0
- package/lib/module/core/OxyServices.js +43 -2164
- package/lib/module/core/OxyServices.js.map +1 -1
- package/lib/module/core/mixins/OxyServices.analytics.js +56 -0
- package/lib/module/core/mixins/OxyServices.analytics.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.assets.js +402 -0
- package/lib/module/core/mixins/OxyServices.assets.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.auth.js +299 -0
- package/lib/module/core/mixins/OxyServices.auth.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.developer.js +111 -0
- package/lib/module/core/mixins/OxyServices.developer.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.devices.js +115 -0
- package/lib/module/core/mixins/OxyServices.devices.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.karma.js +113 -0
- package/lib/module/core/mixins/OxyServices.karma.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.language.js +120 -0
- package/lib/module/core/mixins/OxyServices.language.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.location.js +51 -0
- package/lib/module/core/mixins/OxyServices.location.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.payment.js +62 -0
- package/lib/module/core/mixins/OxyServices.payment.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.privacy.js +170 -0
- package/lib/module/core/mixins/OxyServices.privacy.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.totp.js +49 -0
- package/lib/module/core/mixins/OxyServices.totp.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.user.js +385 -0
- package/lib/module/core/mixins/OxyServices.user.js.map +1 -0
- package/lib/module/core/mixins/OxyServices.utility.js +156 -0
- package/lib/module/core/mixins/OxyServices.utility.js.map +1 -0
- package/lib/module/core/mixins/index.js +36 -0
- package/lib/module/core/mixins/index.js.map +1 -0
- package/lib/module/core/mixins/mixinHelpers.js +56 -0
- package/lib/module/core/mixins/mixinHelpers.js.map +1 -0
- package/lib/module/ui/context/OxyContext.js +27 -2
- package/lib/module/ui/context/OxyContext.js.map +1 -1
- package/lib/typescript/core/OxyServices.base.d.ts +123 -0
- package/lib/typescript/core/OxyServices.base.d.ts.map +1 -0
- package/lib/typescript/core/OxyServices.d.ts +969 -746
- package/lib/typescript/core/OxyServices.d.ts.map +1 -1
- package/lib/typescript/core/OxyServices.errors.d.ts +12 -0
- package/lib/typescript/core/OxyServices.errors.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.analytics.d.ts +70 -0
- package/lib/typescript/core/mixins/OxyServices.analytics.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts +159 -0
- package/lib/typescript/core/mixins/OxyServices.assets.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.auth.d.ts +168 -0
- package/lib/typescript/core/mixins/OxyServices.auth.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.developer.d.ts +103 -0
- package/lib/typescript/core/mixins/OxyServices.developer.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts +93 -0
- package/lib/typescript/core/mixins/OxyServices.devices.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.karma.d.ts +89 -0
- package/lib/typescript/core/mixins/OxyServices.karma.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.language.d.ts +85 -0
- package/lib/typescript/core/mixins/OxyServices.language.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.location.d.ts +68 -0
- package/lib/typescript/core/mixins/OxyServices.location.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.payment.d.ts +74 -0
- package/lib/typescript/core/mixins/OxyServices.payment.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.privacy.d.ts +126 -0
- package/lib/typescript/core/mixins/OxyServices.privacy.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.totp.d.ts +69 -0
- package/lib/typescript/core/mixins/OxyServices.totp.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.user.d.ts +189 -0
- package/lib/typescript/core/mixins/OxyServices.user.d.ts.map +1 -0
- package/lib/typescript/core/mixins/OxyServices.utility.d.ts +97 -0
- package/lib/typescript/core/mixins/OxyServices.utility.d.ts.map +1 -0
- package/lib/typescript/core/mixins/index.d.ts +898 -0
- package/lib/typescript/core/mixins/index.d.ts.map +1 -0
- package/lib/typescript/core/mixins/mixinHelpers.d.ts +32 -0
- package/lib/typescript/core/mixins/mixinHelpers.d.ts.map +1 -0
- package/lib/typescript/models/interfaces.d.ts +10 -0
- package/lib/typescript/models/interfaces.d.ts.map +1 -1
- package/lib/typescript/ui/context/OxyContext.d.ts +2 -0
- package/lib/typescript/ui/context/OxyContext.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/OxyServices.base.ts +311 -0
- package/src/core/OxyServices.errors.ts +26 -0
- package/src/core/OxyServices.ts +43 -2199
- package/src/core/mixins/OxyServices.analytics.ts +53 -0
- package/src/core/mixins/OxyServices.assets.ts +390 -0
- package/src/core/mixins/OxyServices.auth.ts +275 -0
- package/src/core/mixins/OxyServices.developer.ts +114 -0
- package/src/core/mixins/OxyServices.devices.ts +103 -0
- package/src/core/mixins/OxyServices.karma.ts +111 -0
- package/src/core/mixins/OxyServices.language.ts +127 -0
- package/src/core/mixins/OxyServices.location.ts +46 -0
- package/src/core/mixins/OxyServices.payment.ts +59 -0
- package/src/core/mixins/OxyServices.privacy.ts +182 -0
- package/src/core/mixins/OxyServices.totp.ts +36 -0
- package/src/core/mixins/OxyServices.user.ts +380 -0
- package/src/core/mixins/OxyServices.utility.ts +187 -0
- package/src/core/mixins/index.ts +58 -0
- package/src/core/mixins/mixinHelpers.ts +69 -0
- package/src/models/interfaces.ts +12 -0
- package/src/ui/context/OxyContext.tsx +36 -0
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* // Upload a file (browser File API)
|
|
23
23
|
* const fileInput = document.querySelector('input[type=file]');
|
|
24
24
|
* const file = fileInput.files[0];
|
|
25
|
-
* await oxy.
|
|
25
|
+
* await oxy.uploadRawFile(file);
|
|
26
26
|
*
|
|
27
27
|
* // Get a file stream URL for <img src>
|
|
28
28
|
* const url = oxy.getFileStreamUrl('fileId');
|
|
@@ -58,2181 +58,60 @@
|
|
|
58
58
|
*
|
|
59
59
|
* See method JSDoc for more details and options.
|
|
60
60
|
*/
|
|
61
|
-
import { jwtDecode } from 'jwt-decode';
|
|
62
|
-
import { normalizeLanguageCode, getLanguageMetadata, getLanguageName, getNativeLanguageName } from '../utils/languageUtils';
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
* OxyConfig - Configuration for OxyServices
|
|
66
|
-
* @property baseURL - The Oxy API base URL (e.g., https://api.oxy.so)
|
|
67
|
-
* @property cloudURL - The Oxy Cloud (file storage/CDN) URL (e.g., https://cloud.oxy.so)
|
|
68
|
-
*/
|
|
62
|
+
import { OxyAuthenticationError, OxyAuthenticationTimeoutError } from './OxyServices.errors';
|
|
69
63
|
|
|
70
|
-
|
|
71
|
-
import {
|
|
72
|
-
import { HttpClient } from './HttpClient';
|
|
73
|
-
import { RequestManager } from './RequestManager';
|
|
74
|
-
/**
|
|
75
|
-
* Custom error types for better error handling
|
|
76
|
-
*/
|
|
77
|
-
export class OxyAuthenticationError extends Error {
|
|
78
|
-
constructor(message, code = 'AUTH_ERROR', status = 401) {
|
|
79
|
-
super(message);
|
|
80
|
-
this.name = 'OxyAuthenticationError';
|
|
81
|
-
this.code = code;
|
|
82
|
-
this.status = status;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
export class OxyAuthenticationTimeoutError extends OxyAuthenticationError {
|
|
86
|
-
constructor(operationName, timeoutMs) {
|
|
87
|
-
super(`Authentication timeout (${timeoutMs}ms): ${operationName} requires user authentication. Please ensure the user is logged in before calling this method.`, 'AUTH_TIMEOUT', 408);
|
|
88
|
-
this.name = 'OxyAuthenticationTimeoutError';
|
|
89
|
-
}
|
|
90
|
-
}
|
|
64
|
+
// Import mixin composition helper
|
|
65
|
+
import { composeOxyServices } from './mixins';
|
|
91
66
|
|
|
92
67
|
/**
|
|
93
68
|
* OxyServices - Unified client library for interacting with the Oxy API
|
|
94
69
|
*
|
|
95
70
|
* This class provides all API functionality in one simple, easy-to-use interface.
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* -
|
|
99
|
-
* -
|
|
71
|
+
*
|
|
72
|
+
* ## Architecture
|
|
73
|
+
* - **HttpClient**: Handles HTTP communication and authentication
|
|
74
|
+
* - **RequestManager**: Handles caching, deduplication, queuing, and retry
|
|
75
|
+
* - **OxyServices**: Provides high-level API methods
|
|
76
|
+
*
|
|
77
|
+
* ## Mixin Composition
|
|
78
|
+
* The class is composed using TypeScript mixins for better code organization:
|
|
79
|
+
* - **Base**: Core infrastructure (HTTP client, request management, error handling)
|
|
80
|
+
* - **Auth**: Authentication and session management
|
|
81
|
+
* - **User**: User profiles, follow, notifications
|
|
82
|
+
* - **TOTP**: Two-factor authentication enrollment
|
|
83
|
+
* - **Privacy**: Blocked and restricted users
|
|
84
|
+
* - **Language**: Language detection and metadata
|
|
85
|
+
* - **Payment**: Payment processing
|
|
86
|
+
* - **Karma**: Karma system
|
|
87
|
+
* - **Assets**: File upload and asset management
|
|
88
|
+
* - **Developer**: Developer API management
|
|
89
|
+
* - **Location**: Location-based features
|
|
90
|
+
* - **Analytics**: Analytics tracking
|
|
91
|
+
* - **Devices**: Device management
|
|
92
|
+
* - **Utility**: Utility methods and Express middleware
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const oxy = new OxyServices({
|
|
97
|
+
* baseURL: 'https://api.oxy.so',
|
|
98
|
+
* cloudURL: 'https://cloud.oxy.so'
|
|
99
|
+
* });
|
|
100
|
+
* ```
|
|
100
101
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
* Creates a new instance of the OxyServices client
|
|
104
|
-
* @param config - Configuration for the client
|
|
105
|
-
* config.baseURL: Oxy API URL (e.g., https://api.oxy.so)
|
|
106
|
-
* config.cloudURL: Oxy Cloud URL (e.g., https://cloud.oxy.so)
|
|
107
|
-
*/
|
|
108
|
-
constructor(config) {
|
|
109
|
-
this.config = config;
|
|
110
|
-
this.cloudURL = config.cloudURL || OXY_CLOUD_URL;
|
|
111
|
-
|
|
112
|
-
// Initialize HTTP client (handles authentication and interceptors)
|
|
113
|
-
this.httpClient = new HttpClient(config);
|
|
114
|
-
|
|
115
|
-
// Initialize request manager (handles caching, deduplication, queuing, retry)
|
|
116
|
-
this.requestManager = new RequestManager(this.httpClient, config);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Test-only utility to reset global tokens between jest tests
|
|
120
|
-
static __resetTokensForTests() {
|
|
121
|
-
HttpClient.__resetTokensForTests();
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Make a request with all performance optimizations
|
|
126
|
-
* This is the main method for all API calls - ensures authentication and performance features
|
|
127
|
-
*/
|
|
128
|
-
async makeRequest(method, url, data, options = {}) {
|
|
129
|
-
return this.requestManager.request(method, url, data, options);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// ============================================================================
|
|
133
|
-
// CORE METHODS (HTTP Client, Token Management, Error Handling)
|
|
134
|
-
// ============================================================================
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Get the configured Oxy API base URL
|
|
138
|
-
*/
|
|
139
|
-
getBaseURL() {
|
|
140
|
-
return this.httpClient.getBaseURL();
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Get the HTTP client instance
|
|
145
|
-
* Useful for advanced use cases where direct access to the HTTP client is needed
|
|
146
|
-
*/
|
|
147
|
-
getClient() {
|
|
148
|
-
return this.httpClient;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Get performance metrics
|
|
153
|
-
*/
|
|
154
|
-
getMetrics() {
|
|
155
|
-
return this.requestManager.getMetrics();
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Clear request cache
|
|
160
|
-
*/
|
|
161
|
-
clearCache() {
|
|
162
|
-
this.requestManager.clearCache();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Clear specific cache entry
|
|
167
|
-
*/
|
|
168
|
-
clearCacheEntry(key) {
|
|
169
|
-
this.requestManager.clearCacheEntry(key);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Get cache statistics
|
|
174
|
-
*/
|
|
175
|
-
getCacheStats() {
|
|
176
|
-
return this.requestManager.getCacheStats();
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Get the configured Oxy Cloud (file storage/CDN) URL
|
|
181
|
-
*/
|
|
182
|
-
getCloudURL() {
|
|
183
|
-
return this.cloudURL;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Set authentication tokens
|
|
188
|
-
*/
|
|
189
|
-
setTokens(accessToken, refreshToken = '') {
|
|
190
|
-
this.httpClient.setTokens(accessToken, refreshToken);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Clear stored authentication tokens
|
|
195
|
-
*/
|
|
196
|
-
clearTokens() {
|
|
197
|
-
this.httpClient.clearTokens();
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Get the current user ID from the access token
|
|
202
|
-
*/
|
|
203
|
-
getCurrentUserId() {
|
|
204
|
-
const accessToken = this.httpClient.getAccessToken();
|
|
205
|
-
if (!accessToken) {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
try {
|
|
209
|
-
const decoded = jwtDecode(accessToken);
|
|
210
|
-
return decoded.userId || decoded.id || null;
|
|
211
|
-
} catch (error) {
|
|
212
|
-
return null;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Check if the client has a valid access token
|
|
218
|
-
*/
|
|
219
|
-
hasAccessToken() {
|
|
220
|
-
return this.httpClient.hasAccessToken();
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Check if the client has a valid access token (public method)
|
|
225
|
-
*/
|
|
226
|
-
hasValidToken() {
|
|
227
|
-
return this.httpClient.hasAccessToken();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Get the raw access token (for constructing anchor URLs when needed)
|
|
232
|
-
*/
|
|
233
|
-
getAccessToken() {
|
|
234
|
-
return this.httpClient.getAccessToken();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Wait for authentication to be ready (public method)
|
|
239
|
-
* Useful for apps that want to ensure authentication is complete before proceeding
|
|
240
|
-
*/
|
|
241
|
-
async waitForAuth(timeoutMs = 5000) {
|
|
242
|
-
return this.waitForAuthentication(timeoutMs);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Wait for authentication to be ready with timeout
|
|
247
|
-
*/
|
|
248
|
-
async waitForAuthentication(timeoutMs = 5000) {
|
|
249
|
-
const startTime = Date.now();
|
|
250
|
-
const checkInterval = 100; // Check every 100ms
|
|
251
|
-
|
|
252
|
-
while (Date.now() - startTime < timeoutMs) {
|
|
253
|
-
if (this.httpClient.hasAccessToken()) {
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
|
257
|
-
}
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* Execute a function with automatic authentication retry logic
|
|
263
|
-
* This handles the common case where API calls are made before authentication completes
|
|
264
|
-
*/
|
|
265
|
-
async withAuthRetry(operation, operationName, options = {}) {
|
|
266
|
-
const {
|
|
267
|
-
maxRetries = 2,
|
|
268
|
-
retryDelay = 1000,
|
|
269
|
-
authTimeoutMs = 5000
|
|
270
|
-
} = options;
|
|
271
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
272
|
-
try {
|
|
273
|
-
// First attempt: check if we have a token
|
|
274
|
-
if (!this.httpClient.hasAccessToken()) {
|
|
275
|
-
if (attempt === 0) {
|
|
276
|
-
// On first attempt, wait briefly for authentication to complete
|
|
277
|
-
const authReady = await this.waitForAuthentication(authTimeoutMs);
|
|
278
|
-
if (!authReady) {
|
|
279
|
-
throw new OxyAuthenticationTimeoutError(operationName, authTimeoutMs);
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
// On retry attempts, fail immediately if no token
|
|
283
|
-
throw new OxyAuthenticationError(`Authentication required: ${operationName} requires a valid access token.`, 'AUTH_REQUIRED');
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// Execute the operation
|
|
288
|
-
return await operation();
|
|
289
|
-
} catch (error) {
|
|
290
|
-
const isLastAttempt = attempt === maxRetries;
|
|
291
|
-
const isAuthError = error?.response?.status === 401 || error?.code === 'MISSING_TOKEN' || error?.message?.includes('Authentication') || error instanceof OxyAuthenticationError;
|
|
292
|
-
if (isAuthError && !isLastAttempt && !(error instanceof OxyAuthenticationTimeoutError)) {
|
|
293
|
-
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// If it's not an auth error, or it's the last attempt, throw the error
|
|
298
|
-
if (error instanceof OxyAuthenticationError) {
|
|
299
|
-
throw error;
|
|
300
|
-
}
|
|
301
|
-
throw this.handleError(error);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
// This should never be reached, but TypeScript requires it
|
|
306
|
-
throw new OxyAuthenticationError(`${operationName} failed after ${maxRetries + 1} attempts`);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
/**
|
|
310
|
-
* Validate the current access token with the server
|
|
311
|
-
*/
|
|
312
|
-
async validate() {
|
|
313
|
-
if (!this.hasAccessToken()) {
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
try {
|
|
317
|
-
const res = await this.makeRequest('GET', '/api/auth/validate', undefined, {
|
|
318
|
-
cache: false,
|
|
319
|
-
retry: false
|
|
320
|
-
});
|
|
321
|
-
return res.valid === true;
|
|
322
|
-
} catch (error) {
|
|
323
|
-
return false;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Centralized error handling
|
|
329
|
-
*/
|
|
330
|
-
handleError(error) {
|
|
331
|
-
const api = handleHttpError(error);
|
|
332
|
-
const err = new Error(api.message);
|
|
333
|
-
err.code = api.code;
|
|
334
|
-
err.status = api.status;
|
|
335
|
-
err.details = api.details;
|
|
336
|
-
return err;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Health check endpoint
|
|
341
|
-
*/
|
|
342
|
-
async healthCheck() {
|
|
343
|
-
try {
|
|
344
|
-
return await this.makeRequest('GET', '/health', undefined, {
|
|
345
|
-
cache: false
|
|
346
|
-
});
|
|
347
|
-
} catch (error) {
|
|
348
|
-
throw this.handleError(error);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ============================================================================
|
|
353
|
-
// AUTHENTICATION METHODS
|
|
354
|
-
// ============================================================================
|
|
355
|
-
|
|
356
|
-
/**
|
|
357
|
-
* Sign up a new user
|
|
358
|
-
*/
|
|
359
|
-
async signUp(username, email, password) {
|
|
360
|
-
try {
|
|
361
|
-
const res = await this.makeRequest('POST', '/api/auth/signup', {
|
|
362
|
-
username,
|
|
363
|
-
email,
|
|
364
|
-
password
|
|
365
|
-
}, {
|
|
366
|
-
cache: false
|
|
367
|
-
});
|
|
368
|
-
if (!res || typeof res === 'object' && Object.keys(res).length === 0) {
|
|
369
|
-
throw new OxyAuthenticationError('Sign up failed', 'SIGNUP_FAILED', 400);
|
|
370
|
-
}
|
|
371
|
-
return res;
|
|
372
|
-
} catch (error) {
|
|
373
|
-
throw this.handleError(error);
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Request account recovery (send verification code)
|
|
379
|
-
*/
|
|
380
|
-
async requestRecovery(identifier) {
|
|
381
|
-
try {
|
|
382
|
-
return await this.makeRequest('POST', '/api/auth/recover/request', {
|
|
383
|
-
identifier
|
|
384
|
-
}, {
|
|
385
|
-
cache: false
|
|
386
|
-
});
|
|
387
|
-
} catch (error) {
|
|
388
|
-
throw this.handleError(error);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/**
|
|
393
|
-
* Verify recovery code
|
|
394
|
-
*/
|
|
395
|
-
async verifyRecoveryCode(identifier, code) {
|
|
396
|
-
try {
|
|
397
|
-
return await this.makeRequest('POST', '/api/auth/recover/verify', {
|
|
398
|
-
identifier,
|
|
399
|
-
code
|
|
400
|
-
}, {
|
|
401
|
-
cache: false
|
|
402
|
-
});
|
|
403
|
-
} catch (error) {
|
|
404
|
-
throw this.handleError(error);
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
/**
|
|
409
|
-
* Reset password using verified code
|
|
410
|
-
*/
|
|
411
|
-
async resetPassword(identifier, code, newPassword) {
|
|
412
|
-
try {
|
|
413
|
-
return await this.makeRequest('POST', '/api/auth/recover/reset', {
|
|
414
|
-
identifier,
|
|
415
|
-
code,
|
|
416
|
-
newPassword
|
|
417
|
-
}, {
|
|
418
|
-
cache: false
|
|
419
|
-
});
|
|
420
|
-
} catch (error) {
|
|
421
|
-
throw this.handleError(error);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
/**
|
|
426
|
-
* Reset password using TOTP code (recommended recovery)
|
|
427
|
-
*/
|
|
428
|
-
async resetPasswordWithTotp(identifier, code, newPassword) {
|
|
429
|
-
try {
|
|
430
|
-
return await this.makeRequest('POST', '/api/auth/recover/totp/reset', {
|
|
431
|
-
identifier,
|
|
432
|
-
code,
|
|
433
|
-
newPassword
|
|
434
|
-
}, {
|
|
435
|
-
cache: false
|
|
436
|
-
});
|
|
437
|
-
} catch (error) {
|
|
438
|
-
throw this.handleError(error);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
async resetPasswordWithBackupCode(identifier, backupCode, newPassword) {
|
|
442
|
-
try {
|
|
443
|
-
return await this.makeRequest('POST', '/api/auth/recover/backup/reset', {
|
|
444
|
-
identifier,
|
|
445
|
-
backupCode,
|
|
446
|
-
newPassword
|
|
447
|
-
}, {
|
|
448
|
-
cache: false
|
|
449
|
-
});
|
|
450
|
-
} catch (error) {
|
|
451
|
-
throw this.handleError(error);
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
async resetPasswordWithRecoveryKey(identifier, recoveryKey, newPassword) {
|
|
455
|
-
try {
|
|
456
|
-
return await this.makeRequest('POST', '/api/auth/recover/recovery-key/reset', {
|
|
457
|
-
identifier,
|
|
458
|
-
recoveryKey,
|
|
459
|
-
newPassword
|
|
460
|
-
}, {
|
|
461
|
-
cache: false
|
|
462
|
-
});
|
|
463
|
-
} catch (error) {
|
|
464
|
-
throw this.handleError(error);
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Sign in with device management
|
|
470
|
-
*/
|
|
471
|
-
async signIn(username, password, deviceName, deviceFingerprint) {
|
|
472
|
-
try {
|
|
473
|
-
return await this.makeRequest('POST', '/api/auth/login', {
|
|
474
|
-
username,
|
|
475
|
-
password,
|
|
476
|
-
deviceName,
|
|
477
|
-
deviceFingerprint
|
|
478
|
-
}, {
|
|
479
|
-
cache: false
|
|
480
|
-
});
|
|
481
|
-
} catch (error) {
|
|
482
|
-
throw this.handleError(error);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Complete login by verifying TOTP with MFA token
|
|
488
|
-
*/
|
|
489
|
-
async verifyTotpLogin(mfaToken, code) {
|
|
490
|
-
try {
|
|
491
|
-
return await this.makeRequest('POST', '/api/auth/totp/verify-login', {
|
|
492
|
-
mfaToken,
|
|
493
|
-
code
|
|
494
|
-
}, {
|
|
495
|
-
cache: false
|
|
496
|
-
});
|
|
497
|
-
} catch (error) {
|
|
498
|
-
throw this.handleError(error);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Get user by session ID
|
|
504
|
-
*/
|
|
505
|
-
async getUserBySession(sessionId) {
|
|
506
|
-
try {
|
|
507
|
-
return await this.makeRequest('GET', `/api/session/user/${sessionId}`, undefined, {
|
|
508
|
-
cache: true,
|
|
509
|
-
cacheTTL: 2 * 60 * 1000 // 2 minutes cache for user data
|
|
510
|
-
});
|
|
511
|
-
} catch (error) {
|
|
512
|
-
throw this.handleError(error);
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/**
|
|
517
|
-
* Batch get multiple user profiles by session IDs (optimized for account switching)
|
|
518
|
-
* Returns array of { sessionId, user } objects
|
|
519
|
-
*/
|
|
520
|
-
async getUsersBySessions(sessionIds) {
|
|
521
|
-
try {
|
|
522
|
-
if (!Array.isArray(sessionIds) || sessionIds.length === 0) {
|
|
523
|
-
return [];
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
// Deduplicate and sort sessionIds for consistent cache keys
|
|
527
|
-
const uniqueSessionIds = Array.from(new Set(sessionIds)).sort();
|
|
528
|
-
return await this.makeRequest('POST', '/api/session/users/batch', {
|
|
529
|
-
sessionIds: uniqueSessionIds
|
|
530
|
-
}, {
|
|
531
|
-
cache: true,
|
|
532
|
-
cacheTTL: 2 * 60 * 1000,
|
|
533
|
-
// 2 minutes cache
|
|
534
|
-
deduplicate: true // Important for batch requests
|
|
535
|
-
});
|
|
536
|
-
} catch (error) {
|
|
537
|
-
throw this.handleError(error);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Get access token by session ID and set it in the token store
|
|
543
|
-
*/
|
|
544
|
-
async getTokenBySession(sessionId) {
|
|
545
|
-
try {
|
|
546
|
-
const res = await this.makeRequest('GET', `/api/session/token/${sessionId}`, undefined, {
|
|
547
|
-
cache: false,
|
|
548
|
-
retry: false
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
// Set the token in the centralized token store
|
|
552
|
-
this.setTokens(res.accessToken);
|
|
553
|
-
return res;
|
|
554
|
-
} catch (error) {
|
|
555
|
-
throw this.handleError(error);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* Get sessions by session ID
|
|
561
|
-
*/
|
|
562
|
-
async getSessionsBySessionId(sessionId) {
|
|
563
|
-
try {
|
|
564
|
-
return await this.makeRequest('GET', `/api/session/sessions/${sessionId}`, undefined, {
|
|
565
|
-
cache: false
|
|
566
|
-
});
|
|
567
|
-
} catch (error) {
|
|
568
|
-
throw this.handleError(error);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
/**
|
|
573
|
-
* Logout from a specific session
|
|
574
|
-
*/
|
|
575
|
-
async logoutSession(sessionId, targetSessionId) {
|
|
576
|
-
try {
|
|
577
|
-
const url = targetSessionId ? `/api/session/logout/${sessionId}/${targetSessionId}` : `/api/session/logout/${sessionId}`;
|
|
578
|
-
await this.makeRequest('POST', url, undefined, {
|
|
579
|
-
cache: false
|
|
580
|
-
});
|
|
581
|
-
} catch (error) {
|
|
582
|
-
throw this.handleError(error);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Logout from all sessions
|
|
588
|
-
*/
|
|
589
|
-
async logoutAllSessions(sessionId) {
|
|
590
|
-
try {
|
|
591
|
-
await this.makeRequest('POST', `/api/session/logout-all/${sessionId}`, undefined, {
|
|
592
|
-
cache: false
|
|
593
|
-
});
|
|
594
|
-
} catch (error) {
|
|
595
|
-
throw this.handleError(error);
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
/**
|
|
600
|
-
* Validate session
|
|
601
|
-
*/
|
|
602
|
-
async validateSession(sessionId, options = {}) {
|
|
603
|
-
try {
|
|
604
|
-
const params = new URLSearchParams();
|
|
605
|
-
if (options.deviceFingerprint) {
|
|
606
|
-
params.append('deviceFingerprint', options.deviceFingerprint);
|
|
607
|
-
}
|
|
608
|
-
if (options.useHeaderValidation) {
|
|
609
|
-
params.append('useHeaderValidation', 'true');
|
|
610
|
-
}
|
|
611
|
-
const url = `/api/session/validate/${sessionId}`;
|
|
612
|
-
const urlParams = {};
|
|
613
|
-
if (options.deviceFingerprint) urlParams.deviceFingerprint = options.deviceFingerprint;
|
|
614
|
-
if (options.useHeaderValidation) urlParams.useHeaderValidation = 'true';
|
|
615
|
-
return await this.makeRequest('GET', url, urlParams, {
|
|
616
|
-
cache: false
|
|
617
|
-
});
|
|
618
|
-
} catch (error) {
|
|
619
|
-
throw this.handleError(error);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Check username availability
|
|
625
|
-
*/
|
|
626
|
-
async checkUsernameAvailability(username) {
|
|
627
|
-
try {
|
|
628
|
-
return await this.makeRequest('GET', `/api/auth/check-username/${username}`, undefined, {
|
|
629
|
-
cache: false
|
|
630
|
-
});
|
|
631
|
-
} catch (error) {
|
|
632
|
-
throw this.handleError(error);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
/**
|
|
637
|
-
* Check email availability
|
|
638
|
-
*/
|
|
639
|
-
async checkEmailAvailability(email) {
|
|
640
|
-
try {
|
|
641
|
-
return await this.makeRequest('GET', `/api/auth/check-email/${email}`, undefined, {
|
|
642
|
-
cache: false
|
|
643
|
-
});
|
|
644
|
-
} catch (error) {
|
|
645
|
-
throw this.handleError(error);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// ============================================================================
|
|
650
|
-
// USER METHODS
|
|
651
|
-
// ============================================================================
|
|
652
|
-
|
|
653
|
-
/**
|
|
654
|
-
* Get profile by username
|
|
655
|
-
*/
|
|
656
|
-
async getProfileByUsername(username) {
|
|
657
|
-
try {
|
|
658
|
-
return await this.makeRequest('GET', `/api/profiles/username/${username}`, undefined, {
|
|
659
|
-
cache: true,
|
|
660
|
-
cacheTTL: 5 * 60 * 1000 // 5 minutes cache for profiles
|
|
661
|
-
});
|
|
662
|
-
} catch (error) {
|
|
663
|
-
throw this.handleError(error);
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// ============================================================================
|
|
668
|
-
// TOTP ENROLLMENT
|
|
669
|
-
// ============================================================================
|
|
670
|
-
|
|
671
|
-
async startTotpEnrollment(sessionId) {
|
|
672
|
-
try {
|
|
673
|
-
// Note: x-session-id header is handled by HttpClient interceptors if needed
|
|
674
|
-
return await this.makeRequest('POST', '/api/auth/totp/enroll/start', {
|
|
675
|
-
sessionId
|
|
676
|
-
}, {
|
|
677
|
-
cache: false
|
|
678
|
-
});
|
|
679
|
-
} catch (error) {
|
|
680
|
-
throw this.handleError(error);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
async verifyTotpEnrollment(sessionId, code) {
|
|
684
|
-
try {
|
|
685
|
-
return await this.makeRequest('POST', '/api/auth/totp/enroll/verify', {
|
|
686
|
-
sessionId,
|
|
687
|
-
code
|
|
688
|
-
}, {
|
|
689
|
-
cache: false
|
|
690
|
-
});
|
|
691
|
-
} catch (error) {
|
|
692
|
-
throw this.handleError(error);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
async disableTotp(sessionId, code) {
|
|
696
|
-
try {
|
|
697
|
-
return await this.makeRequest('POST', '/api/auth/totp/disable', {
|
|
698
|
-
sessionId,
|
|
699
|
-
code
|
|
700
|
-
}, {
|
|
701
|
-
cache: false
|
|
702
|
-
});
|
|
703
|
-
} catch (error) {
|
|
704
|
-
throw this.handleError(error);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
/**
|
|
709
|
-
* Search user profiles
|
|
710
|
-
*/
|
|
711
|
-
async searchProfiles(query, pagination) {
|
|
712
|
-
try {
|
|
713
|
-
const params = {
|
|
714
|
-
query,
|
|
715
|
-
...pagination
|
|
716
|
-
};
|
|
717
|
-
const searchParams = buildSearchParams(params);
|
|
718
|
-
const paramsObj = {};
|
|
719
|
-
searchParams.forEach((value, key) => {
|
|
720
|
-
paramsObj[key] = value;
|
|
721
|
-
});
|
|
722
|
-
return await this.makeRequest('GET', '/api/profiles/search', paramsObj, {
|
|
723
|
-
cache: true,
|
|
724
|
-
cacheTTL: 2 * 60 * 1000 // 2 minutes cache
|
|
725
|
-
});
|
|
726
|
-
} catch (error) {
|
|
727
|
-
throw this.handleError(error);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
/**
|
|
732
|
-
* Get profile recommendations
|
|
733
|
-
*/
|
|
734
|
-
async getProfileRecommendations() {
|
|
735
|
-
return this.withAuthRetry(async () => {
|
|
736
|
-
return await this.makeRequest('GET', '/api/profiles/recommendations', undefined, {
|
|
737
|
-
cache: true
|
|
738
|
-
});
|
|
739
|
-
}, 'getProfileRecommendations');
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
/**
|
|
743
|
-
* Get user by ID
|
|
744
|
-
*/
|
|
745
|
-
async getUserById(userId) {
|
|
746
|
-
try {
|
|
747
|
-
return await this.makeRequest('GET', `/api/users/${userId}`, undefined, {
|
|
748
|
-
cache: true,
|
|
749
|
-
cacheTTL: 5 * 60 * 1000 // 5 minutes cache
|
|
750
|
-
});
|
|
751
|
-
} catch (error) {
|
|
752
|
-
throw this.handleError(error);
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/**
|
|
757
|
-
* Get current user
|
|
758
|
-
*/
|
|
759
|
-
async getCurrentUser() {
|
|
760
|
-
return this.withAuthRetry(async () => {
|
|
761
|
-
return await this.makeRequest('GET', '/api/users/me', undefined, {
|
|
762
|
-
cache: true,
|
|
763
|
-
cacheTTL: 1 * 60 * 1000 // 1 minute cache for current user
|
|
764
|
-
});
|
|
765
|
-
}, 'getCurrentUser');
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
/**
|
|
769
|
-
* Update user profile
|
|
770
|
-
*/
|
|
771
|
-
async updateProfile(updates) {
|
|
772
|
-
try {
|
|
773
|
-
return await this.makeRequest('PUT', '/api/users/me', updates, {
|
|
774
|
-
cache: false
|
|
775
|
-
});
|
|
776
|
-
} catch (error) {
|
|
777
|
-
throw this.handleError(error);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
/**
|
|
782
|
-
* Get privacy settings for a user
|
|
783
|
-
* @param userId - The user ID (defaults to current user)
|
|
784
|
-
*/
|
|
785
|
-
async getPrivacySettings(userId) {
|
|
786
|
-
try {
|
|
787
|
-
const id = userId || (await this.getCurrentUser()).id;
|
|
788
|
-
return await this.makeRequest('GET', `/api/privacy/${id}/privacy`, undefined, {
|
|
789
|
-
cache: true,
|
|
790
|
-
cacheTTL: 2 * 60 * 1000 // 2 minutes cache
|
|
791
|
-
});
|
|
792
|
-
} catch (error) {
|
|
793
|
-
throw this.handleError(error);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
/**
|
|
798
|
-
* Update privacy settings
|
|
799
|
-
* @param settings - Partial privacy settings object
|
|
800
|
-
* @param userId - The user ID (defaults to current user)
|
|
801
|
-
*/
|
|
802
|
-
async updatePrivacySettings(settings, userId) {
|
|
803
|
-
try {
|
|
804
|
-
const id = userId || (await this.getCurrentUser()).id;
|
|
805
|
-
return await this.makeRequest('PATCH', `/api/privacy/${id}/privacy`, settings, {
|
|
806
|
-
cache: false
|
|
807
|
-
});
|
|
808
|
-
} catch (error) {
|
|
809
|
-
throw this.handleError(error);
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
// ============================================================================
|
|
814
|
-
// BLOCKED USERS METHODS
|
|
815
|
-
// ============================================================================
|
|
816
|
-
|
|
817
|
-
/**
|
|
818
|
-
* Get list of blocked users
|
|
819
|
-
* @returns Array of blocked users
|
|
820
|
-
*/
|
|
821
|
-
async getBlockedUsers() {
|
|
822
|
-
try {
|
|
823
|
-
return await this.makeRequest('GET', '/api/privacy/blocked', undefined, {
|
|
824
|
-
cache: true,
|
|
825
|
-
cacheTTL: 1 * 60 * 1000 // 1 minute cache
|
|
826
|
-
});
|
|
827
|
-
} catch (error) {
|
|
828
|
-
throw this.handleError(error);
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Block a user
|
|
834
|
-
* @param userId - The user ID to block
|
|
835
|
-
* @returns Success message
|
|
836
|
-
*/
|
|
837
|
-
async blockUser(userId) {
|
|
838
|
-
try {
|
|
839
|
-
if (!userId) {
|
|
840
|
-
throw new Error('User ID is required');
|
|
841
|
-
}
|
|
842
|
-
return await this.makeRequest('POST', `/api/privacy/blocked/${userId}`, undefined, {
|
|
843
|
-
cache: false
|
|
844
|
-
});
|
|
845
|
-
} catch (error) {
|
|
846
|
-
throw this.handleError(error);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
/**
|
|
851
|
-
* Unblock a user
|
|
852
|
-
* @param userId - The user ID to unblock
|
|
853
|
-
* @returns Success message
|
|
854
|
-
*/
|
|
855
|
-
async unblockUser(userId) {
|
|
856
|
-
try {
|
|
857
|
-
if (!userId) {
|
|
858
|
-
throw new Error('User ID is required');
|
|
859
|
-
}
|
|
860
|
-
return await this.makeRequest('DELETE', `/api/privacy/blocked/${userId}`, undefined, {
|
|
861
|
-
cache: false
|
|
862
|
-
});
|
|
863
|
-
} catch (error) {
|
|
864
|
-
throw this.handleError(error);
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
/**
|
|
869
|
-
* Extract user ID from blocked/restricted user object
|
|
870
|
-
* @private
|
|
871
|
-
*/
|
|
872
|
-
extractUserId(userIdField) {
|
|
873
|
-
return typeof userIdField === 'string' ? userIdField : userIdField._id;
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Check if a user is in a list (blocked or restricted)
|
|
878
|
-
* @private
|
|
879
|
-
*/
|
|
880
|
-
async isUserInList(userId, getUserList, getIdField) {
|
|
881
|
-
try {
|
|
882
|
-
if (!userId) {
|
|
883
|
-
return false;
|
|
884
|
-
}
|
|
885
|
-
const users = await getUserList();
|
|
886
|
-
return users.some(item => {
|
|
887
|
-
const itemId = this.extractUserId(getIdField(item));
|
|
888
|
-
return itemId === userId;
|
|
889
|
-
});
|
|
890
|
-
} catch (error) {
|
|
891
|
-
// If there's an error, assume not in list to avoid breaking functionality
|
|
892
|
-
if (__DEV__) {
|
|
893
|
-
console.warn('Error checking user list:', error);
|
|
894
|
-
}
|
|
895
|
-
return false;
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
/**
|
|
900
|
-
* Check if a user is blocked
|
|
901
|
-
* @param userId - The user ID to check
|
|
902
|
-
* @returns True if the user is blocked, false otherwise
|
|
903
|
-
*/
|
|
904
|
-
async isUserBlocked(userId) {
|
|
905
|
-
return this.isUserInList(userId, () => this.getBlockedUsers(), block => block.blockedId);
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
// ============================================================================
|
|
909
|
-
// RESTRICTED USERS METHODS
|
|
910
|
-
// ============================================================================
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* Get list of restricted users
|
|
914
|
-
* @returns Array of restricted users
|
|
915
|
-
*/
|
|
916
|
-
async getRestrictedUsers() {
|
|
917
|
-
try {
|
|
918
|
-
return await this.makeRequest('GET', '/api/privacy/restricted', undefined, {
|
|
919
|
-
cache: true,
|
|
920
|
-
cacheTTL: 1 * 60 * 1000 // 1 minute cache
|
|
921
|
-
});
|
|
922
|
-
} catch (error) {
|
|
923
|
-
throw this.handleError(error);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
/**
|
|
928
|
-
* Restrict a user (limit their interactions without fully blocking)
|
|
929
|
-
* @param userId - The user ID to restrict
|
|
930
|
-
* @returns Success message
|
|
931
|
-
*/
|
|
932
|
-
async restrictUser(userId) {
|
|
933
|
-
try {
|
|
934
|
-
if (!userId) {
|
|
935
|
-
throw new Error('User ID is required');
|
|
936
|
-
}
|
|
937
|
-
return await this.makeRequest('POST', `/api/privacy/restricted/${userId}`, undefined, {
|
|
938
|
-
cache: false
|
|
939
|
-
});
|
|
940
|
-
} catch (error) {
|
|
941
|
-
throw this.handleError(error);
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
/**
|
|
946
|
-
* Unrestrict a user
|
|
947
|
-
* @param userId - The user ID to unrestrict
|
|
948
|
-
* @returns Success message
|
|
949
|
-
*/
|
|
950
|
-
async unrestrictUser(userId) {
|
|
951
|
-
try {
|
|
952
|
-
if (!userId) {
|
|
953
|
-
throw new Error('User ID is required');
|
|
954
|
-
}
|
|
955
|
-
return await this.makeRequest('DELETE', `/api/privacy/restricted/${userId}`, undefined, {
|
|
956
|
-
cache: false
|
|
957
|
-
});
|
|
958
|
-
} catch (error) {
|
|
959
|
-
throw this.handleError(error);
|
|
960
|
-
}
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
/**
|
|
964
|
-
* Check if a user is restricted
|
|
965
|
-
* @param userId - The user ID to check
|
|
966
|
-
* @returns True if the user is restricted, false otherwise
|
|
967
|
-
*/
|
|
968
|
-
async isUserRestricted(userId) {
|
|
969
|
-
return this.isUserInList(userId, () => this.getRestrictedUsers(), restrict => restrict.restrictedId);
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
/**
|
|
973
|
-
* Request account verification
|
|
974
|
-
*/
|
|
975
|
-
async requestAccountVerification(reason, evidence) {
|
|
976
|
-
try {
|
|
977
|
-
return await this.makeRequest('POST', '/api/users/verify/request', {
|
|
978
|
-
reason,
|
|
979
|
-
evidence
|
|
980
|
-
}, {
|
|
981
|
-
cache: false
|
|
982
|
-
});
|
|
983
|
-
} catch (error) {
|
|
984
|
-
throw this.handleError(error);
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
/**
|
|
989
|
-
* Download account data export
|
|
990
|
-
*/
|
|
991
|
-
async downloadAccountData(format = 'json') {
|
|
992
|
-
try {
|
|
993
|
-
// Use axios instance directly for blob responses since RequestManager doesn't handle blobs
|
|
994
|
-
const axiosInstance = this.httpClient.getAxiosInstance();
|
|
995
|
-
const response = await axiosInstance.get(`/api/users/me/data?format=${format}`, {
|
|
996
|
-
responseType: 'blob'
|
|
997
|
-
});
|
|
998
|
-
return response.data;
|
|
999
|
-
} catch (error) {
|
|
1000
|
-
throw this.handleError(error);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
/**
|
|
1005
|
-
* Delete account permanently
|
|
1006
|
-
* @param password - User password for confirmation
|
|
1007
|
-
* @param confirmText - Confirmation text (usually username)
|
|
1008
|
-
*/
|
|
1009
|
-
async deleteAccount(password, confirmText) {
|
|
1010
|
-
try {
|
|
1011
|
-
return await this.makeRequest('DELETE', '/api/users/me', {
|
|
1012
|
-
password,
|
|
1013
|
-
confirmText
|
|
1014
|
-
}, {
|
|
1015
|
-
cache: false
|
|
1016
|
-
});
|
|
1017
|
-
} catch (error) {
|
|
1018
|
-
throw this.handleError(error);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// ============================================================================
|
|
1023
|
-
// LANGUAGE METHODS
|
|
1024
|
-
// ============================================================================
|
|
102
|
+
// Compose all mixins into the final OxyServices class
|
|
103
|
+
const OxyServicesComposed = composeOxyServices();
|
|
1025
104
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
*/
|
|
1031
|
-
async getCurrentLanguage(storageKeyPrefix = 'oxy_session') {
|
|
1032
|
-
try {
|
|
1033
|
-
// First try to get from user profile if authenticated
|
|
1034
|
-
try {
|
|
1035
|
-
const user = await this.getCurrentUser();
|
|
1036
|
-
const userLanguage = user?.language;
|
|
1037
|
-
if (userLanguage) {
|
|
1038
|
-
return normalizeLanguageCode(userLanguage) || userLanguage;
|
|
1039
|
-
}
|
|
1040
|
-
} catch (e) {
|
|
1041
|
-
// User not authenticated or error, continue to storage
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
// Fall back to storage
|
|
1045
|
-
const storage = await this.getStorage();
|
|
1046
|
-
const storageKey = `${storageKeyPrefix}_language`;
|
|
1047
|
-
const storedLanguage = await storage.getItem(storageKey);
|
|
1048
|
-
if (storedLanguage) {
|
|
1049
|
-
return normalizeLanguageCode(storedLanguage) || storedLanguage;
|
|
1050
|
-
}
|
|
1051
|
-
return null;
|
|
1052
|
-
} catch (error) {
|
|
1053
|
-
if (__DEV__) {
|
|
1054
|
-
console.warn('Failed to get current language:', error);
|
|
1055
|
-
}
|
|
1056
|
-
return null;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
/**
|
|
1061
|
-
* Get the current language with metadata (name, nativeName, etc.)
|
|
1062
|
-
* @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
|
|
1063
|
-
* @returns Language metadata object or null if not set
|
|
1064
|
-
*/
|
|
1065
|
-
async getCurrentLanguageMetadata(storageKeyPrefix = 'oxy_session') {
|
|
1066
|
-
const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
|
|
1067
|
-
return getLanguageMetadata(languageCode);
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
/**
|
|
1071
|
-
* Get the current language name (e.g., 'English')
|
|
1072
|
-
* @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
|
|
1073
|
-
* @returns Language name or null if not set
|
|
1074
|
-
*/
|
|
1075
|
-
async getCurrentLanguageName(storageKeyPrefix = 'oxy_session') {
|
|
1076
|
-
const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
|
|
1077
|
-
if (!languageCode) return null;
|
|
1078
|
-
return getLanguageName(languageCode);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
/**
|
|
1082
|
-
* Get the current native language name (e.g., 'Español')
|
|
1083
|
-
* @param storageKeyPrefix - Optional prefix for storage key (default: 'oxy_session')
|
|
1084
|
-
* @returns Native language name or null if not set
|
|
1085
|
-
*/
|
|
1086
|
-
async getCurrentNativeLanguageName(storageKeyPrefix = 'oxy_session') {
|
|
1087
|
-
const languageCode = await this.getCurrentLanguage(storageKeyPrefix);
|
|
1088
|
-
if (!languageCode) return null;
|
|
1089
|
-
return getNativeLanguageName(languageCode);
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
/**
|
|
1093
|
-
* Get appropriate storage for the platform (similar to DeviceManager)
|
|
1094
|
-
* @private
|
|
1095
|
-
*/
|
|
1096
|
-
async getStorage() {
|
|
1097
|
-
const isReactNative = typeof navigator !== 'undefined' && navigator.product === 'ReactNative';
|
|
1098
|
-
if (isReactNative) {
|
|
1099
|
-
try {
|
|
1100
|
-
const asyncStorageModule = await import('@react-native-async-storage/async-storage');
|
|
1101
|
-
const storage = asyncStorageModule.default;
|
|
1102
|
-
return {
|
|
1103
|
-
getItem: storage.getItem.bind(storage),
|
|
1104
|
-
setItem: storage.setItem.bind(storage),
|
|
1105
|
-
removeItem: storage.removeItem.bind(storage)
|
|
1106
|
-
};
|
|
1107
|
-
} catch (error) {
|
|
1108
|
-
console.error('AsyncStorage not available in React Native:', error);
|
|
1109
|
-
throw new Error('AsyncStorage is required in React Native environment');
|
|
1110
|
-
}
|
|
1111
|
-
} else {
|
|
1112
|
-
// Use localStorage for web
|
|
1113
|
-
return {
|
|
1114
|
-
getItem: async key => {
|
|
1115
|
-
if (typeof window !== 'undefined' && window.localStorage) {
|
|
1116
|
-
return localStorage.getItem(key);
|
|
1117
|
-
}
|
|
1118
|
-
return null;
|
|
1119
|
-
},
|
|
1120
|
-
setItem: async (key, value) => {
|
|
1121
|
-
if (typeof window !== 'undefined' && window.localStorage) {
|
|
1122
|
-
localStorage.setItem(key, value);
|
|
1123
|
-
}
|
|
1124
|
-
},
|
|
1125
|
-
removeItem: async key => {
|
|
1126
|
-
if (typeof window !== 'undefined' && window.localStorage) {
|
|
1127
|
-
localStorage.removeItem(key);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
};
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
/**
|
|
1135
|
-
* Update user by ID (admin function)
|
|
1136
|
-
*/
|
|
1137
|
-
async updateUser(userId, updates) {
|
|
1138
|
-
try {
|
|
1139
|
-
return await this.makeRequest('PUT', `/api/users/${userId}`, updates, {
|
|
1140
|
-
cache: false
|
|
1141
|
-
});
|
|
1142
|
-
} catch (error) {
|
|
1143
|
-
throw this.handleError(error);
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
/**
|
|
1148
|
-
* Follow a user
|
|
1149
|
-
*/
|
|
1150
|
-
async followUser(userId) {
|
|
1151
|
-
try {
|
|
1152
|
-
return await this.makeRequest('POST', `/api/users/${userId}/follow`, undefined, {
|
|
1153
|
-
cache: false
|
|
1154
|
-
});
|
|
1155
|
-
} catch (error) {
|
|
1156
|
-
throw this.handleError(error);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
/**
|
|
1161
|
-
* Unfollow a user
|
|
1162
|
-
*/
|
|
1163
|
-
async unfollowUser(userId) {
|
|
1164
|
-
try {
|
|
1165
|
-
return await this.makeRequest('DELETE', `/api/users/${userId}/follow`, undefined, {
|
|
1166
|
-
cache: false
|
|
1167
|
-
});
|
|
1168
|
-
} catch (error) {
|
|
1169
|
-
throw this.handleError(error);
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
/**
|
|
1174
|
-
* Get follow status
|
|
1175
|
-
*/
|
|
1176
|
-
async getFollowStatus(userId) {
|
|
1177
|
-
try {
|
|
1178
|
-
return await this.makeRequest('GET', `/api/users/${userId}/follow-status`, undefined, {
|
|
1179
|
-
cache: true,
|
|
1180
|
-
cacheTTL: 1 * 60 * 1000 // 1 minute cache
|
|
1181
|
-
});
|
|
1182
|
-
} catch (error) {
|
|
1183
|
-
throw this.handleError(error);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
/**
|
|
1188
|
-
* Get user followers
|
|
1189
|
-
*/
|
|
1190
|
-
async getUserFollowers(userId, pagination) {
|
|
1191
|
-
try {
|
|
1192
|
-
const params = buildPaginationParams(pagination || {});
|
|
1193
|
-
const response = await this.makeRequest('GET', `/api/users/${userId}/followers`, params, {
|
|
1194
|
-
cache: true,
|
|
1195
|
-
cacheTTL: 2 * 60 * 1000 // 2 minutes cache
|
|
1196
|
-
});
|
|
1197
|
-
return {
|
|
1198
|
-
followers: response.data || [],
|
|
1199
|
-
total: response.pagination.total,
|
|
1200
|
-
hasMore: response.pagination.hasMore
|
|
1201
|
-
};
|
|
1202
|
-
} catch (error) {
|
|
1203
|
-
throw this.handleError(error);
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
/**
|
|
1208
|
-
* Get user following
|
|
1209
|
-
*/
|
|
1210
|
-
async getUserFollowing(userId, pagination) {
|
|
1211
|
-
try {
|
|
1212
|
-
const params = buildPaginationParams(pagination || {});
|
|
1213
|
-
const response = await this.makeRequest('GET', `/api/users/${userId}/following`, params, {
|
|
1214
|
-
cache: true,
|
|
1215
|
-
cacheTTL: 2 * 60 * 1000 // 2 minutes cache
|
|
1216
|
-
});
|
|
1217
|
-
return {
|
|
1218
|
-
following: response.data || [],
|
|
1219
|
-
total: response.pagination.total,
|
|
1220
|
-
hasMore: response.pagination.hasMore
|
|
1221
|
-
};
|
|
1222
|
-
} catch (error) {
|
|
1223
|
-
throw this.handleError(error);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
/**
|
|
1228
|
-
* Get notifications
|
|
1229
|
-
*/
|
|
1230
|
-
async getNotifications() {
|
|
1231
|
-
return this.withAuthRetry(async () => {
|
|
1232
|
-
return await this.makeRequest('GET', '/api/notifications', undefined, {
|
|
1233
|
-
cache: false // Don't cache notifications - always get fresh data
|
|
1234
|
-
});
|
|
1235
|
-
}, 'getNotifications');
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
/**
|
|
1239
|
-
* Get unread notification count
|
|
1240
|
-
*/
|
|
1241
|
-
async getUnreadCount() {
|
|
1242
|
-
try {
|
|
1243
|
-
const res = await this.makeRequest('GET', '/api/notifications/unread-count', undefined, {
|
|
1244
|
-
cache: false // Don't cache unread count - always get fresh data
|
|
1245
|
-
});
|
|
1246
|
-
return res.count;
|
|
1247
|
-
} catch (error) {
|
|
1248
|
-
throw this.handleError(error);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
/**
|
|
1253
|
-
* Create notification
|
|
1254
|
-
*/
|
|
1255
|
-
async createNotification(data) {
|
|
1256
|
-
try {
|
|
1257
|
-
return await this.makeRequest('POST', '/api/notifications', data, {
|
|
1258
|
-
cache: false
|
|
1259
|
-
});
|
|
1260
|
-
} catch (error) {
|
|
1261
|
-
throw this.handleError(error);
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
/**
|
|
1266
|
-
* Mark notification as read
|
|
1267
|
-
*/
|
|
1268
|
-
async markNotificationAsRead(notificationId) {
|
|
1269
|
-
try {
|
|
1270
|
-
await this.makeRequest('PUT', `/api/notifications/${notificationId}/read`, undefined, {
|
|
1271
|
-
cache: false
|
|
1272
|
-
});
|
|
1273
|
-
} catch (error) {
|
|
1274
|
-
throw this.handleError(error);
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
|
|
1278
|
-
/**
|
|
1279
|
-
* Mark all notifications as read
|
|
1280
|
-
*/
|
|
1281
|
-
async markAllNotificationsAsRead() {
|
|
1282
|
-
try {
|
|
1283
|
-
await this.makeRequest('PUT', '/api/notifications/read-all', undefined, {
|
|
1284
|
-
cache: false
|
|
1285
|
-
});
|
|
1286
|
-
} catch (error) {
|
|
1287
|
-
throw this.handleError(error);
|
|
1288
|
-
}
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
/**
|
|
1292
|
-
* Delete notification
|
|
1293
|
-
*/
|
|
1294
|
-
async deleteNotification(notificationId) {
|
|
1295
|
-
try {
|
|
1296
|
-
await this.makeRequest('DELETE', `/api/notifications/${notificationId}`, undefined, {
|
|
1297
|
-
cache: false
|
|
1298
|
-
});
|
|
1299
|
-
} catch (error) {
|
|
1300
|
-
throw this.handleError(error);
|
|
1301
|
-
}
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
// ============================================================================
|
|
1305
|
-
// PAYMENT METHODS
|
|
1306
|
-
// ============================================================================
|
|
1307
|
-
|
|
1308
|
-
/**
|
|
1309
|
-
* Create a payment
|
|
1310
|
-
*/
|
|
1311
|
-
async createPayment(data) {
|
|
1312
|
-
try {
|
|
1313
|
-
return await this.makeRequest('POST', '/api/payments', data, {
|
|
1314
|
-
cache: false
|
|
1315
|
-
});
|
|
1316
|
-
} catch (error) {
|
|
1317
|
-
throw this.handleError(error);
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
/**
|
|
1322
|
-
* Get payment by ID
|
|
1323
|
-
*/
|
|
1324
|
-
async getPayment(paymentId) {
|
|
1325
|
-
try {
|
|
1326
|
-
return await this.makeRequest('GET', `/api/payments/${paymentId}`, undefined, {
|
|
1327
|
-
cache: true,
|
|
1328
|
-
cacheTTL: 5 * 60 * 1000 // 5 minutes cache
|
|
1329
|
-
});
|
|
1330
|
-
} catch (error) {
|
|
1331
|
-
throw this.handleError(error);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
/**
|
|
1336
|
-
* Get user payments
|
|
1337
|
-
*/
|
|
1338
|
-
async getUserPayments() {
|
|
1339
|
-
try {
|
|
1340
|
-
return await this.makeRequest('GET', '/api/payments/user', undefined, {
|
|
1341
|
-
cache: false // Don't cache user payments - always get fresh data
|
|
1342
|
-
});
|
|
1343
|
-
} catch (error) {
|
|
1344
|
-
throw this.handleError(error);
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
|
|
1348
|
-
// ============================================================================
|
|
1349
|
-
// KARMA METHODS
|
|
1350
|
-
// ============================================================================
|
|
1351
|
-
|
|
1352
|
-
/**
|
|
1353
|
-
* Get user karma
|
|
1354
|
-
*/
|
|
1355
|
-
async getUserKarma(userId) {
|
|
1356
|
-
try {
|
|
1357
|
-
return await this.makeRequest('GET', `/api/karma/${userId}`, undefined, {
|
|
1358
|
-
cache: true,
|
|
1359
|
-
cacheTTL: 2 * 60 * 1000 // 2 minutes cache
|
|
1360
|
-
});
|
|
1361
|
-
} catch (error) {
|
|
1362
|
-
throw this.handleError(error);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
|
|
1366
|
-
/**
|
|
1367
|
-
* Give karma to user
|
|
1368
|
-
*/
|
|
1369
|
-
async giveKarma(userId, amount, reason) {
|
|
1370
|
-
try {
|
|
1371
|
-
return await this.makeRequest('POST', `/api/karma/${userId}/give`, {
|
|
1372
|
-
amount,
|
|
1373
|
-
reason
|
|
1374
|
-
}, {
|
|
1375
|
-
cache: false
|
|
1376
|
-
});
|
|
1377
|
-
} catch (error) {
|
|
1378
|
-
throw this.handleError(error);
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
/**
|
|
1383
|
-
* Get user karma total
|
|
1384
|
-
*/
|
|
1385
|
-
async getUserKarmaTotal(userId) {
|
|
1386
|
-
try {
|
|
1387
|
-
return await this.makeRequest('GET', `/api/karma/${userId}/total`, undefined, {
|
|
1388
|
-
cache: true,
|
|
1389
|
-
cacheTTL: 2 * 60 * 1000 // 2 minutes cache
|
|
1390
|
-
});
|
|
1391
|
-
} catch (error) {
|
|
1392
|
-
throw this.handleError(error);
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
/**
|
|
1397
|
-
* Get user karma history
|
|
1398
|
-
*/
|
|
1399
|
-
async getUserKarmaHistory(userId, limit, offset) {
|
|
1400
|
-
try {
|
|
1401
|
-
const params = {};
|
|
1402
|
-
if (limit) params.limit = limit;
|
|
1403
|
-
if (offset) params.offset = offset;
|
|
1404
|
-
return await this.makeRequest('GET', `/api/karma/${userId}/history`, params, {
|
|
1405
|
-
cache: true,
|
|
1406
|
-
cacheTTL: 2 * 60 * 1000 // 2 minutes cache
|
|
1407
|
-
});
|
|
1408
|
-
} catch (error) {
|
|
1409
|
-
throw this.handleError(error);
|
|
1410
|
-
}
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
/**
|
|
1414
|
-
* Get karma leaderboard
|
|
1415
|
-
*/
|
|
1416
|
-
async getKarmaLeaderboard() {
|
|
1417
|
-
try {
|
|
1418
|
-
return await this.makeRequest('GET', '/api/karma/leaderboard', undefined, {
|
|
1419
|
-
cache: true,
|
|
1420
|
-
cacheTTL: 5 * 60 * 1000 // 5 minutes cache
|
|
1421
|
-
});
|
|
1422
|
-
} catch (error) {
|
|
1423
|
-
throw this.handleError(error);
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
|
|
1427
|
-
/**
|
|
1428
|
-
* Get karma rules
|
|
1429
|
-
*/
|
|
1430
|
-
async getKarmaRules() {
|
|
1431
|
-
try {
|
|
1432
|
-
return await this.makeRequest('GET', '/api/karma/rules', undefined, {
|
|
1433
|
-
cache: true,
|
|
1434
|
-
cacheTTL: 30 * 60 * 1000 // 30 minutes cache (rules don't change often)
|
|
1435
|
-
});
|
|
1436
|
-
} catch (error) {
|
|
1437
|
-
throw this.handleError(error);
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
// ============================================================================
|
|
1442
|
-
// FILE METHODS (LEGACY - Using Asset Service)
|
|
1443
|
-
// ============================================================================
|
|
1444
|
-
|
|
1445
|
-
/**
|
|
1446
|
-
* Delete file
|
|
1447
|
-
*/
|
|
1448
|
-
async deleteFile(fileId) {
|
|
1449
|
-
try {
|
|
1450
|
-
// Central Asset Service delete with force=true behavior controlled by caller via assetDelete
|
|
1451
|
-
return await this.makeRequest('DELETE', `/api/assets/${encodeURIComponent(fileId)}`, undefined, {
|
|
1452
|
-
cache: false
|
|
1453
|
-
});
|
|
1454
|
-
} catch (error) {
|
|
1455
|
-
throw this.handleError(error);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
/**
|
|
1460
|
-
* Get file download URL (API streaming proxy, attaches token for <img src>)
|
|
1461
|
-
*/
|
|
1462
|
-
getFileDownloadUrl(fileId, variant, expiresIn) {
|
|
1463
|
-
const base = this.getBaseURL();
|
|
1464
|
-
const params = new URLSearchParams();
|
|
1465
|
-
if (variant) params.set('variant', variant);
|
|
1466
|
-
if (expiresIn) params.set('expiresIn', String(expiresIn));
|
|
1467
|
-
params.set('fallback', 'placeholderVisible');
|
|
1468
|
-
const token = this.httpClient.getAccessToken();
|
|
1469
|
-
if (token) params.set('token', token);
|
|
1470
|
-
|
|
1471
|
-
// Use params.toString() to detect whether there are query params.
|
|
1472
|
-
// URLSearchParams.size is not a standard property across all JS runtimes
|
|
1473
|
-
// (some environments like React Native may not implement it), which
|
|
1474
|
-
// caused the query string to be omitted on native. Checking the
|
|
1475
|
-
// serialized string is reliable everywhere.
|
|
1476
|
-
const qs = params.toString();
|
|
1477
|
-
return `${base}/api/assets/${encodeURIComponent(fileId)}/stream${qs ? `?${qs}` : ''}`;
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
/**
|
|
1481
|
-
* Get file stream URL (direct Oxy Cloud/CDN URL, no token)
|
|
1482
|
-
*/
|
|
1483
|
-
getFileStreamUrl(fileId) {
|
|
1484
|
-
return `${this.getCloudURL()}/files/${fileId}/stream`;
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
// ...existing code...
|
|
1488
|
-
|
|
1489
|
-
/**
|
|
1490
|
-
* List user files
|
|
1491
|
-
*/
|
|
1492
|
-
async listUserFiles(limit, offset) {
|
|
1493
|
-
try {
|
|
1494
|
-
const paramsObj = {};
|
|
1495
|
-
if (limit) paramsObj.limit = limit;
|
|
1496
|
-
if (offset) paramsObj.offset = offset;
|
|
1497
|
-
return await this.makeRequest('GET', '/api/assets', paramsObj, {
|
|
1498
|
-
cache: false // Don't cache file lists - always get fresh data
|
|
1499
|
-
});
|
|
1500
|
-
} catch (error) {
|
|
1501
|
-
throw this.handleError(error);
|
|
1502
|
-
}
|
|
1503
|
-
}
|
|
1504
|
-
|
|
1505
|
-
// (removed legacy downloadFileContent; use getFileContentAsBlob/Text which resolve CAS URL first)
|
|
1506
|
-
|
|
1507
|
-
/**
|
|
1508
|
-
* Get file content as text
|
|
1509
|
-
*/
|
|
1510
|
-
async getFileContentAsText(fileId, variant) {
|
|
1511
|
-
try {
|
|
1512
|
-
const params = variant ? {
|
|
1513
|
-
variant
|
|
1514
|
-
} : undefined;
|
|
1515
|
-
const urlRes = await this.makeRequest('GET', `/api/assets/${encodeURIComponent(fileId)}/url`, params, {
|
|
1516
|
-
cache: true,
|
|
1517
|
-
cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
|
|
1518
|
-
});
|
|
1519
|
-
const downloadUrl = urlRes?.url;
|
|
1520
|
-
const response = await fetch(downloadUrl);
|
|
1521
|
-
return await response.text();
|
|
1522
|
-
} catch (error) {
|
|
1523
|
-
throw this.handleError(error);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
/**
|
|
1528
|
-
* Get file content as blob
|
|
1529
|
-
*/
|
|
1530
|
-
async getFileContentAsBlob(fileId, variant) {
|
|
1531
|
-
try {
|
|
1532
|
-
const params = variant ? {
|
|
1533
|
-
variant
|
|
1534
|
-
} : undefined;
|
|
1535
|
-
const urlRes = await this.makeRequest('GET', `/api/assets/${encodeURIComponent(fileId)}/url`, params, {
|
|
1536
|
-
cache: true,
|
|
1537
|
-
cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
|
|
1538
|
-
});
|
|
1539
|
-
const downloadUrl = urlRes?.url;
|
|
1540
|
-
const response = await fetch(downloadUrl);
|
|
1541
|
-
return await response.blob();
|
|
1542
|
-
} catch (error) {
|
|
1543
|
-
throw this.handleError(error);
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
/**
|
|
1548
|
-
* Upload raw file data
|
|
1549
|
-
*/
|
|
1550
|
-
async uploadRawFile(file, visibility, metadata) {
|
|
1551
|
-
// Switch to Central Asset Service upload flow
|
|
1552
|
-
return this.assetUpload(file, visibility, metadata);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
// ============================================================================
|
|
1556
|
-
// CENTRAL ASSET SERVICE METHODS
|
|
1557
|
-
// ============================================================================
|
|
1558
|
-
|
|
1559
|
-
/**
|
|
1560
|
-
* Calculate SHA256 hash of file content
|
|
1561
|
-
*/
|
|
1562
|
-
async calculateSHA256(file) {
|
|
1563
|
-
const buffer = await file.arrayBuffer();
|
|
1564
|
-
const hashBuffer = await crypto.subtle.digest('SHA-256', buffer);
|
|
1565
|
-
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
1566
|
-
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
|
1567
|
-
}
|
|
1568
|
-
|
|
1569
|
-
/**
|
|
1570
|
-
* Initialize asset upload - returns pre-signed URL and file ID
|
|
1571
|
-
*/
|
|
1572
|
-
async assetInit(sha256, size, mime) {
|
|
1573
|
-
try {
|
|
1574
|
-
return await this.makeRequest('POST', '/api/assets/init', {
|
|
1575
|
-
sha256,
|
|
1576
|
-
size,
|
|
1577
|
-
mime
|
|
1578
|
-
}, {
|
|
1579
|
-
cache: false
|
|
1580
|
-
});
|
|
1581
|
-
} catch (error) {
|
|
1582
|
-
throw this.handleError(error);
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
/**
|
|
1587
|
-
* Complete asset upload - commit metadata and trigger variant generation
|
|
1588
|
-
*/
|
|
1589
|
-
async assetComplete(fileId, originalName, size, mime, visibility, metadata) {
|
|
1590
|
-
try {
|
|
1591
|
-
return await this.makeRequest('POST', '/api/assets/complete', {
|
|
1592
|
-
fileId,
|
|
1593
|
-
originalName,
|
|
1594
|
-
size,
|
|
1595
|
-
mime,
|
|
1596
|
-
visibility,
|
|
1597
|
-
metadata
|
|
1598
|
-
}, {
|
|
1599
|
-
cache: false
|
|
1600
|
-
});
|
|
1601
|
-
} catch (error) {
|
|
1602
|
-
throw this.handleError(error);
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
/**
|
|
1607
|
-
* Upload file using Central Asset Service
|
|
1608
|
-
*/
|
|
1609
|
-
async assetUpload(file, visibility, metadata, onProgress) {
|
|
1610
|
-
try {
|
|
1611
|
-
// Calculate SHA256
|
|
1612
|
-
const sha256 = await this.calculateSHA256(file);
|
|
1613
|
-
|
|
1614
|
-
// Initialize upload
|
|
1615
|
-
const initResponse = await this.assetInit(sha256, file.size, file.type);
|
|
1616
|
-
|
|
1617
|
-
// Try presigned URL first
|
|
1618
|
-
try {
|
|
1619
|
-
await this.uploadToPresignedUrl(initResponse.uploadUrl, file, onProgress);
|
|
1620
|
-
} catch (e) {
|
|
1621
|
-
// Fallback: direct upload via API to avoid CORS issues
|
|
1622
|
-
const fd = new FormData();
|
|
1623
|
-
fd.append('file', file);
|
|
1624
|
-
// Use httpClient directly for FormData uploads (bypasses RequestManager for special handling)
|
|
1625
|
-
await this.httpClient.request({
|
|
1626
|
-
method: 'POST',
|
|
1627
|
-
url: `/api/assets/${encodeURIComponent(initResponse.fileId)}/upload-direct`,
|
|
1628
|
-
data: fd
|
|
1629
|
-
});
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
// Complete upload
|
|
1633
|
-
return await this.assetComplete(initResponse.fileId, file.name, file.size, file.type, visibility, metadata);
|
|
1634
|
-
} catch (error) {
|
|
1635
|
-
throw this.handleError(error);
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
/**
|
|
1640
|
-
* Upload file to pre-signed URL
|
|
1641
|
-
*/
|
|
1642
|
-
async uploadToPresignedUrl(url, file, onProgress) {
|
|
1643
|
-
return new Promise((resolve, reject) => {
|
|
1644
|
-
const xhr = new XMLHttpRequest();
|
|
1645
|
-
xhr.upload.addEventListener('progress', event => {
|
|
1646
|
-
if (event.lengthComputable && onProgress) {
|
|
1647
|
-
const progress = event.loaded / event.total * 100;
|
|
1648
|
-
onProgress(progress);
|
|
1649
|
-
}
|
|
1650
|
-
});
|
|
1651
|
-
xhr.addEventListener('load', () => {
|
|
1652
|
-
if (xhr.status >= 200 && xhr.status < 300) {
|
|
1653
|
-
resolve();
|
|
1654
|
-
} else {
|
|
1655
|
-
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
1656
|
-
}
|
|
1657
|
-
});
|
|
1658
|
-
xhr.addEventListener('error', () => {
|
|
1659
|
-
reject(new Error('Upload failed'));
|
|
1660
|
-
});
|
|
1661
|
-
xhr.open('PUT', url);
|
|
1662
|
-
xhr.setRequestHeader('Content-Type', file.type);
|
|
1663
|
-
xhr.send(file);
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
/**
|
|
1668
|
-
* Link asset to an entity
|
|
1669
|
-
*/
|
|
1670
|
-
async assetLink(fileId, app, entityType, entityId, visibility, webhookUrl) {
|
|
1671
|
-
try {
|
|
1672
|
-
const body = {
|
|
1673
|
-
app,
|
|
1674
|
-
entityType,
|
|
1675
|
-
entityId
|
|
1676
|
-
};
|
|
1677
|
-
if (visibility) body.visibility = visibility;
|
|
1678
|
-
if (webhookUrl) body.webhookUrl = webhookUrl;
|
|
1679
|
-
return await this.makeRequest('POST', `/api/assets/${fileId}/links`, body, {
|
|
1680
|
-
cache: false
|
|
1681
|
-
});
|
|
1682
|
-
} catch (error) {
|
|
1683
|
-
throw this.handleError(error);
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
/**
|
|
1688
|
-
* Unlink asset from an entity
|
|
1689
|
-
*/
|
|
1690
|
-
async assetUnlink(fileId, app, entityType, entityId) {
|
|
1691
|
-
try {
|
|
1692
|
-
return await this.makeRequest('DELETE', `/api/assets/${fileId}/links`, {
|
|
1693
|
-
app,
|
|
1694
|
-
entityType,
|
|
1695
|
-
entityId
|
|
1696
|
-
}, {
|
|
1697
|
-
cache: false
|
|
1698
|
-
});
|
|
1699
|
-
} catch (error) {
|
|
1700
|
-
throw this.handleError(error);
|
|
1701
|
-
}
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
/**
|
|
1705
|
-
* Get asset metadata
|
|
1706
|
-
*/
|
|
1707
|
-
async assetGet(fileId) {
|
|
1708
|
-
try {
|
|
1709
|
-
return await this.makeRequest('GET', `/api/assets/${fileId}`, undefined, {
|
|
1710
|
-
cache: true,
|
|
1711
|
-
cacheTTL: 5 * 60 * 1000 // 5 minutes cache
|
|
1712
|
-
});
|
|
1713
|
-
} catch (error) {
|
|
1714
|
-
throw this.handleError(error);
|
|
1715
|
-
}
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
/**
|
|
1719
|
-
* Get asset URL (CDN or signed URL)
|
|
1720
|
-
*/
|
|
1721
|
-
async assetGetUrl(fileId, variant, expiresIn) {
|
|
1722
|
-
try {
|
|
1723
|
-
const params = {};
|
|
1724
|
-
if (variant) params.variant = variant;
|
|
1725
|
-
if (expiresIn) params.expiresIn = expiresIn;
|
|
1726
|
-
return await this.makeRequest('GET', `/api/assets/${fileId}/url`, params, {
|
|
1727
|
-
cache: true,
|
|
1728
|
-
cacheTTL: 10 * 60 * 1000 // 10 minutes cache for URLs
|
|
1729
|
-
});
|
|
1730
|
-
} catch (error) {
|
|
1731
|
-
throw this.handleError(error);
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
/**
|
|
1736
|
-
* Restore asset from trash
|
|
1737
|
-
*/
|
|
1738
|
-
async assetRestore(fileId) {
|
|
1739
|
-
try {
|
|
1740
|
-
return await this.makeRequest('POST', `/api/assets/${fileId}/restore`, undefined, {
|
|
1741
|
-
cache: false
|
|
1742
|
-
});
|
|
1743
|
-
} catch (error) {
|
|
1744
|
-
throw this.handleError(error);
|
|
1745
|
-
}
|
|
1746
|
-
}
|
|
1747
|
-
|
|
1748
|
-
/**
|
|
1749
|
-
* Delete asset with optional force
|
|
1750
|
-
*/
|
|
1751
|
-
async assetDelete(fileId, force = false) {
|
|
1752
|
-
try {
|
|
1753
|
-
const params = force ? {
|
|
1754
|
-
force: 'true'
|
|
1755
|
-
} : undefined;
|
|
1756
|
-
return await this.makeRequest('DELETE', `/api/assets/${fileId}`, params, {
|
|
1757
|
-
cache: false
|
|
1758
|
-
});
|
|
1759
|
-
} catch (error) {
|
|
1760
|
-
throw this.handleError(error);
|
|
1761
|
-
}
|
|
1762
|
-
}
|
|
1763
|
-
|
|
1764
|
-
/**
|
|
1765
|
-
* Get list of available variants for an asset
|
|
1766
|
-
*/
|
|
1767
|
-
async assetGetVariants(fileId) {
|
|
1768
|
-
try {
|
|
1769
|
-
const assetData = await this.assetGet(fileId);
|
|
1770
|
-
return assetData.file?.variants || [];
|
|
1771
|
-
} catch (error) {
|
|
1772
|
-
throw this.handleError(error);
|
|
1773
|
-
}
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
/**
|
|
1777
|
-
* Update asset visibility
|
|
1778
|
-
* @param fileId - The file ID
|
|
1779
|
-
* @param visibility - New visibility level ('private', 'public', or 'unlisted')
|
|
1780
|
-
* @returns Updated asset information
|
|
1781
|
-
*/
|
|
1782
|
-
async assetUpdateVisibility(fileId, visibility) {
|
|
1783
|
-
try {
|
|
1784
|
-
return await this.makeRequest('PATCH', `/api/assets/${fileId}/visibility`, {
|
|
1785
|
-
visibility
|
|
1786
|
-
}, {
|
|
1787
|
-
cache: false
|
|
1788
|
-
});
|
|
1789
|
-
} catch (error) {
|
|
1790
|
-
throw this.handleError(error);
|
|
1791
|
-
}
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
/**
|
|
1795
|
-
* Helper: Upload and link avatar with automatic public visibility
|
|
1796
|
-
* @param file - The avatar file
|
|
1797
|
-
* @param userId - User ID to link to
|
|
1798
|
-
* @param app - App name (defaults to 'profiles')
|
|
1799
|
-
* @returns The uploaded and linked asset
|
|
1800
|
-
*/
|
|
1801
|
-
async uploadAvatar(file, userId, app = 'profiles') {
|
|
1802
|
-
try {
|
|
1803
|
-
// Upload as public
|
|
1804
|
-
const asset = await this.assetUpload(file, 'public');
|
|
1805
|
-
|
|
1806
|
-
// Link to user profile as avatar
|
|
1807
|
-
await this.assetLink(asset.file.id, app, 'avatar', userId, 'public');
|
|
1808
|
-
return asset;
|
|
1809
|
-
} catch (error) {
|
|
1810
|
-
throw this.handleError(error);
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
/**
|
|
1815
|
-
* Helper: Upload and link profile banner with automatic public visibility
|
|
1816
|
-
* @param file - The banner file
|
|
1817
|
-
* @param userId - User ID to link to
|
|
1818
|
-
* @param app - App name (defaults to 'profiles')
|
|
1819
|
-
* @returns The uploaded and linked asset
|
|
1820
|
-
*/
|
|
1821
|
-
async uploadProfileBanner(file, userId, app = 'profiles') {
|
|
1822
|
-
try {
|
|
1823
|
-
// Upload as public
|
|
1824
|
-
const asset = await this.assetUpload(file, 'public');
|
|
1825
|
-
|
|
1826
|
-
// Link to user profile as banner
|
|
1827
|
-
await this.assetLink(asset.file.id, app, 'profile-banner', userId, 'public');
|
|
1828
|
-
return asset;
|
|
1829
|
-
} catch (error) {
|
|
1830
|
-
throw this.handleError(error);
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
// ============================================================================
|
|
1835
|
-
// DEVELOPER API METHODS
|
|
1836
|
-
// ============================================================================
|
|
1837
|
-
|
|
1838
|
-
/**
|
|
1839
|
-
* Get developer apps for the current user
|
|
1840
|
-
*/
|
|
1841
|
-
async getDeveloperApps() {
|
|
1842
|
-
try {
|
|
1843
|
-
const res = await this.makeRequest('GET', '/api/developer/apps', undefined, {
|
|
1844
|
-
cache: true,
|
|
1845
|
-
cacheTTL: 2 * 60 * 1000 // 2 minutes cache
|
|
1846
|
-
});
|
|
1847
|
-
return res.apps || [];
|
|
1848
|
-
} catch (error) {
|
|
1849
|
-
throw this.handleError(error);
|
|
1850
|
-
}
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
/**
|
|
1854
|
-
* Create a new developer app
|
|
1855
|
-
*/
|
|
1856
|
-
async createDeveloperApp(data) {
|
|
1857
|
-
try {
|
|
1858
|
-
const res = await this.makeRequest('POST', '/api/developer/apps', data, {
|
|
1859
|
-
cache: false
|
|
1860
|
-
});
|
|
1861
|
-
return res.app;
|
|
1862
|
-
} catch (error) {
|
|
1863
|
-
throw this.handleError(error);
|
|
1864
|
-
}
|
|
1865
|
-
}
|
|
1866
|
-
|
|
1867
|
-
/**
|
|
1868
|
-
* Get a specific developer app
|
|
1869
|
-
*/
|
|
1870
|
-
async getDeveloperApp(appId) {
|
|
1871
|
-
try {
|
|
1872
|
-
const res = await this.makeRequest('GET', `/api/developer/apps/${appId}`, undefined, {
|
|
1873
|
-
cache: true,
|
|
1874
|
-
cacheTTL: 5 * 60 * 1000 // 5 minutes cache
|
|
1875
|
-
});
|
|
1876
|
-
return res.app;
|
|
1877
|
-
} catch (error) {
|
|
1878
|
-
throw this.handleError(error);
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
|
|
1882
|
-
/**
|
|
1883
|
-
* Update a developer app
|
|
1884
|
-
*/
|
|
1885
|
-
async updateDeveloperApp(appId, data) {
|
|
1886
|
-
try {
|
|
1887
|
-
const res = await this.makeRequest('PATCH', `/api/developer/apps/${appId}`, data, {
|
|
1888
|
-
cache: false
|
|
1889
|
-
});
|
|
1890
|
-
return res.app;
|
|
1891
|
-
} catch (error) {
|
|
1892
|
-
throw this.handleError(error);
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
/**
|
|
1897
|
-
* Regenerate API secret for a developer app
|
|
1898
|
-
*/
|
|
1899
|
-
async regenerateDeveloperAppSecret(appId) {
|
|
1900
|
-
try {
|
|
1901
|
-
return await this.makeRequest('POST', `/api/developer/apps/${appId}/regenerate-secret`, undefined, {
|
|
1902
|
-
cache: false
|
|
1903
|
-
});
|
|
1904
|
-
} catch (error) {
|
|
1905
|
-
throw this.handleError(error);
|
|
1906
|
-
}
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
/**
|
|
1910
|
-
* Delete a developer app
|
|
1911
|
-
*/
|
|
1912
|
-
async deleteDeveloperApp(appId) {
|
|
1913
|
-
try {
|
|
1914
|
-
return await this.makeRequest('DELETE', `/api/developer/apps/${appId}`, undefined, {
|
|
1915
|
-
cache: false
|
|
1916
|
-
});
|
|
1917
|
-
} catch (error) {
|
|
1918
|
-
throw this.handleError(error);
|
|
1919
|
-
}
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
// ============================================================================
|
|
1923
|
-
// LOCATION METHODS
|
|
1924
|
-
// ============================================================================
|
|
1925
|
-
|
|
1926
|
-
/**
|
|
1927
|
-
* Update user location
|
|
1928
|
-
*/
|
|
1929
|
-
async updateLocation(latitude, longitude) {
|
|
1930
|
-
try {
|
|
1931
|
-
return await this.makeRequest('POST', '/api/location', {
|
|
1932
|
-
latitude,
|
|
1933
|
-
longitude
|
|
1934
|
-
}, {
|
|
1935
|
-
cache: false
|
|
1936
|
-
});
|
|
1937
|
-
} catch (error) {
|
|
1938
|
-
throw this.handleError(error);
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
/**
|
|
1943
|
-
* Get nearby users
|
|
1944
|
-
*/
|
|
1945
|
-
async getNearbyUsers(radius) {
|
|
1946
|
-
try {
|
|
1947
|
-
const params = radius ? {
|
|
1948
|
-
radius
|
|
1949
|
-
} : undefined;
|
|
1950
|
-
return await this.makeRequest('GET', '/api/location/nearby', params, {
|
|
1951
|
-
cache: false // Don't cache location data - always get fresh data
|
|
1952
|
-
});
|
|
1953
|
-
} catch (error) {
|
|
1954
|
-
throw this.handleError(error);
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
// ============================================================================
|
|
1959
|
-
// ANALYTICS METHODS
|
|
1960
|
-
// ============================================================================
|
|
1961
|
-
|
|
1962
|
-
/**
|
|
1963
|
-
* Track event
|
|
1964
|
-
*/
|
|
1965
|
-
async trackEvent(eventName, properties) {
|
|
1966
|
-
try {
|
|
1967
|
-
await this.makeRequest('POST', '/api/analytics/events', {
|
|
1968
|
-
event: eventName,
|
|
1969
|
-
properties
|
|
1970
|
-
}, {
|
|
1971
|
-
cache: false,
|
|
1972
|
-
retry: false
|
|
1973
|
-
}); // Don't retry analytics events
|
|
1974
|
-
} catch (error) {
|
|
1975
|
-
throw this.handleError(error);
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
|
|
1979
|
-
/**
|
|
1980
|
-
* Get analytics data
|
|
1981
|
-
*/
|
|
1982
|
-
async getAnalytics(startDate, endDate) {
|
|
1983
|
-
try {
|
|
1984
|
-
const params = {};
|
|
1985
|
-
if (startDate) params.startDate = startDate;
|
|
1986
|
-
if (endDate) params.endDate = endDate;
|
|
1987
|
-
return await this.makeRequest('GET', '/api/analytics', params, {
|
|
1988
|
-
cache: true,
|
|
1989
|
-
cacheTTL: 5 * 60 * 1000 // 5 minutes cache
|
|
1990
|
-
});
|
|
1991
|
-
} catch (error) {
|
|
1992
|
-
throw this.handleError(error);
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
// ============================================================================
|
|
1997
|
-
// DEVICE METHODS
|
|
1998
|
-
// ============================================================================
|
|
1999
|
-
|
|
2000
|
-
/**
|
|
2001
|
-
* Register device
|
|
2002
|
-
*/
|
|
2003
|
-
async registerDevice(deviceData) {
|
|
2004
|
-
try {
|
|
2005
|
-
return await this.makeRequest('POST', '/api/devices', deviceData, {
|
|
2006
|
-
cache: false
|
|
2007
|
-
});
|
|
2008
|
-
} catch (error) {
|
|
2009
|
-
throw this.handleError(error);
|
|
2010
|
-
}
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
/**
|
|
2014
|
-
* Get user devices
|
|
2015
|
-
*/
|
|
2016
|
-
async getUserDevices() {
|
|
2017
|
-
try {
|
|
2018
|
-
return await this.makeRequest('GET', '/api/devices', undefined, {
|
|
2019
|
-
cache: false // Don't cache device list - always get fresh data
|
|
2020
|
-
});
|
|
2021
|
-
} catch (error) {
|
|
2022
|
-
throw this.handleError(error);
|
|
2023
|
-
}
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
/**
|
|
2027
|
-
* Remove device
|
|
2028
|
-
*/
|
|
2029
|
-
async removeDevice(deviceId) {
|
|
2030
|
-
try {
|
|
2031
|
-
await this.makeRequest('DELETE', `/api/devices/${deviceId}`, undefined, {
|
|
2032
|
-
cache: false
|
|
2033
|
-
});
|
|
2034
|
-
} catch (error) {
|
|
2035
|
-
throw this.handleError(error);
|
|
2036
|
-
}
|
|
2037
|
-
}
|
|
2038
|
-
|
|
2039
|
-
/**
|
|
2040
|
-
* Get device sessions
|
|
2041
|
-
* Note: Not cached by default to ensure fresh data, but can be cached via makeRequest if needed
|
|
2042
|
-
*/
|
|
2043
|
-
async getDeviceSessions(sessionId) {
|
|
2044
|
-
try {
|
|
2045
|
-
// Use makeRequest for consistent error handling and optional caching
|
|
2046
|
-
// Cache disabled by default to ensure fresh session data
|
|
2047
|
-
return await this.makeRequest('GET', `/api/session/device/sessions/${sessionId}`, undefined, {
|
|
2048
|
-
cache: false,
|
|
2049
|
-
// Don't cache sessions - always get fresh data
|
|
2050
|
-
deduplicate: true // Deduplicate concurrent requests for same sessionId
|
|
2051
|
-
});
|
|
2052
|
-
} catch (error) {
|
|
2053
|
-
throw this.handleError(error);
|
|
2054
|
-
}
|
|
2055
|
-
}
|
|
2056
|
-
|
|
2057
|
-
/**
|
|
2058
|
-
* Logout all device sessions
|
|
2059
|
-
*/
|
|
2060
|
-
async logoutAllDeviceSessions(sessionId, deviceId, excludeCurrent) {
|
|
2061
|
-
try {
|
|
2062
|
-
const params = new URLSearchParams();
|
|
2063
|
-
if (deviceId) params.append('deviceId', deviceId);
|
|
2064
|
-
if (excludeCurrent) params.append('excludeCurrent', 'true');
|
|
2065
|
-
const urlParams = {};
|
|
2066
|
-
params.forEach((value, key) => {
|
|
2067
|
-
urlParams[key] = value;
|
|
2068
|
-
});
|
|
2069
|
-
return await this.makeRequest('POST', `/api/session/device/logout-all/${sessionId}`, urlParams, {
|
|
2070
|
-
cache: false
|
|
2071
|
-
});
|
|
2072
|
-
} catch (error) {
|
|
2073
|
-
throw this.handleError(error);
|
|
2074
|
-
}
|
|
2075
|
-
}
|
|
2076
|
-
|
|
2077
|
-
/**
|
|
2078
|
-
* Update device name
|
|
2079
|
-
*/
|
|
2080
|
-
async updateDeviceName(sessionId, deviceName) {
|
|
2081
|
-
try {
|
|
2082
|
-
return await this.makeRequest('PUT', `/api/session/device/name/${sessionId}`, {
|
|
2083
|
-
deviceName
|
|
2084
|
-
}, {
|
|
2085
|
-
cache: false
|
|
2086
|
-
});
|
|
2087
|
-
} catch (error) {
|
|
2088
|
-
throw this.handleError(error);
|
|
2089
|
-
}
|
|
2090
|
-
}
|
|
2091
|
-
|
|
2092
|
-
// ============================================================================
|
|
2093
|
-
// UTILITY METHODS
|
|
2094
|
-
// ============================================================================
|
|
2095
|
-
|
|
2096
|
-
/**
|
|
2097
|
-
* Fetch link metadata
|
|
2098
|
-
*/
|
|
2099
|
-
async fetchLinkMetadata(url) {
|
|
2100
|
-
try {
|
|
2101
|
-
return await this.makeRequest('GET', '/api/link-metadata', {
|
|
2102
|
-
url
|
|
2103
|
-
}, {
|
|
2104
|
-
cache: true,
|
|
2105
|
-
cacheTTL: 30 * 60 * 1000 // 30 minutes cache for link metadata
|
|
2106
|
-
});
|
|
2107
|
-
} catch (error) {
|
|
2108
|
-
throw this.handleError(error);
|
|
2109
|
-
}
|
|
2110
|
-
}
|
|
2111
|
-
|
|
2112
|
-
/**
|
|
2113
|
-
* Simple Express.js authentication middleware
|
|
2114
|
-
*
|
|
2115
|
-
* Built-in authentication middleware that validates JWT tokens and adds user data to requests.
|
|
2116
|
-
*
|
|
2117
|
-
* @example
|
|
2118
|
-
* ```typescript
|
|
2119
|
-
* // Basic usage - just add it to your routes
|
|
2120
|
-
* app.use('/api/protected', oxyServices.auth());
|
|
2121
|
-
*
|
|
2122
|
-
* // With debug logging
|
|
2123
|
-
* app.use('/api/protected', oxyServices.auth({ debug: true }));
|
|
2124
|
-
*
|
|
2125
|
-
* // With custom error handling
|
|
2126
|
-
* app.use('/api/protected', oxyServices.auth({
|
|
2127
|
-
* onError: (error) => console.error('Auth failed:', error)
|
|
2128
|
-
* }));
|
|
2129
|
-
*
|
|
2130
|
-
* // Load full user data
|
|
2131
|
-
* app.use('/api/protected', oxyServices.auth({ loadUser: true }));
|
|
2132
|
-
* ```
|
|
2133
|
-
*
|
|
2134
|
-
* @param options Optional configuration
|
|
2135
|
-
* @param options.debug Enable debug logging (default: false)
|
|
2136
|
-
* @param options.onError Custom error handler
|
|
2137
|
-
* @param options.loadUser Load full user data (default: false for performance)
|
|
2138
|
-
* @param options.session Use session-based validation (default: false)
|
|
2139
|
-
* @returns Express middleware function
|
|
2140
|
-
*/
|
|
2141
|
-
auth(options = {}) {
|
|
2142
|
-
const {
|
|
2143
|
-
debug = false,
|
|
2144
|
-
onError,
|
|
2145
|
-
loadUser = false,
|
|
2146
|
-
session = false
|
|
2147
|
-
} = options;
|
|
2148
|
-
|
|
2149
|
-
// Return a synchronous middleware function
|
|
2150
|
-
return (req, res, next) => {
|
|
2151
|
-
try {
|
|
2152
|
-
// Extract token from Authorization header
|
|
2153
|
-
const authHeader = req.headers['authorization'];
|
|
2154
|
-
const token = authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : null;
|
|
2155
|
-
if (debug) {
|
|
2156
|
-
console.log(`🔐 Auth: Processing ${req.method} ${req.path}`);
|
|
2157
|
-
console.log(`🔐 Auth: Token present: ${!!token}`);
|
|
2158
|
-
}
|
|
2159
|
-
if (!token) {
|
|
2160
|
-
const error = {
|
|
2161
|
-
message: 'Access token required',
|
|
2162
|
-
code: 'MISSING_TOKEN',
|
|
2163
|
-
status: 401
|
|
2164
|
-
};
|
|
2165
|
-
if (debug) console.log(`❌ Auth: Missing token`);
|
|
2166
|
-
if (onError) return onError(error);
|
|
2167
|
-
return res.status(401).json(error);
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
// Decode and validate token
|
|
2171
|
-
let decoded;
|
|
2172
|
-
try {
|
|
2173
|
-
decoded = jwtDecode(token);
|
|
2174
|
-
if (debug) {
|
|
2175
|
-
console.log(`🔐 Auth: Token decoded, User ID: ${decoded.userId || decoded.id}`);
|
|
2176
|
-
}
|
|
2177
|
-
} catch (decodeError) {
|
|
2178
|
-
const error = {
|
|
2179
|
-
message: 'Invalid token format',
|
|
2180
|
-
code: 'INVALID_TOKEN_FORMAT',
|
|
2181
|
-
status: 403
|
|
2182
|
-
};
|
|
2183
|
-
if (debug) console.log(`❌ Auth: Token decode failed`);
|
|
2184
|
-
if (onError) return onError(error);
|
|
2185
|
-
return res.status(403).json(error);
|
|
2186
|
-
}
|
|
2187
|
-
const userId = decoded.userId || decoded.id;
|
|
2188
|
-
if (!userId) {
|
|
2189
|
-
const error = {
|
|
2190
|
-
message: 'Token missing user ID',
|
|
2191
|
-
code: 'INVALID_TOKEN_PAYLOAD',
|
|
2192
|
-
status: 403
|
|
2193
|
-
};
|
|
2194
|
-
if (debug) console.log(`❌ Auth: Token missing user ID`);
|
|
2195
|
-
if (onError) return onError(error);
|
|
2196
|
-
return res.status(403).json(error);
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
// Check token expiration
|
|
2200
|
-
if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
|
|
2201
|
-
const error = {
|
|
2202
|
-
message: 'Token expired',
|
|
2203
|
-
code: 'TOKEN_EXPIRED',
|
|
2204
|
-
status: 403
|
|
2205
|
-
};
|
|
2206
|
-
if (debug) console.log(`❌ Auth: Token expired`);
|
|
2207
|
-
if (onError) return onError(error);
|
|
2208
|
-
return res.status(403).json(error);
|
|
2209
|
-
}
|
|
2210
|
-
|
|
2211
|
-
// For now, skip session validation to keep it simple
|
|
2212
|
-
// Session validation can be added later if needed
|
|
2213
|
-
|
|
2214
|
-
// Set request properties immediately
|
|
2215
|
-
req.userId = userId;
|
|
2216
|
-
req.accessToken = token;
|
|
2217
|
-
req.user = {
|
|
2218
|
-
id: userId
|
|
2219
|
-
};
|
|
2220
|
-
if (debug) {
|
|
2221
|
-
console.log(`✅ Auth: Authentication successful for user ${userId}`);
|
|
2222
|
-
}
|
|
2223
|
-
next();
|
|
2224
|
-
} catch (error) {
|
|
2225
|
-
const apiError = this.handleError(error);
|
|
2226
|
-
if (debug) {
|
|
2227
|
-
console.log(`❌ Auth: Unexpected error:`, apiError);
|
|
2228
|
-
}
|
|
2229
|
-
if (onError) return onError(apiError);
|
|
2230
|
-
return res.status(apiError && apiError.status || 500).json(apiError);
|
|
2231
|
-
}
|
|
2232
|
-
};
|
|
105
|
+
// Export as a named class to avoid TypeScript issues with anonymous class types
|
|
106
|
+
export class OxyServices extends OxyServicesComposed {
|
|
107
|
+
constructor(config) {
|
|
108
|
+
super(config);
|
|
2233
109
|
}
|
|
2234
110
|
}
|
|
2235
111
|
|
|
112
|
+
// Re-export error classes for convenience
|
|
113
|
+
export { OxyAuthenticationError, OxyAuthenticationTimeoutError };
|
|
114
|
+
|
|
2236
115
|
/**
|
|
2237
116
|
* Export the default Oxy Cloud URL (for backward compatibility)
|
|
2238
117
|
*/
|