@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
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthContext,
|
|
3
|
+
AuthProvider,
|
|
4
|
+
createAuthApiClient,
|
|
5
|
+
createTokenStorage,
|
|
6
|
+
useAuth,
|
|
7
|
+
useAuthContext,
|
|
8
|
+
useAuthError,
|
|
9
|
+
useAuthStatus,
|
|
10
|
+
useProfileSync,
|
|
11
|
+
useRequireAuth,
|
|
12
|
+
useToken,
|
|
13
|
+
useUser
|
|
14
|
+
} from "./chunk-E34M2RJD.mjs";
|
|
15
|
+
export {
|
|
16
|
+
AuthContext,
|
|
17
|
+
AuthProvider,
|
|
18
|
+
createAuthApiClient,
|
|
19
|
+
createTokenStorage,
|
|
20
|
+
useAuth,
|
|
21
|
+
useAuthContext,
|
|
22
|
+
useAuthError,
|
|
23
|
+
useAuthStatus,
|
|
24
|
+
useProfileSync,
|
|
25
|
+
useRequireAuth,
|
|
26
|
+
useToken,
|
|
27
|
+
useUser
|
|
28
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pixygon/auth",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared authentication package for all Pixygon applications",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.mjs",
|
|
12
|
+
"require": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./components": {
|
|
15
|
+
"types": "./dist/components/index.d.ts",
|
|
16
|
+
"import": "./dist/components/index.mjs",
|
|
17
|
+
"require": "./dist/components/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts src/components/index.ts --format cjs,esm --dts --clean",
|
|
22
|
+
"dev": "tsup src/index.ts src/components/index.ts --format cjs,esm --dts --watch",
|
|
23
|
+
"lint": "eslint src/",
|
|
24
|
+
"typecheck": "tsc --noEmit"
|
|
25
|
+
},
|
|
26
|
+
"peerDependencies": {
|
|
27
|
+
"react": ">=17.0.0",
|
|
28
|
+
"react-dom": ">=17.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/react": "^18.2.0",
|
|
32
|
+
"@types/react-dom": "^18.2.0",
|
|
33
|
+
"react": "^18.2.0",
|
|
34
|
+
"react-dom": "^18.2.0",
|
|
35
|
+
"tsup": "^8.0.0",
|
|
36
|
+
"typescript": "^5.3.0"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"dist",
|
|
40
|
+
"src"
|
|
41
|
+
],
|
|
42
|
+
"keywords": [
|
|
43
|
+
"pixygon",
|
|
44
|
+
"auth",
|
|
45
|
+
"authentication",
|
|
46
|
+
"react",
|
|
47
|
+
"typescript"
|
|
48
|
+
],
|
|
49
|
+
"author": "Pixygon",
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
},
|
|
54
|
+
"repository": {
|
|
55
|
+
"type": "git",
|
|
56
|
+
"url": "https://github.com/pixygon/pixygon-packages"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pixygon/auth - API Client
|
|
3
|
+
* Unified API client with automatic token injection and refresh
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
AuthConfig,
|
|
8
|
+
AuthError,
|
|
9
|
+
AuthErrorCode,
|
|
10
|
+
LoginRequest,
|
|
11
|
+
LoginResponse,
|
|
12
|
+
RegisterRequest,
|
|
13
|
+
RegisterResponse,
|
|
14
|
+
VerifyRequest,
|
|
15
|
+
VerifyResponse,
|
|
16
|
+
ForgotPasswordRequest,
|
|
17
|
+
ForgotPasswordResponse,
|
|
18
|
+
RecoverPasswordRequest,
|
|
19
|
+
RecoverPasswordResponse,
|
|
20
|
+
RefreshTokenRequest,
|
|
21
|
+
RefreshTokenResponse,
|
|
22
|
+
ResendVerificationRequest,
|
|
23
|
+
ResendVerificationResponse,
|
|
24
|
+
User,
|
|
25
|
+
} from '../types';
|
|
26
|
+
import type { TokenStorage } from '../utils/storage';
|
|
27
|
+
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Error Handling
|
|
30
|
+
// ============================================================================
|
|
31
|
+
|
|
32
|
+
function parseErrorCode(status: number, message?: string): AuthErrorCode {
|
|
33
|
+
if (message?.toLowerCase().includes('not found')) return 'USER_NOT_FOUND';
|
|
34
|
+
if (message?.toLowerCase().includes('exists')) return 'USER_EXISTS';
|
|
35
|
+
if (message?.toLowerCase().includes('verified')) return 'EMAIL_NOT_VERIFIED';
|
|
36
|
+
if (message?.toLowerCase().includes('invalid') && message?.toLowerCase().includes('code')) {
|
|
37
|
+
return 'INVALID_VERIFICATION_CODE';
|
|
38
|
+
}
|
|
39
|
+
if (message?.toLowerCase().includes('expired') && message?.toLowerCase().includes('code')) {
|
|
40
|
+
return 'EXPIRED_VERIFICATION_CODE';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
switch (status) {
|
|
44
|
+
case 401:
|
|
45
|
+
return 'INVALID_CREDENTIALS';
|
|
46
|
+
case 403:
|
|
47
|
+
return 'TOKEN_INVALID';
|
|
48
|
+
case 404:
|
|
49
|
+
return 'USER_NOT_FOUND';
|
|
50
|
+
case 409:
|
|
51
|
+
return 'USER_EXISTS';
|
|
52
|
+
case 500:
|
|
53
|
+
return 'SERVER_ERROR';
|
|
54
|
+
default:
|
|
55
|
+
return 'UNKNOWN_ERROR';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function createAuthError(status: number, message?: string, details?: Record<string, unknown>): AuthError {
|
|
60
|
+
return {
|
|
61
|
+
code: parseErrorCode(status, message),
|
|
62
|
+
message: message || 'An unexpected error occurred',
|
|
63
|
+
details,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ============================================================================
|
|
68
|
+
// API Client
|
|
69
|
+
// ============================================================================
|
|
70
|
+
|
|
71
|
+
export interface AuthApiClient {
|
|
72
|
+
// Auth endpoints
|
|
73
|
+
login: (data: LoginRequest) => Promise<LoginResponse>;
|
|
74
|
+
register: (data: RegisterRequest) => Promise<RegisterResponse>;
|
|
75
|
+
verify: (data: VerifyRequest) => Promise<VerifyResponse>;
|
|
76
|
+
resendVerification: (data: ResendVerificationRequest) => Promise<ResendVerificationResponse>;
|
|
77
|
+
forgotPassword: (data: ForgotPasswordRequest) => Promise<ForgotPasswordResponse>;
|
|
78
|
+
recoverPassword: (data: RecoverPasswordRequest) => Promise<RecoverPasswordResponse>;
|
|
79
|
+
refreshToken: (data: RefreshTokenRequest) => Promise<RefreshTokenResponse>;
|
|
80
|
+
|
|
81
|
+
// User endpoints
|
|
82
|
+
getMe: () => Promise<User>;
|
|
83
|
+
updateProfile: (data: Partial<User>) => Promise<User>;
|
|
84
|
+
|
|
85
|
+
// Utilities
|
|
86
|
+
setAccessToken: (token: string | null) => void;
|
|
87
|
+
getAccessToken: () => string | null;
|
|
88
|
+
request: <T>(endpoint: string, options?: RequestInit) => Promise<T>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createAuthApiClient(config: AuthConfig, tokenStorage: TokenStorage): AuthApiClient {
|
|
92
|
+
let currentAccessToken: string | null = null;
|
|
93
|
+
let isRefreshing = false;
|
|
94
|
+
let refreshPromise: Promise<void> | null = null;
|
|
95
|
+
|
|
96
|
+
const log = (...args: unknown[]) => {
|
|
97
|
+
if (config.debug) {
|
|
98
|
+
console.log('[PixygonAuth]', ...args);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Make an authenticated request
|
|
104
|
+
*/
|
|
105
|
+
async function request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
|
106
|
+
const url = `${config.baseUrl}${endpoint}`;
|
|
107
|
+
const headers: Record<string, string> = {
|
|
108
|
+
'Content-Type': 'application/json',
|
|
109
|
+
...(options.headers as Record<string, string>),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Add auth header if we have a token
|
|
113
|
+
if (currentAccessToken) {
|
|
114
|
+
headers['Authorization'] = `Bearer ${currentAccessToken}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
log(`Request: ${options.method || 'GET'} ${endpoint}`);
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(url, {
|
|
121
|
+
...options,
|
|
122
|
+
headers,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Handle token expiry
|
|
126
|
+
if (response.status === 401 || response.status === 403) {
|
|
127
|
+
// Try to refresh token
|
|
128
|
+
if (currentAccessToken && !isRefreshing) {
|
|
129
|
+
log('Token expired, attempting refresh...');
|
|
130
|
+
try {
|
|
131
|
+
await refreshTokens();
|
|
132
|
+
// Retry the original request with new token
|
|
133
|
+
headers['Authorization'] = `Bearer ${currentAccessToken}`;
|
|
134
|
+
const retryResponse = await fetch(url, { ...options, headers });
|
|
135
|
+
if (!retryResponse.ok) {
|
|
136
|
+
const errorData = await retryResponse.json().catch(() => ({}));
|
|
137
|
+
throw createAuthError(retryResponse.status, errorData.message);
|
|
138
|
+
}
|
|
139
|
+
return retryResponse.json();
|
|
140
|
+
} catch (refreshError) {
|
|
141
|
+
log('Token refresh failed', refreshError);
|
|
142
|
+
throw createAuthError(401, 'Session expired. Please log in again.');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
throw createAuthError(response.status, 'Authentication required');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
const errorData = await response.json().catch(() => ({}));
|
|
150
|
+
throw createAuthError(response.status, errorData.message, errorData);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const data = await response.json();
|
|
154
|
+
return data;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if ((error as AuthError).code) {
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
log('Network error:', error);
|
|
160
|
+
throw {
|
|
161
|
+
code: 'NETWORK_ERROR',
|
|
162
|
+
message: 'Unable to connect to the server',
|
|
163
|
+
details: { originalError: error },
|
|
164
|
+
} as AuthError;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Refresh the access token
|
|
170
|
+
*/
|
|
171
|
+
async function refreshTokens(): Promise<void> {
|
|
172
|
+
if (isRefreshing && refreshPromise) {
|
|
173
|
+
return refreshPromise;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
isRefreshing = true;
|
|
177
|
+
refreshPromise = (async () => {
|
|
178
|
+
try {
|
|
179
|
+
const refreshToken = await tokenStorage.getRefreshToken();
|
|
180
|
+
if (!refreshToken) {
|
|
181
|
+
throw new Error('No refresh token available');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const response = await fetch(`${config.baseUrl}/auth/refresh`, {
|
|
185
|
+
method: 'POST',
|
|
186
|
+
headers: { 'Content-Type': 'application/json' },
|
|
187
|
+
body: JSON.stringify({ refreshToken }),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw new Error('Refresh failed');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const data: RefreshTokenResponse = await response.json();
|
|
195
|
+
currentAccessToken = data.token;
|
|
196
|
+
|
|
197
|
+
await tokenStorage.updateTokens({
|
|
198
|
+
accessToken: data.token,
|
|
199
|
+
refreshToken: data.refreshToken,
|
|
200
|
+
expiresIn: data.expiresIn,
|
|
201
|
+
refreshExpiresIn: data.expiresIn * 24, // Approximate
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
config.onTokenRefresh?.({
|
|
205
|
+
accessToken: data.token,
|
|
206
|
+
refreshToken: data.refreshToken,
|
|
207
|
+
expiresIn: data.expiresIn,
|
|
208
|
+
refreshExpiresIn: data.expiresIn * 24,
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
log('Token refreshed successfully');
|
|
212
|
+
} finally {
|
|
213
|
+
isRefreshing = false;
|
|
214
|
+
refreshPromise = null;
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
|
|
218
|
+
return refreshPromise;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return {
|
|
222
|
+
// ========================================================================
|
|
223
|
+
// Auth Endpoints
|
|
224
|
+
// ========================================================================
|
|
225
|
+
|
|
226
|
+
async login(data: LoginRequest): Promise<LoginResponse> {
|
|
227
|
+
const response = await request<LoginResponse>('/auth/login', {
|
|
228
|
+
method: 'POST',
|
|
229
|
+
body: JSON.stringify(data),
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
currentAccessToken = response.token;
|
|
233
|
+
await tokenStorage.setTokens(
|
|
234
|
+
response.token,
|
|
235
|
+
response.refreshToken || null,
|
|
236
|
+
response.expiresIn || null,
|
|
237
|
+
response.user
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
config.onLogin?.(response.user);
|
|
241
|
+
log('Login successful:', response.user.userName);
|
|
242
|
+
|
|
243
|
+
return response;
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
async register(data: RegisterRequest): Promise<RegisterResponse> {
|
|
247
|
+
const response = await request<RegisterResponse>('/auth/register', {
|
|
248
|
+
method: 'POST',
|
|
249
|
+
body: JSON.stringify(data),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
log('Registration successful:', response.user.userName);
|
|
253
|
+
return response;
|
|
254
|
+
},
|
|
255
|
+
|
|
256
|
+
async verify(data: VerifyRequest): Promise<VerifyResponse> {
|
|
257
|
+
const response = await request<VerifyResponse>('/auth/verify', {
|
|
258
|
+
method: 'POST',
|
|
259
|
+
body: JSON.stringify(data),
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
currentAccessToken = response.token;
|
|
263
|
+
await tokenStorage.setTokens(
|
|
264
|
+
response.token,
|
|
265
|
+
response.refreshToken || null,
|
|
266
|
+
null,
|
|
267
|
+
response.user
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
config.onLogin?.(response.user);
|
|
271
|
+
log('Verification successful:', response.user.userName);
|
|
272
|
+
|
|
273
|
+
return response;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
async resendVerification(data: ResendVerificationRequest): Promise<ResendVerificationResponse> {
|
|
277
|
+
const response = await request<ResendVerificationResponse>('/auth/resendVerificationEmail', {
|
|
278
|
+
method: 'POST',
|
|
279
|
+
body: JSON.stringify(data),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
log('Verification email resent');
|
|
283
|
+
return response;
|
|
284
|
+
},
|
|
285
|
+
|
|
286
|
+
async forgotPassword(data: ForgotPasswordRequest): Promise<ForgotPasswordResponse> {
|
|
287
|
+
const response = await request<ForgotPasswordResponse>('/auth/forgotPassword', {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
body: JSON.stringify(data),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
log('Password reset email sent');
|
|
293
|
+
return response;
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
async recoverPassword(data: RecoverPasswordRequest): Promise<RecoverPasswordResponse> {
|
|
297
|
+
const response = await request<RecoverPasswordResponse>('/auth/recoverPassword', {
|
|
298
|
+
method: 'POST',
|
|
299
|
+
body: JSON.stringify(data),
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
log('Password recovered successfully');
|
|
303
|
+
return response;
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
async refreshToken(data: RefreshTokenRequest): Promise<RefreshTokenResponse> {
|
|
307
|
+
const response = await request<RefreshTokenResponse>('/auth/refresh', {
|
|
308
|
+
method: 'POST',
|
|
309
|
+
body: JSON.stringify(data),
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
currentAccessToken = response.token;
|
|
313
|
+
await tokenStorage.updateTokens({
|
|
314
|
+
accessToken: response.token,
|
|
315
|
+
refreshToken: response.refreshToken,
|
|
316
|
+
expiresIn: response.expiresIn,
|
|
317
|
+
refreshExpiresIn: response.expiresIn * 24,
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
return response;
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// ========================================================================
|
|
324
|
+
// User Endpoints
|
|
325
|
+
// ========================================================================
|
|
326
|
+
|
|
327
|
+
async getMe(): Promise<User> {
|
|
328
|
+
const response = await request<User>('/users/me');
|
|
329
|
+
await tokenStorage.updateUser(response);
|
|
330
|
+
return response;
|
|
331
|
+
},
|
|
332
|
+
|
|
333
|
+
async updateProfile(data: Partial<User>): Promise<User> {
|
|
334
|
+
const response = await request<User>('/users/me', {
|
|
335
|
+
method: 'PATCH',
|
|
336
|
+
body: JSON.stringify(data),
|
|
337
|
+
});
|
|
338
|
+
await tokenStorage.updateUser(response);
|
|
339
|
+
return response;
|
|
340
|
+
},
|
|
341
|
+
|
|
342
|
+
// ========================================================================
|
|
343
|
+
// Utilities
|
|
344
|
+
// ========================================================================
|
|
345
|
+
|
|
346
|
+
setAccessToken(token: string | null) {
|
|
347
|
+
currentAccessToken = token;
|
|
348
|
+
},
|
|
349
|
+
|
|
350
|
+
getAccessToken() {
|
|
351
|
+
return currentAccessToken;
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
request,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export type { TokenStorage };
|