@apowerb/apowerb-sdk 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/package.json +44 -0
- package/src/api.js +830 -0
- package/src/authStorage.js +770 -0
- package/src/config.js +86 -0
- package/src/index.js +26 -0
|
@@ -0,0 +1,770 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth storage utility for localStorage persistence.
|
|
3
|
+
* Handles user session and profile data.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const STORAGE_KEYS = {
|
|
7
|
+
USER: "th2_auth_user",
|
|
8
|
+
TOKEN: "th2_auth_token",
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// Auth API calls use relative URLs to go through the Next.js proxy,
|
|
12
|
+
// avoiding CORS issues (browser → Next.js → backend).
|
|
13
|
+
const API_URL = "";
|
|
14
|
+
|
|
15
|
+
// Set to true to use mock API, false to use real backend
|
|
16
|
+
const USE_MOCK_API = process.env.NEXT_PUBLIC_USE_MOCK_AUTH === "true";
|
|
17
|
+
|
|
18
|
+
export const authStorage = {
|
|
19
|
+
/**
|
|
20
|
+
* Get stored user data
|
|
21
|
+
*/
|
|
22
|
+
getUser() {
|
|
23
|
+
if (typeof window === "undefined") return null;
|
|
24
|
+
try {
|
|
25
|
+
const userJson = localStorage.getItem(STORAGE_KEYS.USER);
|
|
26
|
+
return userJson ? JSON.parse(userJson) : null;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("[authStorage] Failed to get user:", error);
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Store user data
|
|
35
|
+
*/
|
|
36
|
+
setUser(user) {
|
|
37
|
+
if (typeof window === "undefined") return;
|
|
38
|
+
try {
|
|
39
|
+
if (user) {
|
|
40
|
+
localStorage.setItem(STORAGE_KEYS.USER, JSON.stringify(user));
|
|
41
|
+
} else {
|
|
42
|
+
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error("[authStorage] Failed to set user:", error);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get stored auth token
|
|
51
|
+
*/
|
|
52
|
+
getToken() {
|
|
53
|
+
if (typeof window === "undefined") return null;
|
|
54
|
+
try {
|
|
55
|
+
return localStorage.getItem(STORAGE_KEYS.TOKEN);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error("[authStorage] Failed to get token:", error);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Store auth token
|
|
64
|
+
*/
|
|
65
|
+
setToken(token) {
|
|
66
|
+
if (typeof window === "undefined") return;
|
|
67
|
+
try {
|
|
68
|
+
if (token) {
|
|
69
|
+
localStorage.setItem(STORAGE_KEYS.TOKEN, token);
|
|
70
|
+
} else {
|
|
71
|
+
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error("[authStorage] Failed to set token:", error);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Clear all auth data (logout)
|
|
80
|
+
*/
|
|
81
|
+
clear() {
|
|
82
|
+
if (typeof window === "undefined") return;
|
|
83
|
+
localStorage.removeItem(STORAGE_KEYS.USER);
|
|
84
|
+
localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Real API for authentication - connects to th2agent backend
|
|
90
|
+
*/
|
|
91
|
+
// Map a backend user payload (snake_case) to the frontend user shape (camelCase).
|
|
92
|
+
// onboarding_completed stays snake_case on purpose: HomeDashboard reads it as such.
|
|
93
|
+
function mapBackendUser(data) {
|
|
94
|
+
return {
|
|
95
|
+
id: data.user_id,
|
|
96
|
+
email: data.email,
|
|
97
|
+
username: data.username || data.email?.split("@")[0],
|
|
98
|
+
firstName: data.first_name || data.full_name?.split(" ")[0] || "",
|
|
99
|
+
lastName:
|
|
100
|
+
data.last_name || data.full_name?.split(" ").slice(1).join(" ") || "",
|
|
101
|
+
role: data.role?.toLowerCase() || "user",
|
|
102
|
+
avatar: data.avatar_url || null,
|
|
103
|
+
mfaEnabled: data.mfa_enabled || false,
|
|
104
|
+
onboarding_completed: data.onboarding_completed === true,
|
|
105
|
+
createdAt: data.created_at,
|
|
106
|
+
updatedAt: data.updated_at,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const realAuthApi = {
|
|
111
|
+
/**
|
|
112
|
+
* Login with email and password
|
|
113
|
+
* Backend expects form-data with 'username' (email) and 'password'
|
|
114
|
+
*/
|
|
115
|
+
async login(email, password) {
|
|
116
|
+
if (!email || !password) {
|
|
117
|
+
throw new Error("Email and password are required");
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Backend uses OAuth2PasswordRequestForm which expects form-data
|
|
121
|
+
const formData = new URLSearchParams();
|
|
122
|
+
formData.append("username", email); // Backend uses 'username' for email
|
|
123
|
+
formData.append("password", password);
|
|
124
|
+
|
|
125
|
+
const response = await fetch(`${API_URL}/api/auth/token`, {
|
|
126
|
+
method: "POST",
|
|
127
|
+
headers: {
|
|
128
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
129
|
+
},
|
|
130
|
+
body: formData,
|
|
131
|
+
credentials: "include", // For refresh token cookie
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
if (response.status === 403) {
|
|
136
|
+
const err = await response.json().catch(() => ({}));
|
|
137
|
+
if (err.detail === "email_not_verified") {
|
|
138
|
+
const e = new Error("Email not verified");
|
|
139
|
+
e.code = "email_not_verified";
|
|
140
|
+
e.email = email;
|
|
141
|
+
throw e;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (response.status === 400 || response.status === 401) {
|
|
145
|
+
throw new Error("Incorrect email or password");
|
|
146
|
+
}
|
|
147
|
+
if (response.status === 404) {
|
|
148
|
+
throw new Error("User not found");
|
|
149
|
+
}
|
|
150
|
+
throw new Error("Server connection error");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
console.log("[authApi] login response:", { mfa_required: data.mfa_required, has_access_token: !!data.access_token });
|
|
155
|
+
|
|
156
|
+
// Check if MFA is required
|
|
157
|
+
if (data.mfa_required) {
|
|
158
|
+
return { mfaRequired: true, mfaToken: data.mfa_token };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const token = data.access_token;
|
|
162
|
+
|
|
163
|
+
// Fetch user profile with the token
|
|
164
|
+
const user = await this.getProfile(token);
|
|
165
|
+
|
|
166
|
+
return { user, token };
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Login via OAuth provider (Google, GitHub, Microsoft)
|
|
171
|
+
*/
|
|
172
|
+
async oauthLogin(provider, code, redirectUri) {
|
|
173
|
+
const response = await fetch(`${API_URL}/api/users/${provider}`, {
|
|
174
|
+
method: "POST",
|
|
175
|
+
headers: { "Content-Type": "application/json" },
|
|
176
|
+
body: JSON.stringify({ code, redirect_uri: redirectUri }),
|
|
177
|
+
credentials: "include",
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
if (response.status === 400) {
|
|
182
|
+
throw new Error("Invalid or expired authorization code");
|
|
183
|
+
}
|
|
184
|
+
throw new Error("OAuth login error");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const data = await response.json();
|
|
188
|
+
console.log("[authApi] oauthLogin response:", { mfa_required: data.mfa_required, has_access_token: !!data.access_token });
|
|
189
|
+
|
|
190
|
+
// Check if MFA is required
|
|
191
|
+
if (data.mfa_required) {
|
|
192
|
+
return { mfaRequired: true, mfaToken: data.mfa_token };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const token = data.access_token;
|
|
196
|
+
|
|
197
|
+
const user = {
|
|
198
|
+
id: data.user?.id || data.user?.user_id,
|
|
199
|
+
email: data.user?.email,
|
|
200
|
+
username: data.user?.username || data.user?.email?.split("@")[0],
|
|
201
|
+
firstName:
|
|
202
|
+
data.user?.full_name?.split(" ")[0] || data.user?.first_name || "",
|
|
203
|
+
lastName:
|
|
204
|
+
data.user?.full_name?.split(" ").slice(1).join(" ") ||
|
|
205
|
+
data.user?.last_name ||
|
|
206
|
+
"",
|
|
207
|
+
avatar: data.user?.avatar_url || null,
|
|
208
|
+
role: data.user?.role?.toLowerCase() || "user",
|
|
209
|
+
onboarding_completed: data.user?.onboarding_completed === true,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return { user, token };
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get current user profile
|
|
217
|
+
*/
|
|
218
|
+
async getProfile(token) {
|
|
219
|
+
const response = await fetch(`${API_URL}/api/users/me`, {
|
|
220
|
+
method: "GET",
|
|
221
|
+
headers: {
|
|
222
|
+
Authorization: `Bearer ${token}`,
|
|
223
|
+
"Content-Type": "application/json",
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
throw new Error("Unable to retrieve profile");
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const data = await response.json();
|
|
232
|
+
|
|
233
|
+
// Map backend user to frontend format
|
|
234
|
+
return mapBackendUser(data);
|
|
235
|
+
},
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Register new user
|
|
239
|
+
*/
|
|
240
|
+
async register({ email, password, firstName, lastName }) {
|
|
241
|
+
if (!email || !password) {
|
|
242
|
+
throw new Error("Email and password are required");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (password.length < 6) {
|
|
246
|
+
throw new Error("Password must be at least 6 characters");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const response = await fetch(`${API_URL}/api/users/`, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers: {
|
|
252
|
+
"Content-Type": "application/json",
|
|
253
|
+
},
|
|
254
|
+
body: JSON.stringify({
|
|
255
|
+
email,
|
|
256
|
+
password,
|
|
257
|
+
first_name: firstName || email.split("@")[0],
|
|
258
|
+
last_name: lastName || "",
|
|
259
|
+
}),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
if (response.status === 400) {
|
|
264
|
+
const error = await response.json().catch(() => ({}));
|
|
265
|
+
if (error.detail?.includes("email")) {
|
|
266
|
+
throw new Error("This email is already in use");
|
|
267
|
+
}
|
|
268
|
+
throw new Error(error.detail || "Invalid data");
|
|
269
|
+
}
|
|
270
|
+
throw new Error("Error creating account");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// After registration, try to auto-login. When the email-verification
|
|
274
|
+
// gate is active the backend returns 403 -> surface a verify-email state.
|
|
275
|
+
try {
|
|
276
|
+
return await this.login(email, password);
|
|
277
|
+
} catch (e) {
|
|
278
|
+
if (e.code === "email_not_verified") {
|
|
279
|
+
return { needsVerification: true, email };
|
|
280
|
+
}
|
|
281
|
+
throw e;
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Confirm an email-verification token.
|
|
287
|
+
*/
|
|
288
|
+
async verifyEmail(token) {
|
|
289
|
+
const response = await fetch(`${API_URL}/api/auth/verify-email`, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers: { "Content-Type": "application/json" },
|
|
292
|
+
body: JSON.stringify({ token }),
|
|
293
|
+
});
|
|
294
|
+
if (!response.ok) {
|
|
295
|
+
throw new Error("Invalid or expired verification link");
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Re-send the verification email (always resolves; anti-enumeration).
|
|
302
|
+
*/
|
|
303
|
+
async resendVerification(email) {
|
|
304
|
+
try {
|
|
305
|
+
await fetch(`${API_URL}/api/auth/resend-verification`, {
|
|
306
|
+
method: "POST",
|
|
307
|
+
headers: { "Content-Type": "application/json" },
|
|
308
|
+
body: JSON.stringify({ email }),
|
|
309
|
+
});
|
|
310
|
+
} catch (_) {}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Reset the password using a token from the reset email.
|
|
315
|
+
*/
|
|
316
|
+
async resetPassword(token, newPassword) {
|
|
317
|
+
const response = await fetch(`${API_URL}/api/auth/reset-password`, {
|
|
318
|
+
method: "POST",
|
|
319
|
+
headers: { "Content-Type": "application/json" },
|
|
320
|
+
body: JSON.stringify({ token, new_password: newPassword }),
|
|
321
|
+
});
|
|
322
|
+
if (!response.ok) {
|
|
323
|
+
throw new Error("Lien de réinitialisation invalide ou expiré");
|
|
324
|
+
}
|
|
325
|
+
return true;
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Refresh access token using refresh token cookie
|
|
330
|
+
*/
|
|
331
|
+
async refreshToken() {
|
|
332
|
+
const response = await fetch(`${API_URL}/api/auth/refresh-token`, {
|
|
333
|
+
method: "POST",
|
|
334
|
+
credentials: "include", // Send refresh token cookie
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
if (!response.ok) {
|
|
338
|
+
throw new Error("Session expired, please sign in again");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const data = await response.json();
|
|
342
|
+
return data.access_token;
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Update user profile
|
|
347
|
+
*/
|
|
348
|
+
async updateProfile(userId, updates, token) {
|
|
349
|
+
// Only send fields that were actually provided, so a partial update
|
|
350
|
+
// (e.g. just onboarding_completed) never wipes first/last name.
|
|
351
|
+
const payload = {};
|
|
352
|
+
if (updates.firstName !== undefined) payload.first_name = updates.firstName;
|
|
353
|
+
if (updates.lastName !== undefined) payload.last_name = updates.lastName;
|
|
354
|
+
if (updates.username !== undefined) payload.username = updates.username;
|
|
355
|
+
if (updates.onboarding_completed !== undefined)
|
|
356
|
+
payload.onboarding_completed = updates.onboarding_completed;
|
|
357
|
+
|
|
358
|
+
const response = await fetch(`${API_URL}/api/users/${userId}`, {
|
|
359
|
+
method: "PATCH",
|
|
360
|
+
headers: {
|
|
361
|
+
Authorization: `Bearer ${token}`,
|
|
362
|
+
"Content-Type": "application/json",
|
|
363
|
+
},
|
|
364
|
+
body: JSON.stringify(payload),
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
if (!response.ok) {
|
|
368
|
+
throw new Error("Error updating profile");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const data = await response.json();
|
|
372
|
+
// Return the normalized frontend shape so AuthContext never merges raw
|
|
373
|
+
// snake_case backend keys into the user object.
|
|
374
|
+
return mapBackendUser(data);
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* MFA: Start setup — returns secret + QR code
|
|
379
|
+
*/
|
|
380
|
+
async mfaSetup(token) {
|
|
381
|
+
const response = await fetch(`${API_URL}/api/auth/mfa/setup`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: {
|
|
384
|
+
Authorization: `Bearer ${token}`,
|
|
385
|
+
"Content-Type": "application/json",
|
|
386
|
+
},
|
|
387
|
+
});
|
|
388
|
+
if (!response.ok) throw new Error("Failed to setup MFA");
|
|
389
|
+
return response.json();
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* MFA: Enable — verify code and activate MFA
|
|
394
|
+
* The secret is stored server-side during /mfa/setup, so only the code is needed.
|
|
395
|
+
*/
|
|
396
|
+
async mfaEnable(code, token) {
|
|
397
|
+
const response = await fetch(`${API_URL}/api/auth/mfa/enable`, {
|
|
398
|
+
method: "POST",
|
|
399
|
+
headers: {
|
|
400
|
+
Authorization: `Bearer ${token}`,
|
|
401
|
+
"Content-Type": "application/json",
|
|
402
|
+
},
|
|
403
|
+
body: JSON.stringify({ code }),
|
|
404
|
+
});
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
const err = await response.json().catch(() => ({}));
|
|
407
|
+
throw new Error(err.detail || "Invalid verification code");
|
|
408
|
+
}
|
|
409
|
+
return response.json();
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* MFA: Disable — verify code and deactivate MFA
|
|
414
|
+
*/
|
|
415
|
+
async mfaDisable(code, token) {
|
|
416
|
+
const response = await fetch(`${API_URL}/api/auth/mfa/disable`, {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: {
|
|
419
|
+
Authorization: `Bearer ${token}`,
|
|
420
|
+
"Content-Type": "application/json",
|
|
421
|
+
},
|
|
422
|
+
body: JSON.stringify({ code }),
|
|
423
|
+
});
|
|
424
|
+
if (!response.ok) {
|
|
425
|
+
const err = await response.json().catch(() => ({}));
|
|
426
|
+
throw new Error(err.detail || "Invalid verification code");
|
|
427
|
+
}
|
|
428
|
+
return response.json();
|
|
429
|
+
},
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* MFA: Verify — validate TOTP code during login (no auth required)
|
|
433
|
+
*/
|
|
434
|
+
async mfaVerify(mfaToken, code) {
|
|
435
|
+
const response = await fetch(`${API_URL}/api/auth/mfa/verify`, {
|
|
436
|
+
method: "POST",
|
|
437
|
+
headers: { "Content-Type": "application/json" },
|
|
438
|
+
body: JSON.stringify({ mfa_token: mfaToken, code }),
|
|
439
|
+
credentials: "include",
|
|
440
|
+
});
|
|
441
|
+
if (!response.ok) {
|
|
442
|
+
const err = await response.json().catch(() => ({}));
|
|
443
|
+
throw new Error(err.detail || "Invalid verification code");
|
|
444
|
+
}
|
|
445
|
+
return response.json();
|
|
446
|
+
},
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* MFA: Get backup codes
|
|
450
|
+
*/
|
|
451
|
+
async mfaBackupCodes(token) {
|
|
452
|
+
const response = await fetch(`${API_URL}/api/auth/mfa/backup-codes`, {
|
|
453
|
+
method: "GET",
|
|
454
|
+
headers: {
|
|
455
|
+
Authorization: `Bearer ${token}`,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
if (!response.ok) throw new Error("Failed to get backup codes");
|
|
459
|
+
return response.json();
|
|
460
|
+
},
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Logout - clear cookies
|
|
464
|
+
*/
|
|
465
|
+
async logout() {
|
|
466
|
+
try {
|
|
467
|
+
await fetch(`${API_URL}/api/auth/logout`, {
|
|
468
|
+
method: "POST",
|
|
469
|
+
credentials: "include",
|
|
470
|
+
});
|
|
471
|
+
} catch {
|
|
472
|
+
// Best-effort — local state will be cleared anyway
|
|
473
|
+
}
|
|
474
|
+
return true;
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Request password reset
|
|
479
|
+
*/
|
|
480
|
+
async requestPasswordReset(email) {
|
|
481
|
+
if (!email) {
|
|
482
|
+
throw new Error("Email is required");
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const response = await fetch(`${API_URL}/api/auth/forgot-password`, {
|
|
486
|
+
method: "POST",
|
|
487
|
+
headers: {
|
|
488
|
+
"Content-Type": "application/json",
|
|
489
|
+
},
|
|
490
|
+
body: JSON.stringify({ email }),
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (!response.ok) {
|
|
494
|
+
// Don't reveal if email exists or not for security
|
|
495
|
+
// Just return success in all cases
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
return true;
|
|
499
|
+
},
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Mock API for authentication (for development/testing)
|
|
504
|
+
*/
|
|
505
|
+
export const mockAuthApi = {
|
|
506
|
+
/**
|
|
507
|
+
* Demo credentials for testing
|
|
508
|
+
*/
|
|
509
|
+
DEMO_USERS: {
|
|
510
|
+
"admin@th2.ai": {
|
|
511
|
+
password: "admin123",
|
|
512
|
+
user: {
|
|
513
|
+
id: "user_admin",
|
|
514
|
+
email: "admin@th2.ai",
|
|
515
|
+
username: "admin",
|
|
516
|
+
firstName: "Admin",
|
|
517
|
+
lastName: "TH2",
|
|
518
|
+
avatar: null,
|
|
519
|
+
role: "admin",
|
|
520
|
+
createdAt: "2024-01-01T00:00:00.000Z",
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
"demo@th2.ai": {
|
|
524
|
+
password: "demo123",
|
|
525
|
+
user: {
|
|
526
|
+
id: "user_demo",
|
|
527
|
+
email: "demo@th2.ai",
|
|
528
|
+
username: "demo",
|
|
529
|
+
firstName: "Demo",
|
|
530
|
+
lastName: "User",
|
|
531
|
+
avatar: null,
|
|
532
|
+
role: "user",
|
|
533
|
+
createdAt: "2024-01-15T00:00:00.000Z",
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Simulate login
|
|
540
|
+
*/
|
|
541
|
+
async login(email, password) {
|
|
542
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
543
|
+
|
|
544
|
+
if (!email || !password) {
|
|
545
|
+
throw new Error("Email and password are required");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const demoUser = this.DEMO_USERS[email.toLowerCase()];
|
|
549
|
+
if (demoUser) {
|
|
550
|
+
if (demoUser.password !== password) {
|
|
551
|
+
throw new Error("Incorrect password");
|
|
552
|
+
}
|
|
553
|
+
const token = `mock_token_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
554
|
+
return { user: { ...demoUser.user }, token };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (password.length < 6) {
|
|
558
|
+
throw new Error("Incorrect password");
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const user = {
|
|
562
|
+
id: `user_${Date.now()}`,
|
|
563
|
+
email,
|
|
564
|
+
username: email.split("@")[0],
|
|
565
|
+
firstName: "",
|
|
566
|
+
lastName: "",
|
|
567
|
+
avatar: null,
|
|
568
|
+
role: "user",
|
|
569
|
+
createdAt: new Date().toISOString(),
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
const token = `mock_token_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
573
|
+
return { user, token };
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Simulate OAuth login
|
|
578
|
+
*/
|
|
579
|
+
async oauthLogin(provider, code, redirectUri) {
|
|
580
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
581
|
+
const user = {
|
|
582
|
+
id: `user_oauth_${Date.now()}`,
|
|
583
|
+
email: `${provider}user@example.com`,
|
|
584
|
+
username: `${provider}user`,
|
|
585
|
+
firstName: "OAuth",
|
|
586
|
+
lastName: "User",
|
|
587
|
+
avatar: null,
|
|
588
|
+
role: "user",
|
|
589
|
+
createdAt: new Date().toISOString(),
|
|
590
|
+
};
|
|
591
|
+
const token = `mock_oauth_token_${Date.now()}`;
|
|
592
|
+
return { user, token };
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Simulate registration
|
|
597
|
+
*/
|
|
598
|
+
async register({ email, password, username, firstName, lastName }) {
|
|
599
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
600
|
+
|
|
601
|
+
if (!email || !password) {
|
|
602
|
+
throw new Error("All required fields must be filled");
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (password.length < 6) {
|
|
606
|
+
throw new Error("Password must be at least 6 characters");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
|
610
|
+
throw new Error("Invalid email format");
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const user = {
|
|
614
|
+
id: `user_${Date.now()}`,
|
|
615
|
+
email,
|
|
616
|
+
username: username || email.split("@")[0],
|
|
617
|
+
firstName: firstName || "",
|
|
618
|
+
lastName: lastName || "",
|
|
619
|
+
avatar: null,
|
|
620
|
+
role: "user",
|
|
621
|
+
createdAt: new Date().toISOString(),
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const token = `mock_token_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
625
|
+
return { user, token };
|
|
626
|
+
},
|
|
627
|
+
|
|
628
|
+
async verifyEmail() {
|
|
629
|
+
return true;
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
async resetPassword() {
|
|
633
|
+
return true;
|
|
634
|
+
},
|
|
635
|
+
|
|
636
|
+
async resendVerification() {
|
|
637
|
+
return;
|
|
638
|
+
},
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Simulate MFA setup
|
|
642
|
+
*/
|
|
643
|
+
async mfaSetup() {
|
|
644
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
645
|
+
return {
|
|
646
|
+
secret: "JBSWY3DPEHPK3PXP",
|
|
647
|
+
qr_code_uri:
|
|
648
|
+
"otpauth://totp/TH2:demo@th2.ai?secret=JBSWY3DPEHPK3PXP&issuer=TH2",
|
|
649
|
+
qr_code_base64:
|
|
650
|
+
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
|
|
651
|
+
};
|
|
652
|
+
},
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Simulate MFA enable
|
|
656
|
+
*/
|
|
657
|
+
async mfaEnable(code) {
|
|
658
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
659
|
+
if (code.length !== 6) throw new Error("Invalid verification code");
|
|
660
|
+
return {
|
|
661
|
+
status: "ok",
|
|
662
|
+
message: "MFA enabled",
|
|
663
|
+
backup_codes: [
|
|
664
|
+
"a1b2c3d4",
|
|
665
|
+
"e5f6g7h8",
|
|
666
|
+
"i9j0k1l2",
|
|
667
|
+
"m3n4o5p6",
|
|
668
|
+
"q7r8s9t0",
|
|
669
|
+
"u1v2w3x4",
|
|
670
|
+
"y5z6a7b8",
|
|
671
|
+
"c9d0e1f2",
|
|
672
|
+
],
|
|
673
|
+
};
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Simulate MFA disable
|
|
678
|
+
*/
|
|
679
|
+
async mfaDisable(code) {
|
|
680
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
681
|
+
if (code.length !== 6) throw new Error("Invalid verification code");
|
|
682
|
+
return { status: "ok", message: "MFA disabled" };
|
|
683
|
+
},
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Simulate MFA verify
|
|
687
|
+
*/
|
|
688
|
+
async mfaVerify(mfaToken, code) {
|
|
689
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
690
|
+
if (code.length !== 6) throw new Error("Invalid verification code");
|
|
691
|
+
return {
|
|
692
|
+
access_token: `mock_token_${Date.now()}`,
|
|
693
|
+
token_type: "bearer",
|
|
694
|
+
};
|
|
695
|
+
},
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Simulate MFA backup codes
|
|
699
|
+
*/
|
|
700
|
+
async mfaBackupCodes() {
|
|
701
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
702
|
+
return {
|
|
703
|
+
backup_codes: [
|
|
704
|
+
"a1b2c3d4",
|
|
705
|
+
"e5f6g7h8",
|
|
706
|
+
"i9j0k1l2",
|
|
707
|
+
"m3n4o5p6",
|
|
708
|
+
"q7r8s9t0",
|
|
709
|
+
"u1v2w3x4",
|
|
710
|
+
"y5z6a7b8",
|
|
711
|
+
"c9d0e1f2",
|
|
712
|
+
],
|
|
713
|
+
};
|
|
714
|
+
},
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Simulate profile update
|
|
718
|
+
*/
|
|
719
|
+
async updateProfile(userId, updates) {
|
|
720
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
721
|
+
return {
|
|
722
|
+
...updates,
|
|
723
|
+
id: userId,
|
|
724
|
+
updatedAt: new Date().toISOString(),
|
|
725
|
+
};
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Simulate avatar upload
|
|
730
|
+
*/
|
|
731
|
+
async uploadAvatar(userId, file) {
|
|
732
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
733
|
+
return new Promise((resolve, reject) => {
|
|
734
|
+
const reader = new FileReader();
|
|
735
|
+
reader.onload = () => resolve(reader.result);
|
|
736
|
+
reader.onerror = () => reject(new Error("Error loading image"));
|
|
737
|
+
reader.readAsDataURL(file);
|
|
738
|
+
});
|
|
739
|
+
},
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Simulate logout
|
|
743
|
+
*/
|
|
744
|
+
async logout() {
|
|
745
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
746
|
+
return true;
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Simulate password reset request
|
|
751
|
+
*/
|
|
752
|
+
async requestPasswordReset(email) {
|
|
753
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
754
|
+
|
|
755
|
+
if (!email) {
|
|
756
|
+
throw new Error("Email is required");
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// In mock mode, always return success (simulates email sent)
|
|
760
|
+
// In real implementation, backend would send email
|
|
761
|
+
console.log(`[Mock] Password reset email would be sent to: ${email}`);
|
|
762
|
+
return true;
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Export the appropriate API based on configuration
|
|
768
|
+
* Set NEXT_PUBLIC_USE_MOCK_AUTH=true in .env for mock API
|
|
769
|
+
*/
|
|
770
|
+
export const authApi = USE_MOCK_API ? mockAuthApi : realAuthApi;
|