@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,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pixygon/auth - Auth Provider
|
|
3
|
+
* React context provider for authentication state management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createContext,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useState,
|
|
11
|
+
useCallback,
|
|
12
|
+
useMemo,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
} from 'react';
|
|
15
|
+
import type {
|
|
16
|
+
AuthConfig,
|
|
17
|
+
AuthContextValue,
|
|
18
|
+
AuthState,
|
|
19
|
+
AuthStatus,
|
|
20
|
+
AuthError,
|
|
21
|
+
UserRole,
|
|
22
|
+
LoginRequest,
|
|
23
|
+
LoginResponse,
|
|
24
|
+
RegisterRequest,
|
|
25
|
+
RegisterResponse,
|
|
26
|
+
VerifyRequest,
|
|
27
|
+
VerifyResponse,
|
|
28
|
+
ForgotPasswordRequest,
|
|
29
|
+
ForgotPasswordResponse,
|
|
30
|
+
RecoverPasswordRequest,
|
|
31
|
+
RecoverPasswordResponse,
|
|
32
|
+
ResendVerificationRequest,
|
|
33
|
+
ResendVerificationResponse,
|
|
34
|
+
} from '../types';
|
|
35
|
+
import { createTokenStorage } from '../utils/storage';
|
|
36
|
+
import { createAuthApiClient } from '../api/client';
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Context
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
43
|
+
|
|
44
|
+
// ============================================================================
|
|
45
|
+
// Default Config
|
|
46
|
+
// ============================================================================
|
|
47
|
+
|
|
48
|
+
const defaultConfig: Partial<AuthConfig> = {
|
|
49
|
+
autoRefresh: true,
|
|
50
|
+
refreshThreshold: 300, // 5 minutes before expiry
|
|
51
|
+
debug: false,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Provider Props
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
export interface AuthProviderProps {
|
|
59
|
+
config: AuthConfig;
|
|
60
|
+
children: ReactNode;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ============================================================================
|
|
64
|
+
// Provider Component
|
|
65
|
+
// ============================================================================
|
|
66
|
+
|
|
67
|
+
export function AuthProvider({ config: userConfig, children }: AuthProviderProps) {
|
|
68
|
+
const config = useMemo(() => ({ ...defaultConfig, ...userConfig }), [userConfig]);
|
|
69
|
+
|
|
70
|
+
// Initialize storage and API client
|
|
71
|
+
const tokenStorage = useMemo(
|
|
72
|
+
() => createTokenStorage(config.appId, config.storage),
|
|
73
|
+
[config.appId, config.storage]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const apiClient = useMemo(
|
|
77
|
+
() => createAuthApiClient(config as AuthConfig, tokenStorage),
|
|
78
|
+
[config, tokenStorage]
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Auth state
|
|
82
|
+
const [state, setState] = useState<AuthState>({
|
|
83
|
+
user: null,
|
|
84
|
+
accessToken: null,
|
|
85
|
+
refreshToken: null,
|
|
86
|
+
status: 'idle',
|
|
87
|
+
isLoading: true,
|
|
88
|
+
error: null,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const log = useCallback(
|
|
92
|
+
(...args: unknown[]) => {
|
|
93
|
+
if (config.debug) {
|
|
94
|
+
console.log('[PixygonAuth]', ...args);
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
[config.debug]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// ========================================================================
|
|
101
|
+
// Initialization - Restore auth state from storage
|
|
102
|
+
// ========================================================================
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
let mounted = true;
|
|
106
|
+
|
|
107
|
+
async function initializeAuth() {
|
|
108
|
+
log('Initializing auth...');
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const stored = await tokenStorage.getAll();
|
|
112
|
+
|
|
113
|
+
if (!stored.accessToken || !stored.user) {
|
|
114
|
+
log('No stored auth found');
|
|
115
|
+
if (mounted) {
|
|
116
|
+
setState((prev) => ({
|
|
117
|
+
...prev,
|
|
118
|
+
status: 'unauthenticated',
|
|
119
|
+
isLoading: false,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if token is expired
|
|
126
|
+
const isExpired = await tokenStorage.isTokenExpired();
|
|
127
|
+
|
|
128
|
+
if (isExpired && stored.refreshToken) {
|
|
129
|
+
log('Token expired, attempting refresh...');
|
|
130
|
+
try {
|
|
131
|
+
const response = await apiClient.refreshToken({
|
|
132
|
+
refreshToken: stored.refreshToken,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
if (mounted) {
|
|
136
|
+
setState({
|
|
137
|
+
user: stored.user,
|
|
138
|
+
accessToken: response.token,
|
|
139
|
+
refreshToken: response.refreshToken,
|
|
140
|
+
status: 'authenticated',
|
|
141
|
+
isLoading: false,
|
|
142
|
+
error: null,
|
|
143
|
+
});
|
|
144
|
+
apiClient.setAccessToken(response.token);
|
|
145
|
+
}
|
|
146
|
+
} catch (error) {
|
|
147
|
+
log('Token refresh failed:', error);
|
|
148
|
+
await tokenStorage.clear();
|
|
149
|
+
if (mounted) {
|
|
150
|
+
setState({
|
|
151
|
+
user: null,
|
|
152
|
+
accessToken: null,
|
|
153
|
+
refreshToken: null,
|
|
154
|
+
status: 'unauthenticated',
|
|
155
|
+
isLoading: false,
|
|
156
|
+
error: null,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else if (!isExpired) {
|
|
161
|
+
log('Restoring auth from storage');
|
|
162
|
+
apiClient.setAccessToken(stored.accessToken);
|
|
163
|
+
|
|
164
|
+
if (mounted) {
|
|
165
|
+
setState({
|
|
166
|
+
user: stored.user,
|
|
167
|
+
accessToken: stored.accessToken,
|
|
168
|
+
refreshToken: stored.refreshToken,
|
|
169
|
+
status: stored.user.isVerified ? 'authenticated' : 'verifying',
|
|
170
|
+
isLoading: false,
|
|
171
|
+
error: null,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
log('Token expired and no refresh token available');
|
|
176
|
+
await tokenStorage.clear();
|
|
177
|
+
if (mounted) {
|
|
178
|
+
setState({
|
|
179
|
+
user: null,
|
|
180
|
+
accessToken: null,
|
|
181
|
+
refreshToken: null,
|
|
182
|
+
status: 'unauthenticated',
|
|
183
|
+
isLoading: false,
|
|
184
|
+
error: null,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
log('Auth initialization error:', error);
|
|
190
|
+
if (mounted) {
|
|
191
|
+
setState((prev) => ({
|
|
192
|
+
...prev,
|
|
193
|
+
status: 'unauthenticated',
|
|
194
|
+
isLoading: false,
|
|
195
|
+
error: {
|
|
196
|
+
code: 'UNKNOWN_ERROR',
|
|
197
|
+
message: 'Failed to initialize authentication',
|
|
198
|
+
},
|
|
199
|
+
}));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
initializeAuth();
|
|
205
|
+
|
|
206
|
+
return () => {
|
|
207
|
+
mounted = false;
|
|
208
|
+
};
|
|
209
|
+
}, [apiClient, tokenStorage, log]);
|
|
210
|
+
|
|
211
|
+
// ========================================================================
|
|
212
|
+
// Auto Token Refresh
|
|
213
|
+
// ========================================================================
|
|
214
|
+
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (!config.autoRefresh || state.status !== 'authenticated') {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const checkAndRefresh = async () => {
|
|
221
|
+
const willExpire = await tokenStorage.willExpireSoon(config.refreshThreshold || 300);
|
|
222
|
+
if (willExpire && state.refreshToken) {
|
|
223
|
+
log('Token expiring soon, refreshing...');
|
|
224
|
+
try {
|
|
225
|
+
const response = await apiClient.refreshToken({
|
|
226
|
+
refreshToken: state.refreshToken,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
setState((prev) => ({
|
|
230
|
+
...prev,
|
|
231
|
+
accessToken: response.token,
|
|
232
|
+
refreshToken: response.refreshToken,
|
|
233
|
+
}));
|
|
234
|
+
apiClient.setAccessToken(response.token);
|
|
235
|
+
} catch (error) {
|
|
236
|
+
log('Auto-refresh failed:', error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
// Check every minute
|
|
242
|
+
const interval = setInterval(checkAndRefresh, 60000);
|
|
243
|
+
|
|
244
|
+
return () => clearInterval(interval);
|
|
245
|
+
}, [
|
|
246
|
+
config.autoRefresh,
|
|
247
|
+
config.refreshThreshold,
|
|
248
|
+
state.status,
|
|
249
|
+
state.refreshToken,
|
|
250
|
+
apiClient,
|
|
251
|
+
tokenStorage,
|
|
252
|
+
log,
|
|
253
|
+
]);
|
|
254
|
+
|
|
255
|
+
// ========================================================================
|
|
256
|
+
// Auth Actions
|
|
257
|
+
// ========================================================================
|
|
258
|
+
|
|
259
|
+
const login = useCallback(
|
|
260
|
+
async (credentials: LoginRequest): Promise<LoginResponse> => {
|
|
261
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const response = await apiClient.login(credentials);
|
|
265
|
+
|
|
266
|
+
const newStatus: AuthStatus = response.user.isVerified ? 'authenticated' : 'verifying';
|
|
267
|
+
|
|
268
|
+
setState({
|
|
269
|
+
user: response.user,
|
|
270
|
+
accessToken: response.token,
|
|
271
|
+
refreshToken: response.refreshToken || null,
|
|
272
|
+
status: newStatus,
|
|
273
|
+
isLoading: false,
|
|
274
|
+
error: null,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return response;
|
|
278
|
+
} catch (error) {
|
|
279
|
+
const authError = error as AuthError;
|
|
280
|
+
setState((prev) => ({
|
|
281
|
+
...prev,
|
|
282
|
+
isLoading: false,
|
|
283
|
+
error: authError,
|
|
284
|
+
}));
|
|
285
|
+
config.onError?.(authError);
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
[apiClient, config]
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const register = useCallback(
|
|
293
|
+
async (data: RegisterRequest): Promise<RegisterResponse> => {
|
|
294
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const response = await apiClient.register(data);
|
|
298
|
+
|
|
299
|
+
setState((prev) => ({
|
|
300
|
+
...prev,
|
|
301
|
+
isLoading: false,
|
|
302
|
+
}));
|
|
303
|
+
|
|
304
|
+
return response;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
const authError = error as AuthError;
|
|
307
|
+
setState((prev) => ({
|
|
308
|
+
...prev,
|
|
309
|
+
isLoading: false,
|
|
310
|
+
error: authError,
|
|
311
|
+
}));
|
|
312
|
+
config.onError?.(authError);
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
[apiClient, config]
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const verify = useCallback(
|
|
320
|
+
async (data: VerifyRequest): Promise<VerifyResponse> => {
|
|
321
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const response = await apiClient.verify(data);
|
|
325
|
+
|
|
326
|
+
setState({
|
|
327
|
+
user: response.user,
|
|
328
|
+
accessToken: response.token,
|
|
329
|
+
refreshToken: response.refreshToken || null,
|
|
330
|
+
status: 'authenticated',
|
|
331
|
+
isLoading: false,
|
|
332
|
+
error: null,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
return response;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
const authError = error as AuthError;
|
|
338
|
+
setState((prev) => ({
|
|
339
|
+
...prev,
|
|
340
|
+
isLoading: false,
|
|
341
|
+
error: authError,
|
|
342
|
+
}));
|
|
343
|
+
config.onError?.(authError);
|
|
344
|
+
throw error;
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
[apiClient, config]
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const resendVerification = useCallback(
|
|
351
|
+
async (data: ResendVerificationRequest): Promise<ResendVerificationResponse> => {
|
|
352
|
+
return apiClient.resendVerification(data);
|
|
353
|
+
},
|
|
354
|
+
[apiClient]
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
const forgotPassword = useCallback(
|
|
358
|
+
async (data: ForgotPasswordRequest): Promise<ForgotPasswordResponse> => {
|
|
359
|
+
return apiClient.forgotPassword(data);
|
|
360
|
+
},
|
|
361
|
+
[apiClient]
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
const recoverPassword = useCallback(
|
|
365
|
+
async (data: RecoverPasswordRequest): Promise<RecoverPasswordResponse> => {
|
|
366
|
+
return apiClient.recoverPassword(data);
|
|
367
|
+
},
|
|
368
|
+
[apiClient]
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
const logout = useCallback(async (): Promise<void> => {
|
|
372
|
+
log('Logging out...');
|
|
373
|
+
|
|
374
|
+
await tokenStorage.clear();
|
|
375
|
+
apiClient.setAccessToken(null);
|
|
376
|
+
|
|
377
|
+
setState({
|
|
378
|
+
user: null,
|
|
379
|
+
accessToken: null,
|
|
380
|
+
refreshToken: null,
|
|
381
|
+
status: 'unauthenticated',
|
|
382
|
+
isLoading: false,
|
|
383
|
+
error: null,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
config.onLogout?.();
|
|
387
|
+
}, [tokenStorage, apiClient, config, log]);
|
|
388
|
+
|
|
389
|
+
const refreshTokens = useCallback(async (): Promise<void> => {
|
|
390
|
+
if (!state.refreshToken) {
|
|
391
|
+
throw new Error('No refresh token available');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const response = await apiClient.refreshToken({
|
|
395
|
+
refreshToken: state.refreshToken,
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
setState((prev) => ({
|
|
399
|
+
...prev,
|
|
400
|
+
accessToken: response.token,
|
|
401
|
+
refreshToken: response.refreshToken,
|
|
402
|
+
}));
|
|
403
|
+
}, [apiClient, state.refreshToken]);
|
|
404
|
+
|
|
405
|
+
// ========================================================================
|
|
406
|
+
// Utility Functions
|
|
407
|
+
// ========================================================================
|
|
408
|
+
|
|
409
|
+
const getAccessToken = useCallback(() => state.accessToken, [state.accessToken]);
|
|
410
|
+
|
|
411
|
+
const hasRole = useCallback(
|
|
412
|
+
(role: UserRole | UserRole[]): boolean => {
|
|
413
|
+
if (!state.user) return false;
|
|
414
|
+
|
|
415
|
+
const roles = Array.isArray(role) ? role : [role];
|
|
416
|
+
return roles.includes(state.user.role);
|
|
417
|
+
},
|
|
418
|
+
[state.user]
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
// ========================================================================
|
|
422
|
+
// Context Value
|
|
423
|
+
// ========================================================================
|
|
424
|
+
|
|
425
|
+
const contextValue = useMemo<AuthContextValue>(
|
|
426
|
+
() => ({
|
|
427
|
+
// State
|
|
428
|
+
...state,
|
|
429
|
+
isAuthenticated: state.status === 'authenticated',
|
|
430
|
+
isVerified: state.user?.isVerified ?? false,
|
|
431
|
+
|
|
432
|
+
// Actions
|
|
433
|
+
login,
|
|
434
|
+
register,
|
|
435
|
+
logout,
|
|
436
|
+
verify,
|
|
437
|
+
resendVerification,
|
|
438
|
+
forgotPassword,
|
|
439
|
+
recoverPassword,
|
|
440
|
+
refreshTokens,
|
|
441
|
+
|
|
442
|
+
// Utilities
|
|
443
|
+
getAccessToken,
|
|
444
|
+
hasRole,
|
|
445
|
+
|
|
446
|
+
// Config
|
|
447
|
+
config: config as AuthConfig,
|
|
448
|
+
}),
|
|
449
|
+
[
|
|
450
|
+
state,
|
|
451
|
+
login,
|
|
452
|
+
register,
|
|
453
|
+
logout,
|
|
454
|
+
verify,
|
|
455
|
+
resendVerification,
|
|
456
|
+
forgotPassword,
|
|
457
|
+
recoverPassword,
|
|
458
|
+
refreshTokens,
|
|
459
|
+
getAccessToken,
|
|
460
|
+
hasRole,
|
|
461
|
+
config,
|
|
462
|
+
]
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
return <AuthContext.Provider value={contextValue}>{children}</AuthContext.Provider>;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ============================================================================
|
|
469
|
+
// Hook
|
|
470
|
+
// ============================================================================
|
|
471
|
+
|
|
472
|
+
export function useAuthContext(): AuthContextValue {
|
|
473
|
+
const context = useContext(AuthContext);
|
|
474
|
+
if (!context) {
|
|
475
|
+
throw new Error('useAuthContext must be used within an AuthProvider');
|
|
476
|
+
}
|
|
477
|
+
return context;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export { AuthContext };
|