@pixygon/auth 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1003 @@
1
+ // src/utils/storage.ts
2
+ var getKeys = (appId) => ({
3
+ ACCESS_TOKEN: `${appId}_access_token`,
4
+ REFRESH_TOKEN: `${appId}_refresh_token`,
5
+ USER: `${appId}_user`,
6
+ EXPIRES_AT: `${appId}_expires_at`
7
+ });
8
+ var defaultStorage = {
9
+ getItem: (key) => {
10
+ if (typeof window === "undefined") return null;
11
+ return localStorage.getItem(key);
12
+ },
13
+ setItem: (key, value) => {
14
+ if (typeof window === "undefined") return;
15
+ localStorage.setItem(key, value);
16
+ },
17
+ removeItem: (key) => {
18
+ if (typeof window === "undefined") return;
19
+ localStorage.removeItem(key);
20
+ }
21
+ };
22
+ function createTokenStorage(appId, storage = defaultStorage) {
23
+ const keys = getKeys(appId);
24
+ return {
25
+ /**
26
+ * Get the stored access token
27
+ */
28
+ getAccessToken: async () => {
29
+ const token = await storage.getItem(keys.ACCESS_TOKEN);
30
+ return token;
31
+ },
32
+ /**
33
+ * Get the stored refresh token
34
+ */
35
+ getRefreshToken: async () => {
36
+ const token = await storage.getItem(keys.REFRESH_TOKEN);
37
+ return token;
38
+ },
39
+ /**
40
+ * Get the stored user
41
+ */
42
+ getUser: async () => {
43
+ const userJson = await storage.getItem(keys.USER);
44
+ if (!userJson) return null;
45
+ try {
46
+ return JSON.parse(userJson);
47
+ } catch {
48
+ return null;
49
+ }
50
+ },
51
+ /**
52
+ * Get token expiration time
53
+ */
54
+ getExpiresAt: async () => {
55
+ const expiresAt = await storage.getItem(keys.EXPIRES_AT);
56
+ return expiresAt ? parseInt(expiresAt, 10) : null;
57
+ },
58
+ /**
59
+ * Check if access token is expired
60
+ */
61
+ isTokenExpired: async () => {
62
+ const expiresAt = await storage.getItem(keys.EXPIRES_AT);
63
+ if (!expiresAt) return true;
64
+ return Date.now() >= parseInt(expiresAt, 10);
65
+ },
66
+ /**
67
+ * Check if token will expire within threshold (in seconds)
68
+ */
69
+ willExpireSoon: async (thresholdSeconds = 300) => {
70
+ const expiresAt = await storage.getItem(keys.EXPIRES_AT);
71
+ if (!expiresAt) return true;
72
+ const expiryTime = parseInt(expiresAt, 10);
73
+ const thresholdMs = thresholdSeconds * 1e3;
74
+ return Date.now() >= expiryTime - thresholdMs;
75
+ },
76
+ /**
77
+ * Store tokens and user data
78
+ */
79
+ setTokens: async (accessToken, refreshToken, expiresIn, user) => {
80
+ await storage.setItem(keys.ACCESS_TOKEN, accessToken);
81
+ if (refreshToken) {
82
+ await storage.setItem(keys.REFRESH_TOKEN, refreshToken);
83
+ }
84
+ if (expiresIn) {
85
+ const expiresAt = Date.now() + expiresIn * 1e3;
86
+ await storage.setItem(keys.EXPIRES_AT, expiresAt.toString());
87
+ }
88
+ await storage.setItem(keys.USER, JSON.stringify(user));
89
+ },
90
+ /**
91
+ * Update tokens after refresh
92
+ */
93
+ updateTokens: async (tokens) => {
94
+ await storage.setItem(keys.ACCESS_TOKEN, tokens.accessToken);
95
+ await storage.setItem(keys.REFRESH_TOKEN, tokens.refreshToken);
96
+ const expiresAt = Date.now() + tokens.expiresIn * 1e3;
97
+ await storage.setItem(keys.EXPIRES_AT, expiresAt.toString());
98
+ },
99
+ /**
100
+ * Update user data
101
+ */
102
+ updateUser: async (user) => {
103
+ await storage.setItem(keys.USER, JSON.stringify(user));
104
+ },
105
+ /**
106
+ * Clear all stored auth data
107
+ */
108
+ clear: async () => {
109
+ await storage.removeItem(keys.ACCESS_TOKEN);
110
+ await storage.removeItem(keys.REFRESH_TOKEN);
111
+ await storage.removeItem(keys.USER);
112
+ await storage.removeItem(keys.EXPIRES_AT);
113
+ },
114
+ /**
115
+ * Get all stored auth data
116
+ */
117
+ getAll: async () => {
118
+ const [accessToken, refreshToken, user, expiresAt] = await Promise.all([
119
+ storage.getItem(keys.ACCESS_TOKEN),
120
+ storage.getItem(keys.REFRESH_TOKEN),
121
+ storage.getItem(keys.USER),
122
+ storage.getItem(keys.EXPIRES_AT)
123
+ ]);
124
+ return {
125
+ accessToken,
126
+ refreshToken,
127
+ user: user ? JSON.parse(user) : null,
128
+ expiresAt: expiresAt ? parseInt(expiresAt, 10) : null
129
+ };
130
+ }
131
+ };
132
+ }
133
+
134
+ // src/api/client.ts
135
+ function parseErrorCode(status, message) {
136
+ if (message?.toLowerCase().includes("not found")) return "USER_NOT_FOUND";
137
+ if (message?.toLowerCase().includes("exists")) return "USER_EXISTS";
138
+ if (message?.toLowerCase().includes("verified")) return "EMAIL_NOT_VERIFIED";
139
+ if (message?.toLowerCase().includes("invalid") && message?.toLowerCase().includes("code")) {
140
+ return "INVALID_VERIFICATION_CODE";
141
+ }
142
+ if (message?.toLowerCase().includes("expired") && message?.toLowerCase().includes("code")) {
143
+ return "EXPIRED_VERIFICATION_CODE";
144
+ }
145
+ switch (status) {
146
+ case 401:
147
+ return "INVALID_CREDENTIALS";
148
+ case 403:
149
+ return "TOKEN_INVALID";
150
+ case 404:
151
+ return "USER_NOT_FOUND";
152
+ case 409:
153
+ return "USER_EXISTS";
154
+ case 500:
155
+ return "SERVER_ERROR";
156
+ default:
157
+ return "UNKNOWN_ERROR";
158
+ }
159
+ }
160
+ function createAuthError(status, message, details) {
161
+ return {
162
+ code: parseErrorCode(status, message),
163
+ message: message || "An unexpected error occurred",
164
+ details
165
+ };
166
+ }
167
+ function createAuthApiClient(config, tokenStorage) {
168
+ let currentAccessToken = null;
169
+ let isRefreshing = false;
170
+ let refreshPromise = null;
171
+ const log = (...args) => {
172
+ if (config.debug) {
173
+ console.log("[PixygonAuth]", ...args);
174
+ }
175
+ };
176
+ async function request(endpoint, options = {}) {
177
+ const url = `${config.baseUrl}${endpoint}`;
178
+ const headers = {
179
+ "Content-Type": "application/json",
180
+ ...options.headers
181
+ };
182
+ if (currentAccessToken) {
183
+ headers["Authorization"] = `Bearer ${currentAccessToken}`;
184
+ }
185
+ log(`Request: ${options.method || "GET"} ${endpoint}`);
186
+ try {
187
+ const response = await fetch(url, {
188
+ ...options,
189
+ headers
190
+ });
191
+ if (response.status === 401 || response.status === 403) {
192
+ if (currentAccessToken && !isRefreshing) {
193
+ log("Token expired, attempting refresh...");
194
+ try {
195
+ await refreshTokens();
196
+ headers["Authorization"] = `Bearer ${currentAccessToken}`;
197
+ const retryResponse = await fetch(url, { ...options, headers });
198
+ if (!retryResponse.ok) {
199
+ const errorData = await retryResponse.json().catch(() => ({}));
200
+ throw createAuthError(retryResponse.status, errorData.message);
201
+ }
202
+ return retryResponse.json();
203
+ } catch (refreshError) {
204
+ log("Token refresh failed", refreshError);
205
+ throw createAuthError(401, "Session expired. Please log in again.");
206
+ }
207
+ }
208
+ throw createAuthError(response.status, "Authentication required");
209
+ }
210
+ if (!response.ok) {
211
+ const errorData = await response.json().catch(() => ({}));
212
+ throw createAuthError(response.status, errorData.message, errorData);
213
+ }
214
+ const data = await response.json();
215
+ return data;
216
+ } catch (error) {
217
+ if (error.code) {
218
+ throw error;
219
+ }
220
+ log("Network error:", error);
221
+ throw {
222
+ code: "NETWORK_ERROR",
223
+ message: "Unable to connect to the server",
224
+ details: { originalError: error }
225
+ };
226
+ }
227
+ }
228
+ async function refreshTokens() {
229
+ if (isRefreshing && refreshPromise) {
230
+ return refreshPromise;
231
+ }
232
+ isRefreshing = true;
233
+ refreshPromise = (async () => {
234
+ try {
235
+ const refreshToken = await tokenStorage.getRefreshToken();
236
+ if (!refreshToken) {
237
+ throw new Error("No refresh token available");
238
+ }
239
+ const response = await fetch(`${config.baseUrl}/auth/refresh`, {
240
+ method: "POST",
241
+ headers: { "Content-Type": "application/json" },
242
+ body: JSON.stringify({ refreshToken })
243
+ });
244
+ if (!response.ok) {
245
+ throw new Error("Refresh failed");
246
+ }
247
+ const data = await response.json();
248
+ currentAccessToken = data.token;
249
+ await tokenStorage.updateTokens({
250
+ accessToken: data.token,
251
+ refreshToken: data.refreshToken,
252
+ expiresIn: data.expiresIn,
253
+ refreshExpiresIn: data.expiresIn * 24
254
+ // Approximate
255
+ });
256
+ config.onTokenRefresh?.({
257
+ accessToken: data.token,
258
+ refreshToken: data.refreshToken,
259
+ expiresIn: data.expiresIn,
260
+ refreshExpiresIn: data.expiresIn * 24
261
+ });
262
+ log("Token refreshed successfully");
263
+ } finally {
264
+ isRefreshing = false;
265
+ refreshPromise = null;
266
+ }
267
+ })();
268
+ return refreshPromise;
269
+ }
270
+ return {
271
+ // ========================================================================
272
+ // Auth Endpoints
273
+ // ========================================================================
274
+ async login(data) {
275
+ const response = await request("/auth/login", {
276
+ method: "POST",
277
+ body: JSON.stringify(data)
278
+ });
279
+ currentAccessToken = response.token;
280
+ await tokenStorage.setTokens(
281
+ response.token,
282
+ response.refreshToken || null,
283
+ response.expiresIn || null,
284
+ response.user
285
+ );
286
+ config.onLogin?.(response.user);
287
+ log("Login successful:", response.user.userName);
288
+ return response;
289
+ },
290
+ async register(data) {
291
+ const response = await request("/auth/register", {
292
+ method: "POST",
293
+ body: JSON.stringify(data)
294
+ });
295
+ log("Registration successful:", response.user.userName);
296
+ return response;
297
+ },
298
+ async verify(data) {
299
+ const response = await request("/auth/verify", {
300
+ method: "POST",
301
+ body: JSON.stringify(data)
302
+ });
303
+ currentAccessToken = response.token;
304
+ await tokenStorage.setTokens(
305
+ response.token,
306
+ response.refreshToken || null,
307
+ null,
308
+ response.user
309
+ );
310
+ config.onLogin?.(response.user);
311
+ log("Verification successful:", response.user.userName);
312
+ return response;
313
+ },
314
+ async resendVerification(data) {
315
+ const response = await request("/auth/resendVerificationEmail", {
316
+ method: "POST",
317
+ body: JSON.stringify(data)
318
+ });
319
+ log("Verification email resent");
320
+ return response;
321
+ },
322
+ async forgotPassword(data) {
323
+ const response = await request("/auth/forgotPassword", {
324
+ method: "POST",
325
+ body: JSON.stringify(data)
326
+ });
327
+ log("Password reset email sent");
328
+ return response;
329
+ },
330
+ async recoverPassword(data) {
331
+ const response = await request("/auth/recoverPassword", {
332
+ method: "POST",
333
+ body: JSON.stringify(data)
334
+ });
335
+ log("Password recovered successfully");
336
+ return response;
337
+ },
338
+ async refreshToken(data) {
339
+ const response = await request("/auth/refresh", {
340
+ method: "POST",
341
+ body: JSON.stringify(data)
342
+ });
343
+ currentAccessToken = response.token;
344
+ await tokenStorage.updateTokens({
345
+ accessToken: response.token,
346
+ refreshToken: response.refreshToken,
347
+ expiresIn: response.expiresIn,
348
+ refreshExpiresIn: response.expiresIn * 24
349
+ });
350
+ return response;
351
+ },
352
+ // ========================================================================
353
+ // User Endpoints
354
+ // ========================================================================
355
+ async getMe() {
356
+ const response = await request("/users/me");
357
+ await tokenStorage.updateUser(response);
358
+ return response;
359
+ },
360
+ async updateProfile(data) {
361
+ const response = await request("/users/me", {
362
+ method: "PATCH",
363
+ body: JSON.stringify(data)
364
+ });
365
+ await tokenStorage.updateUser(response);
366
+ return response;
367
+ },
368
+ // ========================================================================
369
+ // Utilities
370
+ // ========================================================================
371
+ setAccessToken(token) {
372
+ currentAccessToken = token;
373
+ },
374
+ getAccessToken() {
375
+ return currentAccessToken;
376
+ },
377
+ request
378
+ };
379
+ }
380
+
381
+ // src/providers/AuthProvider.tsx
382
+ import {
383
+ createContext,
384
+ useContext,
385
+ useEffect,
386
+ useState,
387
+ useCallback,
388
+ useMemo
389
+ } from "react";
390
+ import { jsx } from "react/jsx-runtime";
391
+ var AuthContext = createContext(null);
392
+ var defaultConfig = {
393
+ autoRefresh: true,
394
+ refreshThreshold: 300,
395
+ // 5 minutes before expiry
396
+ debug: false
397
+ };
398
+ function AuthProvider({ config: userConfig, children }) {
399
+ const config = useMemo(() => ({ ...defaultConfig, ...userConfig }), [userConfig]);
400
+ const tokenStorage = useMemo(
401
+ () => createTokenStorage(config.appId, config.storage),
402
+ [config.appId, config.storage]
403
+ );
404
+ const apiClient = useMemo(
405
+ () => createAuthApiClient(config, tokenStorage),
406
+ [config, tokenStorage]
407
+ );
408
+ const [state, setState] = useState({
409
+ user: null,
410
+ accessToken: null,
411
+ refreshToken: null,
412
+ status: "idle",
413
+ isLoading: true,
414
+ error: null
415
+ });
416
+ const log = useCallback(
417
+ (...args) => {
418
+ if (config.debug) {
419
+ console.log("[PixygonAuth]", ...args);
420
+ }
421
+ },
422
+ [config.debug]
423
+ );
424
+ useEffect(() => {
425
+ let mounted = true;
426
+ async function initializeAuth() {
427
+ log("Initializing auth...");
428
+ try {
429
+ const stored = await tokenStorage.getAll();
430
+ if (!stored.accessToken || !stored.user) {
431
+ log("No stored auth found");
432
+ if (mounted) {
433
+ setState((prev) => ({
434
+ ...prev,
435
+ status: "unauthenticated",
436
+ isLoading: false
437
+ }));
438
+ }
439
+ return;
440
+ }
441
+ const isExpired = await tokenStorage.isTokenExpired();
442
+ if (isExpired && stored.refreshToken) {
443
+ log("Token expired, attempting refresh...");
444
+ try {
445
+ const response = await apiClient.refreshToken({
446
+ refreshToken: stored.refreshToken
447
+ });
448
+ if (mounted) {
449
+ setState({
450
+ user: stored.user,
451
+ accessToken: response.token,
452
+ refreshToken: response.refreshToken,
453
+ status: "authenticated",
454
+ isLoading: false,
455
+ error: null
456
+ });
457
+ apiClient.setAccessToken(response.token);
458
+ }
459
+ } catch (error) {
460
+ log("Token refresh failed:", error);
461
+ await tokenStorage.clear();
462
+ if (mounted) {
463
+ setState({
464
+ user: null,
465
+ accessToken: null,
466
+ refreshToken: null,
467
+ status: "unauthenticated",
468
+ isLoading: false,
469
+ error: null
470
+ });
471
+ }
472
+ }
473
+ } else if (!isExpired) {
474
+ log("Restoring auth from storage");
475
+ apiClient.setAccessToken(stored.accessToken);
476
+ if (mounted) {
477
+ setState({
478
+ user: stored.user,
479
+ accessToken: stored.accessToken,
480
+ refreshToken: stored.refreshToken,
481
+ status: stored.user.isVerified ? "authenticated" : "verifying",
482
+ isLoading: false,
483
+ error: null
484
+ });
485
+ }
486
+ } else {
487
+ log("Token expired and no refresh token available");
488
+ await tokenStorage.clear();
489
+ if (mounted) {
490
+ setState({
491
+ user: null,
492
+ accessToken: null,
493
+ refreshToken: null,
494
+ status: "unauthenticated",
495
+ isLoading: false,
496
+ error: null
497
+ });
498
+ }
499
+ }
500
+ } catch (error) {
501
+ log("Auth initialization error:", error);
502
+ if (mounted) {
503
+ setState((prev) => ({
504
+ ...prev,
505
+ status: "unauthenticated",
506
+ isLoading: false,
507
+ error: {
508
+ code: "UNKNOWN_ERROR",
509
+ message: "Failed to initialize authentication"
510
+ }
511
+ }));
512
+ }
513
+ }
514
+ }
515
+ initializeAuth();
516
+ return () => {
517
+ mounted = false;
518
+ };
519
+ }, [apiClient, tokenStorage, log]);
520
+ useEffect(() => {
521
+ if (!config.autoRefresh || state.status !== "authenticated") {
522
+ return;
523
+ }
524
+ const checkAndRefresh = async () => {
525
+ const willExpire = await tokenStorage.willExpireSoon(config.refreshThreshold || 300);
526
+ if (willExpire && state.refreshToken) {
527
+ log("Token expiring soon, refreshing...");
528
+ try {
529
+ const response = await apiClient.refreshToken({
530
+ refreshToken: state.refreshToken
531
+ });
532
+ setState((prev) => ({
533
+ ...prev,
534
+ accessToken: response.token,
535
+ refreshToken: response.refreshToken
536
+ }));
537
+ apiClient.setAccessToken(response.token);
538
+ } catch (error) {
539
+ log("Auto-refresh failed:", error);
540
+ }
541
+ }
542
+ };
543
+ const interval = setInterval(checkAndRefresh, 6e4);
544
+ return () => clearInterval(interval);
545
+ }, [
546
+ config.autoRefresh,
547
+ config.refreshThreshold,
548
+ state.status,
549
+ state.refreshToken,
550
+ apiClient,
551
+ tokenStorage,
552
+ log
553
+ ]);
554
+ const login = useCallback(
555
+ async (credentials) => {
556
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
557
+ try {
558
+ const response = await apiClient.login(credentials);
559
+ const newStatus = response.user.isVerified ? "authenticated" : "verifying";
560
+ setState({
561
+ user: response.user,
562
+ accessToken: response.token,
563
+ refreshToken: response.refreshToken || null,
564
+ status: newStatus,
565
+ isLoading: false,
566
+ error: null
567
+ });
568
+ return response;
569
+ } catch (error) {
570
+ const authError = error;
571
+ setState((prev) => ({
572
+ ...prev,
573
+ isLoading: false,
574
+ error: authError
575
+ }));
576
+ config.onError?.(authError);
577
+ throw error;
578
+ }
579
+ },
580
+ [apiClient, config]
581
+ );
582
+ const register = useCallback(
583
+ async (data) => {
584
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
585
+ try {
586
+ const response = await apiClient.register(data);
587
+ setState((prev) => ({
588
+ ...prev,
589
+ isLoading: false
590
+ }));
591
+ return response;
592
+ } catch (error) {
593
+ const authError = error;
594
+ setState((prev) => ({
595
+ ...prev,
596
+ isLoading: false,
597
+ error: authError
598
+ }));
599
+ config.onError?.(authError);
600
+ throw error;
601
+ }
602
+ },
603
+ [apiClient, config]
604
+ );
605
+ const verify = useCallback(
606
+ async (data) => {
607
+ setState((prev) => ({ ...prev, isLoading: true, error: null }));
608
+ try {
609
+ const response = await apiClient.verify(data);
610
+ setState({
611
+ user: response.user,
612
+ accessToken: response.token,
613
+ refreshToken: response.refreshToken || null,
614
+ status: "authenticated",
615
+ isLoading: false,
616
+ error: null
617
+ });
618
+ return response;
619
+ } catch (error) {
620
+ const authError = error;
621
+ setState((prev) => ({
622
+ ...prev,
623
+ isLoading: false,
624
+ error: authError
625
+ }));
626
+ config.onError?.(authError);
627
+ throw error;
628
+ }
629
+ },
630
+ [apiClient, config]
631
+ );
632
+ const resendVerification = useCallback(
633
+ async (data) => {
634
+ return apiClient.resendVerification(data);
635
+ },
636
+ [apiClient]
637
+ );
638
+ const forgotPassword = useCallback(
639
+ async (data) => {
640
+ return apiClient.forgotPassword(data);
641
+ },
642
+ [apiClient]
643
+ );
644
+ const recoverPassword = useCallback(
645
+ async (data) => {
646
+ return apiClient.recoverPassword(data);
647
+ },
648
+ [apiClient]
649
+ );
650
+ const logout = useCallback(async () => {
651
+ log("Logging out...");
652
+ await tokenStorage.clear();
653
+ apiClient.setAccessToken(null);
654
+ setState({
655
+ user: null,
656
+ accessToken: null,
657
+ refreshToken: null,
658
+ status: "unauthenticated",
659
+ isLoading: false,
660
+ error: null
661
+ });
662
+ config.onLogout?.();
663
+ }, [tokenStorage, apiClient, config, log]);
664
+ const refreshTokens = useCallback(async () => {
665
+ if (!state.refreshToken) {
666
+ throw new Error("No refresh token available");
667
+ }
668
+ const response = await apiClient.refreshToken({
669
+ refreshToken: state.refreshToken
670
+ });
671
+ setState((prev) => ({
672
+ ...prev,
673
+ accessToken: response.token,
674
+ refreshToken: response.refreshToken
675
+ }));
676
+ }, [apiClient, state.refreshToken]);
677
+ const getAccessToken = useCallback(() => state.accessToken, [state.accessToken]);
678
+ const hasRole = useCallback(
679
+ (role) => {
680
+ if (!state.user) return false;
681
+ const roles = Array.isArray(role) ? role : [role];
682
+ return roles.includes(state.user.role);
683
+ },
684
+ [state.user]
685
+ );
686
+ const contextValue = useMemo(
687
+ () => ({
688
+ // State
689
+ ...state,
690
+ isAuthenticated: state.status === "authenticated",
691
+ isVerified: state.user?.isVerified ?? false,
692
+ // Actions
693
+ login,
694
+ register,
695
+ logout,
696
+ verify,
697
+ resendVerification,
698
+ forgotPassword,
699
+ recoverPassword,
700
+ refreshTokens,
701
+ // Utilities
702
+ getAccessToken,
703
+ hasRole,
704
+ // Config
705
+ config
706
+ }),
707
+ [
708
+ state,
709
+ login,
710
+ register,
711
+ logout,
712
+ verify,
713
+ resendVerification,
714
+ forgotPassword,
715
+ recoverPassword,
716
+ refreshTokens,
717
+ getAccessToken,
718
+ hasRole,
719
+ config
720
+ ]
721
+ );
722
+ return /* @__PURE__ */ jsx(AuthContext.Provider, { value: contextValue, children });
723
+ }
724
+ function useAuthContext() {
725
+ const context = useContext(AuthContext);
726
+ if (!context) {
727
+ throw new Error("useAuthContext must be used within an AuthProvider");
728
+ }
729
+ return context;
730
+ }
731
+
732
+ // src/hooks/useProfileSync.ts
733
+ import { useState as useState2, useEffect as useEffect2, useCallback as useCallback2 } from "react";
734
+ function useProfileSync() {
735
+ const { user, config, getAccessToken } = useAuthContext();
736
+ const [profile, setProfile] = useState2(null);
737
+ const [isLoading, setIsLoading] = useState2(true);
738
+ const [isSyncing, setIsSyncing] = useState2(false);
739
+ const [error, setError] = useState2(null);
740
+ const fetchProfile = useCallback2(async () => {
741
+ const token = getAccessToken();
742
+ if (!token || !user) {
743
+ setProfile(null);
744
+ setIsLoading(false);
745
+ return;
746
+ }
747
+ try {
748
+ const response = await fetch(`${config.baseUrl}/users/profile`, {
749
+ headers: {
750
+ "Authorization": `Bearer ${token}`,
751
+ "Content-Type": "application/json",
752
+ "X-App-Id": config.appId
753
+ }
754
+ });
755
+ if (!response.ok) {
756
+ throw new Error("Failed to fetch profile");
757
+ }
758
+ const data = await response.json();
759
+ setProfile({
760
+ ...user,
761
+ ...data.user,
762
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
763
+ });
764
+ setError(null);
765
+ } catch (err) {
766
+ setError(err instanceof Error ? err : new Error("Failed to fetch profile"));
767
+ setProfile(user ? { ...user, lastSyncedAt: void 0 } : null);
768
+ } finally {
769
+ setIsLoading(false);
770
+ }
771
+ }, [config.baseUrl, config.appId, getAccessToken, user]);
772
+ useEffect2(() => {
773
+ if (user) {
774
+ fetchProfile();
775
+ } else {
776
+ setProfile(null);
777
+ setIsLoading(false);
778
+ }
779
+ }, [user, fetchProfile]);
780
+ const syncProfile = useCallback2(async () => {
781
+ if (!user) return;
782
+ setIsSyncing(true);
783
+ await fetchProfile();
784
+ setIsSyncing(false);
785
+ }, [user, fetchProfile]);
786
+ const updateProfile = useCallback2(async (updates) => {
787
+ const token = getAccessToken();
788
+ if (!token || !profile) {
789
+ throw new Error("Not authenticated");
790
+ }
791
+ setIsSyncing(true);
792
+ try {
793
+ const response = await fetch(`${config.baseUrl}/users/profile`, {
794
+ method: "PATCH",
795
+ headers: {
796
+ "Authorization": `Bearer ${token}`,
797
+ "Content-Type": "application/json",
798
+ "X-App-Id": config.appId
799
+ },
800
+ body: JSON.stringify(updates)
801
+ });
802
+ if (!response.ok) {
803
+ const errorData = await response.json().catch(() => ({}));
804
+ throw new Error(errorData.message || "Failed to update profile");
805
+ }
806
+ const data = await response.json();
807
+ setProfile({
808
+ ...profile,
809
+ ...data.user,
810
+ lastSyncedAt: (/* @__PURE__ */ new Date()).toISOString()
811
+ });
812
+ setError(null);
813
+ } catch (err) {
814
+ const error2 = err instanceof Error ? err : new Error("Failed to update profile");
815
+ setError(error2);
816
+ throw error2;
817
+ } finally {
818
+ setIsSyncing(false);
819
+ }
820
+ }, [config.baseUrl, config.appId, getAccessToken, profile]);
821
+ const updatePreferences = useCallback2(async (preferences) => {
822
+ await updateProfile({
823
+ preferences: {
824
+ ...profile?.preferences,
825
+ ...preferences
826
+ }
827
+ });
828
+ }, [updateProfile, profile?.preferences]);
829
+ const linkApp = useCallback2(async (appId) => {
830
+ const token = getAccessToken();
831
+ if (!token) {
832
+ throw new Error("Not authenticated");
833
+ }
834
+ setIsSyncing(true);
835
+ try {
836
+ const response = await fetch(`${config.baseUrl}/users/link-app`, {
837
+ method: "POST",
838
+ headers: {
839
+ "Authorization": `Bearer ${token}`,
840
+ "Content-Type": "application/json",
841
+ "X-App-Id": config.appId
842
+ },
843
+ body: JSON.stringify({ appId })
844
+ });
845
+ if (!response.ok) {
846
+ const errorData = await response.json().catch(() => ({}));
847
+ throw new Error(errorData.message || "Failed to link app");
848
+ }
849
+ await fetchProfile();
850
+ setError(null);
851
+ } catch (err) {
852
+ const error2 = err instanceof Error ? err : new Error("Failed to link app");
853
+ setError(error2);
854
+ throw error2;
855
+ } finally {
856
+ setIsSyncing(false);
857
+ }
858
+ }, [config.baseUrl, config.appId, getAccessToken, fetchProfile]);
859
+ return {
860
+ profile,
861
+ isLoading,
862
+ isSyncing,
863
+ error,
864
+ updateProfile,
865
+ syncProfile,
866
+ updatePreferences,
867
+ linkApp
868
+ };
869
+ }
870
+
871
+ // src/hooks/index.ts
872
+ import { useMemo as useMemo2, useEffect as useEffect3 } from "react";
873
+ function useAuth() {
874
+ const context = useAuthContext();
875
+ return useMemo2(
876
+ () => ({
877
+ // State
878
+ user: context.user,
879
+ status: context.status,
880
+ isLoading: context.isLoading,
881
+ isAuthenticated: context.isAuthenticated,
882
+ isVerified: context.isVerified,
883
+ error: context.error,
884
+ // Actions
885
+ login: context.login,
886
+ register: context.register,
887
+ logout: context.logout,
888
+ verify: context.verify,
889
+ resendVerification: context.resendVerification,
890
+ forgotPassword: context.forgotPassword,
891
+ recoverPassword: context.recoverPassword,
892
+ // Utilities
893
+ hasRole: context.hasRole
894
+ }),
895
+ [context]
896
+ );
897
+ }
898
+ function useUser() {
899
+ const { user, isAuthenticated, isVerified, hasRole } = useAuthContext();
900
+ return useMemo2(() => {
901
+ const dailyTokens = user?.dailyTokens ?? 0;
902
+ const purchasedTokens = user?.purchasedTokens ?? 0;
903
+ const bonusTokens = user?.bonusTokens ?? 0;
904
+ const subscriptionTokens = user?.subscriptionTokens ?? 0;
905
+ return {
906
+ user,
907
+ isAuthenticated,
908
+ isVerified,
909
+ role: user?.role ?? null,
910
+ hasRole,
911
+ // Shortcuts
912
+ userId: user?._id ?? null,
913
+ userName: user?.userName ?? null,
914
+ email: user?.email ?? null,
915
+ profilePicture: user?.profilePicture ?? null,
916
+ subscriptionTier: user?.subscriptionTier ?? null,
917
+ // Tokens
918
+ dailyTokens,
919
+ purchasedTokens,
920
+ bonusTokens,
921
+ subscriptionTokens,
922
+ totalTokens: dailyTokens + purchasedTokens + bonusTokens + subscriptionTokens
923
+ };
924
+ }, [user, isAuthenticated, isVerified, hasRole]);
925
+ }
926
+ function useToken() {
927
+ const { accessToken, refreshToken, getAccessToken, refreshTokens, isAuthenticated } = useAuthContext();
928
+ return useMemo2(
929
+ () => ({
930
+ accessToken,
931
+ refreshToken,
932
+ getAccessToken,
933
+ refreshTokens,
934
+ isAuthenticated
935
+ }),
936
+ [accessToken, refreshToken, getAccessToken, refreshTokens, isAuthenticated]
937
+ );
938
+ }
939
+ function useAuthStatus() {
940
+ const { status, isLoading } = useAuthContext();
941
+ return useMemo2(
942
+ () => ({
943
+ status,
944
+ isIdle: status === "idle",
945
+ isLoading,
946
+ isAuthenticated: status === "authenticated",
947
+ isUnauthenticated: status === "unauthenticated",
948
+ isVerifying: status === "verifying"
949
+ }),
950
+ [status, isLoading]
951
+ );
952
+ }
953
+ function useRequireAuth(options = {}) {
954
+ const { roles, onUnauthorized } = options;
955
+ const { user, isAuthenticated, isLoading, hasRole } = useAuthContext();
956
+ const isAuthorized = useMemo2(() => {
957
+ if (!isAuthenticated) return false;
958
+ if (roles && roles.length > 0 && !hasRole(roles)) return false;
959
+ return true;
960
+ }, [isAuthenticated, roles, hasRole]);
961
+ useEffect3(() => {
962
+ if (!isLoading && !isAuthorized && onUnauthorized) {
963
+ onUnauthorized();
964
+ }
965
+ }, [isLoading, isAuthorized, onUnauthorized]);
966
+ return useMemo2(
967
+ () => ({
968
+ isAuthorized,
969
+ isLoading,
970
+ user
971
+ }),
972
+ [isAuthorized, isLoading, user]
973
+ );
974
+ }
975
+ function useAuthError() {
976
+ const context = useAuthContext();
977
+ return useMemo2(
978
+ () => ({
979
+ error: context.error,
980
+ hasError: !!context.error,
981
+ errorMessage: context.error?.message ?? null,
982
+ errorCode: context.error?.code ?? null,
983
+ clearError: () => {
984
+ }
985
+ }),
986
+ [context.error]
987
+ );
988
+ }
989
+
990
+ export {
991
+ createTokenStorage,
992
+ createAuthApiClient,
993
+ AuthContext,
994
+ AuthProvider,
995
+ useAuthContext,
996
+ useProfileSync,
997
+ useAuth,
998
+ useUser,
999
+ useToken,
1000
+ useAuthStatus,
1001
+ useRequireAuth,
1002
+ useAuthError
1003
+ };