@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.
@@ -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;