@plyaz/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/.github/pull_request_template.md +71 -0
- package/.github/workflows/deploy.yml +9 -0
- package/.github/workflows/publish.yml +14 -0
- package/.github/workflows/security.yml +20 -0
- package/README.md +89 -0
- package/commits.txt +5 -0
- package/dist/common/index.cjs +48 -0
- package/dist/common/index.cjs.map +1 -0
- package/dist/common/index.mjs +43 -0
- package/dist/common/index.mjs.map +1 -0
- package/dist/index.cjs +20411 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +5139 -0
- package/dist/index.mjs.map +1 -0
- package/eslint.config.mjs +13 -0
- package/index.html +13 -0
- package/package.json +141 -0
- package/src/adapters/auth-adapter-factory.ts +26 -0
- package/src/adapters/auth-adapter.mapper.ts +53 -0
- package/src/adapters/base-auth.adapter.ts +119 -0
- package/src/adapters/clerk/clerk.adapter.ts +204 -0
- package/src/adapters/custom/custom.adapter.ts +119 -0
- package/src/adapters/index.ts +4 -0
- package/src/adapters/next-auth/authOptions.ts +81 -0
- package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
- package/src/api/client.ts +37 -0
- package/src/audit/audit.logger.ts +52 -0
- package/src/client/components/ProtectedRoute.tsx +37 -0
- package/src/client/hooks/useAuth.ts +128 -0
- package/src/client/hooks/useConnectedAccounts.ts +108 -0
- package/src/client/hooks/usePermissions.ts +36 -0
- package/src/client/hooks/useRBAC.ts +36 -0
- package/src/client/hooks/useSession.ts +18 -0
- package/src/client/providers/AuthProvider.tsx +104 -0
- package/src/client/store/auth.store.ts +306 -0
- package/src/client/utils/storage.ts +70 -0
- package/src/common/constants/oauth-providers.ts +49 -0
- package/src/common/errors/auth.errors.ts +64 -0
- package/src/common/errors/specific-auth-errors.ts +201 -0
- package/src/common/index.ts +19 -0
- package/src/common/regex/index.ts +27 -0
- package/src/common/types/auth.types.ts +641 -0
- package/src/common/types/index.ts +297 -0
- package/src/common/utils/index.ts +84 -0
- package/src/core/blacklist/token.blacklist.ts +60 -0
- package/src/core/index.ts +2 -0
- package/src/core/jwt/jwt.manager.ts +131 -0
- package/src/core/session/session.manager.ts +56 -0
- package/src/db/repositories/connected-account.repository.ts +415 -0
- package/src/db/repositories/role.repository.ts +519 -0
- package/src/db/repositories/session.repository.ts +308 -0
- package/src/db/repositories/user.repository.ts +320 -0
- package/src/flows/index.ts +2 -0
- package/src/flows/sign-in.flow.ts +106 -0
- package/src/flows/sign-up.flow.ts +121 -0
- package/src/index.ts +54 -0
- package/src/libs/clerk.helper.ts +36 -0
- package/src/libs/supabase.helper.ts +255 -0
- package/src/libs/supabaseClient.ts +6 -0
- package/src/providers/base/auth-provider.interface.ts +42 -0
- package/src/providers/base/index.ts +1 -0
- package/src/providers/index.ts +2 -0
- package/src/providers/oauth/facebook.provider.ts +97 -0
- package/src/providers/oauth/github.provider.ts +148 -0
- package/src/providers/oauth/google.provider.ts +126 -0
- package/src/providers/oauth/index.ts +3 -0
- package/src/rbac/dynamic-roles.ts +552 -0
- package/src/rbac/index.ts +4 -0
- package/src/rbac/permission-checker.ts +464 -0
- package/src/rbac/role-hierarchy.ts +545 -0
- package/src/rbac/role.manager.ts +75 -0
- package/src/security/csrf/csrf.protection.ts +37 -0
- package/src/security/index.ts +3 -0
- package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
- package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
- package/src/security/rate-limiting/auth.module.ts +32 -0
- package/src/server/auth.module.ts +158 -0
- package/src/server/decorators/auth.decorator.ts +43 -0
- package/src/server/decorators/auth.decorators.ts +31 -0
- package/src/server/decorators/current-user.decorator.ts +49 -0
- package/src/server/decorators/permission.decorator.ts +49 -0
- package/src/server/guards/auth.guard.ts +56 -0
- package/src/server/guards/custom-throttler.guard.ts +46 -0
- package/src/server/guards/permissions.guard.ts +115 -0
- package/src/server/guards/roles.guard.ts +31 -0
- package/src/server/middleware/auth.middleware.ts +46 -0
- package/src/server/middleware/index.ts +2 -0
- package/src/server/middleware/middleware.ts +11 -0
- package/src/server/middleware/session.middleware.ts +255 -0
- package/src/server/services/account.service.ts +269 -0
- package/src/server/services/auth.service.ts +79 -0
- package/src/server/services/brute-force.service.ts +98 -0
- package/src/server/services/index.ts +15 -0
- package/src/server/services/rate-limiter.service.ts +60 -0
- package/src/server/services/session.service.ts +287 -0
- package/src/server/services/token.service.ts +262 -0
- package/src/session/cookie-store.ts +255 -0
- package/src/session/enhanced-session-manager.ts +406 -0
- package/src/session/index.ts +14 -0
- package/src/session/memory-store.ts +320 -0
- package/src/session/redis-store.ts +443 -0
- package/src/strategies/oauth.strategy.ts +128 -0
- package/src/strategies/traditional-auth.strategy.ts +116 -0
- package/src/tokens/index.ts +4 -0
- package/src/tokens/refresh-token-manager.ts +448 -0
- package/src/tokens/token-validator.ts +311 -0
- package/tsconfig.build.json +28 -0
- package/tsconfig.json +38 -0
- package/tsup.config.mjs +28 -0
- package/vitest.config.mjs +16 -0
- package/vitest.setup.d.ts +2 -0
- package/vitest.setup.d.ts.map +1 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Common types and utility types for @plyaz/auth
|
|
3
|
+
* @module @plyaz/auth/common/types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Utility type to make specific properties optional
|
|
8
|
+
* @template T - The base type
|
|
9
|
+
* @template K - Keys to make optional
|
|
10
|
+
*/
|
|
11
|
+
export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Utility type to make specific properties required
|
|
15
|
+
* @template T - The base type
|
|
16
|
+
* @template K - Keys to make required
|
|
17
|
+
*/
|
|
18
|
+
export type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Utility type for deep partial objects
|
|
22
|
+
* @template T - The type to make deeply partial
|
|
23
|
+
*/
|
|
24
|
+
export type DeepPartial<T> = {
|
|
25
|
+
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* User account status enumeration
|
|
30
|
+
*/
|
|
31
|
+
export type UserStatus = 'active' | 'inactive' | 'suspended' | 'pending';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Available permission actions for RBAC
|
|
35
|
+
*/
|
|
36
|
+
export type PermissionAction = 'create' | 'read' | 'update' | 'delete' | 'manage' | '*';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resource type identifier (e.g., 'users', 'posts', 'campaigns')
|
|
40
|
+
*/
|
|
41
|
+
export type ResourceType = string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Permission string format: resource:action
|
|
45
|
+
* @example 'users:read', 'posts:create', 'campaigns:manage'
|
|
46
|
+
*/
|
|
47
|
+
export type PermissionString = `${ResourceType}:${PermissionAction}`;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Base configuration interface for all auth components
|
|
51
|
+
*/
|
|
52
|
+
export interface BaseConfig {
|
|
53
|
+
/** Whether the component is enabled */
|
|
54
|
+
enabled: boolean;
|
|
55
|
+
/** Enable debug logging */
|
|
56
|
+
debug?: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Authentication event structure for audit logging
|
|
61
|
+
*/
|
|
62
|
+
export interface AuthEvent {
|
|
63
|
+
/** Event type identifier */
|
|
64
|
+
type: string;
|
|
65
|
+
/** User ID associated with the event */
|
|
66
|
+
userId?: string;
|
|
67
|
+
/** Session ID associated with the event */
|
|
68
|
+
sessionId?: string;
|
|
69
|
+
/** Event timestamp */
|
|
70
|
+
timestamp: Date;
|
|
71
|
+
/** Additional event metadata */
|
|
72
|
+
metadata?: Record<string, string>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generic callback function for authentication events
|
|
77
|
+
* @template T - The data type passed to the callback
|
|
78
|
+
*/
|
|
79
|
+
export type AuthCallback<T > = (data: T) => void | Promise<void>;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Error callback function type
|
|
83
|
+
*/
|
|
84
|
+
export type ErrorCallback = (error: Error) => void | Promise<void>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Standard API response structure
|
|
88
|
+
* @template T - The data type returned in the response
|
|
89
|
+
*/
|
|
90
|
+
export interface ApiResponse<T> {
|
|
91
|
+
/** Whether the request was successful */
|
|
92
|
+
success: boolean;
|
|
93
|
+
/** Response data (if successful) */
|
|
94
|
+
data?: T;
|
|
95
|
+
/** Error information (if failed) */
|
|
96
|
+
error?: {
|
|
97
|
+
/** Error code for programmatic handling */
|
|
98
|
+
code: string;
|
|
99
|
+
/** Human-readable error message */
|
|
100
|
+
message: string;
|
|
101
|
+
/** Additional error details */
|
|
102
|
+
details?: unknown;
|
|
103
|
+
};
|
|
104
|
+
/** Response metadata */
|
|
105
|
+
meta?: {
|
|
106
|
+
/** Response timestamp */
|
|
107
|
+
timestamp: string;
|
|
108
|
+
/** Unique request identifier */
|
|
109
|
+
requestId?: string;
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Pagination parameters for list queries
|
|
115
|
+
*/
|
|
116
|
+
export interface PaginationParams {
|
|
117
|
+
/** Page number (1-based) */
|
|
118
|
+
page?: number;
|
|
119
|
+
/** Number of items per page */
|
|
120
|
+
limit?: number;
|
|
121
|
+
/** Number of items to skip */
|
|
122
|
+
offset?: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Paginated response structure
|
|
127
|
+
* @template T - The type of items in the response
|
|
128
|
+
*/
|
|
129
|
+
export interface PaginatedResponse<T> {
|
|
130
|
+
/** Array of items for the current page */
|
|
131
|
+
items: T[];
|
|
132
|
+
/** Total number of items across all pages */
|
|
133
|
+
total: number;
|
|
134
|
+
/** Current page number */
|
|
135
|
+
page: number;
|
|
136
|
+
/** Number of items per page */
|
|
137
|
+
limit: number;
|
|
138
|
+
/** Whether there is a next page */
|
|
139
|
+
hasNext: boolean;
|
|
140
|
+
/** Whether there is a previous page */
|
|
141
|
+
hasPrev: boolean;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Generic filter parameters for queries
|
|
146
|
+
*/
|
|
147
|
+
export interface FilterParams {
|
|
148
|
+
[key: string]: unknown;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Sort parameters for ordered queries
|
|
153
|
+
*/
|
|
154
|
+
export interface SortParams {
|
|
155
|
+
/** Field to sort by */
|
|
156
|
+
field: string;
|
|
157
|
+
/** Sort order */
|
|
158
|
+
order: 'asc' | 'desc';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Audit log entry for tracking system changes
|
|
163
|
+
*/
|
|
164
|
+
export interface AuditLog {
|
|
165
|
+
/** Unique audit log identifier */
|
|
166
|
+
id: string;
|
|
167
|
+
/** Action performed (e.g., 'create', 'update', 'delete') */
|
|
168
|
+
action: string;
|
|
169
|
+
/** Resource type affected */
|
|
170
|
+
resource: string;
|
|
171
|
+
/** Specific resource identifier */
|
|
172
|
+
resourceId?: string;
|
|
173
|
+
/** User who performed the action */
|
|
174
|
+
userId?: string;
|
|
175
|
+
/** Session identifier */
|
|
176
|
+
sessionId?: string;
|
|
177
|
+
/** Client IP address */
|
|
178
|
+
ipAddress?: string;
|
|
179
|
+
/** Client user agent */
|
|
180
|
+
userAgent?: string;
|
|
181
|
+
/** When the action occurred */
|
|
182
|
+
timestamp: Date;
|
|
183
|
+
/** Field-level changes made */
|
|
184
|
+
changes?: Record<string, { from: string; to: string }>;
|
|
185
|
+
/** Additional audit metadata */
|
|
186
|
+
metadata?: Record<string, string>;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Cache entry structure
|
|
191
|
+
* @template T - The type of cached value
|
|
192
|
+
*/
|
|
193
|
+
export interface CacheEntry<T> {
|
|
194
|
+
/** Cached value */
|
|
195
|
+
value: T;
|
|
196
|
+
/** Expiration timestamp (Unix timestamp) */
|
|
197
|
+
expiresAt: number;
|
|
198
|
+
/** Creation timestamp (Unix timestamp) */
|
|
199
|
+
createdAt: number;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Cache performance statistics
|
|
204
|
+
*/
|
|
205
|
+
export interface CacheStats {
|
|
206
|
+
/** Number of cache hits */
|
|
207
|
+
hits: number;
|
|
208
|
+
/** Number of cache misses */
|
|
209
|
+
misses: number;
|
|
210
|
+
/** Current cache size */
|
|
211
|
+
size: number;
|
|
212
|
+
/** Hit rate percentage (0-1) */
|
|
213
|
+
hitRate: number;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Validation rule definition for form fields
|
|
218
|
+
*/
|
|
219
|
+
export interface ValidationRule {
|
|
220
|
+
/** Field name to validate */
|
|
221
|
+
field: string;
|
|
222
|
+
/** Whether the field is required */
|
|
223
|
+
required?: boolean;
|
|
224
|
+
/** Expected data type */
|
|
225
|
+
type?: 'string' | 'number' | 'boolean' | 'email' | 'url';
|
|
226
|
+
/** Minimum length for strings */
|
|
227
|
+
minLength?: number;
|
|
228
|
+
/** Maximum length for strings */
|
|
229
|
+
maxLength?: number;
|
|
230
|
+
/** Regular expression pattern to match */
|
|
231
|
+
pattern?: RegExp;
|
|
232
|
+
/** Custom validation function */
|
|
233
|
+
custom?: (value: unknown) => boolean | string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Result of field validation
|
|
238
|
+
*/
|
|
239
|
+
export interface ValidationResult {
|
|
240
|
+
/** Whether validation passed */
|
|
241
|
+
valid: boolean;
|
|
242
|
+
/** Array of validation errors */
|
|
243
|
+
errors: Array<{
|
|
244
|
+
/** Field that failed validation */
|
|
245
|
+
field: string;
|
|
246
|
+
/** Error message */
|
|
247
|
+
message: string;
|
|
248
|
+
/** Error code for programmatic handling */
|
|
249
|
+
code: string;
|
|
250
|
+
}>;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Database connection configuration
|
|
255
|
+
*/
|
|
256
|
+
export interface DatabaseConfig {
|
|
257
|
+
/** Database host */
|
|
258
|
+
host: string;
|
|
259
|
+
/** Database port */
|
|
260
|
+
port: number;
|
|
261
|
+
/** Database name */
|
|
262
|
+
database: string;
|
|
263
|
+
/** Database username */
|
|
264
|
+
username: string;
|
|
265
|
+
/** Database password */
|
|
266
|
+
password: string;
|
|
267
|
+
/** Enable SSL connection */
|
|
268
|
+
ssl?: boolean;
|
|
269
|
+
/** Connection pool settings */
|
|
270
|
+
pool?: {
|
|
271
|
+
/** Minimum pool connections */
|
|
272
|
+
min: number;
|
|
273
|
+
/** Maximum pool connections */
|
|
274
|
+
max: number;
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Generic metrics collection
|
|
280
|
+
*/
|
|
281
|
+
export interface Metrics {
|
|
282
|
+
[key: string]: number | string | boolean;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Performance metrics for monitoring
|
|
287
|
+
*/
|
|
288
|
+
export interface PerformanceMetrics {
|
|
289
|
+
/** Operation duration in milliseconds */
|
|
290
|
+
duration: number;
|
|
291
|
+
/** Memory usage in bytes */
|
|
292
|
+
memory: number;
|
|
293
|
+
/** CPU usage percentage */
|
|
294
|
+
cpu: number;
|
|
295
|
+
/** Measurement timestamp */
|
|
296
|
+
timestamp: Date;
|
|
297
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Common utility functions for @plyaz/auth
|
|
3
|
+
* @module @plyaz/auth/common/utils
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { NUMERIX } from "@plyaz/config";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate a random string of specified length using alphanumeric characters
|
|
10
|
+
* @param length - Length of the random string (default: 32)
|
|
11
|
+
* @returns Random alphanumeric string
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const randomId = generateRandomString(16); // "a1B2c3D4e5F6g7H8"
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function generateRandomString(length: number = 32): string {
|
|
18
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
|
19
|
+
let result = '';
|
|
20
|
+
for (let i = 0; i < length; i++) {
|
|
21
|
+
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
22
|
+
}
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Generate a cryptographically secure random ID with timestamp prefix
|
|
28
|
+
* @returns Unique identifier with timestamp and random suffix
|
|
29
|
+
* @example
|
|
30
|
+
* ```typescript
|
|
31
|
+
* const id = generateSecureId(); // "1703123456789_a1B2c3D4e5F6g7H8"
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function generateSecureId(): string {
|
|
35
|
+
return `${Date.now()}_${generateRandomString(NUMERIX.SIXTEEN)}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Sleep for specified milliseconds (async delay)
|
|
40
|
+
* @param ms - Milliseconds to sleep
|
|
41
|
+
* @returns Promise that resolves after the delay
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* await sleep(1000); // Wait 1 second
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function sleep(ms: number): Promise<void> {
|
|
48
|
+
return new Promise(resolve => globalThis.setTimeout(resolve, ms));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Mask sensitive data fields for safe logging
|
|
53
|
+
* @param data - Object containing potentially sensitive data
|
|
54
|
+
* @param sensitiveFields - Array of field names to mask (default: ['password', 'token', 'secret'])
|
|
55
|
+
* @returns Object with sensitive fields masked
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const masked = maskSensitiveData({ email: 'user@example.com', password: 'secret123' });
|
|
59
|
+
* // Result: { email: 'user@example.com', password: 'secr*****' }
|
|
60
|
+
* ```
|
|
61
|
+
*/
|
|
62
|
+
export function maskSensitiveData(
|
|
63
|
+
data: unknown,
|
|
64
|
+
sensitiveFields: string[] = ['password', 'token', 'secret']
|
|
65
|
+
): unknown {
|
|
66
|
+
if (typeof data !== 'object' || data === null) {
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Type assertion to allow indexing by string
|
|
71
|
+
const masked: Record<string, string> = { ...(data as Record<string, string>) };
|
|
72
|
+
const four = 4;
|
|
73
|
+
for (const field of sensitiveFields) {
|
|
74
|
+
if (field in masked) {
|
|
75
|
+
const value = masked[field];
|
|
76
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
77
|
+
masked[field] = value.substring(0, four) + '*'.repeat(Math.max(0, value.length - four));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return masked;
|
|
83
|
+
}
|
|
84
|
+
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface TokenBlacklistConfig {
|
|
2
|
+
redisUrl?: string;
|
|
3
|
+
keyPrefix: string;
|
|
4
|
+
defaultTTL: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class TokenBlacklist {
|
|
8
|
+
private blacklistedTokens = new Map<string, number>();
|
|
9
|
+
|
|
10
|
+
constructor(private config: TokenBlacklistConfig) {}
|
|
11
|
+
|
|
12
|
+
async add(tokenId: string, expiresAt: number): Promise<void> {
|
|
13
|
+
const ttl = Math.max(0, expiresAt - Date.now());
|
|
14
|
+
this.blacklistedTokens.set(tokenId, Date.now() + ttl);
|
|
15
|
+
|
|
16
|
+
// Auto-cleanup expired tokens
|
|
17
|
+
globalThis.setTimeout(() => {
|
|
18
|
+
this.blacklistedTokens.delete(tokenId);
|
|
19
|
+
}, ttl);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async blacklistToken(tokenId: string, expiresAt: number): Promise<void> {
|
|
23
|
+
return this.add(tokenId, expiresAt);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async isBlacklisted(tokenId: string): Promise<boolean> {
|
|
27
|
+
const expiry = this.blacklistedTokens.get(tokenId);
|
|
28
|
+
if (!expiry) return false;
|
|
29
|
+
|
|
30
|
+
if (expiry < Date.now()) {
|
|
31
|
+
this.blacklistedTokens.delete(tokenId);
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async blacklistAllUserTokens(userId: string, issuedBefore: number): Promise<void> {
|
|
39
|
+
// In production, this would use Redis with a user-specific key
|
|
40
|
+
// For now, we'll use a simple approach
|
|
41
|
+
const userKey = `user:${userId}:blacklist_before`;
|
|
42
|
+
this.blacklistedTokens.set(userKey, issuedBefore);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async isUserTokenBlacklisted(userId: string, tokenIssuedAt: number): Promise<boolean> {
|
|
46
|
+
const userKey = `user:${userId}:blacklist_before`;
|
|
47
|
+
const blacklistBefore = this.blacklistedTokens.get(userKey);
|
|
48
|
+
|
|
49
|
+
return blacklistBefore ? tokenIssuedAt < blacklistBefore : false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
cleanup(): void {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
this.blacklistedTokens.forEach((expiry, key) => {
|
|
55
|
+
if (expiry < now) {
|
|
56
|
+
this.blacklistedTokens.delete(key);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { sign, verify, type JwtPayload, type SignOptions } from "jsonwebtoken";
|
|
2
|
+
import type { AuthTokens } from '@plyaz/types';
|
|
3
|
+
import { NUMERIX } from "@plyaz/config";
|
|
4
|
+
|
|
5
|
+
// Define the token payload interface
|
|
6
|
+
export interface TokenPayload extends JwtPayload {
|
|
7
|
+
sub: string;
|
|
8
|
+
type: "access" | "refresh" | "verification";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Define the StringValue type to match what jsonwebtoken expects
|
|
12
|
+
// Define the StringValue type to match what jsonwebtoken expects
|
|
13
|
+
type StringValue = `${number}s` | `${number}m` | `${number}h` | `${number}d`;
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export class JwtManager {
|
|
17
|
+
private config: {
|
|
18
|
+
privateKey: string;
|
|
19
|
+
publicKey: string;
|
|
20
|
+
issuer: string;
|
|
21
|
+
audience: string;
|
|
22
|
+
};
|
|
23
|
+
private algorithm: "RS256" | "HS256" | "ES256" | "HS512";
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
config: typeof JwtManager.prototype.config,
|
|
27
|
+
algorithm: "RS256" | "HS256" | "ES256" | "HS512"
|
|
28
|
+
) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.algorithm = algorithm;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// -------------------
|
|
34
|
+
// Generic Token Generator
|
|
35
|
+
// -------------------
|
|
36
|
+
private generateToken(
|
|
37
|
+
payload: Omit<TokenPayload, "iss" | "aud" | "iat" | "exp">,
|
|
38
|
+
expiresIn: string | number = "1h"
|
|
39
|
+
): string {
|
|
40
|
+
const signOptions: SignOptions = {
|
|
41
|
+
algorithm: this.algorithm,
|
|
42
|
+
expiresIn: expiresIn as StringValue | number,
|
|
43
|
+
issuer: this.config.issuer,
|
|
44
|
+
audience: this.config.audience,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return sign(payload, this.config.privateKey, signOptions);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
generateTokens(
|
|
51
|
+
user: { id: string },
|
|
52
|
+
accessTokenExpiry: string | number = "1h",
|
|
53
|
+
refreshTokenExpiry: string | number = "7d"
|
|
54
|
+
): AuthTokens {
|
|
55
|
+
const accessToken = this.generateToken({ sub: user.id, type: "access" }, accessTokenExpiry);
|
|
56
|
+
const refreshToken = this.generateToken({ sub: user.id, type: "refresh" }, refreshTokenExpiry);
|
|
57
|
+
|
|
58
|
+
// Calculate the actual expiration time in seconds
|
|
59
|
+
let expiresIn: number;
|
|
60
|
+
if (typeof accessTokenExpiry === 'string') {
|
|
61
|
+
// Parse time strings like "1h", "30m", etc.
|
|
62
|
+
const match = accessTokenExpiry.match(/^(\d+)([smhd])$/);
|
|
63
|
+
if (match) {
|
|
64
|
+
const value = globalThis.parseInt(match[1], NUMERIX.TEN);
|
|
65
|
+
const unit = match[2];
|
|
66
|
+
const multipliers: Record<string, number> = { s: 1, m: 60, h: 3600, d: 86400 };
|
|
67
|
+
expiresIn = value * multipliers[unit];
|
|
68
|
+
} else {
|
|
69
|
+
expiresIn = NUMERIX.THIRTY_SIX_HUNDERD; // Default to 1 hour
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
expiresIn = accessTokenExpiry;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
accessToken,
|
|
77
|
+
refreshToken,
|
|
78
|
+
expiresIn,
|
|
79
|
+
tokenType: "Bearer"
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Generate access token
|
|
84
|
+
generateAccessToken(userId: string, expiresIn: string | number = "1h"): string {
|
|
85
|
+
return this.generateToken({ sub: userId, type: "access" }, expiresIn);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Generate refresh token
|
|
89
|
+
generateRefreshToken(userId: string, expiresIn: string | number = "7d"): string {
|
|
90
|
+
return this.generateToken({ sub: userId, type: "refresh" }, expiresIn);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Generate verification token (email confirmation)
|
|
94
|
+
generateVerificationToken(userId: string, expiresIn: string | number = "24h"): string {
|
|
95
|
+
return this.generateToken({ sub: userId, type: "verification" }, expiresIn);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// -------------------
|
|
99
|
+
// Token Verification
|
|
100
|
+
// -------------------
|
|
101
|
+
private verifyToken(token: string, tokenType: "access" | "refresh" | "verification"): TokenPayload {
|
|
102
|
+
const decoded = verify(token, this.config.publicKey, {
|
|
103
|
+
algorithms: [this.algorithm],
|
|
104
|
+
issuer: this.config.issuer,
|
|
105
|
+
audience: this.config.audience,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (typeof decoded === "string") throw new Error(`Invalid ${tokenType} token payload`);
|
|
109
|
+
|
|
110
|
+
const payload = decoded as JwtPayload;
|
|
111
|
+
|
|
112
|
+
// Verify the token type
|
|
113
|
+
if (payload.type !== tokenType) {
|
|
114
|
+
throw new Error(`Invalid token type. Expected ${tokenType}, got ${payload.type}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return payload as TokenPayload;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
verifyAccessToken(token: string): TokenPayload {
|
|
121
|
+
return this.verifyToken(token, "access");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
verifyRefreshToken(token: string): TokenPayload {
|
|
125
|
+
return this.verifyToken(token, "refresh");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
verifyVerificationToken(token: string): TokenPayload {
|
|
129
|
+
return this.verifyToken(token, "verification");
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
|
|
2
|
+
import type { Session, SessionRepository, SessionConfig, CreateSessionData } from '@plyaz/types';
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
export class SessionManager {
|
|
7
|
+
constructor(
|
|
8
|
+
private sessionRepo: SessionRepository,
|
|
9
|
+
private config: SessionConfig
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
async createSession(userId: string, deviceInfo: Record<string,unknown>): Promise<Session> {
|
|
13
|
+
await this.enforceSessionLimits(userId);
|
|
14
|
+
|
|
15
|
+
const sessionData: CreateSessionData = {
|
|
16
|
+
userId,
|
|
17
|
+
provider: 'default',
|
|
18
|
+
expiresAt: new Date(Date.now() + this.config.sessionTTL),
|
|
19
|
+
metadata:deviceInfo,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return this.sessionRepo.create(sessionData);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async getSession(sessionId: string): Promise<Session | null> {
|
|
26
|
+
const session = await this.sessionRepo.findById(sessionId);
|
|
27
|
+
if (!session || session.expiresAt < new Date()) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return session;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async updateLastActive(sessionId: string): Promise<void> {
|
|
34
|
+
await this.sessionRepo.updateLastActive(sessionId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async invalidateSession(sessionId: string): Promise<void> {
|
|
38
|
+
await this.sessionRepo.delete(sessionId);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async invalidateAllUserSessions(userId: string): Promise<void> {
|
|
42
|
+
await this.sessionRepo.deleteByUserId(userId);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
private async enforceSessionLimits(userId: string): Promise<void> {
|
|
46
|
+
const sessions = await this.sessionRepo.findByUserId(userId);
|
|
47
|
+
if (sessions.length >= this.config.maxConcurrentSessions) {
|
|
48
|
+
const oldestSession = sessions.sort((a, b) =>
|
|
49
|
+
a.lastActivityAt.getTime() - b.lastActivityAt.getTime()
|
|
50
|
+
)[0];
|
|
51
|
+
await this.sessionRepo.delete(oldestSession.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
}
|