seaverse-auth-sdk 0.1.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/README.md +399 -0
- package/dist/index.cjs +2432 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +903 -0
- package/dist/index.js +2403 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/auth-modal.css +944 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2432 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
var fs = require('fs/promises');
|
|
5
|
+
var path = require('path');
|
|
6
|
+
var os = require('os');
|
|
7
|
+
|
|
8
|
+
function _interopNamespaceDefault(e) {
|
|
9
|
+
var n = Object.create(null);
|
|
10
|
+
if (e) {
|
|
11
|
+
Object.keys(e).forEach(function (k) {
|
|
12
|
+
if (k !== 'default') {
|
|
13
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
14
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
get: function () { return e[k]; }
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
n.default = e;
|
|
22
|
+
return Object.freeze(n);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
26
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
27
|
+
var os__namespace = /*#__PURE__*/_interopNamespaceDefault(os);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 生成UUID的跨平台函数
|
|
31
|
+
* 支持 Node.js 和浏览器环境
|
|
32
|
+
*/
|
|
33
|
+
function generateUUID() {
|
|
34
|
+
// Node.js 环境
|
|
35
|
+
if (typeof globalThis !== 'undefined' && globalThis.crypto?.randomUUID) {
|
|
36
|
+
return globalThis.crypto.randomUUID();
|
|
37
|
+
}
|
|
38
|
+
// 浏览器环境 - crypto.randomUUID (现代浏览器)
|
|
39
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
40
|
+
return crypto.randomUUID();
|
|
41
|
+
}
|
|
42
|
+
// 浏览器环境 - crypto.getRandomValues (备用方案)
|
|
43
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
44
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
45
|
+
const r = (crypto.getRandomValues(new Uint8Array(1))[0] % 16) | 0;
|
|
46
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
47
|
+
return v.toString(16);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Fallback - 使用 Math.random() (不推荐用于生产环境)
|
|
51
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
52
|
+
const r = (Math.random() * 16) | 0;
|
|
53
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
54
|
+
return v.toString(16);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 内置Hook集合
|
|
59
|
+
*/
|
|
60
|
+
class BuiltInHooks {
|
|
61
|
+
/**
|
|
62
|
+
* 创建日志记录Hook
|
|
63
|
+
*/
|
|
64
|
+
static createLoggerHook(options) {
|
|
65
|
+
const logLevel = options?.logLevel || 'info';
|
|
66
|
+
const logRequestBody = options?.logRequestBody ?? false;
|
|
67
|
+
const logResponseBody = options?.logResponseBody ?? false;
|
|
68
|
+
const logHeaders = options?.logHeaders ?? false;
|
|
69
|
+
return {
|
|
70
|
+
type: 'beforeRequest',
|
|
71
|
+
name: 'logger',
|
|
72
|
+
priority: 100,
|
|
73
|
+
handler: async (context) => {
|
|
74
|
+
const { operationId, config } = context;
|
|
75
|
+
console.log(`[${logLevel.toUpperCase()}] ${config.method?.toUpperCase()} ${config.url}`);
|
|
76
|
+
console.log(`Operation ID: ${operationId}`);
|
|
77
|
+
if (logHeaders && config.headers) {
|
|
78
|
+
console.log('Headers:', JSON.stringify(config.headers, null, 2));
|
|
79
|
+
}
|
|
80
|
+
if (logRequestBody && config.data) {
|
|
81
|
+
console.log('Request Body:', JSON.stringify(config.data, null, 2));
|
|
82
|
+
}
|
|
83
|
+
// 添加afterResponse日志
|
|
84
|
+
if (context.response) {
|
|
85
|
+
console.log(`Response Status: ${context.response.status}`);
|
|
86
|
+
if (logResponseBody && context.response.data) {
|
|
87
|
+
console.log('Response Body:', JSON.stringify(context.response.data, null, 2));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* 创建请求ID Hook
|
|
95
|
+
*/
|
|
96
|
+
static createRequestIdHook() {
|
|
97
|
+
return {
|
|
98
|
+
type: 'beforeRequest',
|
|
99
|
+
name: 'request-id',
|
|
100
|
+
priority: 10,
|
|
101
|
+
handler: async (context) => {
|
|
102
|
+
const requestId = generateUUID();
|
|
103
|
+
context.config.headers = {
|
|
104
|
+
...context.config.headers,
|
|
105
|
+
'X-Request-ID': requestId,
|
|
106
|
+
};
|
|
107
|
+
context.metadata = context.metadata || {};
|
|
108
|
+
context.metadata.requestId = requestId;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* 创建速率限制Hook
|
|
114
|
+
*/
|
|
115
|
+
static createRateLimitHook(options) {
|
|
116
|
+
const requestsPerSecond = options?.requestsPerSecond;
|
|
117
|
+
// TODO: 实现基于分钟的速率限制
|
|
118
|
+
// const requestsPerMinute = options?.requestsPerMinute;
|
|
119
|
+
// 简单的令牌桶实现
|
|
120
|
+
let tokens = requestsPerSecond || 10;
|
|
121
|
+
let lastRefill = Date.now();
|
|
122
|
+
const refillTokens = () => {
|
|
123
|
+
const now = Date.now();
|
|
124
|
+
const timePassed = now - lastRefill;
|
|
125
|
+
if (requestsPerSecond && timePassed >= 1000) {
|
|
126
|
+
tokens = Math.min(requestsPerSecond, tokens + requestsPerSecond);
|
|
127
|
+
lastRefill = now;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
type: 'beforeRequest',
|
|
132
|
+
name: 'rate-limit',
|
|
133
|
+
priority: 5,
|
|
134
|
+
handler: async (_context) => {
|
|
135
|
+
refillTokens();
|
|
136
|
+
if (tokens <= 0) {
|
|
137
|
+
const waitTime = 1000 - (Date.now() - lastRefill);
|
|
138
|
+
if (waitTime > 0) {
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
140
|
+
refillTokens();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
tokens--;
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 创建缓存Hook(简化版)
|
|
149
|
+
*/
|
|
150
|
+
static createCacheHook(options) {
|
|
151
|
+
const cache = new Map();
|
|
152
|
+
// TODO: 实现缓存过期逻辑
|
|
153
|
+
// const ttl = options?.ttl || 60000; // 默认60秒
|
|
154
|
+
const methods = options?.methods || ['GET'];
|
|
155
|
+
return {
|
|
156
|
+
type: 'beforeRequest',
|
|
157
|
+
name: 'cache',
|
|
158
|
+
priority: 20,
|
|
159
|
+
handler: async (context) => {
|
|
160
|
+
const method = context.config.method?.toUpperCase();
|
|
161
|
+
if (!method || !methods.includes(method)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const cacheKey = `${method}:${context.config.url}:${JSON.stringify(context.config.params)}`;
|
|
165
|
+
const cached = cache.get(cacheKey);
|
|
166
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
167
|
+
// 从缓存返回(这里需要特殊处理,通常在拦截器中实现)
|
|
168
|
+
context.metadata = context.metadata || {};
|
|
169
|
+
context.metadata.cacheHit = true;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// 缓存miss,需要在afterResponse中缓存结果
|
|
173
|
+
context.metadata = context.metadata || {};
|
|
174
|
+
context.metadata.cacheKey = cacheKey;
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 创建性能监控Hook
|
|
181
|
+
*/
|
|
182
|
+
static createMetricsHook(options) {
|
|
183
|
+
const collector = options?.collector || ((metrics) => {
|
|
184
|
+
console.log('Metrics:', metrics);
|
|
185
|
+
});
|
|
186
|
+
return {
|
|
187
|
+
type: 'beforeRequest',
|
|
188
|
+
name: 'metrics',
|
|
189
|
+
priority: 1,
|
|
190
|
+
handler: async (context) => {
|
|
191
|
+
context.metadata = context.metadata || {};
|
|
192
|
+
context.metadata.startTime = Date.now();
|
|
193
|
+
// 在afterResponse或onError中计算duration
|
|
194
|
+
if (context.response) {
|
|
195
|
+
const duration = Date.now() - context.metadata.startTime;
|
|
196
|
+
const metrics = {
|
|
197
|
+
operationId: context.operationId,
|
|
198
|
+
method: context.config.method?.toUpperCase() || 'UNKNOWN',
|
|
199
|
+
path: context.config.url || '',
|
|
200
|
+
statusCode: context.response.status,
|
|
201
|
+
duration,
|
|
202
|
+
timestamp: Date.now(),
|
|
203
|
+
};
|
|
204
|
+
collector(metrics);
|
|
205
|
+
}
|
|
206
|
+
else if (context.error) {
|
|
207
|
+
const duration = Date.now() - context.metadata.startTime;
|
|
208
|
+
const metrics = {
|
|
209
|
+
operationId: context.operationId,
|
|
210
|
+
method: context.config.method?.toUpperCase() || 'UNKNOWN',
|
|
211
|
+
path: context.config.url || '',
|
|
212
|
+
duration,
|
|
213
|
+
timestamp: Date.now(),
|
|
214
|
+
error: context.error.message,
|
|
215
|
+
};
|
|
216
|
+
collector(metrics);
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 创建重试Hook(需要在interceptor中实现真正的重试逻辑)
|
|
223
|
+
*/
|
|
224
|
+
static createRetryHook(maxRetries = 3, retryDelay = 1000) {
|
|
225
|
+
return {
|
|
226
|
+
type: 'onRetry',
|
|
227
|
+
name: 'retry',
|
|
228
|
+
priority: 1,
|
|
229
|
+
handler: async (context) => {
|
|
230
|
+
const retryCount = context.retryCount || 0;
|
|
231
|
+
console.log(`Retrying request (${retryCount}/${maxRetries})...`);
|
|
232
|
+
if (retryCount > 0) {
|
|
233
|
+
// 指数退避
|
|
234
|
+
const delay = retryDelay * Math.pow(2, retryCount - 1);
|
|
235
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Hook管理器
|
|
244
|
+
*/
|
|
245
|
+
class HookManager {
|
|
246
|
+
constructor(options) {
|
|
247
|
+
this.hooks = new Map();
|
|
248
|
+
// 初始化hook类型映射
|
|
249
|
+
this.hooks.set('beforeRequest', []);
|
|
250
|
+
this.hooks.set('afterResponse', []);
|
|
251
|
+
this.hooks.set('onError', []);
|
|
252
|
+
this.hooks.set('onRetry', []);
|
|
253
|
+
// 注册用户自定义hooks
|
|
254
|
+
if (options?.hooks) {
|
|
255
|
+
for (const hook of options.hooks) {
|
|
256
|
+
this.register(hook);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// 注册内置hooks
|
|
260
|
+
if (options?.enableBuiltInHooks) {
|
|
261
|
+
this.registerBuiltInHooks(options);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* 注册hook
|
|
266
|
+
*/
|
|
267
|
+
register(hook) {
|
|
268
|
+
const hooks = this.hooks.get(hook.type) || [];
|
|
269
|
+
hooks.push(hook);
|
|
270
|
+
// 按优先级排序(优先级数字越小越优先)
|
|
271
|
+
hooks.sort((a, b) => a.priority - b.priority);
|
|
272
|
+
this.hooks.set(hook.type, hooks);
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* 注销hook
|
|
276
|
+
*/
|
|
277
|
+
unregister(type, name) {
|
|
278
|
+
const hooks = this.hooks.get(type) || [];
|
|
279
|
+
const filtered = hooks.filter((h) => h.name !== name);
|
|
280
|
+
this.hooks.set(type, filtered);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 执行beforeRequest hooks
|
|
284
|
+
*/
|
|
285
|
+
async executeBeforeRequest(context) {
|
|
286
|
+
await this.executeHooks('beforeRequest', context);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* 执行afterResponse hooks
|
|
290
|
+
*/
|
|
291
|
+
async executeAfterResponse(context) {
|
|
292
|
+
await this.executeHooks('afterResponse', context);
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* 执行onError hooks
|
|
296
|
+
*/
|
|
297
|
+
async executeOnError(context) {
|
|
298
|
+
await this.executeHooks('onError', context);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* 执行onRetry hooks
|
|
302
|
+
*/
|
|
303
|
+
async executeOnRetry(context) {
|
|
304
|
+
await this.executeHooks('onRetry', context);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* 执行指定类型的hooks
|
|
308
|
+
*/
|
|
309
|
+
async executeHooks(type, context) {
|
|
310
|
+
const hooks = this.hooks.get(type) || [];
|
|
311
|
+
for (const hook of hooks) {
|
|
312
|
+
// 检查过滤条件
|
|
313
|
+
if (!this.shouldExecuteHook(hook, context)) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
try {
|
|
317
|
+
await hook.handler(context);
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
// Hook执行失败不应阻断请求
|
|
321
|
+
console.error(`Hook ${hook.name} failed:`, error);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* 检查是否应该执行hook
|
|
327
|
+
*/
|
|
328
|
+
shouldExecuteHook(hook, context) {
|
|
329
|
+
if (!hook.filter) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
const filter = hook.filter;
|
|
333
|
+
// 检查operationId过滤
|
|
334
|
+
if (filter.operationIds && filter.operationIds.length > 0) {
|
|
335
|
+
if (!filter.operationIds.includes(context.operationId)) {
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
// 检查method过滤
|
|
340
|
+
if (filter.methods && filter.methods.length > 0) {
|
|
341
|
+
const method = context.config.method?.toUpperCase();
|
|
342
|
+
if (!method || !filter.methods.includes(method)) {
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// 检查path pattern过滤
|
|
347
|
+
if (filter.pathPatterns && filter.pathPatterns.length > 0) {
|
|
348
|
+
const url = context.config.url || '';
|
|
349
|
+
const matched = filter.pathPatterns.some((pattern) => pattern.test(url));
|
|
350
|
+
if (!matched) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* 注册内置hooks
|
|
358
|
+
*/
|
|
359
|
+
registerBuiltInHooks(options) {
|
|
360
|
+
const { enableBuiltInHooks } = options;
|
|
361
|
+
if (enableBuiltInHooks?.logger) {
|
|
362
|
+
this.register(BuiltInHooks.createLoggerHook(options.loggerOptions));
|
|
363
|
+
}
|
|
364
|
+
if (enableBuiltInHooks?.requestId) {
|
|
365
|
+
this.register(BuiltInHooks.createRequestIdHook());
|
|
366
|
+
}
|
|
367
|
+
if (enableBuiltInHooks?.retry) {
|
|
368
|
+
// Retry hook需要特殊处理,通常在axios interceptor中实现
|
|
369
|
+
console.warn('Retry hook should be implemented in axios interceptor');
|
|
370
|
+
}
|
|
371
|
+
if (enableBuiltInHooks?.rateLimit) {
|
|
372
|
+
this.register(BuiltInHooks.createRateLimitHook(options.rateLimitOptions));
|
|
373
|
+
}
|
|
374
|
+
if (enableBuiltInHooks?.cache) {
|
|
375
|
+
// Cache hook需要特殊处理
|
|
376
|
+
console.warn('Cache hook requires special implementation');
|
|
377
|
+
}
|
|
378
|
+
if (enableBuiltInHooks?.metrics) {
|
|
379
|
+
this.register(BuiltInHooks.createMetricsHook(options.metricsOptions));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* 获取所有注册的hooks
|
|
384
|
+
*/
|
|
385
|
+
getAllHooks() {
|
|
386
|
+
return new Map(this.hooks);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* 获取指定类型的hooks
|
|
390
|
+
*/
|
|
391
|
+
getHooks(type) {
|
|
392
|
+
return this.hooks.get(type) || [];
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* 清除所有hooks
|
|
396
|
+
*/
|
|
397
|
+
clear() {
|
|
398
|
+
this.hooks.clear();
|
|
399
|
+
this.hooks.set('beforeRequest', []);
|
|
400
|
+
this.hooks.set('afterResponse', []);
|
|
401
|
+
this.hooks.set('onError', []);
|
|
402
|
+
this.hooks.set('onRetry', []);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* HTTP客户端,集成认证和Hook系统
|
|
408
|
+
*/
|
|
409
|
+
class HttpClient {
|
|
410
|
+
constructor(options) {
|
|
411
|
+
this.auth = options.auth;
|
|
412
|
+
this.hookManager = new HookManager(options.hooks);
|
|
413
|
+
this.retryOptions = {
|
|
414
|
+
maxRetries: 3,
|
|
415
|
+
retryDelay: 1000,
|
|
416
|
+
retryStatusCodes: [408, 429, 500, 502, 503, 504],
|
|
417
|
+
...options.retryOptions,
|
|
418
|
+
};
|
|
419
|
+
// 创建axios实例
|
|
420
|
+
this.client = axios.create({
|
|
421
|
+
baseURL: options.baseURL,
|
|
422
|
+
timeout: options.timeout || 30000,
|
|
423
|
+
headers: options.headers,
|
|
424
|
+
});
|
|
425
|
+
// 设置拦截器
|
|
426
|
+
this.setupInterceptors();
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* 设置请求和响应拦截器
|
|
430
|
+
*/
|
|
431
|
+
setupInterceptors() {
|
|
432
|
+
// 请求拦截器
|
|
433
|
+
this.client.interceptors.request.use(async (config) => {
|
|
434
|
+
const operationId = config.headers?.['X-Operation-Id'] || 'unknown';
|
|
435
|
+
// 添加认证信息
|
|
436
|
+
if (this.auth) {
|
|
437
|
+
config = await this.auth.attachCredentials(config);
|
|
438
|
+
}
|
|
439
|
+
// 执行beforeRequest hooks
|
|
440
|
+
const hookContext = {
|
|
441
|
+
operationId,
|
|
442
|
+
config,
|
|
443
|
+
metadata: {},
|
|
444
|
+
};
|
|
445
|
+
await this.hookManager.executeBeforeRequest(hookContext);
|
|
446
|
+
return hookContext.config;
|
|
447
|
+
}, (error) => {
|
|
448
|
+
return Promise.reject(error);
|
|
449
|
+
});
|
|
450
|
+
// 响应拦截器
|
|
451
|
+
this.client.interceptors.response.use(async (response) => {
|
|
452
|
+
const operationId = response.config.headers?.['X-Operation-Id'] || 'unknown';
|
|
453
|
+
// 执行afterResponse hooks
|
|
454
|
+
const hookContext = {
|
|
455
|
+
operationId,
|
|
456
|
+
config: response.config,
|
|
457
|
+
response,
|
|
458
|
+
metadata: {},
|
|
459
|
+
};
|
|
460
|
+
await this.hookManager.executeAfterResponse(hookContext);
|
|
461
|
+
return hookContext.response || response;
|
|
462
|
+
}, async (error) => {
|
|
463
|
+
const operationId = error.config?.headers?.['X-Operation-Id'] || 'unknown';
|
|
464
|
+
// 执行onError hooks
|
|
465
|
+
const hookContext = {
|
|
466
|
+
operationId,
|
|
467
|
+
config: error.config,
|
|
468
|
+
error,
|
|
469
|
+
metadata: {},
|
|
470
|
+
};
|
|
471
|
+
await this.hookManager.executeOnError(hookContext);
|
|
472
|
+
// 重试逻辑
|
|
473
|
+
const shouldRetry = this.shouldRetryRequest(error);
|
|
474
|
+
const retryCount = error.config?._retryCount || 0;
|
|
475
|
+
if (shouldRetry && retryCount < this.retryOptions.maxRetries) {
|
|
476
|
+
// 执行onRetry hooks
|
|
477
|
+
hookContext.retryCount = retryCount;
|
|
478
|
+
await this.hookManager.executeOnRetry(hookContext);
|
|
479
|
+
// 计算退避延迟
|
|
480
|
+
const delay = this.retryOptions.retryDelay * Math.pow(2, retryCount);
|
|
481
|
+
await this.delay(delay);
|
|
482
|
+
// 更新重试计数
|
|
483
|
+
const config = error.config;
|
|
484
|
+
config._retryCount = retryCount + 1;
|
|
485
|
+
// 重新发起请求
|
|
486
|
+
return this.client.request(config);
|
|
487
|
+
}
|
|
488
|
+
return Promise.reject(hookContext.error || error);
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* 判断是否应该重试
|
|
493
|
+
*/
|
|
494
|
+
shouldRetryRequest(error) {
|
|
495
|
+
// 自定义重试判断
|
|
496
|
+
if (this.retryOptions.shouldRetry) {
|
|
497
|
+
return this.retryOptions.shouldRetry(error);
|
|
498
|
+
}
|
|
499
|
+
// 没有响应或网络错误
|
|
500
|
+
if (!error.response) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
// 根据状态码判断
|
|
504
|
+
const statusCode = error.response.status;
|
|
505
|
+
return this.retryOptions.retryStatusCodes?.includes(statusCode) || false;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* 延迟函数
|
|
509
|
+
*/
|
|
510
|
+
delay(ms) {
|
|
511
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* 发起请求
|
|
515
|
+
*/
|
|
516
|
+
async request(config) {
|
|
517
|
+
return this.client.request(config);
|
|
518
|
+
}
|
|
519
|
+
/**
|
|
520
|
+
* GET请求
|
|
521
|
+
*/
|
|
522
|
+
async get(url, config) {
|
|
523
|
+
return this.client.get(url, config);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* POST请求
|
|
527
|
+
*/
|
|
528
|
+
async post(url, data, config) {
|
|
529
|
+
return this.client.post(url, data, config);
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* PUT请求
|
|
533
|
+
*/
|
|
534
|
+
async put(url, data, config) {
|
|
535
|
+
return this.client.put(url, data, config);
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* DELETE请求
|
|
539
|
+
*/
|
|
540
|
+
async delete(url, config) {
|
|
541
|
+
return this.client.delete(url, config);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* PATCH请求
|
|
545
|
+
*/
|
|
546
|
+
async patch(url, data, config) {
|
|
547
|
+
return this.client.patch(url, data, config);
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* 获取底层axios实例
|
|
551
|
+
*/
|
|
552
|
+
getAxiosInstance() {
|
|
553
|
+
return this.client;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* 认证提供者抽象基类
|
|
559
|
+
*/
|
|
560
|
+
class AuthProvider {
|
|
561
|
+
constructor(credentials) {
|
|
562
|
+
this.credentials = credentials;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* 获取当前凭证
|
|
566
|
+
*/
|
|
567
|
+
getCredentials() {
|
|
568
|
+
return this.credentials;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* 更新凭证
|
|
572
|
+
*/
|
|
573
|
+
updateCredentials(credentials) {
|
|
574
|
+
this.credentials = { ...this.credentials, ...credentials };
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* API Key认证提供者
|
|
580
|
+
*/
|
|
581
|
+
class ApiKeyProvider extends AuthProvider {
|
|
582
|
+
constructor(credentials) {
|
|
583
|
+
super(credentials);
|
|
584
|
+
this.credentials = credentials;
|
|
585
|
+
}
|
|
586
|
+
async attachCredentials(config) {
|
|
587
|
+
const newConfig = { ...config };
|
|
588
|
+
if (this.credentials.location === 'header') {
|
|
589
|
+
// 添加到Header
|
|
590
|
+
newConfig.headers = {
|
|
591
|
+
...newConfig.headers,
|
|
592
|
+
[this.credentials.name]: this.credentials.apiKey,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
else if (this.credentials.location === 'query') {
|
|
596
|
+
// 添加到Query参数
|
|
597
|
+
newConfig.params = {
|
|
598
|
+
...newConfig.params,
|
|
599
|
+
[this.credentials.name]: this.credentials.apiKey,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
return newConfig;
|
|
603
|
+
}
|
|
604
|
+
async refreshCredentials() {
|
|
605
|
+
// API Key通常不需要刷新
|
|
606
|
+
return Promise.resolve();
|
|
607
|
+
}
|
|
608
|
+
async isValid() {
|
|
609
|
+
return Boolean(this.credentials.apiKey);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* OAuth2认证提供者
|
|
615
|
+
*/
|
|
616
|
+
class OAuth2Provider extends AuthProvider {
|
|
617
|
+
constructor(credentials) {
|
|
618
|
+
super(credentials);
|
|
619
|
+
this.credentials = credentials;
|
|
620
|
+
}
|
|
621
|
+
async attachCredentials(config) {
|
|
622
|
+
// 确保有有效的access token
|
|
623
|
+
if (!(await this.isValid())) {
|
|
624
|
+
await this.refreshCredentials();
|
|
625
|
+
}
|
|
626
|
+
const newConfig = { ...config };
|
|
627
|
+
newConfig.headers = {
|
|
628
|
+
...newConfig.headers,
|
|
629
|
+
Authorization: `Bearer ${this.credentials.accessToken}`,
|
|
630
|
+
};
|
|
631
|
+
return newConfig;
|
|
632
|
+
}
|
|
633
|
+
async refreshCredentials() {
|
|
634
|
+
try {
|
|
635
|
+
const params = new URLSearchParams();
|
|
636
|
+
params.append('client_id', this.credentials.clientId);
|
|
637
|
+
params.append('client_secret', this.credentials.clientSecret);
|
|
638
|
+
if (this.credentials.refreshToken) {
|
|
639
|
+
// 使用refresh token刷新
|
|
640
|
+
params.append('grant_type', 'refresh_token');
|
|
641
|
+
params.append('refresh_token', this.credentials.refreshToken);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
// 使用client credentials流程
|
|
645
|
+
params.append('grant_type', 'client_credentials');
|
|
646
|
+
if (this.credentials.scope) {
|
|
647
|
+
params.append('scope', this.credentials.scope);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const response = await axios.post(this.credentials.tokenUrl, params, {
|
|
651
|
+
headers: {
|
|
652
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
653
|
+
},
|
|
654
|
+
});
|
|
655
|
+
const tokenData = response.data;
|
|
656
|
+
// 更新凭证
|
|
657
|
+
this.credentials.accessToken = tokenData.access_token;
|
|
658
|
+
if (tokenData.refresh_token) {
|
|
659
|
+
this.credentials.refreshToken = tokenData.refresh_token;
|
|
660
|
+
}
|
|
661
|
+
if (tokenData.expires_in) {
|
|
662
|
+
this.credentials.expiresAt = Date.now() + tokenData.expires_in * 1000;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
throw new Error(`Failed to refresh OAuth2 token: ${error}`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async isValid() {
|
|
670
|
+
if (!this.credentials.accessToken) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
if (this.credentials.expiresAt) {
|
|
674
|
+
// 提前5分钟刷新
|
|
675
|
+
const bufferTime = 5 * 60 * 1000;
|
|
676
|
+
return Date.now() < this.credentials.expiresAt - bufferTime;
|
|
677
|
+
}
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
/**
|
|
683
|
+
* JWT认证提供者
|
|
684
|
+
*/
|
|
685
|
+
class JWTProvider extends AuthProvider {
|
|
686
|
+
constructor(credentials) {
|
|
687
|
+
super(credentials);
|
|
688
|
+
this.credentials = credentials;
|
|
689
|
+
}
|
|
690
|
+
async attachCredentials(config) {
|
|
691
|
+
const newConfig = { ...config };
|
|
692
|
+
const headerName = this.credentials.headerName || 'Authorization';
|
|
693
|
+
const token = this.credentials.token;
|
|
694
|
+
newConfig.headers = {
|
|
695
|
+
...newConfig.headers,
|
|
696
|
+
[headerName]: headerName === 'Authorization' ? `Bearer ${token}` : token,
|
|
697
|
+
};
|
|
698
|
+
return newConfig;
|
|
699
|
+
}
|
|
700
|
+
async refreshCredentials() {
|
|
701
|
+
// JWT通常由外部系统管理,这里不实现刷新逻辑
|
|
702
|
+
return Promise.resolve();
|
|
703
|
+
}
|
|
704
|
+
async isValid() {
|
|
705
|
+
if (!this.credentials.token) {
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
// 可以在这里添加JWT过期检查
|
|
709
|
+
try {
|
|
710
|
+
const parts = this.credentials.token.split('.');
|
|
711
|
+
if (parts.length !== 3) {
|
|
712
|
+
return false;
|
|
713
|
+
}
|
|
714
|
+
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
|
|
715
|
+
if (payload.exp) {
|
|
716
|
+
// 检查是否过期(提前1分钟)
|
|
717
|
+
return Date.now() < payload.exp * 1000 - 60000;
|
|
718
|
+
}
|
|
719
|
+
return true;
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
return false;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Basic认证提供者
|
|
729
|
+
*/
|
|
730
|
+
class BasicAuthProvider extends AuthProvider {
|
|
731
|
+
constructor(credentials) {
|
|
732
|
+
super(credentials);
|
|
733
|
+
this.credentials = credentials;
|
|
734
|
+
}
|
|
735
|
+
async attachCredentials(config) {
|
|
736
|
+
const newConfig = { ...config };
|
|
737
|
+
const token = Buffer.from(`${this.credentials.username}:${this.credentials.password}`).toString('base64');
|
|
738
|
+
newConfig.headers = {
|
|
739
|
+
...newConfig.headers,
|
|
740
|
+
Authorization: `Basic ${token}`,
|
|
741
|
+
};
|
|
742
|
+
return newConfig;
|
|
743
|
+
}
|
|
744
|
+
async refreshCredentials() {
|
|
745
|
+
// Basic Auth不需要刷新
|
|
746
|
+
return Promise.resolve();
|
|
747
|
+
}
|
|
748
|
+
async isValid() {
|
|
749
|
+
return Boolean(this.credentials.username && this.credentials.password);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* 自定义认证提供者
|
|
755
|
+
*/
|
|
756
|
+
class CustomAuthProvider extends AuthProvider {
|
|
757
|
+
constructor(credentials) {
|
|
758
|
+
super(credentials);
|
|
759
|
+
this.credentials = credentials;
|
|
760
|
+
}
|
|
761
|
+
async attachCredentials(config) {
|
|
762
|
+
return this.credentials.handler(config);
|
|
763
|
+
}
|
|
764
|
+
async refreshCredentials() {
|
|
765
|
+
// 自定义认证由用户实现
|
|
766
|
+
return Promise.resolve();
|
|
767
|
+
}
|
|
768
|
+
async isValid() {
|
|
769
|
+
// 自定义认证默认认为总是有效
|
|
770
|
+
return true;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/**
|
|
775
|
+
* 配置管理器
|
|
776
|
+
*/
|
|
777
|
+
class ConfigManager {
|
|
778
|
+
constructor(configDir) {
|
|
779
|
+
this.configDir = configDir || path__namespace.join(os__namespace.homedir(), '.openapi-sdk');
|
|
780
|
+
this.configPath = path__namespace.join(this.configDir, 'config.json');
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* 保存认证配置
|
|
784
|
+
*/
|
|
785
|
+
async saveAuth(profileName, authConfig) {
|
|
786
|
+
await this.ensureConfigDir();
|
|
787
|
+
const config = await this.loadConfig();
|
|
788
|
+
config.profiles[profileName] = authConfig;
|
|
789
|
+
await fs__namespace.writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* 加载认证配置
|
|
793
|
+
*/
|
|
794
|
+
async loadAuth(profileName) {
|
|
795
|
+
const config = await this.loadConfig();
|
|
796
|
+
const authConfig = config.profiles[profileName];
|
|
797
|
+
if (!authConfig) {
|
|
798
|
+
throw new Error(`Profile '${profileName}' not found`);
|
|
799
|
+
}
|
|
800
|
+
return authConfig;
|
|
801
|
+
}
|
|
802
|
+
/**
|
|
803
|
+
* 列出所有profile
|
|
804
|
+
*/
|
|
805
|
+
async listProfiles() {
|
|
806
|
+
const config = await this.loadConfig();
|
|
807
|
+
return Object.keys(config.profiles);
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* 删除profile
|
|
811
|
+
*/
|
|
812
|
+
async removeProfile(profileName) {
|
|
813
|
+
const config = await this.loadConfig();
|
|
814
|
+
delete config.profiles[profileName];
|
|
815
|
+
await fs__namespace.writeFile(this.configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
816
|
+
}
|
|
817
|
+
/**
|
|
818
|
+
* 从文件加载配置
|
|
819
|
+
*/
|
|
820
|
+
async loadFromFile(filePath) {
|
|
821
|
+
try {
|
|
822
|
+
const content = await fs__namespace.readFile(filePath, 'utf-8');
|
|
823
|
+
return JSON.parse(content);
|
|
824
|
+
}
|
|
825
|
+
catch (error) {
|
|
826
|
+
throw new Error(`Failed to load config from ${filePath}: ${error}`);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* 保存配置到文件
|
|
831
|
+
*/
|
|
832
|
+
async saveToFile(filePath, authConfig) {
|
|
833
|
+
try {
|
|
834
|
+
await fs__namespace.writeFile(filePath, JSON.stringify(authConfig, null, 2), 'utf-8');
|
|
835
|
+
}
|
|
836
|
+
catch (error) {
|
|
837
|
+
throw new Error(`Failed to save config to ${filePath}: ${error}`);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* 加载完整配置文件
|
|
842
|
+
*/
|
|
843
|
+
async loadConfig() {
|
|
844
|
+
try {
|
|
845
|
+
const content = await fs__namespace.readFile(this.configPath, 'utf-8');
|
|
846
|
+
return JSON.parse(content);
|
|
847
|
+
}
|
|
848
|
+
catch (error) {
|
|
849
|
+
if (error.code === 'ENOENT') {
|
|
850
|
+
// 文件不存在,返回默认配置
|
|
851
|
+
return { profiles: {} };
|
|
852
|
+
}
|
|
853
|
+
throw error;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* 确保配置目录存在
|
|
858
|
+
*/
|
|
859
|
+
async ensureConfigDir() {
|
|
860
|
+
try {
|
|
861
|
+
await fs__namespace.mkdir(this.configDir, { recursive: true });
|
|
862
|
+
}
|
|
863
|
+
catch (error) {
|
|
864
|
+
// 忽略错误,可能目录已存在
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* 获取配置目录路径
|
|
869
|
+
*/
|
|
870
|
+
getConfigDir() {
|
|
871
|
+
return this.configDir;
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* 获取配置文件路径
|
|
875
|
+
*/
|
|
876
|
+
getConfigPath() {
|
|
877
|
+
return this.configPath;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* 跨平台环境配置工具
|
|
883
|
+
* 支持 Node.js 和浏览器环境
|
|
884
|
+
*/
|
|
885
|
+
/**
|
|
886
|
+
* 全局配置存储(用于浏览器环境)
|
|
887
|
+
*/
|
|
888
|
+
const globalConfig = {};
|
|
889
|
+
/**
|
|
890
|
+
* 获取环境变量或配置值
|
|
891
|
+
* 优先级:globalConfig > process.env (仅 Node.js)
|
|
892
|
+
*/
|
|
893
|
+
function getEnvConfig(key) {
|
|
894
|
+
// 优先使用全局配置
|
|
895
|
+
if (globalConfig[key] !== undefined) {
|
|
896
|
+
return globalConfig[key];
|
|
897
|
+
}
|
|
898
|
+
// Node.js 环境
|
|
899
|
+
if (typeof process !== 'undefined' && process.env) {
|
|
900
|
+
return process.env[key];
|
|
901
|
+
}
|
|
902
|
+
return undefined;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* 认证提供者工厂
|
|
907
|
+
*/
|
|
908
|
+
class AuthFactory {
|
|
909
|
+
/**
|
|
910
|
+
* 根据配置创建认证提供者
|
|
911
|
+
*/
|
|
912
|
+
static create(config) {
|
|
913
|
+
return this.createFromCredentials(config.credentials);
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* 根据凭证创建认证提供者
|
|
917
|
+
*/
|
|
918
|
+
static createFromCredentials(credentials) {
|
|
919
|
+
switch (credentials.type) {
|
|
920
|
+
case 'apiKey':
|
|
921
|
+
return new ApiKeyProvider(credentials);
|
|
922
|
+
case 'oauth2':
|
|
923
|
+
return new OAuth2Provider(credentials);
|
|
924
|
+
case 'jwt':
|
|
925
|
+
return new JWTProvider(credentials);
|
|
926
|
+
case 'basic':
|
|
927
|
+
return new BasicAuthProvider(credentials);
|
|
928
|
+
case 'custom':
|
|
929
|
+
return new CustomAuthProvider(credentials);
|
|
930
|
+
default:
|
|
931
|
+
throw new Error(`Unsupported auth type: ${credentials.type}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* 从环境变量创建认证提供者
|
|
936
|
+
*/
|
|
937
|
+
static fromEnv() {
|
|
938
|
+
const authType = getEnvConfig('AUTH_TYPE');
|
|
939
|
+
if (!authType) {
|
|
940
|
+
throw new Error('AUTH_TYPE environment variable is not set');
|
|
941
|
+
}
|
|
942
|
+
let credentials;
|
|
943
|
+
switch (authType) {
|
|
944
|
+
case 'apiKey': {
|
|
945
|
+
const apiKey = getEnvConfig('API_KEY');
|
|
946
|
+
const location = getEnvConfig('API_KEY_LOCATION') || 'header';
|
|
947
|
+
const name = getEnvConfig('API_KEY_NAME') || 'X-API-Key';
|
|
948
|
+
if (!apiKey) {
|
|
949
|
+
throw new Error('API_KEY environment variable is required for apiKey auth');
|
|
950
|
+
}
|
|
951
|
+
credentials = {
|
|
952
|
+
type: 'apiKey',
|
|
953
|
+
apiKey,
|
|
954
|
+
location,
|
|
955
|
+
name,
|
|
956
|
+
};
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
case 'oauth2': {
|
|
960
|
+
const clientId = getEnvConfig('OAUTH_CLIENT_ID');
|
|
961
|
+
const clientSecret = getEnvConfig('OAUTH_CLIENT_SECRET');
|
|
962
|
+
const tokenUrl = getEnvConfig('OAUTH_TOKEN_URL');
|
|
963
|
+
const accessToken = getEnvConfig('ACCESS_TOKEN');
|
|
964
|
+
const refreshToken = getEnvConfig('REFRESH_TOKEN');
|
|
965
|
+
const scope = getEnvConfig('OAUTH_SCOPE');
|
|
966
|
+
if (!clientId || !clientSecret || !tokenUrl) {
|
|
967
|
+
throw new Error('OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, and OAUTH_TOKEN_URL are required for oauth2 auth');
|
|
968
|
+
}
|
|
969
|
+
credentials = {
|
|
970
|
+
type: 'oauth2',
|
|
971
|
+
clientId,
|
|
972
|
+
clientSecret,
|
|
973
|
+
tokenUrl,
|
|
974
|
+
accessToken,
|
|
975
|
+
refreshToken,
|
|
976
|
+
scope,
|
|
977
|
+
};
|
|
978
|
+
break;
|
|
979
|
+
}
|
|
980
|
+
case 'jwt': {
|
|
981
|
+
const token = getEnvConfig('JWT_TOKEN');
|
|
982
|
+
const headerName = getEnvConfig('JWT_HEADER_NAME');
|
|
983
|
+
if (!token) {
|
|
984
|
+
throw new Error('JWT_TOKEN environment variable is required for jwt auth');
|
|
985
|
+
}
|
|
986
|
+
credentials = {
|
|
987
|
+
type: 'jwt',
|
|
988
|
+
token,
|
|
989
|
+
headerName,
|
|
990
|
+
};
|
|
991
|
+
break;
|
|
992
|
+
}
|
|
993
|
+
case 'basic': {
|
|
994
|
+
const username = getEnvConfig('BASIC_USERNAME');
|
|
995
|
+
const password = getEnvConfig('BASIC_PASSWORD');
|
|
996
|
+
if (!username || !password) {
|
|
997
|
+
throw new Error('BASIC_USERNAME and BASIC_PASSWORD are required for basic auth');
|
|
998
|
+
}
|
|
999
|
+
credentials = {
|
|
1000
|
+
type: 'basic',
|
|
1001
|
+
username,
|
|
1002
|
+
password,
|
|
1003
|
+
};
|
|
1004
|
+
break;
|
|
1005
|
+
}
|
|
1006
|
+
default:
|
|
1007
|
+
throw new Error(`Unsupported AUTH_TYPE: ${authType}`);
|
|
1008
|
+
}
|
|
1009
|
+
return this.createFromCredentials(credentials);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* 从配置文件创建认证提供者
|
|
1013
|
+
*/
|
|
1014
|
+
static async fromFile(path) {
|
|
1015
|
+
const configManager = new ConfigManager();
|
|
1016
|
+
const config = await configManager.loadFromFile(path);
|
|
1017
|
+
return this.create(config);
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* 从profile创建认证提供者
|
|
1021
|
+
*/
|
|
1022
|
+
static async fromProfile(profileName = 'default') {
|
|
1023
|
+
const configManager = new ConfigManager();
|
|
1024
|
+
const config = await configManager.loadAuth(profileName);
|
|
1025
|
+
return this.create(config);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* Type definitions for SeaVerse Dispatcher API
|
|
1031
|
+
* Generated from auth.yaml OpenAPI specification
|
|
1032
|
+
*/
|
|
1033
|
+
|
|
1034
|
+
var models = /*#__PURE__*/Object.freeze({
|
|
1035
|
+
__proto__: null
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
/**
|
|
1039
|
+
* SeaVerse Backend API Client
|
|
1040
|
+
* SeaVerse Dispatcher API - 云原生 AI Agent 平台的统一网关服务
|
|
1041
|
+
* @version 2.0.0
|
|
1042
|
+
*/
|
|
1043
|
+
class SeaVerseBackendAPIClient {
|
|
1044
|
+
constructor(options = {}) {
|
|
1045
|
+
const httpOptions = {
|
|
1046
|
+
baseURL: options.baseURL || 'https://web.seaverse.dev',
|
|
1047
|
+
timeout: options.timeout,
|
|
1048
|
+
headers: options.headers,
|
|
1049
|
+
auth: options.auth || this.getDefaultAuth(),
|
|
1050
|
+
hooks: options.hooks || this.getDefaultHooks(),
|
|
1051
|
+
};
|
|
1052
|
+
this.httpClient = new HttpClient(httpOptions);
|
|
1053
|
+
}
|
|
1054
|
+
/**
|
|
1055
|
+
* Get default authentication configuration
|
|
1056
|
+
*/
|
|
1057
|
+
getDefaultAuth() {
|
|
1058
|
+
const token = getEnvConfig('JWT_TOKEN');
|
|
1059
|
+
if (!token)
|
|
1060
|
+
return undefined;
|
|
1061
|
+
return AuthFactory.create({
|
|
1062
|
+
type: 'jwt',
|
|
1063
|
+
credentials: {
|
|
1064
|
+
type: 'jwt',
|
|
1065
|
+
token,
|
|
1066
|
+
},
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Get default hooks configuration
|
|
1071
|
+
*/
|
|
1072
|
+
getDefaultHooks() {
|
|
1073
|
+
return {
|
|
1074
|
+
hooks: [
|
|
1075
|
+
BuiltInHooks.createLoggerHook({
|
|
1076
|
+
logLevel: 'info',
|
|
1077
|
+
logRequestBody: true,
|
|
1078
|
+
logResponseBody: true,
|
|
1079
|
+
}),
|
|
1080
|
+
BuiltInHooks.createRequestIdHook(),
|
|
1081
|
+
],
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
// ============================================================================
|
|
1085
|
+
// Authentication & OAuth APIs
|
|
1086
|
+
// ============================================================================
|
|
1087
|
+
/**
|
|
1088
|
+
* Health check
|
|
1089
|
+
* Check if the service is healthy
|
|
1090
|
+
*/
|
|
1091
|
+
async getHealth(options) {
|
|
1092
|
+
const config = {
|
|
1093
|
+
method: 'GET',
|
|
1094
|
+
url: `/health`,
|
|
1095
|
+
headers: {
|
|
1096
|
+
'X-Operation-Id': 'getHealth',
|
|
1097
|
+
...options?.headers,
|
|
1098
|
+
},
|
|
1099
|
+
...options,
|
|
1100
|
+
};
|
|
1101
|
+
const response = await this.httpClient.request(config);
|
|
1102
|
+
return response.data;
|
|
1103
|
+
}
|
|
1104
|
+
/**
|
|
1105
|
+
* User registration
|
|
1106
|
+
* Register a new user with email verification
|
|
1107
|
+
*/
|
|
1108
|
+
async register(data, options) {
|
|
1109
|
+
const config = {
|
|
1110
|
+
method: 'POST',
|
|
1111
|
+
url: `/api/auth/register`,
|
|
1112
|
+
data,
|
|
1113
|
+
headers: {
|
|
1114
|
+
'X-Operation-Id': 'register',
|
|
1115
|
+
...options?.headers,
|
|
1116
|
+
},
|
|
1117
|
+
...options,
|
|
1118
|
+
};
|
|
1119
|
+
const response = await this.httpClient.request(config);
|
|
1120
|
+
return response.data;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* User login
|
|
1124
|
+
* Login with email and password
|
|
1125
|
+
*/
|
|
1126
|
+
async login(data, options) {
|
|
1127
|
+
const config = {
|
|
1128
|
+
method: 'POST',
|
|
1129
|
+
url: `/api/auth/login`,
|
|
1130
|
+
data,
|
|
1131
|
+
headers: {
|
|
1132
|
+
'X-Operation-Id': 'login',
|
|
1133
|
+
...options?.headers,
|
|
1134
|
+
},
|
|
1135
|
+
...options,
|
|
1136
|
+
};
|
|
1137
|
+
const response = await this.httpClient.request(config);
|
|
1138
|
+
return response.data;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Get current user
|
|
1142
|
+
* Get the authenticated user's information
|
|
1143
|
+
*/
|
|
1144
|
+
async getCurrentUser(options) {
|
|
1145
|
+
const config = {
|
|
1146
|
+
method: 'GET',
|
|
1147
|
+
url: `/api/auth/me`,
|
|
1148
|
+
headers: {
|
|
1149
|
+
'X-Operation-Id': 'getCurrentUser',
|
|
1150
|
+
...options?.headers,
|
|
1151
|
+
},
|
|
1152
|
+
...options,
|
|
1153
|
+
};
|
|
1154
|
+
const response = await this.httpClient.request(config);
|
|
1155
|
+
return response.data;
|
|
1156
|
+
}
|
|
1157
|
+
/**
|
|
1158
|
+
* User logout
|
|
1159
|
+
* Logout the current user
|
|
1160
|
+
*/
|
|
1161
|
+
async logout(options) {
|
|
1162
|
+
const config = {
|
|
1163
|
+
method: 'POST',
|
|
1164
|
+
url: `/api/auth/logout`,
|
|
1165
|
+
headers: {
|
|
1166
|
+
'X-Operation-Id': 'logout',
|
|
1167
|
+
...options?.headers,
|
|
1168
|
+
},
|
|
1169
|
+
...options,
|
|
1170
|
+
};
|
|
1171
|
+
const response = await this.httpClient.request(config);
|
|
1172
|
+
return response.data;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Request password reset
|
|
1176
|
+
* Send password reset email
|
|
1177
|
+
*/
|
|
1178
|
+
async forgotPassword(data, options) {
|
|
1179
|
+
const config = {
|
|
1180
|
+
method: 'POST',
|
|
1181
|
+
url: `/api/auth/forgot-password`,
|
|
1182
|
+
data,
|
|
1183
|
+
headers: {
|
|
1184
|
+
'X-Operation-Id': 'forgotPassword',
|
|
1185
|
+
...options?.headers,
|
|
1186
|
+
},
|
|
1187
|
+
...options,
|
|
1188
|
+
};
|
|
1189
|
+
const response = await this.httpClient.request(config);
|
|
1190
|
+
return response.data;
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Reset password
|
|
1194
|
+
* Reset password with token from email
|
|
1195
|
+
*/
|
|
1196
|
+
async resetPassword(data, options) {
|
|
1197
|
+
const config = {
|
|
1198
|
+
method: 'POST',
|
|
1199
|
+
url: `/api/auth/reset-password`,
|
|
1200
|
+
data,
|
|
1201
|
+
headers: {
|
|
1202
|
+
'X-Operation-Id': 'resetPassword',
|
|
1203
|
+
...options?.headers,
|
|
1204
|
+
},
|
|
1205
|
+
...options,
|
|
1206
|
+
};
|
|
1207
|
+
const response = await this.httpClient.request(config);
|
|
1208
|
+
return response.data;
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Get api-service token
|
|
1212
|
+
* Generate token for accessing api-service from sandbox
|
|
1213
|
+
*/
|
|
1214
|
+
async getApiServiceToken(options) {
|
|
1215
|
+
const config = {
|
|
1216
|
+
method: 'GET',
|
|
1217
|
+
url: `/api/auth/api-service-token`,
|
|
1218
|
+
headers: {
|
|
1219
|
+
'X-Operation-Id': 'getApiServiceToken',
|
|
1220
|
+
...options?.headers,
|
|
1221
|
+
},
|
|
1222
|
+
...options,
|
|
1223
|
+
};
|
|
1224
|
+
const response = await this.httpClient.request(config);
|
|
1225
|
+
return response.data;
|
|
1226
|
+
}
|
|
1227
|
+
// ============================================================================
|
|
1228
|
+
// OAuth APIs
|
|
1229
|
+
// ============================================================================
|
|
1230
|
+
/**
|
|
1231
|
+
* Exchange Google code for token
|
|
1232
|
+
* Exchange Google authorization code for JWT token (for app)
|
|
1233
|
+
*/
|
|
1234
|
+
async googleCodeToToken(data, options) {
|
|
1235
|
+
const config = {
|
|
1236
|
+
method: 'POST',
|
|
1237
|
+
url: `/api/auth/google/code2token`,
|
|
1238
|
+
data,
|
|
1239
|
+
headers: {
|
|
1240
|
+
'X-Operation-Id': 'googleCodeToToken',
|
|
1241
|
+
...options?.headers,
|
|
1242
|
+
},
|
|
1243
|
+
...options,
|
|
1244
|
+
};
|
|
1245
|
+
const response = await this.httpClient.request(config);
|
|
1246
|
+
return response.data;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Unlink Google account
|
|
1250
|
+
* Unlink the user's Google account
|
|
1251
|
+
*/
|
|
1252
|
+
async unlinkGoogle(options) {
|
|
1253
|
+
const config = {
|
|
1254
|
+
method: 'POST',
|
|
1255
|
+
url: `/api/auth/google/unlink`,
|
|
1256
|
+
headers: {
|
|
1257
|
+
'X-Operation-Id': 'unlinkGoogle',
|
|
1258
|
+
...options?.headers,
|
|
1259
|
+
},
|
|
1260
|
+
...options,
|
|
1261
|
+
};
|
|
1262
|
+
const response = await this.httpClient.request(config);
|
|
1263
|
+
return response.data;
|
|
1264
|
+
}
|
|
1265
|
+
/**
|
|
1266
|
+
* Exchange Discord code for token
|
|
1267
|
+
*/
|
|
1268
|
+
async discordCodeToToken(data, options) {
|
|
1269
|
+
const config = {
|
|
1270
|
+
method: 'POST',
|
|
1271
|
+
url: `/api/auth/discord/code2token`,
|
|
1272
|
+
data,
|
|
1273
|
+
headers: {
|
|
1274
|
+
'X-Operation-Id': 'discordCodeToToken',
|
|
1275
|
+
...options?.headers,
|
|
1276
|
+
},
|
|
1277
|
+
...options,
|
|
1278
|
+
};
|
|
1279
|
+
const response = await this.httpClient.request(config);
|
|
1280
|
+
return response.data;
|
|
1281
|
+
}
|
|
1282
|
+
/**
|
|
1283
|
+
* Unlink Discord account
|
|
1284
|
+
*/
|
|
1285
|
+
async unlinkDiscord(options) {
|
|
1286
|
+
const config = {
|
|
1287
|
+
method: 'POST',
|
|
1288
|
+
url: `/api/auth/discord/unlink`,
|
|
1289
|
+
headers: {
|
|
1290
|
+
'X-Operation-Id': 'unlinkDiscord',
|
|
1291
|
+
...options?.headers,
|
|
1292
|
+
},
|
|
1293
|
+
...options,
|
|
1294
|
+
};
|
|
1295
|
+
const response = await this.httpClient.request(config);
|
|
1296
|
+
return response.data;
|
|
1297
|
+
}
|
|
1298
|
+
/**
|
|
1299
|
+
* Exchange GitHub code for token
|
|
1300
|
+
*/
|
|
1301
|
+
async githubCodeToToken(data, options) {
|
|
1302
|
+
const config = {
|
|
1303
|
+
method: 'POST',
|
|
1304
|
+
url: `/api/auth/github/code2token`,
|
|
1305
|
+
data,
|
|
1306
|
+
headers: {
|
|
1307
|
+
'X-Operation-Id': 'githubCodeToToken',
|
|
1308
|
+
...options?.headers,
|
|
1309
|
+
},
|
|
1310
|
+
...options,
|
|
1311
|
+
};
|
|
1312
|
+
const response = await this.httpClient.request(config);
|
|
1313
|
+
return response.data;
|
|
1314
|
+
}
|
|
1315
|
+
/**
|
|
1316
|
+
* Unlink GitHub account
|
|
1317
|
+
*/
|
|
1318
|
+
async unlinkGithub(options) {
|
|
1319
|
+
const config = {
|
|
1320
|
+
method: 'POST',
|
|
1321
|
+
url: `/api/auth/github/unlink`,
|
|
1322
|
+
headers: {
|
|
1323
|
+
'X-Operation-Id': 'unlinkGithub',
|
|
1324
|
+
...options?.headers,
|
|
1325
|
+
},
|
|
1326
|
+
...options,
|
|
1327
|
+
};
|
|
1328
|
+
const response = await this.httpClient.request(config);
|
|
1329
|
+
return response.data;
|
|
1330
|
+
}
|
|
1331
|
+
// ============================================================================
|
|
1332
|
+
// Container APIs
|
|
1333
|
+
// ============================================================================
|
|
1334
|
+
/**
|
|
1335
|
+
* List all containers
|
|
1336
|
+
*/
|
|
1337
|
+
async listContainers(options) {
|
|
1338
|
+
const config = {
|
|
1339
|
+
method: 'GET',
|
|
1340
|
+
url: `/api/containers`,
|
|
1341
|
+
headers: {
|
|
1342
|
+
'X-Operation-Id': 'listContainers',
|
|
1343
|
+
...options?.headers,
|
|
1344
|
+
},
|
|
1345
|
+
...options,
|
|
1346
|
+
};
|
|
1347
|
+
const response = await this.httpClient.request(config);
|
|
1348
|
+
return response.data;
|
|
1349
|
+
}
|
|
1350
|
+
/**
|
|
1351
|
+
* Register a container
|
|
1352
|
+
* Internal service call to register a new container
|
|
1353
|
+
*/
|
|
1354
|
+
async registerContainer(data, options) {
|
|
1355
|
+
const config = {
|
|
1356
|
+
method: 'POST',
|
|
1357
|
+
url: `/api/containers`,
|
|
1358
|
+
data,
|
|
1359
|
+
headers: {
|
|
1360
|
+
'X-Operation-Id': 'registerContainer',
|
|
1361
|
+
...options?.headers,
|
|
1362
|
+
},
|
|
1363
|
+
...options,
|
|
1364
|
+
};
|
|
1365
|
+
const response = await this.httpClient.request(config);
|
|
1366
|
+
return response.data;
|
|
1367
|
+
}
|
|
1368
|
+
/**
|
|
1369
|
+
* Get container by ID
|
|
1370
|
+
*/
|
|
1371
|
+
async getContainer(id, options) {
|
|
1372
|
+
const config = {
|
|
1373
|
+
method: 'GET',
|
|
1374
|
+
url: `/api/containers/${id}`,
|
|
1375
|
+
headers: {
|
|
1376
|
+
'X-Operation-Id': 'getContainer',
|
|
1377
|
+
...options?.headers,
|
|
1378
|
+
},
|
|
1379
|
+
...options,
|
|
1380
|
+
};
|
|
1381
|
+
const response = await this.httpClient.request(config);
|
|
1382
|
+
return response.data;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Unregister a container
|
|
1386
|
+
* Internal service call to unregister a container
|
|
1387
|
+
*/
|
|
1388
|
+
async unregisterContainer(id, options) {
|
|
1389
|
+
const config = {
|
|
1390
|
+
method: 'DELETE',
|
|
1391
|
+
url: `/api/containers/${id}`,
|
|
1392
|
+
headers: {
|
|
1393
|
+
'X-Operation-Id': 'unregisterContainer',
|
|
1394
|
+
...options?.headers,
|
|
1395
|
+
},
|
|
1396
|
+
...options,
|
|
1397
|
+
};
|
|
1398
|
+
const response = await this.httpClient.request(config);
|
|
1399
|
+
return response.data;
|
|
1400
|
+
}
|
|
1401
|
+
/**
|
|
1402
|
+
* Get container statistics
|
|
1403
|
+
*/
|
|
1404
|
+
async getContainerStats(options) {
|
|
1405
|
+
const config = {
|
|
1406
|
+
method: 'GET',
|
|
1407
|
+
url: `/api/containers/stats`,
|
|
1408
|
+
headers: {
|
|
1409
|
+
'X-Operation-Id': 'getContainerStats',
|
|
1410
|
+
...options?.headers,
|
|
1411
|
+
},
|
|
1412
|
+
...options,
|
|
1413
|
+
};
|
|
1414
|
+
const response = await this.httpClient.request(config);
|
|
1415
|
+
return response.data;
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* Container heartbeat
|
|
1419
|
+
* Internal service call to update container heartbeat
|
|
1420
|
+
*/
|
|
1421
|
+
async containerHeartbeat(id, options) {
|
|
1422
|
+
const config = {
|
|
1423
|
+
method: 'POST',
|
|
1424
|
+
url: `/api/containers/${id}/heartbeat`,
|
|
1425
|
+
headers: {
|
|
1426
|
+
'X-Operation-Id': 'containerHeartbeat',
|
|
1427
|
+
...options?.headers,
|
|
1428
|
+
},
|
|
1429
|
+
...options,
|
|
1430
|
+
};
|
|
1431
|
+
const response = await this.httpClient.request(config);
|
|
1432
|
+
return response.data;
|
|
1433
|
+
}
|
|
1434
|
+
// ============================================================================
|
|
1435
|
+
// Conversation APIs
|
|
1436
|
+
// ============================================================================
|
|
1437
|
+
/**
|
|
1438
|
+
* Get conversation status
|
|
1439
|
+
* Query conversation streaming status from Redis
|
|
1440
|
+
*/
|
|
1441
|
+
async getConversationStatus(conversationId, options) {
|
|
1442
|
+
const config = {
|
|
1443
|
+
method: 'GET',
|
|
1444
|
+
url: `/api/conversations/${conversationId}/status`,
|
|
1445
|
+
headers: {
|
|
1446
|
+
'X-Operation-Id': 'getConversationStatus',
|
|
1447
|
+
...options?.headers,
|
|
1448
|
+
},
|
|
1449
|
+
...options,
|
|
1450
|
+
};
|
|
1451
|
+
const response = await this.httpClient.request(config);
|
|
1452
|
+
return response.data;
|
|
1453
|
+
}
|
|
1454
|
+
// ============================================================================
|
|
1455
|
+
// Skills Marketplace APIs
|
|
1456
|
+
// ============================================================================
|
|
1457
|
+
/**
|
|
1458
|
+
* List marketplace skills
|
|
1459
|
+
*/
|
|
1460
|
+
async listMarketplaceSkills(params, options) {
|
|
1461
|
+
const config = {
|
|
1462
|
+
method: 'GET',
|
|
1463
|
+
url: `/api/marketplace/skills`,
|
|
1464
|
+
params,
|
|
1465
|
+
headers: {
|
|
1466
|
+
'X-Operation-Id': 'listMarketplaceSkills',
|
|
1467
|
+
...options?.headers,
|
|
1468
|
+
},
|
|
1469
|
+
...options,
|
|
1470
|
+
};
|
|
1471
|
+
const response = await this.httpClient.request(config);
|
|
1472
|
+
return response.data;
|
|
1473
|
+
}
|
|
1474
|
+
/**
|
|
1475
|
+
* Get skill details
|
|
1476
|
+
*/
|
|
1477
|
+
async getMarketplaceSkill(skillId, options) {
|
|
1478
|
+
const config = {
|
|
1479
|
+
method: 'GET',
|
|
1480
|
+
url: `/api/marketplace/skills/${skillId}`,
|
|
1481
|
+
headers: {
|
|
1482
|
+
'X-Operation-Id': 'getMarketplaceSkill',
|
|
1483
|
+
...options?.headers,
|
|
1484
|
+
},
|
|
1485
|
+
...options,
|
|
1486
|
+
};
|
|
1487
|
+
const response = await this.httpClient.request(config);
|
|
1488
|
+
return response.data;
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1491
|
+
* Install skill from marketplace
|
|
1492
|
+
*/
|
|
1493
|
+
async installSkill(skillId, options) {
|
|
1494
|
+
const config = {
|
|
1495
|
+
method: 'POST',
|
|
1496
|
+
url: `/api/marketplace/skills/${skillId}/download`,
|
|
1497
|
+
headers: {
|
|
1498
|
+
'X-Operation-Id': 'installSkill',
|
|
1499
|
+
...options?.headers,
|
|
1500
|
+
},
|
|
1501
|
+
...options,
|
|
1502
|
+
};
|
|
1503
|
+
const response = await this.httpClient.request(config);
|
|
1504
|
+
return response.data;
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* List user installed skills
|
|
1508
|
+
*/
|
|
1509
|
+
async listUserSkills(options) {
|
|
1510
|
+
const config = {
|
|
1511
|
+
method: 'GET',
|
|
1512
|
+
url: `/api/user/skills`,
|
|
1513
|
+
headers: {
|
|
1514
|
+
'X-Operation-Id': 'listUserSkills',
|
|
1515
|
+
...options?.headers,
|
|
1516
|
+
},
|
|
1517
|
+
...options,
|
|
1518
|
+
};
|
|
1519
|
+
const response = await this.httpClient.request(config);
|
|
1520
|
+
return response.data;
|
|
1521
|
+
}
|
|
1522
|
+
/**
|
|
1523
|
+
* Uninstall skill
|
|
1524
|
+
*/
|
|
1525
|
+
async uninstallSkill(localName, options) {
|
|
1526
|
+
const config = {
|
|
1527
|
+
method: 'DELETE',
|
|
1528
|
+
url: `/api/user/skills/${localName}`,
|
|
1529
|
+
headers: {
|
|
1530
|
+
'X-Operation-Id': 'uninstallSkill',
|
|
1531
|
+
...options?.headers,
|
|
1532
|
+
},
|
|
1533
|
+
...options,
|
|
1534
|
+
};
|
|
1535
|
+
await this.httpClient.request(config);
|
|
1536
|
+
}
|
|
1537
|
+
// ============================================================================
|
|
1538
|
+
// Speech Service APIs
|
|
1539
|
+
// ============================================================================
|
|
1540
|
+
/**
|
|
1541
|
+
* Get Azure Speech token
|
|
1542
|
+
* Get temporary token for Azure Speech Service
|
|
1543
|
+
*/
|
|
1544
|
+
async getSpeechToken(options) {
|
|
1545
|
+
const config = {
|
|
1546
|
+
method: 'GET',
|
|
1547
|
+
url: `/api/speech/token`,
|
|
1548
|
+
headers: {
|
|
1549
|
+
'X-Operation-Id': 'getSpeechToken',
|
|
1550
|
+
...options?.headers,
|
|
1551
|
+
},
|
|
1552
|
+
...options,
|
|
1553
|
+
};
|
|
1554
|
+
const response = await this.httpClient.request(config);
|
|
1555
|
+
return response.data;
|
|
1556
|
+
}
|
|
1557
|
+
// ============================================================================
|
|
1558
|
+
// Deprecated/Legacy Methods (for backward compatibility)
|
|
1559
|
+
// ============================================================================
|
|
1560
|
+
/**
|
|
1561
|
+
* @deprecated Use register() instead
|
|
1562
|
+
*/
|
|
1563
|
+
async postapiauthregister(data, options) {
|
|
1564
|
+
return this.register(data, options);
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* @deprecated Use login() instead
|
|
1568
|
+
*/
|
|
1569
|
+
async postapiauthlogin(data, options) {
|
|
1570
|
+
return this.login(data, options);
|
|
1571
|
+
}
|
|
1572
|
+
/**
|
|
1573
|
+
* @deprecated Use getHealth() instead
|
|
1574
|
+
*/
|
|
1575
|
+
async gethealth(options) {
|
|
1576
|
+
return this.getHealth(options);
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* @deprecated Use listContainers() instead
|
|
1580
|
+
*/
|
|
1581
|
+
async getapicontainers(options) {
|
|
1582
|
+
return this.listContainers(options);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
class AuthModal {
|
|
1587
|
+
constructor(options) {
|
|
1588
|
+
this.modal = null;
|
|
1589
|
+
this.currentView = 'login';
|
|
1590
|
+
this.client = options.client;
|
|
1591
|
+
this.options = options;
|
|
1592
|
+
}
|
|
1593
|
+
/**
|
|
1594
|
+
* Show the authentication modal
|
|
1595
|
+
*/
|
|
1596
|
+
show(initialView = 'login') {
|
|
1597
|
+
this.currentView = initialView;
|
|
1598
|
+
if (!this.modal) {
|
|
1599
|
+
this.createModal();
|
|
1600
|
+
}
|
|
1601
|
+
this.modal.classList.remove('hidden');
|
|
1602
|
+
this.switchView(initialView);
|
|
1603
|
+
}
|
|
1604
|
+
/**
|
|
1605
|
+
* Hide the authentication modal
|
|
1606
|
+
*/
|
|
1607
|
+
hide() {
|
|
1608
|
+
if (this.modal) {
|
|
1609
|
+
this.modal.classList.add('hidden');
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Destroy the modal and remove from DOM
|
|
1614
|
+
*/
|
|
1615
|
+
destroy() {
|
|
1616
|
+
if (this.modal) {
|
|
1617
|
+
this.modal.remove();
|
|
1618
|
+
this.modal = null;
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
createModal() {
|
|
1622
|
+
const theme = this.options.theme || 'dark';
|
|
1623
|
+
// Create modal container
|
|
1624
|
+
this.modal = document.createElement('div');
|
|
1625
|
+
this.modal.id = 'authModal';
|
|
1626
|
+
this.modal.className = `auth-modal auth-modal-${theme}`;
|
|
1627
|
+
// Create backdrop
|
|
1628
|
+
const backdrop = document.createElement('div');
|
|
1629
|
+
backdrop.className = 'auth-modal-backdrop';
|
|
1630
|
+
this.modal.appendChild(backdrop);
|
|
1631
|
+
// Create content container
|
|
1632
|
+
const content = document.createElement('div');
|
|
1633
|
+
content.className = 'auth-modal-content';
|
|
1634
|
+
this.modal.appendChild(content);
|
|
1635
|
+
// Create left panel (branding)
|
|
1636
|
+
const leftPanel = this.createLeftPanel();
|
|
1637
|
+
content.appendChild(leftPanel);
|
|
1638
|
+
// Create right panel (forms)
|
|
1639
|
+
const rightPanel = this.createRightPanel();
|
|
1640
|
+
content.appendChild(rightPanel);
|
|
1641
|
+
// Append to body
|
|
1642
|
+
document.body.appendChild(this.modal);
|
|
1643
|
+
// Bind event listeners
|
|
1644
|
+
this.bindEventListeners();
|
|
1645
|
+
}
|
|
1646
|
+
createLeftPanel() {
|
|
1647
|
+
const leftPanel = document.createElement('div');
|
|
1648
|
+
leftPanel.className = 'auth-modal-left';
|
|
1649
|
+
// Logo
|
|
1650
|
+
const logo = document.createElement('div');
|
|
1651
|
+
logo.className = 'auth-modal-logo';
|
|
1652
|
+
const logoText = document.createElement('span');
|
|
1653
|
+
logoText.className = 'logo-text';
|
|
1654
|
+
logoText.textContent = 'SeaVerse';
|
|
1655
|
+
logo.appendChild(logoText);
|
|
1656
|
+
leftPanel.appendChild(logo);
|
|
1657
|
+
// Decoration
|
|
1658
|
+
const decoration = document.createElement('div');
|
|
1659
|
+
decoration.className = 'auth-modal-decoration';
|
|
1660
|
+
const orb1 = document.createElement('div');
|
|
1661
|
+
orb1.className = 'glow-orb glow-orb-1';
|
|
1662
|
+
const orb2 = document.createElement('div');
|
|
1663
|
+
orb2.className = 'glow-orb glow-orb-2';
|
|
1664
|
+
decoration.appendChild(orb1);
|
|
1665
|
+
decoration.appendChild(orb2);
|
|
1666
|
+
leftPanel.appendChild(decoration);
|
|
1667
|
+
// Branding
|
|
1668
|
+
const branding = document.createElement('div');
|
|
1669
|
+
branding.className = 'auth-modal-branding';
|
|
1670
|
+
const brandingTitle = document.createElement('h2');
|
|
1671
|
+
brandingTitle.className = 'branding-title';
|
|
1672
|
+
brandingTitle.textContent = 'Get Started with Us';
|
|
1673
|
+
const brandingSubtitle = document.createElement('p');
|
|
1674
|
+
brandingSubtitle.className = 'branding-subtitle';
|
|
1675
|
+
brandingSubtitle.textContent = "Every conversation changes this place. You're not just using Infinity. You're creating it.";
|
|
1676
|
+
branding.appendChild(brandingTitle);
|
|
1677
|
+
branding.appendChild(brandingSubtitle);
|
|
1678
|
+
leftPanel.appendChild(branding);
|
|
1679
|
+
return leftPanel;
|
|
1680
|
+
}
|
|
1681
|
+
createRightPanel() {
|
|
1682
|
+
const rightPanel = document.createElement('div');
|
|
1683
|
+
rightPanel.className = 'auth-modal-right';
|
|
1684
|
+
// Close button
|
|
1685
|
+
const closeBtn = document.createElement('button');
|
|
1686
|
+
closeBtn.className = 'auth-modal-close';
|
|
1687
|
+
closeBtn.setAttribute('aria-label', 'Close modal');
|
|
1688
|
+
closeBtn.innerHTML = '<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>';
|
|
1689
|
+
rightPanel.appendChild(closeBtn);
|
|
1690
|
+
// Login form
|
|
1691
|
+
const loginForm = this.createLoginForm();
|
|
1692
|
+
rightPanel.appendChild(loginForm);
|
|
1693
|
+
// Signup form
|
|
1694
|
+
const signupForm = this.createSignupForm();
|
|
1695
|
+
rightPanel.appendChild(signupForm);
|
|
1696
|
+
// Forgot password form
|
|
1697
|
+
const forgotForm = this.createForgotPasswordForm();
|
|
1698
|
+
rightPanel.appendChild(forgotForm);
|
|
1699
|
+
// Success message
|
|
1700
|
+
const successMessage = this.createSuccessMessage();
|
|
1701
|
+
rightPanel.appendChild(successMessage);
|
|
1702
|
+
return rightPanel;
|
|
1703
|
+
}
|
|
1704
|
+
createLoginForm() {
|
|
1705
|
+
const container = document.createElement('div');
|
|
1706
|
+
container.id = 'loginForm';
|
|
1707
|
+
container.className = 'auth-form-view';
|
|
1708
|
+
// Header
|
|
1709
|
+
const header = document.createElement('div');
|
|
1710
|
+
header.className = 'auth-form-header';
|
|
1711
|
+
const title = document.createElement('h2');
|
|
1712
|
+
title.className = 'auth-form-title';
|
|
1713
|
+
title.textContent = 'Welcome back.';
|
|
1714
|
+
const subtitle = document.createElement('p');
|
|
1715
|
+
subtitle.className = 'auth-form-subtitle';
|
|
1716
|
+
subtitle.textContent = "Don't have an account? ";
|
|
1717
|
+
const signupLink = document.createElement('a');
|
|
1718
|
+
signupLink.href = '#';
|
|
1719
|
+
signupLink.id = 'showSignup';
|
|
1720
|
+
signupLink.className = 'link-primary';
|
|
1721
|
+
signupLink.textContent = 'Sign up';
|
|
1722
|
+
subtitle.appendChild(signupLink);
|
|
1723
|
+
header.appendChild(title);
|
|
1724
|
+
header.appendChild(subtitle);
|
|
1725
|
+
container.appendChild(header);
|
|
1726
|
+
// Form
|
|
1727
|
+
const form = document.createElement('form');
|
|
1728
|
+
form.id = 'loginFormElement';
|
|
1729
|
+
form.className = 'auth-form';
|
|
1730
|
+
// Email field
|
|
1731
|
+
const emailGroup = this.createFormGroup('loginEmail', 'Email', 'email', 'Email');
|
|
1732
|
+
form.appendChild(emailGroup);
|
|
1733
|
+
// Password field with forgot link
|
|
1734
|
+
const passwordGroup = document.createElement('div');
|
|
1735
|
+
passwordGroup.className = 'form-group';
|
|
1736
|
+
const groupHeader = document.createElement('div');
|
|
1737
|
+
groupHeader.className = 'form-group-header';
|
|
1738
|
+
const passwordLabel = document.createElement('label');
|
|
1739
|
+
passwordLabel.htmlFor = 'loginPassword';
|
|
1740
|
+
passwordLabel.className = 'form-label';
|
|
1741
|
+
passwordLabel.textContent = 'Password';
|
|
1742
|
+
const forgotLink = document.createElement('a');
|
|
1743
|
+
forgotLink.href = '#';
|
|
1744
|
+
forgotLink.id = 'forgotPasswordLink';
|
|
1745
|
+
forgotLink.className = 'forgot-password-link';
|
|
1746
|
+
forgotLink.textContent = 'Forgot Password?';
|
|
1747
|
+
groupHeader.appendChild(passwordLabel);
|
|
1748
|
+
groupHeader.appendChild(forgotLink);
|
|
1749
|
+
passwordGroup.appendChild(groupHeader);
|
|
1750
|
+
const passwordInputWrapper = this.createPasswordInput('loginPassword', 'Password');
|
|
1751
|
+
passwordGroup.appendChild(passwordInputWrapper);
|
|
1752
|
+
form.appendChild(passwordGroup);
|
|
1753
|
+
// Submit button
|
|
1754
|
+
const submitBtn = document.createElement('button');
|
|
1755
|
+
submitBtn.type = 'submit';
|
|
1756
|
+
submitBtn.id = 'loginButton';
|
|
1757
|
+
submitBtn.className = 'btn-auth-primary';
|
|
1758
|
+
const btnText = document.createElement('span');
|
|
1759
|
+
btnText.className = 'btn-text';
|
|
1760
|
+
btnText.textContent = 'Sign In';
|
|
1761
|
+
const btnLoader = document.createElement('span');
|
|
1762
|
+
btnLoader.className = 'btn-loader hidden';
|
|
1763
|
+
btnLoader.innerHTML = '<svg class="spinner" viewBox="0 0 24 24"><circle class="spinner-track" cx="12" cy="12" r="10"></circle><circle class="spinner-circle" cx="12" cy="12" r="10"></circle></svg>';
|
|
1764
|
+
submitBtn.appendChild(btnText);
|
|
1765
|
+
submitBtn.appendChild(btnLoader);
|
|
1766
|
+
form.appendChild(submitBtn);
|
|
1767
|
+
// Divider
|
|
1768
|
+
const divider = document.createElement('div');
|
|
1769
|
+
divider.className = 'divider';
|
|
1770
|
+
divider.textContent = 'OR SIGN IN WITH';
|
|
1771
|
+
form.appendChild(divider);
|
|
1772
|
+
// Social buttons grid (Google + GitHub)
|
|
1773
|
+
const socialGrid = document.createElement('div');
|
|
1774
|
+
socialGrid.className = 'social-buttons-grid';
|
|
1775
|
+
// Google button
|
|
1776
|
+
const googleBtn = this.createSocialButton('google', 'Google', 'login');
|
|
1777
|
+
socialGrid.appendChild(googleBtn);
|
|
1778
|
+
// GitHub button
|
|
1779
|
+
const githubBtn = this.createSocialButton('github', 'Github', 'login');
|
|
1780
|
+
socialGrid.appendChild(githubBtn);
|
|
1781
|
+
form.appendChild(socialGrid);
|
|
1782
|
+
// Discord button (full width)
|
|
1783
|
+
const discordBtn = this.createSocialButton('discord', 'Discord', 'login', true);
|
|
1784
|
+
form.appendChild(discordBtn);
|
|
1785
|
+
container.appendChild(form);
|
|
1786
|
+
return container;
|
|
1787
|
+
}
|
|
1788
|
+
createSignupForm() {
|
|
1789
|
+
const container = document.createElement('div');
|
|
1790
|
+
container.id = 'signupForm';
|
|
1791
|
+
container.className = 'auth-form-view hidden';
|
|
1792
|
+
// Header
|
|
1793
|
+
const header = document.createElement('div');
|
|
1794
|
+
header.className = 'auth-form-header';
|
|
1795
|
+
const title = document.createElement('h2');
|
|
1796
|
+
title.className = 'auth-form-title';
|
|
1797
|
+
title.textContent = 'Create an account.';
|
|
1798
|
+
const subtitle = document.createElement('p');
|
|
1799
|
+
subtitle.className = 'auth-form-subtitle';
|
|
1800
|
+
subtitle.textContent = 'Already have an account? ';
|
|
1801
|
+
const loginLink = document.createElement('a');
|
|
1802
|
+
loginLink.href = '#';
|
|
1803
|
+
loginLink.id = 'showLogin';
|
|
1804
|
+
loginLink.className = 'link-primary';
|
|
1805
|
+
loginLink.textContent = 'Sign in';
|
|
1806
|
+
subtitle.appendChild(loginLink);
|
|
1807
|
+
header.appendChild(title);
|
|
1808
|
+
header.appendChild(subtitle);
|
|
1809
|
+
container.appendChild(header);
|
|
1810
|
+
// Form
|
|
1811
|
+
const form = document.createElement('form');
|
|
1812
|
+
form.id = 'signupFormElement';
|
|
1813
|
+
form.className = 'auth-form';
|
|
1814
|
+
// Email field
|
|
1815
|
+
const emailGroup = this.createFormGroup('signupEmail', 'Email', 'email', 'Email');
|
|
1816
|
+
form.appendChild(emailGroup);
|
|
1817
|
+
// Password field
|
|
1818
|
+
const passwordGroup = document.createElement('div');
|
|
1819
|
+
passwordGroup.className = 'form-group';
|
|
1820
|
+
const passwordLabel = document.createElement('label');
|
|
1821
|
+
passwordLabel.htmlFor = 'signupPassword';
|
|
1822
|
+
passwordLabel.className = 'form-label';
|
|
1823
|
+
passwordLabel.textContent = 'Password';
|
|
1824
|
+
passwordGroup.appendChild(passwordLabel);
|
|
1825
|
+
const passwordInputWrapper = this.createPasswordInput('signupPassword', 'Password');
|
|
1826
|
+
passwordGroup.appendChild(passwordInputWrapper);
|
|
1827
|
+
const strengthText = document.createElement('p');
|
|
1828
|
+
strengthText.className = 'strength-text';
|
|
1829
|
+
strengthText.textContent = 'Use 8+ characters with mix of letters, numbers & symbols';
|
|
1830
|
+
passwordGroup.appendChild(strengthText);
|
|
1831
|
+
form.appendChild(passwordGroup);
|
|
1832
|
+
// Confirm password field
|
|
1833
|
+
const confirmGroup = document.createElement('div');
|
|
1834
|
+
confirmGroup.className = 'form-group';
|
|
1835
|
+
const confirmLabel = document.createElement('label');
|
|
1836
|
+
confirmLabel.htmlFor = 'signupPasswordConfirm';
|
|
1837
|
+
confirmLabel.className = 'form-label';
|
|
1838
|
+
confirmLabel.textContent = 'Confirm Password';
|
|
1839
|
+
confirmGroup.appendChild(confirmLabel);
|
|
1840
|
+
const confirmInputWrapper = this.createPasswordInput('signupPasswordConfirm', 'Confirm Password');
|
|
1841
|
+
confirmGroup.appendChild(confirmInputWrapper);
|
|
1842
|
+
form.appendChild(confirmGroup);
|
|
1843
|
+
// Submit button
|
|
1844
|
+
const submitBtn = document.createElement('button');
|
|
1845
|
+
submitBtn.type = 'submit';
|
|
1846
|
+
submitBtn.id = 'signupButton';
|
|
1847
|
+
submitBtn.className = 'btn-auth-primary';
|
|
1848
|
+
const btnText = document.createElement('span');
|
|
1849
|
+
btnText.className = 'btn-text';
|
|
1850
|
+
btnText.textContent = 'Sign Up';
|
|
1851
|
+
const btnLoader = document.createElement('span');
|
|
1852
|
+
btnLoader.className = 'btn-loader hidden';
|
|
1853
|
+
btnLoader.innerHTML = '<svg class="spinner" viewBox="0 0 24 24"><circle class="spinner-track" cx="12" cy="12" r="10"></circle><circle class="spinner-circle" cx="12" cy="12" r="10"></circle></svg>';
|
|
1854
|
+
submitBtn.appendChild(btnText);
|
|
1855
|
+
submitBtn.appendChild(btnLoader);
|
|
1856
|
+
form.appendChild(submitBtn);
|
|
1857
|
+
// Divider
|
|
1858
|
+
const divider = document.createElement('div');
|
|
1859
|
+
divider.className = 'divider';
|
|
1860
|
+
divider.textContent = 'OR SIGN UP WITH';
|
|
1861
|
+
form.appendChild(divider);
|
|
1862
|
+
// Social buttons grid (Google + GitHub)
|
|
1863
|
+
const socialGrid = document.createElement('div');
|
|
1864
|
+
socialGrid.className = 'social-buttons-grid';
|
|
1865
|
+
// Google button
|
|
1866
|
+
const googleBtn = this.createSocialButton('google', 'Google', 'signup');
|
|
1867
|
+
socialGrid.appendChild(googleBtn);
|
|
1868
|
+
// GitHub button
|
|
1869
|
+
const githubBtn = this.createSocialButton('github', 'Github', 'signup');
|
|
1870
|
+
socialGrid.appendChild(githubBtn);
|
|
1871
|
+
form.appendChild(socialGrid);
|
|
1872
|
+
// Discord button (full width)
|
|
1873
|
+
const discordBtn = this.createSocialButton('discord', 'Discord', 'signup', true);
|
|
1874
|
+
form.appendChild(discordBtn);
|
|
1875
|
+
container.appendChild(form);
|
|
1876
|
+
return container;
|
|
1877
|
+
}
|
|
1878
|
+
createForgotPasswordForm() {
|
|
1879
|
+
const container = document.createElement('div');
|
|
1880
|
+
container.id = 'forgotPasswordForm';
|
|
1881
|
+
container.className = 'auth-form-view hidden';
|
|
1882
|
+
// Icon
|
|
1883
|
+
const icon = document.createElement('div');
|
|
1884
|
+
icon.className = 'forgot-password-icon';
|
|
1885
|
+
icon.innerHTML = '<div class="icon-glow"></div><svg class="icon-lock" width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path class="lock-shackle" d="M7 11V7a5 5 0 0 1 10 0v4"/><circle class="lock-keyhole" cx="12" cy="16" r="1.5"/></svg>';
|
|
1886
|
+
container.appendChild(icon);
|
|
1887
|
+
// Header
|
|
1888
|
+
const header = document.createElement('div');
|
|
1889
|
+
header.className = 'auth-form-header';
|
|
1890
|
+
const title = document.createElement('h2');
|
|
1891
|
+
title.className = 'auth-form-title';
|
|
1892
|
+
title.textContent = 'Reset Password';
|
|
1893
|
+
const subtitle = document.createElement('p');
|
|
1894
|
+
subtitle.className = 'auth-form-subtitle';
|
|
1895
|
+
subtitle.textContent = "We'll send a magic link to your inbox";
|
|
1896
|
+
header.appendChild(title);
|
|
1897
|
+
header.appendChild(subtitle);
|
|
1898
|
+
container.appendChild(header);
|
|
1899
|
+
// Form
|
|
1900
|
+
const form = document.createElement('form');
|
|
1901
|
+
form.id = 'forgotPasswordFormElement';
|
|
1902
|
+
form.className = 'auth-form';
|
|
1903
|
+
const emailGroup = this.createFormGroup('resetEmail', 'Email Address', 'email', 'Enter your email');
|
|
1904
|
+
form.appendChild(emailGroup);
|
|
1905
|
+
const submitBtn = document.createElement('button');
|
|
1906
|
+
submitBtn.type = 'submit';
|
|
1907
|
+
submitBtn.id = 'resetButton';
|
|
1908
|
+
submitBtn.className = 'btn-auth-primary btn-forgot-password';
|
|
1909
|
+
submitBtn.innerHTML = '<span class="btn-text">Send Reset Link</span><svg class="btn-arrow" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14M12 5l7 7-7 7"/></svg>';
|
|
1910
|
+
form.appendChild(submitBtn);
|
|
1911
|
+
container.appendChild(form);
|
|
1912
|
+
// Footer
|
|
1913
|
+
const footer = document.createElement('div');
|
|
1914
|
+
footer.className = 'auth-footer forgot-footer';
|
|
1915
|
+
const backLink = document.createElement('a');
|
|
1916
|
+
backLink.href = '#';
|
|
1917
|
+
backLink.id = 'backToLogin';
|
|
1918
|
+
backLink.className = 'back-to-login';
|
|
1919
|
+
backLink.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 12H5M12 19l-7-7 7-7"/></svg><span>Back to Sign In</span>';
|
|
1920
|
+
footer.appendChild(backLink);
|
|
1921
|
+
container.appendChild(footer);
|
|
1922
|
+
return container;
|
|
1923
|
+
}
|
|
1924
|
+
createSuccessMessage() {
|
|
1925
|
+
const container = document.createElement('div');
|
|
1926
|
+
container.id = 'authMessage';
|
|
1927
|
+
container.className = 'auth-form-view hidden';
|
|
1928
|
+
const content = document.createElement('div');
|
|
1929
|
+
content.className = 'auth-message-content';
|
|
1930
|
+
const icon = document.createElement('div');
|
|
1931
|
+
icon.className = 'message-icon';
|
|
1932
|
+
icon.innerHTML = '<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>';
|
|
1933
|
+
content.appendChild(icon);
|
|
1934
|
+
const title = document.createElement('h3');
|
|
1935
|
+
title.id = 'messageTitle';
|
|
1936
|
+
title.className = 'message-title';
|
|
1937
|
+
title.textContent = 'Success!';
|
|
1938
|
+
content.appendChild(title);
|
|
1939
|
+
const text = document.createElement('p');
|
|
1940
|
+
text.id = 'messageText';
|
|
1941
|
+
text.className = 'message-text';
|
|
1942
|
+
text.textContent = 'Operation completed successfully.';
|
|
1943
|
+
content.appendChild(text);
|
|
1944
|
+
const button = document.createElement('button');
|
|
1945
|
+
button.id = 'messageButton';
|
|
1946
|
+
button.className = 'btn-auth-primary';
|
|
1947
|
+
button.textContent = 'Continue';
|
|
1948
|
+
content.appendChild(button);
|
|
1949
|
+
container.appendChild(content);
|
|
1950
|
+
return container;
|
|
1951
|
+
}
|
|
1952
|
+
createFormGroup(id, label, type, placeholder) {
|
|
1953
|
+
const group = document.createElement('div');
|
|
1954
|
+
group.className = 'form-group';
|
|
1955
|
+
const labelEl = document.createElement('label');
|
|
1956
|
+
labelEl.htmlFor = id;
|
|
1957
|
+
labelEl.className = 'form-label';
|
|
1958
|
+
labelEl.textContent = label;
|
|
1959
|
+
group.appendChild(labelEl);
|
|
1960
|
+
const input = document.createElement('input');
|
|
1961
|
+
input.type = type;
|
|
1962
|
+
input.id = id;
|
|
1963
|
+
input.className = 'form-input';
|
|
1964
|
+
input.placeholder = placeholder;
|
|
1965
|
+
input.required = true;
|
|
1966
|
+
group.appendChild(input);
|
|
1967
|
+
return group;
|
|
1968
|
+
}
|
|
1969
|
+
createPasswordInput(id, placeholder) {
|
|
1970
|
+
const wrapper = document.createElement('div');
|
|
1971
|
+
wrapper.className = 'input-with-icon';
|
|
1972
|
+
const input = document.createElement('input');
|
|
1973
|
+
input.type = 'password';
|
|
1974
|
+
input.id = id;
|
|
1975
|
+
input.className = 'form-input';
|
|
1976
|
+
input.placeholder = placeholder;
|
|
1977
|
+
input.required = true;
|
|
1978
|
+
wrapper.appendChild(input);
|
|
1979
|
+
const toggleBtn = document.createElement('button');
|
|
1980
|
+
toggleBtn.type = 'button';
|
|
1981
|
+
toggleBtn.className = 'input-icon-btn toggle-password';
|
|
1982
|
+
toggleBtn.dataset.target = id;
|
|
1983
|
+
toggleBtn.innerHTML = '<svg class="icon-eye" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg><svg class="icon-eye-off hidden" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>';
|
|
1984
|
+
wrapper.appendChild(toggleBtn);
|
|
1985
|
+
return wrapper;
|
|
1986
|
+
}
|
|
1987
|
+
createSocialButton(provider, label, mode, fullWidth = false) {
|
|
1988
|
+
const button = document.createElement('button');
|
|
1989
|
+
button.type = 'button';
|
|
1990
|
+
button.className = fullWidth ? 'btn-social btn-social-full' : 'btn-social';
|
|
1991
|
+
button.id = `${provider}${mode === 'login' ? 'SignIn' : 'SignUp'}Modal`;
|
|
1992
|
+
// SVG icons for each provider
|
|
1993
|
+
const icons = {
|
|
1994
|
+
google: `<svg width="20" height="20" viewBox="0 0 24 24">
|
|
1995
|
+
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
|
|
1996
|
+
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
|
1997
|
+
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
|
1998
|
+
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
|
1999
|
+
</svg>`,
|
|
2000
|
+
github: `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
|
2001
|
+
<path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0 1 12 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z"/>
|
|
2002
|
+
</svg>`,
|
|
2003
|
+
discord: `<svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2">
|
|
2004
|
+
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/>
|
|
2005
|
+
</svg>`
|
|
2006
|
+
};
|
|
2007
|
+
button.innerHTML = icons[provider];
|
|
2008
|
+
const span = document.createElement('span');
|
|
2009
|
+
span.textContent = label;
|
|
2010
|
+
button.appendChild(span);
|
|
2011
|
+
return button;
|
|
2012
|
+
}
|
|
2013
|
+
bindEventListeners() {
|
|
2014
|
+
if (!this.modal)
|
|
2015
|
+
return;
|
|
2016
|
+
// Close button
|
|
2017
|
+
const closeBtn = this.modal.querySelector('.auth-modal-close');
|
|
2018
|
+
closeBtn?.addEventListener('click', () => this.hide());
|
|
2019
|
+
// Backdrop click to close
|
|
2020
|
+
const backdrop = this.modal.querySelector('.auth-modal-backdrop');
|
|
2021
|
+
backdrop?.addEventListener('click', () => this.hide());
|
|
2022
|
+
// View switching
|
|
2023
|
+
const showSignup = this.modal.querySelector('#showSignup');
|
|
2024
|
+
showSignup?.addEventListener('click', (e) => {
|
|
2025
|
+
e.preventDefault();
|
|
2026
|
+
this.switchView('signup');
|
|
2027
|
+
});
|
|
2028
|
+
const showLogin = this.modal.querySelector('#showLogin');
|
|
2029
|
+
showLogin?.addEventListener('click', (e) => {
|
|
2030
|
+
e.preventDefault();
|
|
2031
|
+
this.switchView('login');
|
|
2032
|
+
});
|
|
2033
|
+
const forgotPasswordLink = this.modal.querySelector('#forgotPasswordLink');
|
|
2034
|
+
forgotPasswordLink?.addEventListener('click', (e) => {
|
|
2035
|
+
e.preventDefault();
|
|
2036
|
+
this.switchView('forgot');
|
|
2037
|
+
});
|
|
2038
|
+
const backToLogin = this.modal.querySelector('#backToLogin');
|
|
2039
|
+
backToLogin?.addEventListener('click', (e) => {
|
|
2040
|
+
e.preventDefault();
|
|
2041
|
+
this.switchView('login');
|
|
2042
|
+
});
|
|
2043
|
+
// Password toggle
|
|
2044
|
+
const passwordToggles = this.modal.querySelectorAll('.toggle-password');
|
|
2045
|
+
passwordToggles.forEach((toggle) => {
|
|
2046
|
+
toggle.addEventListener('click', (e) => {
|
|
2047
|
+
e.preventDefault();
|
|
2048
|
+
const target = toggle.dataset.target;
|
|
2049
|
+
if (target) {
|
|
2050
|
+
const input = this.modal.querySelector(`#${target}`);
|
|
2051
|
+
const iconEye = toggle.querySelector('.icon-eye');
|
|
2052
|
+
const iconEyeOff = toggle.querySelector('.icon-eye-off');
|
|
2053
|
+
if (input.type === 'password') {
|
|
2054
|
+
input.type = 'text';
|
|
2055
|
+
iconEye?.classList.add('hidden');
|
|
2056
|
+
iconEyeOff?.classList.remove('hidden');
|
|
2057
|
+
}
|
|
2058
|
+
else {
|
|
2059
|
+
input.type = 'password';
|
|
2060
|
+
iconEye?.classList.remove('hidden');
|
|
2061
|
+
iconEyeOff?.classList.add('hidden');
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
});
|
|
2065
|
+
});
|
|
2066
|
+
// Form submissions
|
|
2067
|
+
const loginForm = this.modal.querySelector('#loginFormElement');
|
|
2068
|
+
loginForm?.addEventListener('submit', (e) => this.handleLogin(e));
|
|
2069
|
+
const signupForm = this.modal.querySelector('#signupFormElement');
|
|
2070
|
+
signupForm?.addEventListener('submit', (e) => this.handleSignup(e));
|
|
2071
|
+
const forgotForm = this.modal.querySelector('#forgotPasswordFormElement');
|
|
2072
|
+
forgotForm?.addEventListener('submit', (e) => this.handleForgotPassword(e));
|
|
2073
|
+
// OAuth social login buttons
|
|
2074
|
+
this.bindSocialLoginButtons();
|
|
2075
|
+
}
|
|
2076
|
+
/**
|
|
2077
|
+
* Bind click events to social login buttons
|
|
2078
|
+
*/
|
|
2079
|
+
bindSocialLoginButtons() {
|
|
2080
|
+
if (!this.modal)
|
|
2081
|
+
return;
|
|
2082
|
+
// Google login buttons
|
|
2083
|
+
const googleSignInBtn = this.modal.querySelector('#googleSignInModal');
|
|
2084
|
+
googleSignInBtn?.addEventListener('click', (e) => {
|
|
2085
|
+
e.preventDefault();
|
|
2086
|
+
this.startOAuthFlow('google');
|
|
2087
|
+
});
|
|
2088
|
+
const googleSignUpBtn = this.modal.querySelector('#googleSignUpModal');
|
|
2089
|
+
googleSignUpBtn?.addEventListener('click', (e) => {
|
|
2090
|
+
e.preventDefault();
|
|
2091
|
+
this.startOAuthFlow('google');
|
|
2092
|
+
});
|
|
2093
|
+
// Discord login buttons
|
|
2094
|
+
const discordSignInBtn = this.modal.querySelector('#discordSignInModal');
|
|
2095
|
+
discordSignInBtn?.addEventListener('click', (e) => {
|
|
2096
|
+
e.preventDefault();
|
|
2097
|
+
this.startOAuthFlow('discord');
|
|
2098
|
+
});
|
|
2099
|
+
const discordSignUpBtn = this.modal.querySelector('#discordSignUpModal');
|
|
2100
|
+
discordSignUpBtn?.addEventListener('click', (e) => {
|
|
2101
|
+
e.preventDefault();
|
|
2102
|
+
this.startOAuthFlow('discord');
|
|
2103
|
+
});
|
|
2104
|
+
// GitHub login buttons
|
|
2105
|
+
const githubSignInBtn = this.modal.querySelector('#githubSignInModal');
|
|
2106
|
+
githubSignInBtn?.addEventListener('click', (e) => {
|
|
2107
|
+
e.preventDefault();
|
|
2108
|
+
this.startOAuthFlow('github');
|
|
2109
|
+
});
|
|
2110
|
+
const githubSignUpBtn = this.modal.querySelector('#githubSignUpModal');
|
|
2111
|
+
githubSignUpBtn?.addEventListener('click', (e) => {
|
|
2112
|
+
e.preventDefault();
|
|
2113
|
+
this.startOAuthFlow('github');
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
switchView(view) {
|
|
2117
|
+
if (!this.modal)
|
|
2118
|
+
return;
|
|
2119
|
+
const views = ['loginForm', 'signupForm', 'forgotPasswordForm', 'authMessage'];
|
|
2120
|
+
views.forEach((viewId) => {
|
|
2121
|
+
const element = this.modal.querySelector(`#${viewId}`);
|
|
2122
|
+
element?.classList.add('hidden');
|
|
2123
|
+
});
|
|
2124
|
+
const viewMap = {
|
|
2125
|
+
login: 'loginForm',
|
|
2126
|
+
signup: 'signupForm',
|
|
2127
|
+
forgot: 'forgotPasswordForm',
|
|
2128
|
+
message: 'authMessage',
|
|
2129
|
+
};
|
|
2130
|
+
const targetView = this.modal.querySelector(`#${viewMap[view]}`);
|
|
2131
|
+
targetView?.classList.remove('hidden');
|
|
2132
|
+
this.currentView = view;
|
|
2133
|
+
}
|
|
2134
|
+
async handleLogin(e) {
|
|
2135
|
+
e.preventDefault();
|
|
2136
|
+
const emailInput = this.modal?.querySelector('#loginEmail');
|
|
2137
|
+
const passwordInput = this.modal?.querySelector('#loginPassword');
|
|
2138
|
+
const submitBtn = this.modal?.querySelector('#loginButton');
|
|
2139
|
+
const btnText = submitBtn?.querySelector('.btn-text');
|
|
2140
|
+
const btnLoader = submitBtn?.querySelector('.btn-loader');
|
|
2141
|
+
if (!emailInput || !passwordInput || !submitBtn)
|
|
2142
|
+
return;
|
|
2143
|
+
const email = emailInput.value;
|
|
2144
|
+
const password = passwordInput.value;
|
|
2145
|
+
try {
|
|
2146
|
+
// Show loading state
|
|
2147
|
+
submitBtn.disabled = true;
|
|
2148
|
+
btnText?.classList.add('hidden');
|
|
2149
|
+
btnLoader?.classList.remove('hidden');
|
|
2150
|
+
// Call login API using new typed method
|
|
2151
|
+
const response = await this.client.login({
|
|
2152
|
+
email,
|
|
2153
|
+
password,
|
|
2154
|
+
});
|
|
2155
|
+
// Handle success
|
|
2156
|
+
if (response.token) {
|
|
2157
|
+
if (this.options.onLoginSuccess) {
|
|
2158
|
+
this.options.onLoginSuccess(response.token, response.user);
|
|
2159
|
+
}
|
|
2160
|
+
this.showMessage('Login Successful', 'Welcome back!');
|
|
2161
|
+
}
|
|
2162
|
+
else {
|
|
2163
|
+
throw new Error('Invalid response from server');
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
catch (error) {
|
|
2167
|
+
// Handle error
|
|
2168
|
+
const errorMessage = error instanceof Error ? error.message : 'Login failed';
|
|
2169
|
+
this.showError(errorMessage);
|
|
2170
|
+
if (this.options.onError) {
|
|
2171
|
+
this.options.onError(error);
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2174
|
+
finally {
|
|
2175
|
+
// Reset loading state
|
|
2176
|
+
submitBtn.disabled = false;
|
|
2177
|
+
btnText?.classList.remove('hidden');
|
|
2178
|
+
btnLoader?.classList.add('hidden');
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
async handleSignup(e) {
|
|
2182
|
+
e.preventDefault();
|
|
2183
|
+
const emailInput = this.modal?.querySelector('#signupEmail');
|
|
2184
|
+
const passwordInput = this.modal?.querySelector('#signupPassword');
|
|
2185
|
+
const passwordConfirmInput = this.modal?.querySelector('#signupPasswordConfirm');
|
|
2186
|
+
const submitBtn = this.modal?.querySelector('#signupButton');
|
|
2187
|
+
const btnText = submitBtn?.querySelector('.btn-text');
|
|
2188
|
+
const btnLoader = submitBtn?.querySelector('.btn-loader');
|
|
2189
|
+
if (!emailInput || !passwordInput || !passwordConfirmInput || !submitBtn)
|
|
2190
|
+
return;
|
|
2191
|
+
const email = emailInput.value;
|
|
2192
|
+
const password = passwordInput.value;
|
|
2193
|
+
const passwordConfirm = passwordConfirmInput.value;
|
|
2194
|
+
// Validate passwords match
|
|
2195
|
+
if (password !== passwordConfirm) {
|
|
2196
|
+
this.showError('Passwords do not match');
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
try {
|
|
2200
|
+
// Show loading state
|
|
2201
|
+
submitBtn.disabled = true;
|
|
2202
|
+
btnText?.classList.add('hidden');
|
|
2203
|
+
btnLoader?.classList.remove('hidden');
|
|
2204
|
+
// Call signup API using new typed method
|
|
2205
|
+
const response = await this.client.register({
|
|
2206
|
+
email,
|
|
2207
|
+
password,
|
|
2208
|
+
});
|
|
2209
|
+
// Handle success - Note: register returns different response format
|
|
2210
|
+
if (response.success) {
|
|
2211
|
+
// After successful registration, perform login to get token
|
|
2212
|
+
const loginResponse = await this.client.login({ email, password });
|
|
2213
|
+
if (loginResponse.token) {
|
|
2214
|
+
if (this.options.onSignupSuccess) {
|
|
2215
|
+
this.options.onSignupSuccess(loginResponse.token, loginResponse.user);
|
|
2216
|
+
}
|
|
2217
|
+
this.showMessage('Account Created', response.message || 'Your account has been created successfully!');
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
else {
|
|
2221
|
+
throw new Error('Registration failed');
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
catch (error) {
|
|
2225
|
+
// Handle error
|
|
2226
|
+
const errorMessage = error instanceof Error ? error.message : 'Signup failed';
|
|
2227
|
+
this.showError(errorMessage);
|
|
2228
|
+
if (this.options.onError) {
|
|
2229
|
+
this.options.onError(error);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
finally {
|
|
2233
|
+
// Reset loading state
|
|
2234
|
+
submitBtn.disabled = false;
|
|
2235
|
+
btnText?.classList.remove('hidden');
|
|
2236
|
+
btnLoader?.classList.add('hidden');
|
|
2237
|
+
}
|
|
2238
|
+
}
|
|
2239
|
+
async handleForgotPassword(e) {
|
|
2240
|
+
e.preventDefault();
|
|
2241
|
+
const emailInput = this.modal?.querySelector('#resetEmail');
|
|
2242
|
+
const submitBtn = this.modal?.querySelector('#resetButton');
|
|
2243
|
+
if (!emailInput || !submitBtn)
|
|
2244
|
+
return;
|
|
2245
|
+
const email = emailInput.value;
|
|
2246
|
+
try {
|
|
2247
|
+
// Show loading state
|
|
2248
|
+
submitBtn.disabled = true;
|
|
2249
|
+
// TODO: Call forgot password API when available
|
|
2250
|
+
// await this.client.postapiauthforgotpassword({ email });
|
|
2251
|
+
// Show success message
|
|
2252
|
+
this.showMessage('Reset Link Sent', `We've sent a password reset link to ${email}`);
|
|
2253
|
+
}
|
|
2254
|
+
catch (error) {
|
|
2255
|
+
// Handle error
|
|
2256
|
+
const errorMessage = error instanceof Error ? error.message : 'Failed to send reset link';
|
|
2257
|
+
this.showError(errorMessage);
|
|
2258
|
+
if (this.options.onError) {
|
|
2259
|
+
this.options.onError(error);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
finally {
|
|
2263
|
+
// Reset loading state
|
|
2264
|
+
submitBtn.disabled = false;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
showMessage(title, message) {
|
|
2268
|
+
if (!this.modal)
|
|
2269
|
+
return;
|
|
2270
|
+
const messageTitle = this.modal.querySelector('#messageTitle');
|
|
2271
|
+
const messageText = this.modal.querySelector('#messageText');
|
|
2272
|
+
const messageButton = this.modal.querySelector('#messageButton');
|
|
2273
|
+
if (messageTitle)
|
|
2274
|
+
messageTitle.textContent = title;
|
|
2275
|
+
if (messageText)
|
|
2276
|
+
messageText.textContent = message;
|
|
2277
|
+
messageButton?.addEventListener('click', () => {
|
|
2278
|
+
this.hide();
|
|
2279
|
+
}, { once: true });
|
|
2280
|
+
this.switchView('message');
|
|
2281
|
+
}
|
|
2282
|
+
showError(message) {
|
|
2283
|
+
// Simple error display - you can enhance this
|
|
2284
|
+
alert(message);
|
|
2285
|
+
}
|
|
2286
|
+
// ============================================================================
|
|
2287
|
+
// OAuth Methods
|
|
2288
|
+
// ============================================================================
|
|
2289
|
+
/**
|
|
2290
|
+
* Start OAuth flow for a given provider
|
|
2291
|
+
*/
|
|
2292
|
+
startOAuthFlow(provider) {
|
|
2293
|
+
const config = this.options.oauthConfig?.[provider];
|
|
2294
|
+
if (!config) {
|
|
2295
|
+
this.showError(`${provider} OAuth is not configured`);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
// Store the current state (to verify callback)
|
|
2299
|
+
const state = this.generateRandomState();
|
|
2300
|
+
sessionStorage.setItem('oauth_state', state);
|
|
2301
|
+
sessionStorage.setItem('oauth_provider', provider);
|
|
2302
|
+
// Build authorization URL
|
|
2303
|
+
const authUrl = this.buildAuthUrl(provider, config, state);
|
|
2304
|
+
// Redirect to OAuth provider
|
|
2305
|
+
window.location.href = authUrl;
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* Generate random state for CSRF protection
|
|
2309
|
+
*/
|
|
2310
|
+
generateRandomState() {
|
|
2311
|
+
const array = new Uint8Array(32);
|
|
2312
|
+
crypto.getRandomValues(array);
|
|
2313
|
+
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join('');
|
|
2314
|
+
}
|
|
2315
|
+
/**
|
|
2316
|
+
* Build authorization URL for each provider
|
|
2317
|
+
*/
|
|
2318
|
+
buildAuthUrl(provider, config, state) {
|
|
2319
|
+
const params = new URLSearchParams({
|
|
2320
|
+
client_id: config.clientId,
|
|
2321
|
+
redirect_uri: config.redirectUri,
|
|
2322
|
+
state,
|
|
2323
|
+
response_type: 'code',
|
|
2324
|
+
});
|
|
2325
|
+
// Provider-specific configurations
|
|
2326
|
+
switch (provider) {
|
|
2327
|
+
case 'google':
|
|
2328
|
+
params.append('scope', config.scope || 'openid email profile');
|
|
2329
|
+
params.append('access_type', 'offline');
|
|
2330
|
+
params.append('prompt', 'consent');
|
|
2331
|
+
return `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
|
|
2332
|
+
case 'discord':
|
|
2333
|
+
params.append('scope', config.scope || 'identify email');
|
|
2334
|
+
return `https://discord.com/api/oauth2/authorize?${params.toString()}`;
|
|
2335
|
+
case 'github':
|
|
2336
|
+
params.append('scope', config.scope || 'read:user user:email');
|
|
2337
|
+
return `https://github.com/login/oauth/authorize?${params.toString()}`;
|
|
2338
|
+
default:
|
|
2339
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Handle OAuth callback
|
|
2344
|
+
* Call this method from your redirect page with the code and state from URL
|
|
2345
|
+
*/
|
|
2346
|
+
async handleOAuthCallback(code, state) {
|
|
2347
|
+
try {
|
|
2348
|
+
// Verify state
|
|
2349
|
+
const storedState = sessionStorage.getItem('oauth_state');
|
|
2350
|
+
const provider = sessionStorage.getItem('oauth_provider');
|
|
2351
|
+
if (!storedState || storedState !== state) {
|
|
2352
|
+
throw new Error('Invalid state parameter - possible CSRF attack');
|
|
2353
|
+
}
|
|
2354
|
+
if (!provider) {
|
|
2355
|
+
throw new Error('No provider stored in session');
|
|
2356
|
+
}
|
|
2357
|
+
// Clear stored state
|
|
2358
|
+
sessionStorage.removeItem('oauth_state');
|
|
2359
|
+
sessionStorage.removeItem('oauth_provider');
|
|
2360
|
+
// Exchange code for token using the appropriate SDK method
|
|
2361
|
+
let response;
|
|
2362
|
+
switch (provider) {
|
|
2363
|
+
case 'google':
|
|
2364
|
+
response = await this.client.googleCodeToToken({ code });
|
|
2365
|
+
break;
|
|
2366
|
+
case 'discord':
|
|
2367
|
+
response = await this.client.discordCodeToToken({ code });
|
|
2368
|
+
break;
|
|
2369
|
+
case 'github':
|
|
2370
|
+
response = await this.client.githubCodeToToken({ code });
|
|
2371
|
+
break;
|
|
2372
|
+
default:
|
|
2373
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
2374
|
+
}
|
|
2375
|
+
// Handle success
|
|
2376
|
+
if (response.token) {
|
|
2377
|
+
if (this.options.onLoginSuccess) {
|
|
2378
|
+
this.options.onLoginSuccess(response.token, response.user);
|
|
2379
|
+
}
|
|
2380
|
+
return {
|
|
2381
|
+
success: true,
|
|
2382
|
+
token: response.token,
|
|
2383
|
+
user: response.user,
|
|
2384
|
+
};
|
|
2385
|
+
}
|
|
2386
|
+
else {
|
|
2387
|
+
throw new Error('No token received from server');
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
catch (error) {
|
|
2391
|
+
const err = error instanceof Error ? error : new Error('OAuth authentication failed');
|
|
2392
|
+
if (this.options.onError) {
|
|
2393
|
+
this.options.onError(err);
|
|
2394
|
+
}
|
|
2395
|
+
return {
|
|
2396
|
+
success: false,
|
|
2397
|
+
error: err,
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
/**
|
|
2402
|
+
* Check if current page is an OAuth callback and handle it automatically
|
|
2403
|
+
*/
|
|
2404
|
+
static async handleOAuthCallbackFromUrl(client, options) {
|
|
2405
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
2406
|
+
const code = urlParams.get('code');
|
|
2407
|
+
const state = urlParams.get('state');
|
|
2408
|
+
if (!code || !state) {
|
|
2409
|
+
return null; // Not an OAuth callback
|
|
2410
|
+
}
|
|
2411
|
+
const modal = new AuthModal({
|
|
2412
|
+
client,
|
|
2413
|
+
...options,
|
|
2414
|
+
});
|
|
2415
|
+
return await modal.handleOAuthCallback(code, state);
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
/**
|
|
2419
|
+
* Create and show auth modal
|
|
2420
|
+
*/
|
|
2421
|
+
function createAuthModal(options) {
|
|
2422
|
+
return new AuthModal(options);
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
exports.AuthFactory = AuthFactory;
|
|
2426
|
+
exports.AuthModal = AuthModal;
|
|
2427
|
+
exports.AuthProvider = AuthProvider;
|
|
2428
|
+
exports.BuiltInHooks = BuiltInHooks;
|
|
2429
|
+
exports.SeaVerseBackendAPIClient = SeaVerseBackendAPIClient;
|
|
2430
|
+
exports.createAuthModal = createAuthModal;
|
|
2431
|
+
exports.models = models;
|
|
2432
|
+
//# sourceMappingURL=index.cjs.map
|