@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.
Files changed (113) hide show
  1. package/.github/pull_request_template.md +71 -0
  2. package/.github/workflows/deploy.yml +9 -0
  3. package/.github/workflows/publish.yml +14 -0
  4. package/.github/workflows/security.yml +20 -0
  5. package/README.md +89 -0
  6. package/commits.txt +5 -0
  7. package/dist/common/index.cjs +48 -0
  8. package/dist/common/index.cjs.map +1 -0
  9. package/dist/common/index.mjs +43 -0
  10. package/dist/common/index.mjs.map +1 -0
  11. package/dist/index.cjs +20411 -0
  12. package/dist/index.cjs.map +1 -0
  13. package/dist/index.mjs +5139 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/eslint.config.mjs +13 -0
  16. package/index.html +13 -0
  17. package/package.json +141 -0
  18. package/src/adapters/auth-adapter-factory.ts +26 -0
  19. package/src/adapters/auth-adapter.mapper.ts +53 -0
  20. package/src/adapters/base-auth.adapter.ts +119 -0
  21. package/src/adapters/clerk/clerk.adapter.ts +204 -0
  22. package/src/adapters/custom/custom.adapter.ts +119 -0
  23. package/src/adapters/index.ts +4 -0
  24. package/src/adapters/next-auth/authOptions.ts +81 -0
  25. package/src/adapters/next-auth/next-auth.adapter.ts +211 -0
  26. package/src/api/client.ts +37 -0
  27. package/src/audit/audit.logger.ts +52 -0
  28. package/src/client/components/ProtectedRoute.tsx +37 -0
  29. package/src/client/hooks/useAuth.ts +128 -0
  30. package/src/client/hooks/useConnectedAccounts.ts +108 -0
  31. package/src/client/hooks/usePermissions.ts +36 -0
  32. package/src/client/hooks/useRBAC.ts +36 -0
  33. package/src/client/hooks/useSession.ts +18 -0
  34. package/src/client/providers/AuthProvider.tsx +104 -0
  35. package/src/client/store/auth.store.ts +306 -0
  36. package/src/client/utils/storage.ts +70 -0
  37. package/src/common/constants/oauth-providers.ts +49 -0
  38. package/src/common/errors/auth.errors.ts +64 -0
  39. package/src/common/errors/specific-auth-errors.ts +201 -0
  40. package/src/common/index.ts +19 -0
  41. package/src/common/regex/index.ts +27 -0
  42. package/src/common/types/auth.types.ts +641 -0
  43. package/src/common/types/index.ts +297 -0
  44. package/src/common/utils/index.ts +84 -0
  45. package/src/core/blacklist/token.blacklist.ts +60 -0
  46. package/src/core/index.ts +2 -0
  47. package/src/core/jwt/jwt.manager.ts +131 -0
  48. package/src/core/session/session.manager.ts +56 -0
  49. package/src/db/repositories/connected-account.repository.ts +415 -0
  50. package/src/db/repositories/role.repository.ts +519 -0
  51. package/src/db/repositories/session.repository.ts +308 -0
  52. package/src/db/repositories/user.repository.ts +320 -0
  53. package/src/flows/index.ts +2 -0
  54. package/src/flows/sign-in.flow.ts +106 -0
  55. package/src/flows/sign-up.flow.ts +121 -0
  56. package/src/index.ts +54 -0
  57. package/src/libs/clerk.helper.ts +36 -0
  58. package/src/libs/supabase.helper.ts +255 -0
  59. package/src/libs/supabaseClient.ts +6 -0
  60. package/src/providers/base/auth-provider.interface.ts +42 -0
  61. package/src/providers/base/index.ts +1 -0
  62. package/src/providers/index.ts +2 -0
  63. package/src/providers/oauth/facebook.provider.ts +97 -0
  64. package/src/providers/oauth/github.provider.ts +148 -0
  65. package/src/providers/oauth/google.provider.ts +126 -0
  66. package/src/providers/oauth/index.ts +3 -0
  67. package/src/rbac/dynamic-roles.ts +552 -0
  68. package/src/rbac/index.ts +4 -0
  69. package/src/rbac/permission-checker.ts +464 -0
  70. package/src/rbac/role-hierarchy.ts +545 -0
  71. package/src/rbac/role.manager.ts +75 -0
  72. package/src/security/csrf/csrf.protection.ts +37 -0
  73. package/src/security/index.ts +3 -0
  74. package/src/security/rate-limiting/auth/auth.controller.ts +12 -0
  75. package/src/security/rate-limiting/auth/rate-limiting.interface.ts +67 -0
  76. package/src/security/rate-limiting/auth.module.ts +32 -0
  77. package/src/server/auth.module.ts +158 -0
  78. package/src/server/decorators/auth.decorator.ts +43 -0
  79. package/src/server/decorators/auth.decorators.ts +31 -0
  80. package/src/server/decorators/current-user.decorator.ts +49 -0
  81. package/src/server/decorators/permission.decorator.ts +49 -0
  82. package/src/server/guards/auth.guard.ts +56 -0
  83. package/src/server/guards/custom-throttler.guard.ts +46 -0
  84. package/src/server/guards/permissions.guard.ts +115 -0
  85. package/src/server/guards/roles.guard.ts +31 -0
  86. package/src/server/middleware/auth.middleware.ts +46 -0
  87. package/src/server/middleware/index.ts +2 -0
  88. package/src/server/middleware/middleware.ts +11 -0
  89. package/src/server/middleware/session.middleware.ts +255 -0
  90. package/src/server/services/account.service.ts +269 -0
  91. package/src/server/services/auth.service.ts +79 -0
  92. package/src/server/services/brute-force.service.ts +98 -0
  93. package/src/server/services/index.ts +15 -0
  94. package/src/server/services/rate-limiter.service.ts +60 -0
  95. package/src/server/services/session.service.ts +287 -0
  96. package/src/server/services/token.service.ts +262 -0
  97. package/src/session/cookie-store.ts +255 -0
  98. package/src/session/enhanced-session-manager.ts +406 -0
  99. package/src/session/index.ts +14 -0
  100. package/src/session/memory-store.ts +320 -0
  101. package/src/session/redis-store.ts +443 -0
  102. package/src/strategies/oauth.strategy.ts +128 -0
  103. package/src/strategies/traditional-auth.strategy.ts +116 -0
  104. package/src/tokens/index.ts +4 -0
  105. package/src/tokens/refresh-token-manager.ts +448 -0
  106. package/src/tokens/token-validator.ts +311 -0
  107. package/tsconfig.build.json +28 -0
  108. package/tsconfig.json +38 -0
  109. package/tsup.config.mjs +28 -0
  110. package/vitest.config.mjs +16 -0
  111. package/vitest.setup.d.ts +2 -0
  112. package/vitest.setup.d.ts.map +1 -0
  113. package/vitest.setup.ts +1 -0
@@ -0,0 +1,443 @@
1
+ /**
2
+ * @fileoverview Redis-based session store for @plyaz/auth
3
+ * @module @plyaz/auth/session/redis-store
4
+ *
5
+ * @description
6
+ * Implements session storage using Redis. Provides persistent, scalable
7
+ * session management with automatic expiration and clustering support.
8
+ * Suitable for production applications requiring session persistence
9
+ * across server restarts and horizontal scaling.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { RedisStore } from '@plyaz/auth';
14
+ *
15
+ * const store = new RedisStore({
16
+ * host: 'localhost',
17
+ * port: 6379,
18
+ * keyPrefix: 'auth:session:'
19
+ * });
20
+ *
21
+ * await store.set('session_123', sessionData, 3600);
22
+ * ```
23
+ */
24
+
25
+ import { NUMERIX } from "@plyaz/config";
26
+ import type { SessionData, SessionStore, SessionStoreConfig } from "@plyaz/types";
27
+
28
+
29
+ /**
30
+ * Redis connection configuration
31
+ */
32
+ export interface RedisConfig {
33
+ /** Redis host */
34
+ host: string;
35
+ /** Redis port */
36
+ port: number;
37
+ /** Redis password */
38
+ password?: string;
39
+ /** Redis database number */
40
+ db?: number;
41
+ /** Connection timeout in milliseconds */
42
+ connectTimeout?: number;
43
+ /** Command timeout in milliseconds */
44
+ commandTimeout?: number;
45
+ /** Enable TLS */
46
+ tls?: boolean;
47
+ }
48
+
49
+ /**
50
+ * Redis store configuration
51
+ */
52
+ export interface RedisStoreConfig extends Partial<SessionStoreConfig> {
53
+ /** Redis connection configuration */
54
+ redis: RedisConfig;
55
+ /** Enable compression for large sessions */
56
+ compression?: boolean;
57
+ /** Serialization format */
58
+ serialization?: 'json' | 'msgpack';
59
+ }
60
+
61
+ /**
62
+ * Mock Redis client interface for development
63
+ * In production, this would be replaced with actual Redis client (ioredis, node-redis, etc.)
64
+ */
65
+ interface MockRedisClient {
66
+ set(key: string, value: string, ex: number): Promise<string>;
67
+ get(key: string): Promise<string | null>;
68
+ del(key: string): Promise<number>;
69
+ keys(pattern: string): Promise<string[]>;
70
+ expire(key: string, seconds: number): Promise<number>;
71
+ exists(key: string): Promise<number>;
72
+ scan(cursor: string, match?: string, count?: number): Promise<[string, string[]]>;
73
+ }
74
+
75
+ /**
76
+ * Redis-based session store implementation
77
+ * Stores session data in Redis with automatic expiration
78
+ */
79
+ export class RedisStore implements SessionStore {
80
+ private readonly config: Required<RedisStoreConfig>;
81
+ private readonly client: MockRedisClient;
82
+ private readonly userSessionsKey = 'user_sessions:';
83
+
84
+ constructor(config: RedisStoreConfig) {
85
+ this.config = {
86
+ defaultTTL: 3600,
87
+ maxSessionsPerUser: 5,
88
+ cleanupInterval: 300,
89
+ keyPrefix: 'session:',
90
+ compression: false,
91
+ serialization: 'json',
92
+ ...config
93
+ };
94
+
95
+ // Initialize mock Redis client
96
+ // In production, this would be: new Redis(config.redis)
97
+ this.client = this.createMockRedisClient();
98
+ }
99
+
100
+ /**
101
+ * Store session data in Redis
102
+ * @param sessionId - Session identifier
103
+ * @param data - Session data to store
104
+ * @param ttlSeconds - Time to live in seconds
105
+ */
106
+ async set(sessionId: string, data: SessionData, ttlSeconds: number): Promise<void> {
107
+ const key = this.getSessionKey(sessionId);
108
+ const userSessionsKey = this.getUserSessionsKey(data.userId);
109
+
110
+ // Enforce session limits per user
111
+ await this.enforceSessionLimits(data.userId);
112
+
113
+ // Serialize session data
114
+ const serializedData = this.serialize(data);
115
+
116
+ // Store session with expiration
117
+ await this.client.set(key, serializedData, ttlSeconds);
118
+
119
+ // Track user sessions (with longer TTL for cleanup)
120
+ const userSessionsTTL = Math.max(ttlSeconds, this.config.defaultTTL);
121
+ await this.client.set(`${userSessionsKey}${sessionId}`, '1', userSessionsTTL);
122
+ }
123
+
124
+ /**
125
+ * Retrieve session data from Redis
126
+ * @param sessionId - Session identifier
127
+ * @returns Session data or null if not found/expired
128
+ */
129
+ async get(sessionId: string): Promise<SessionData | null> {
130
+ const key = this.getSessionKey(sessionId);
131
+ const serializedData = await this.client.get(key);
132
+
133
+ if (!serializedData) {
134
+ return null;
135
+ }
136
+
137
+ try {
138
+ const data = this.deserialize(serializedData);
139
+
140
+ // Double-check expiration (Redis should handle this, but be safe)
141
+ if (data.expiresAt < new Date()) {
142
+ await this.delete(sessionId);
143
+ return null;
144
+ }
145
+
146
+ return data;
147
+ } catch (error) {
148
+ // If deserialization fails, delete the corrupted session
149
+ globalThis.console.log("error",error)
150
+ await this.delete(sessionId);
151
+ return null ;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Delete session from Redis
157
+ * @param sessionId - Session identifier
158
+ */
159
+ async delete(sessionId: string): Promise<void> {
160
+ const key = this.getSessionKey(sessionId);
161
+
162
+ // Get session data to find user ID
163
+ const data = await this.get(sessionId);
164
+
165
+ // Delete session
166
+ await this.client.del(key);
167
+
168
+ // Remove from user sessions tracking
169
+ if (data) {
170
+ const userSessionKey = `${this.getUserSessionsKey(data.userId)}${sessionId}`;
171
+ await this.client.del(userSessionKey);
172
+ }
173
+ }
174
+
175
+ /**
176
+ * Delete all sessions for a user
177
+ * @param userId - User identifier
178
+ * @returns Number of deleted sessions
179
+ */
180
+ async deleteByUserId(userId: string): Promise<number> {
181
+ const userSessionsPattern = `${this.getUserSessionsKey(userId)}*`;
182
+ const userSessionKeys = await this.client.keys(userSessionsPattern);
183
+
184
+ let deletedCount = 0;
185
+
186
+ for (const userSessionKey of userSessionKeys) {
187
+ // Extract session ID from key
188
+ const sessionId = userSessionKey.replace(this.getUserSessionsKey(userId), '');
189
+ const sessionKey = this.getSessionKey(sessionId);
190
+
191
+ // Delete both session and user session tracking
192
+ const sessionDeleted = await this.client.del(sessionKey);
193
+ await this.client.del(userSessionKey);
194
+
195
+ if (sessionDeleted > 0) {
196
+ deletedCount++;
197
+ }
198
+ }
199
+
200
+ return deletedCount;
201
+ }
202
+
203
+ /**
204
+ * Update session activity timestamp
205
+ * @param sessionId - Session identifier
206
+ */
207
+ async updateActivity(sessionId: string): Promise<void> {
208
+ const data = await this.get(sessionId);
209
+
210
+ if (data) {
211
+ data.lastActivityAt = new Date();
212
+
213
+ // Calculate remaining TTL
214
+ const remainingTTL = Math.max(0, Math.floor((data.expiresAt.getTime() - Date.now()) / NUMERIX.THOUSAND));
215
+
216
+ if (remainingTTL > 0) {
217
+ await this.set(sessionId, data, remainingTTL);
218
+ }
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Check if session exists in Redis
224
+ * @param sessionId - Session identifier
225
+ * @returns True if session exists
226
+ */
227
+ async exists(sessionId: string): Promise<boolean> {
228
+ const key = this.getSessionKey(sessionId);
229
+ const exists = await this.client.exists(key);
230
+ return exists > 0;
231
+ }
232
+
233
+ /**
234
+ * Get all active sessions for a user
235
+ * @param userId - User identifier
236
+ * @returns Array of session data
237
+ */
238
+ async getByUserId(userId: string): Promise<SessionData[]> {
239
+ const userSessionsPattern = `${this.getUserSessionsKey(userId)}*`;
240
+ const userSessionKeys = await this.client.keys(userSessionsPattern);
241
+
242
+ const sessions: SessionData[] = [];
243
+
244
+ for (const userSessionKey of userSessionKeys) {
245
+ // Extract session ID from key
246
+ const sessionId = userSessionKey.replace(this.getUserSessionsKey(userId), '');
247
+ const sessionData = await this.get(sessionId);
248
+
249
+ if (sessionData) {
250
+ sessions.push(sessionData);
251
+ }
252
+ }
253
+
254
+ return sessions;
255
+ }
256
+
257
+ /**
258
+ * Clean up expired sessions (Redis handles this automatically, but useful for stats)
259
+ * @returns Number of cleaned sessions (always 0 for Redis due to auto-expiration)
260
+ */
261
+ async cleanup(): Promise<number> {
262
+ // Redis automatically expires keys, so this is mainly for compatibility
263
+ // Could be used to clean up orphaned user session tracking keys
264
+ return 0;
265
+ }
266
+
267
+ /**
268
+ * Get session count for a user
269
+ * @param userId - User identifier
270
+ * @returns Session count
271
+ */
272
+ async getSessionCount(userId: string): Promise<number> {
273
+ const sessions = await this.getByUserId(userId);
274
+ return sessions.length;
275
+ }
276
+
277
+ /**
278
+ * Get Redis connection statistics
279
+ * @returns Connection and usage statistics
280
+ */
281
+ async getStats(): Promise<{
282
+ connected: boolean;
283
+ totalSessions: number;
284
+ memoryUsage: string;
285
+ }> {
286
+ // Mock implementation - in production would use Redis INFO command
287
+ return {
288
+ connected: true,
289
+ totalSessions: 0, // Would use DBSIZE or scan
290
+ memoryUsage: '0MB'
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Close Redis connection
296
+ */
297
+ async disconnect(): Promise<void> {
298
+ // Mock implementation - in production would close Redis connection
299
+ globalThis.console.log('Redis connection closed');
300
+ }
301
+
302
+ /**
303
+ * Get session key with prefix
304
+ * @param sessionId - Session identifier
305
+ * @returns Prefixed session key
306
+ * @private
307
+ */
308
+ private getSessionKey(sessionId: string): string {
309
+ return `${this.config.keyPrefix}${sessionId}`;
310
+ }
311
+
312
+ /**
313
+ * Get user sessions tracking key
314
+ * @param userId - User identifier
315
+ * @returns User sessions key prefix
316
+ * @private
317
+ */
318
+ private getUserSessionsKey(userId: string): string {
319
+ return `${this.config.keyPrefix}${this.userSessionsKey}${userId}:`;
320
+ }
321
+
322
+ /**
323
+ * Serialize session data
324
+ * @param data - Session data to serialize
325
+ * @returns Serialized string
326
+ * @private
327
+ */
328
+ private serialize(data: SessionData): string {
329
+ if (this.config.serialization === 'json') {
330
+ return JSON.stringify(data);
331
+ }
332
+
333
+ // In production, could use msgpack for better compression
334
+ return JSON.stringify(data);
335
+ }
336
+
337
+ /**
338
+ * Deserialize session data
339
+ * @param serializedData - Serialized session data
340
+ * @returns Parsed session data
341
+ * @private
342
+ */
343
+ private deserialize(serializedData: string): SessionData {
344
+ const data = JSON.parse(serializedData);
345
+
346
+ // Convert date strings back to Date objects
347
+ return {
348
+ ...data,
349
+ expiresAt: new Date(data.expiresAt),
350
+ createdAt: new Date(data.createdAt),
351
+ lastActivityAt: new Date(data.lastActivityAt)
352
+ };
353
+ }
354
+
355
+ /**
356
+ * Enforce session limits per user
357
+ * @param userId - User identifier
358
+ * @private
359
+ */
360
+ private async enforceSessionLimits(userId: string): Promise<void> {
361
+ const userSessions = await this.getByUserId(userId);
362
+
363
+ if (userSessions.length >= this.config.maxSessionsPerUser) {
364
+ // Sort by last activity (oldest first)
365
+ userSessions.sort((a, b) => a.lastActivityAt.getTime() - b.lastActivityAt.getTime());
366
+
367
+ // Remove oldest sessions to make room
368
+ const sessionsToRemove = userSessions.length - this.config.maxSessionsPerUser + 1;
369
+ for (let i = 0; i < sessionsToRemove; i++) {
370
+ await this.delete(userSessions[i].id);
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Create mock Redis client for development
377
+ * @returns Mock Redis client
378
+ * @private
379
+ */
380
+ private createMockRedisClient(): MockRedisClient {
381
+ const storage = new Map<string, { value: string; expires: number }>();
382
+
383
+ return {
384
+ async set(key: string, value: string, ex: number): Promise<string> {
385
+ storage.set(key, {
386
+ value,
387
+ expires: Date.now() + ex * NUMERIX.THOUSAND
388
+ });
389
+ return 'OK';
390
+ },
391
+
392
+ async get(key: string): Promise<string | null> {
393
+ const item = storage.get(key);
394
+ if (!item) return null;
395
+
396
+ if (item.expires < Date.now()) {
397
+ storage.delete(key);
398
+ return null;
399
+ }
400
+
401
+ return item.value;
402
+ },
403
+
404
+ async del(key: string): Promise<number> {
405
+ const existed = storage.has(key);
406
+ storage.delete(key);
407
+ return existed ? 1 : 0;
408
+ },
409
+
410
+ async keys(pattern: string): Promise<string[]> {
411
+ const regex = new RegExp(pattern.replace('*', '.*'));
412
+ return Array.from(storage.keys()).filter(key => regex.test(key));
413
+ },
414
+
415
+ async expire(key: string, seconds: number): Promise<number> {
416
+ const item = storage.get(key);
417
+ if (!item) return 0;
418
+
419
+ item.expires = Date.now() + seconds * NUMERIX.THOUSAND;
420
+ storage.set(key, item);
421
+ return 1;
422
+ },
423
+
424
+ async exists(key: string): Promise<number> {
425
+ const item = storage.get(key);
426
+ if (!item) return 0;
427
+
428
+ if (item.expires < Date.now()) {
429
+ storage.delete(key);
430
+ return 0;
431
+ }
432
+
433
+ return 1;
434
+ },
435
+
436
+ async scan(cursor: string, match?: string, count?: number): Promise<[string, string[]]> {
437
+ // Simplified scan implementation
438
+ const keys = match ? await this.keys(match) : Array.from(storage.keys());
439
+ return ['0', keys.slice(0, count ?? NUMERIX.TEN)];
440
+ }
441
+ };
442
+ }
443
+ }
@@ -0,0 +1,128 @@
1
+ import type { UserRepository } from "../db/repositories/user.repository";
2
+ import type { ConnectedAccountRepository } from "../db/repositories/connected-account.repository";
3
+ import type { AuthUser, ConnectedAccount } from "@plyaz/types";
4
+
5
+
6
+ export interface OAuthProfile {
7
+ id: string;
8
+ email: string;
9
+ name: string;
10
+ avatar?: string;
11
+ provider: string;
12
+ }
13
+
14
+ export class OAuthStrategy {
15
+ constructor(
16
+ private userRepo: UserRepository,
17
+ private connectedAccountRepo: ConnectedAccountRepository
18
+ ) {}
19
+
20
+ async authenticate(
21
+ profile: OAuthProfile,
22
+ accessToken: string,
23
+ refreshToken?: string
24
+ ): Promise<AuthUser> {
25
+ let connectedAccount = await this.connectedAccountRepo.findByProviderAndId(
26
+ profile.provider,
27
+ profile.id
28
+ );
29
+
30
+ if (connectedAccount) {
31
+ await this.updateTokens(connectedAccount.id, accessToken, refreshToken);
32
+ const user = await this.userRepo.findById(connectedAccount.userId);
33
+ if (!user) throw new Error("User not found");
34
+ return user;
35
+ }
36
+
37
+ let user = await this.userRepo.findByEmail(profile.email);
38
+ user ??= await this.createUserFromProfile(profile);
39
+ if (!user) throw new Error("Failed to create user");
40
+
41
+ connectedAccount = await this.createConnectedAccount(
42
+ user.id,
43
+ profile,
44
+ accessToken,
45
+ refreshToken
46
+ );
47
+
48
+ return user;
49
+ }
50
+
51
+ private async createUserFromProfile(profile: OAuthProfile): Promise<AuthUser> {
52
+ return this.userRepo.create({
53
+ email: profile.email,
54
+ displayName: profile.name,
55
+ authProvider: "GOOGLE",
56
+ isActive: true,
57
+ isVerified: true,
58
+ });
59
+ }
60
+
61
+ private async createConnectedAccount(
62
+ userId: string,
63
+ profile: OAuthProfile,
64
+ accessToken: string,
65
+ refreshToken?: string
66
+ ): Promise<ConnectedAccount> {
67
+ return this.connectedAccountRepo.create({
68
+ userId,
69
+ providerType: "OAUTH",
70
+ provider: profile.provider,
71
+ providerAccountId: profile.id,
72
+ providerEmail: profile.email,
73
+ providerDisplayName: profile.name,
74
+ providerAvatarUrl: profile.avatar,
75
+ accessTokenEncrypted: accessToken,
76
+ refreshTokenEncrypted: refreshToken,
77
+ isVerified: true,
78
+ });
79
+ }
80
+
81
+ private async updateTokens(
82
+ accountId: string,
83
+ accessToken: string,
84
+ refreshToken?: string
85
+ ): Promise<void> {
86
+ await this.connectedAccountRepo.update(accountId, {
87
+ accessTokenEncrypted: accessToken,
88
+ refreshTokenEncrypted: refreshToken,
89
+ });
90
+ }
91
+
92
+ // getAuthorizationUrl(provider: string, redirectUri: string, state?: string): string {
93
+ // const baseUrls = {
94
+ // google: 'https://accounts.google.com/o/oauth2/v2/auth',
95
+ // facebook: 'https://www.facebook.com/v18.0/dialog/oauth',
96
+ // github: 'https://github.com/login/oauth/authorize'
97
+ // };
98
+
99
+ // const baseUrl = baseUrls[provider as keyof typeof baseUrls] || `https://oauth.${provider}.com/authorize`;
100
+ // const params = new URLSearchParams({
101
+ // response_type: 'code',
102
+ // client_id: 'your_client_id',
103
+ // redirect_uri: redirectUri,
104
+ // scope: 'email profile',
105
+ // ...(state && { state })
106
+ // });
107
+
108
+ // return `${baseUrl}?${params.toString()}`;
109
+ // }
110
+
111
+ async linkAccount(
112
+ userId: string,
113
+ provider: string,
114
+ profileData: {id:string,email:string,avatar:string,name:string}
115
+ ): Promise<ConnectedAccount> {
116
+ return this.connectedAccountRepo.create({
117
+ userId,
118
+ providerType: "OAUTH",
119
+ provider,
120
+ providerAccountId: profileData.id,
121
+ providerEmail: profileData.email,
122
+ providerDisplayName: profileData.name,
123
+ providerAvatarUrl: profileData.avatar,
124
+ isVerified: true,
125
+ isPrimary: false,
126
+ });
127
+ }
128
+ }
@@ -0,0 +1,116 @@
1
+ import { randomBytes, pbkdf2Sync } from 'crypto';
2
+ import type { UserRepository } from '../db/repositories/user.repository';
3
+ import type { SessionManager } from '../core/session/session.manager';
4
+ import type { AuthTokens, AuthUser, Session } from '@plyaz/types';
5
+ import { AuthenticationError } from '@plyaz/errors';
6
+ import { NUMERIX } from '@plyaz/config';
7
+ import type { JwtManager } from '@/core/jwt/jwt.manager';
8
+
9
+ export class TraditionalAuthStrategy {
10
+ constructor(
11
+ private userRepo: UserRepository,
12
+ private jwtManager: JwtManager,
13
+ private sessionManager: SessionManager
14
+ ) {}
15
+
16
+ async authenticate(email: string, password: string): Promise<AuthUser> {
17
+ const user = await this.userRepo.findByEmail(email);
18
+ if (!user?.passwordHash) {
19
+ throw new AuthenticationError('AUTH_INVALID_CREDENTIALS');
20
+ }
21
+
22
+ const isValid = await this.verifyPassword(password, user.passwordHash);
23
+ if (!isValid) {
24
+ throw new AuthenticationError('AUTH_INVALID_CREDENTIALS');
25
+ }
26
+
27
+ if (!user.isActive) {
28
+ throw new AuthenticationError('AUTH_ACCOUNT_LOCKED');
29
+ }
30
+
31
+ if (user.isSuspended) {
32
+ throw new AuthenticationError('AUTH_ACCOUNT_SUSPENDED');
33
+ }
34
+
35
+ return user;
36
+ }
37
+
38
+ async hashPassword(password: string): Promise<string> {
39
+ const SALT_LENGTH = 32;
40
+ const HASH_LENGTH = 64;
41
+ const salt = randomBytes(SALT_LENGTH).toString('hex');
42
+ const hash = pbkdf2Sync(password, salt, NUMERIX.THOUSAND, HASH_LENGTH, 'sha512').toString('hex');
43
+ return `pbkdf2$${salt}$${hash}`;
44
+ }
45
+
46
+ async verifyPassword(password: string, hash: string): Promise<boolean> {
47
+ const HASH_LENGTH = 64;
48
+ if (hash.startsWith('pbkdf2$')) {
49
+ const [, salt, storedHash] = hash.split('$');
50
+ const computedHash = pbkdf2Sync(password, salt, NUMERIX.THOUSAND, HASH_LENGTH, 'sha512').toString('hex');
51
+ return computedHash === storedHash;
52
+ }
53
+ return false;
54
+ }
55
+
56
+ async signUp(data: {
57
+ email: string;
58
+ password: string;
59
+ displayName: string;
60
+ firstName?: string;
61
+ lastName?: string;
62
+ }): Promise<{ user: AuthUser; session: Session; tokens: AuthTokens }> {
63
+ // Check if user already exists
64
+ const existingUser = await this.userRepo.findByEmail(data.email);
65
+ if (existingUser) {
66
+ throw new AuthenticationError('AUTH_INVALID_CREDENTIALS');
67
+ }
68
+
69
+ // Hash password
70
+ const passwordHash = await this.hashPassword(data.password);
71
+
72
+ // Create user
73
+ const user = await this.userRepo.create({
74
+ email: data.email,
75
+ displayName: data.displayName,
76
+ firstName: data.firstName,
77
+ lastName: data.lastName,
78
+ passwordHash,
79
+ authProvider: 'EMAIL',
80
+ isActive: true,
81
+ isVerified: false
82
+ } );
83
+
84
+ // Create session
85
+ const session = await this.sessionManager.createSession(user.id, {
86
+ provider: 'EMAIL',
87
+ userAgent: 'test',
88
+ ipAddress: '127.0.0.1'
89
+ });
90
+
91
+ // Generate tokens
92
+ const tokens = this.jwtManager.generateTokens(user);
93
+
94
+ return { user, session, tokens };
95
+ }
96
+
97
+ async signIn(data: {
98
+ email: string;
99
+ password: string;
100
+ }): Promise<{ user: AuthUser; session: Session; tokens: AuthTokens }> {
101
+ // Authenticate user
102
+ const user = await this.authenticate(data.email, data.password);
103
+
104
+ // Create session
105
+ const session = await this.sessionManager.createSession(user.id, {
106
+ provider: 'EMAIL',
107
+ userAgent: 'test',
108
+ ipAddress: '127.0.0.1'
109
+ });
110
+
111
+ // Generate tokens
112
+ const tokens = this.jwtManager.generateTokens(user);
113
+
114
+ return { user, session, tokens };
115
+ }
116
+ }
@@ -0,0 +1,4 @@
1
+ export * from './token-validator';
2
+ export * from './refresh-token-manager';
3
+ export * from '../core/jwt/jwt.manager';
4
+ export * from '../core/blacklist/token.blacklist';