@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.
- package/README.md +267 -0
- package/dist/chunk-E34M2RJD.mjs +1003 -0
- package/dist/components/index.d.mts +14 -0
- package/dist/components/index.d.ts +14 -0
- package/dist/components/index.js +1720 -0
- package/dist/components/index.mjs +1646 -0
- package/dist/index-CIK2MKl9.d.mts +201 -0
- package/dist/index-CIK2MKl9.d.ts +201 -0
- package/dist/index.d.mts +233 -0
- package/dist/index.d.ts +233 -0
- package/dist/index.js +1037 -0
- package/dist/index.mjs +28 -0
- package/package.json +58 -0
- package/src/api/client.ts +358 -0
- package/src/components/ForgotPasswordForm.tsx +410 -0
- package/src/components/LoginForm.tsx +323 -0
- package/src/components/PixygonAuth.tsx +170 -0
- package/src/components/RegisterForm.tsx +463 -0
- package/src/components/VerifyForm.tsx +411 -0
- package/src/components/index.ts +40 -0
- package/src/components/styles.ts +282 -0
- package/src/hooks/index.ts +284 -0
- package/src/hooks/useProfileSync.ts +201 -0
- package/src/index.ts +99 -0
- package/src/providers/AuthProvider.tsx +480 -0
- package/src/types/index.ts +310 -0
- package/src/utils/storage.ts +167 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pixygon/auth - Component Styles
|
|
3
|
+
* CSS-in-JS styles for auth components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const colors = {
|
|
7
|
+
// Brand colors
|
|
8
|
+
primary: '#6366f1', // Indigo
|
|
9
|
+
primaryHover: '#4f46e5',
|
|
10
|
+
primaryLight: '#818cf8',
|
|
11
|
+
|
|
12
|
+
// Neutrals
|
|
13
|
+
background: '#0f0f0f',
|
|
14
|
+
backgroundLight: '#1a1a1a',
|
|
15
|
+
surface: '#262626',
|
|
16
|
+
surfaceHover: '#333333',
|
|
17
|
+
border: '#404040',
|
|
18
|
+
borderFocus: '#6366f1',
|
|
19
|
+
|
|
20
|
+
// Text
|
|
21
|
+
text: '#ffffff',
|
|
22
|
+
textSecondary: '#a3a3a3',
|
|
23
|
+
textMuted: '#737373',
|
|
24
|
+
|
|
25
|
+
// States
|
|
26
|
+
error: '#ef4444',
|
|
27
|
+
errorLight: '#fca5a5',
|
|
28
|
+
success: '#22c55e',
|
|
29
|
+
successLight: '#86efac',
|
|
30
|
+
warning: '#f59e0b',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const styles = {
|
|
34
|
+
// Container
|
|
35
|
+
container: `
|
|
36
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
37
|
+
background: ${colors.background};
|
|
38
|
+
color: ${colors.text};
|
|
39
|
+
padding: 2rem;
|
|
40
|
+
border-radius: 1rem;
|
|
41
|
+
max-width: 400px;
|
|
42
|
+
width: 100%;
|
|
43
|
+
margin: 0 auto;
|
|
44
|
+
`,
|
|
45
|
+
|
|
46
|
+
// Header
|
|
47
|
+
header: `
|
|
48
|
+
text-align: center;
|
|
49
|
+
margin-bottom: 2rem;
|
|
50
|
+
`,
|
|
51
|
+
|
|
52
|
+
logo: `
|
|
53
|
+
width: 64px;
|
|
54
|
+
height: 64px;
|
|
55
|
+
margin: 0 auto 1rem;
|
|
56
|
+
display: block;
|
|
57
|
+
`,
|
|
58
|
+
|
|
59
|
+
title: `
|
|
60
|
+
font-size: 1.5rem;
|
|
61
|
+
font-weight: 600;
|
|
62
|
+
margin: 0 0 0.5rem;
|
|
63
|
+
color: ${colors.text};
|
|
64
|
+
`,
|
|
65
|
+
|
|
66
|
+
subtitle: `
|
|
67
|
+
font-size: 0.875rem;
|
|
68
|
+
color: ${colors.textSecondary};
|
|
69
|
+
margin: 0;
|
|
70
|
+
`,
|
|
71
|
+
|
|
72
|
+
// Form
|
|
73
|
+
form: `
|
|
74
|
+
display: flex;
|
|
75
|
+
flex-direction: column;
|
|
76
|
+
gap: 1rem;
|
|
77
|
+
`,
|
|
78
|
+
|
|
79
|
+
inputGroup: `
|
|
80
|
+
display: flex;
|
|
81
|
+
flex-direction: column;
|
|
82
|
+
gap: 0.375rem;
|
|
83
|
+
`,
|
|
84
|
+
|
|
85
|
+
label: `
|
|
86
|
+
font-size: 0.875rem;
|
|
87
|
+
font-weight: 500;
|
|
88
|
+
color: ${colors.textSecondary};
|
|
89
|
+
`,
|
|
90
|
+
|
|
91
|
+
input: `
|
|
92
|
+
background: ${colors.surface};
|
|
93
|
+
border: 1px solid ${colors.border};
|
|
94
|
+
border-radius: 0.5rem;
|
|
95
|
+
padding: 0.75rem 1rem;
|
|
96
|
+
font-size: 1rem;
|
|
97
|
+
color: ${colors.text};
|
|
98
|
+
outline: none;
|
|
99
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
100
|
+
`,
|
|
101
|
+
|
|
102
|
+
inputFocus: `
|
|
103
|
+
border-color: ${colors.borderFocus};
|
|
104
|
+
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
|
|
105
|
+
`,
|
|
106
|
+
|
|
107
|
+
inputError: `
|
|
108
|
+
border-color: ${colors.error};
|
|
109
|
+
`,
|
|
110
|
+
|
|
111
|
+
// Buttons
|
|
112
|
+
button: `
|
|
113
|
+
background: ${colors.primary};
|
|
114
|
+
color: white;
|
|
115
|
+
border: none;
|
|
116
|
+
border-radius: 0.5rem;
|
|
117
|
+
padding: 0.875rem 1.5rem;
|
|
118
|
+
font-size: 1rem;
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
transition: background 0.2s, transform 0.1s;
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: center;
|
|
125
|
+
gap: 0.5rem;
|
|
126
|
+
`,
|
|
127
|
+
|
|
128
|
+
buttonHover: `
|
|
129
|
+
background: ${colors.primaryHover};
|
|
130
|
+
`,
|
|
131
|
+
|
|
132
|
+
buttonDisabled: `
|
|
133
|
+
opacity: 0.6;
|
|
134
|
+
cursor: not-allowed;
|
|
135
|
+
`,
|
|
136
|
+
|
|
137
|
+
buttonSecondary: `
|
|
138
|
+
background: transparent;
|
|
139
|
+
color: ${colors.textSecondary};
|
|
140
|
+
border: 1px solid ${colors.border};
|
|
141
|
+
`,
|
|
142
|
+
|
|
143
|
+
buttonSecondaryHover: `
|
|
144
|
+
background: ${colors.surface};
|
|
145
|
+
color: ${colors.text};
|
|
146
|
+
`,
|
|
147
|
+
|
|
148
|
+
// Links
|
|
149
|
+
link: `
|
|
150
|
+
color: ${colors.primary};
|
|
151
|
+
text-decoration: none;
|
|
152
|
+
font-size: 0.875rem;
|
|
153
|
+
cursor: pointer;
|
|
154
|
+
transition: color 0.2s;
|
|
155
|
+
`,
|
|
156
|
+
|
|
157
|
+
linkHover: `
|
|
158
|
+
color: ${colors.primaryLight};
|
|
159
|
+
text-decoration: underline;
|
|
160
|
+
`,
|
|
161
|
+
|
|
162
|
+
// Messages
|
|
163
|
+
error: `
|
|
164
|
+
background: rgba(239, 68, 68, 0.1);
|
|
165
|
+
border: 1px solid ${colors.error};
|
|
166
|
+
border-radius: 0.5rem;
|
|
167
|
+
padding: 0.75rem 1rem;
|
|
168
|
+
color: ${colors.errorLight};
|
|
169
|
+
font-size: 0.875rem;
|
|
170
|
+
display: flex;
|
|
171
|
+
align-items: center;
|
|
172
|
+
gap: 0.5rem;
|
|
173
|
+
`,
|
|
174
|
+
|
|
175
|
+
success: `
|
|
176
|
+
background: rgba(34, 197, 94, 0.1);
|
|
177
|
+
border: 1px solid ${colors.success};
|
|
178
|
+
border-radius: 0.5rem;
|
|
179
|
+
padding: 0.75rem 1rem;
|
|
180
|
+
color: ${colors.successLight};
|
|
181
|
+
font-size: 0.875rem;
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
gap: 0.5rem;
|
|
185
|
+
`,
|
|
186
|
+
|
|
187
|
+
// Divider
|
|
188
|
+
divider: `
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 1rem;
|
|
192
|
+
color: ${colors.textMuted};
|
|
193
|
+
font-size: 0.75rem;
|
|
194
|
+
margin: 0.5rem 0;
|
|
195
|
+
`,
|
|
196
|
+
|
|
197
|
+
dividerLine: `
|
|
198
|
+
flex: 1;
|
|
199
|
+
height: 1px;
|
|
200
|
+
background: ${colors.border};
|
|
201
|
+
`,
|
|
202
|
+
|
|
203
|
+
// Footer
|
|
204
|
+
footer: `
|
|
205
|
+
text-align: center;
|
|
206
|
+
margin-top: 1.5rem;
|
|
207
|
+
font-size: 0.875rem;
|
|
208
|
+
color: ${colors.textSecondary};
|
|
209
|
+
`,
|
|
210
|
+
|
|
211
|
+
// Loading spinner
|
|
212
|
+
spinner: `
|
|
213
|
+
width: 20px;
|
|
214
|
+
height: 20px;
|
|
215
|
+
border: 2px solid transparent;
|
|
216
|
+
border-top-color: currentColor;
|
|
217
|
+
border-radius: 50%;
|
|
218
|
+
animation: spin 0.8s linear infinite;
|
|
219
|
+
`,
|
|
220
|
+
|
|
221
|
+
// Branding
|
|
222
|
+
brandingContainer: `
|
|
223
|
+
display: flex;
|
|
224
|
+
align-items: center;
|
|
225
|
+
justify-content: center;
|
|
226
|
+
gap: 0.5rem;
|
|
227
|
+
margin-bottom: 1.5rem;
|
|
228
|
+
`,
|
|
229
|
+
|
|
230
|
+
brandingText: `
|
|
231
|
+
font-size: 0.75rem;
|
|
232
|
+
color: ${colors.textMuted};
|
|
233
|
+
`,
|
|
234
|
+
|
|
235
|
+
brandingLogo: `
|
|
236
|
+
height: 16px;
|
|
237
|
+
width: auto;
|
|
238
|
+
`,
|
|
239
|
+
|
|
240
|
+
// Verification code input
|
|
241
|
+
codeInputContainer: `
|
|
242
|
+
display: flex;
|
|
243
|
+
gap: 0.5rem;
|
|
244
|
+
justify-content: center;
|
|
245
|
+
`,
|
|
246
|
+
|
|
247
|
+
codeInput: `
|
|
248
|
+
width: 3rem;
|
|
249
|
+
height: 3.5rem;
|
|
250
|
+
text-align: center;
|
|
251
|
+
font-size: 1.5rem;
|
|
252
|
+
font-weight: 600;
|
|
253
|
+
background: ${colors.surface};
|
|
254
|
+
border: 1px solid ${colors.border};
|
|
255
|
+
border-radius: 0.5rem;
|
|
256
|
+
color: ${colors.text};
|
|
257
|
+
outline: none;
|
|
258
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
259
|
+
`,
|
|
260
|
+
|
|
261
|
+
// Password strength
|
|
262
|
+
passwordStrength: `
|
|
263
|
+
display: flex;
|
|
264
|
+
gap: 0.25rem;
|
|
265
|
+
margin-top: 0.5rem;
|
|
266
|
+
`,
|
|
267
|
+
|
|
268
|
+
passwordStrengthBar: `
|
|
269
|
+
flex: 1;
|
|
270
|
+
height: 4px;
|
|
271
|
+
border-radius: 2px;
|
|
272
|
+
background: ${colors.border};
|
|
273
|
+
transition: background 0.3s;
|
|
274
|
+
`,
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// CSS keyframes (to be injected)
|
|
278
|
+
export const keyframes = `
|
|
279
|
+
@keyframes spin {
|
|
280
|
+
to { transform: rotate(360deg); }
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pixygon/auth - Custom Hooks
|
|
3
|
+
* Convenient hooks for accessing auth state and actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback, useMemo, useEffect } from 'react';
|
|
7
|
+
import { useAuthContext } from '../providers/AuthProvider';
|
|
8
|
+
import type { User, UserRole, AuthStatus, AuthError } from '../types';
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// useAuth - Main auth hook
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
export interface UseAuthReturn {
|
|
15
|
+
// State
|
|
16
|
+
user: User | null;
|
|
17
|
+
status: AuthStatus;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
isAuthenticated: boolean;
|
|
20
|
+
isVerified: boolean;
|
|
21
|
+
error: AuthError | null;
|
|
22
|
+
|
|
23
|
+
// Actions
|
|
24
|
+
login: ReturnType<typeof useAuthContext>['login'];
|
|
25
|
+
register: ReturnType<typeof useAuthContext>['register'];
|
|
26
|
+
logout: ReturnType<typeof useAuthContext>['logout'];
|
|
27
|
+
verify: ReturnType<typeof useAuthContext>['verify'];
|
|
28
|
+
resendVerification: ReturnType<typeof useAuthContext>['resendVerification'];
|
|
29
|
+
forgotPassword: ReturnType<typeof useAuthContext>['forgotPassword'];
|
|
30
|
+
recoverPassword: ReturnType<typeof useAuthContext>['recoverPassword'];
|
|
31
|
+
|
|
32
|
+
// Utilities
|
|
33
|
+
hasRole: (role: UserRole | UserRole[]) => boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Main auth hook providing all auth state and actions
|
|
38
|
+
*/
|
|
39
|
+
export function useAuth(): UseAuthReturn {
|
|
40
|
+
const context = useAuthContext();
|
|
41
|
+
|
|
42
|
+
return useMemo(
|
|
43
|
+
() => ({
|
|
44
|
+
// State
|
|
45
|
+
user: context.user,
|
|
46
|
+
status: context.status,
|
|
47
|
+
isLoading: context.isLoading,
|
|
48
|
+
isAuthenticated: context.isAuthenticated,
|
|
49
|
+
isVerified: context.isVerified,
|
|
50
|
+
error: context.error,
|
|
51
|
+
|
|
52
|
+
// Actions
|
|
53
|
+
login: context.login,
|
|
54
|
+
register: context.register,
|
|
55
|
+
logout: context.logout,
|
|
56
|
+
verify: context.verify,
|
|
57
|
+
resendVerification: context.resendVerification,
|
|
58
|
+
forgotPassword: context.forgotPassword,
|
|
59
|
+
recoverPassword: context.recoverPassword,
|
|
60
|
+
|
|
61
|
+
// Utilities
|
|
62
|
+
hasRole: context.hasRole,
|
|
63
|
+
}),
|
|
64
|
+
[context]
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// useUser - User data hook
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
export interface UseUserReturn {
|
|
73
|
+
user: User | null;
|
|
74
|
+
isAuthenticated: boolean;
|
|
75
|
+
isVerified: boolean;
|
|
76
|
+
role: UserRole | null;
|
|
77
|
+
hasRole: (role: UserRole | UserRole[]) => boolean;
|
|
78
|
+
|
|
79
|
+
// User properties shortcuts
|
|
80
|
+
userId: string | null;
|
|
81
|
+
userName: string | null;
|
|
82
|
+
email: string | null;
|
|
83
|
+
profilePicture: string | null;
|
|
84
|
+
subscriptionTier: User['subscriptionTier'] | null;
|
|
85
|
+
|
|
86
|
+
// Token info
|
|
87
|
+
dailyTokens: number;
|
|
88
|
+
purchasedTokens: number;
|
|
89
|
+
bonusTokens: number;
|
|
90
|
+
subscriptionTokens: number;
|
|
91
|
+
totalTokens: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Hook for accessing user data with convenient shortcuts
|
|
96
|
+
*/
|
|
97
|
+
export function useUser(): UseUserReturn {
|
|
98
|
+
const { user, isAuthenticated, isVerified, hasRole } = useAuthContext();
|
|
99
|
+
|
|
100
|
+
return useMemo(() => {
|
|
101
|
+
const dailyTokens = user?.dailyTokens ?? 0;
|
|
102
|
+
const purchasedTokens = user?.purchasedTokens ?? 0;
|
|
103
|
+
const bonusTokens = user?.bonusTokens ?? 0;
|
|
104
|
+
const subscriptionTokens = user?.subscriptionTokens ?? 0;
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
user,
|
|
108
|
+
isAuthenticated,
|
|
109
|
+
isVerified,
|
|
110
|
+
role: user?.role ?? null,
|
|
111
|
+
hasRole,
|
|
112
|
+
|
|
113
|
+
// Shortcuts
|
|
114
|
+
userId: user?._id ?? null,
|
|
115
|
+
userName: user?.userName ?? null,
|
|
116
|
+
email: user?.email ?? null,
|
|
117
|
+
profilePicture: user?.profilePicture ?? null,
|
|
118
|
+
subscriptionTier: user?.subscriptionTier ?? null,
|
|
119
|
+
|
|
120
|
+
// Tokens
|
|
121
|
+
dailyTokens,
|
|
122
|
+
purchasedTokens,
|
|
123
|
+
bonusTokens,
|
|
124
|
+
subscriptionTokens,
|
|
125
|
+
totalTokens: dailyTokens + purchasedTokens + bonusTokens + subscriptionTokens,
|
|
126
|
+
};
|
|
127
|
+
}, [user, isAuthenticated, isVerified, hasRole]);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// useToken - Token management hook
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
export interface UseTokenReturn {
|
|
135
|
+
accessToken: string | null;
|
|
136
|
+
refreshToken: string | null;
|
|
137
|
+
getAccessToken: () => string | null;
|
|
138
|
+
refreshTokens: () => Promise<void>;
|
|
139
|
+
isAuthenticated: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Hook for accessing and managing auth tokens
|
|
144
|
+
*/
|
|
145
|
+
export function useToken(): UseTokenReturn {
|
|
146
|
+
const { accessToken, refreshToken, getAccessToken, refreshTokens, isAuthenticated } =
|
|
147
|
+
useAuthContext();
|
|
148
|
+
|
|
149
|
+
return useMemo(
|
|
150
|
+
() => ({
|
|
151
|
+
accessToken,
|
|
152
|
+
refreshToken,
|
|
153
|
+
getAccessToken,
|
|
154
|
+
refreshTokens,
|
|
155
|
+
isAuthenticated,
|
|
156
|
+
}),
|
|
157
|
+
[accessToken, refreshToken, getAccessToken, refreshTokens, isAuthenticated]
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// useAuthStatus - Status-specific hook
|
|
163
|
+
// ============================================================================
|
|
164
|
+
|
|
165
|
+
export interface UseAuthStatusReturn {
|
|
166
|
+
status: AuthStatus;
|
|
167
|
+
isIdle: boolean;
|
|
168
|
+
isLoading: boolean;
|
|
169
|
+
isAuthenticated: boolean;
|
|
170
|
+
isUnauthenticated: boolean;
|
|
171
|
+
isVerifying: boolean;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Hook for auth status checks
|
|
176
|
+
*/
|
|
177
|
+
export function useAuthStatus(): UseAuthStatusReturn {
|
|
178
|
+
const { status, isLoading } = useAuthContext();
|
|
179
|
+
|
|
180
|
+
return useMemo(
|
|
181
|
+
() => ({
|
|
182
|
+
status,
|
|
183
|
+
isIdle: status === 'idle',
|
|
184
|
+
isLoading,
|
|
185
|
+
isAuthenticated: status === 'authenticated',
|
|
186
|
+
isUnauthenticated: status === 'unauthenticated',
|
|
187
|
+
isVerifying: status === 'verifying',
|
|
188
|
+
}),
|
|
189
|
+
[status, isLoading]
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ============================================================================
|
|
194
|
+
// useRequireAuth - Protected route hook
|
|
195
|
+
// ============================================================================
|
|
196
|
+
|
|
197
|
+
export interface UseRequireAuthOptions {
|
|
198
|
+
/** Redirect path when not authenticated */
|
|
199
|
+
redirectTo?: string;
|
|
200
|
+
/** Required roles (any of) */
|
|
201
|
+
roles?: UserRole[];
|
|
202
|
+
/** Callback when auth check fails */
|
|
203
|
+
onUnauthorized?: () => void;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface UseRequireAuthReturn {
|
|
207
|
+
isAuthorized: boolean;
|
|
208
|
+
isLoading: boolean;
|
|
209
|
+
user: User | null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Hook for protected routes/components
|
|
214
|
+
* Returns authorization state and user
|
|
215
|
+
*/
|
|
216
|
+
export function useRequireAuth(options: UseRequireAuthOptions = {}): UseRequireAuthReturn {
|
|
217
|
+
const { roles, onUnauthorized } = options;
|
|
218
|
+
const { user, isAuthenticated, isLoading, hasRole } = useAuthContext();
|
|
219
|
+
|
|
220
|
+
const isAuthorized = useMemo(() => {
|
|
221
|
+
if (!isAuthenticated) return false;
|
|
222
|
+
if (roles && roles.length > 0 && !hasRole(roles)) return false;
|
|
223
|
+
return true;
|
|
224
|
+
}, [isAuthenticated, roles, hasRole]);
|
|
225
|
+
|
|
226
|
+
// Call onUnauthorized when auth check is complete and user is not authorized
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!isLoading && !isAuthorized && onUnauthorized) {
|
|
229
|
+
onUnauthorized();
|
|
230
|
+
}
|
|
231
|
+
}, [isLoading, isAuthorized, onUnauthorized]);
|
|
232
|
+
|
|
233
|
+
return useMemo(
|
|
234
|
+
() => ({
|
|
235
|
+
isAuthorized,
|
|
236
|
+
isLoading,
|
|
237
|
+
user,
|
|
238
|
+
}),
|
|
239
|
+
[isAuthorized, isLoading, user]
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ============================================================================
|
|
244
|
+
// useProfileSync - Profile synchronization hook
|
|
245
|
+
// ============================================================================
|
|
246
|
+
|
|
247
|
+
export { useProfileSync } from './useProfileSync';
|
|
248
|
+
export type { UserProfile, UseProfileSyncReturn } from './useProfileSync';
|
|
249
|
+
|
|
250
|
+
// ============================================================================
|
|
251
|
+
// useAuthError - Error handling hook
|
|
252
|
+
// ============================================================================
|
|
253
|
+
|
|
254
|
+
export interface UseAuthErrorReturn {
|
|
255
|
+
error: AuthError | null;
|
|
256
|
+
hasError: boolean;
|
|
257
|
+
errorMessage: string | null;
|
|
258
|
+
errorCode: AuthError['code'] | null;
|
|
259
|
+
clearError: () => void;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Hook for auth error handling
|
|
264
|
+
*/
|
|
265
|
+
export function useAuthError(): UseAuthErrorReturn {
|
|
266
|
+
const context = useAuthContext();
|
|
267
|
+
|
|
268
|
+
// Note: clearError would need to be added to the context
|
|
269
|
+
// For now, errors clear on next action
|
|
270
|
+
|
|
271
|
+
return useMemo(
|
|
272
|
+
() => ({
|
|
273
|
+
error: context.error,
|
|
274
|
+
hasError: !!context.error,
|
|
275
|
+
errorMessage: context.error?.message ?? null,
|
|
276
|
+
errorCode: context.error?.code ?? null,
|
|
277
|
+
clearError: () => {
|
|
278
|
+
// Errors are cleared on next action
|
|
279
|
+
// Could be enhanced with explicit clear in context
|
|
280
|
+
},
|
|
281
|
+
}),
|
|
282
|
+
[context.error]
|
|
283
|
+
);
|
|
284
|
+
}
|