@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,415 @@
1
+ import { createClient } from '@supabase/supabase-js';
2
+ import type { ConnectedAccount, ConnectedAccountRepository as IConnectedAccountRepository, CreateConnectedAccountData, UpdateConnectedAccountData } from '@/common/types/auth.types';
3
+
4
+ /**
5
+ * Repository for managing connected accounts (provider account linking)
6
+ *
7
+ * @description
8
+ * Handles CRUD operations for external provider accounts linked to users.
9
+ * Supports OAuth providers (Clerk, Google, Facebook, etc.) and stores tokens.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * const repo = new ConnectedAccountRepository(supabaseUrl, supabaseKey);
14
+ * const account = await repo.findByProvider('clerk', 'user_123');
15
+ * ```
16
+ */
17
+ export class ConnectedAccountRepository implements IConnectedAccountRepository {
18
+ private supabase;
19
+
20
+ /**
21
+ * STEP 1: Initialize repository with Supabase client
22
+ * @param supabaseUrl - Supabase project URL
23
+ * @param supabaseKey - Supabase service role key
24
+ */
25
+ constructor(supabaseUrl: string, supabaseKey: string) {
26
+ this.supabase = createClient(supabaseUrl, supabaseKey);
27
+ }
28
+
29
+
30
+
31
+
32
+
33
+ /**
34
+ * STEP 2: Create a new connected account
35
+ * Links an external provider account to a user
36
+ *
37
+ * @param data - Connected account creation data
38
+ * @returns Promise resolving to created ConnectedAccount
39
+ * @throws Error if creation fails
40
+ */
41
+ async create(data: CreateConnectedAccountData): Promise<ConnectedAccount> {
42
+ const insertData = this.transformToDbFormat(data);
43
+
44
+ const { data: accountData, error } = await this.supabase
45
+ .from('connected_accounts')
46
+ .insert(insertData)
47
+ .select()
48
+ .single();
49
+
50
+ if (error || !accountData) throw new Error(`Failed to create connected account: ${error?.message}`);
51
+ return this.mapToConnectedAccount(accountData);
52
+ }
53
+
54
+ /**
55
+ * STEP 3: Find connected account by ID
56
+ *
57
+ * @param id - Connected account UUID
58
+ * @returns Promise resolving to ConnectedAccount or null if not found
59
+ */
60
+ async findById(id: string): Promise<ConnectedAccount | null> {
61
+ const { data, error } = await this.supabase
62
+ .from('connected_accounts')
63
+ .select('*')
64
+ .eq('id', id)
65
+ .single();
66
+
67
+ if (error || !data) return null;
68
+ return this.mapToConnectedAccount(data);
69
+ }
70
+
71
+ /**
72
+ * STEP 4: Find all connected accounts for a user
73
+ * Returns accounts ordered by creation date (newest first)
74
+ *
75
+ * @param userId - User UUID
76
+ * @returns Promise resolving to array of ConnectedAccount
77
+ */
78
+ async findByUserId(userId: string): Promise<ConnectedAccount[]> {
79
+ const { data, error } = await this.supabase
80
+ .from('connected_accounts')
81
+ .select('*')
82
+ .eq('user_id', userId)
83
+ .order('created_at', { ascending: false });
84
+
85
+ if (error || !data) return [];
86
+ return data.map(this.mapToConnectedAccount);
87
+ }
88
+
89
+ /**
90
+ * STEP 5: Find connected account by provider credentials
91
+ * Used for authentication - matches (provider, provider_account_id)
92
+ *
93
+ * @param provider - Provider name (e.g., 'clerk', 'google')
94
+ * @param providerAccountId - Provider's user ID
95
+ * @returns Promise resolving to ConnectedAccount or null if not found
96
+ */
97
+ async findByProvider(provider: string, providerAccountId: string): Promise<ConnectedAccount | null> {
98
+ const { data, error } = await this.supabase
99
+ .from('connected_accounts')
100
+ .select('*')
101
+ .eq('provider', provider)
102
+ .eq('provider_account_id', providerAccountId)
103
+ .single();
104
+
105
+ if (error || !data) return null;
106
+ return this.mapToConnectedAccount(data);
107
+ }
108
+
109
+ /**
110
+ * Find connected account by provider and ID (alias for compatibility)
111
+ */
112
+ async findByProviderAndId(provider: string, providerAccountId: string): Promise<ConnectedAccount | null> {
113
+ return this.findByProvider(provider, providerAccountId);
114
+ }
115
+
116
+ /**
117
+ * Update tokens for connected account
118
+ */
119
+ async updateTokens(accountId: string, accessToken: string, refreshToken?: string): Promise<void> {
120
+ const updateData = {
121
+ access_token_encrypted: accessToken,
122
+ updated_at: new Date(),
123
+ refresh_token_encrypted:refreshToken
124
+ };
125
+
126
+ if (refreshToken) {
127
+ updateData.refresh_token_encrypted = refreshToken;
128
+ }
129
+
130
+ const { error } = await this.supabase
131
+ .from('connected_accounts')
132
+ .update(updateData)
133
+ .eq('id', accountId);
134
+
135
+ if (error) throw new Error(`Failed to update tokens: ${error.message}`);
136
+ }
137
+
138
+ /**
139
+ * STEP 6: Update connected account
140
+ * Updates tokens, profile info, or metadata
141
+ *
142
+ * @param id - Connected account UUID
143
+ * @param data - Partial update data
144
+ * @returns Promise resolving to updated ConnectedAccount
145
+ * @throws Error if update fails
146
+ */
147
+ async update(id: string, data: UpdateConnectedAccountData): Promise<ConnectedAccount> {
148
+ const updateData = this.buildUpdateData(data);
149
+
150
+ const { data: accountData, error } = await this.supabase
151
+ .from('connected_accounts')
152
+ .update(updateData)
153
+ .eq('id', id)
154
+ .select()
155
+ .single();
156
+
157
+ if (error || !accountData) throw new Error(`Failed to update connected account: ${error?.message}`);
158
+ return this.mapToConnectedAccount(accountData);
159
+ }
160
+
161
+ /**
162
+ * STEP 7: Delete connected account (unlink provider)
163
+ *
164
+ * @param id - Connected account UUID
165
+ * @throws Error if deletion fails
166
+ */
167
+ async delete(id: string): Promise<void> {
168
+ const { error } = await this.supabase
169
+ .from('connected_accounts')
170
+ .delete()
171
+ .eq('id', id);
172
+
173
+ if (error) throw new Error(`Failed to delete connected account: ${error.message}`);
174
+ }
175
+
176
+ /**
177
+ * Find primary connected account for user
178
+ * Query WHERE user_id AND is_primary = true
179
+ * @param userId - User identifier
180
+ * @returns Primary connected account or null
181
+ */
182
+ async findPrimary(userId: string): Promise<ConnectedAccount | null> {
183
+ const { data, error } = await this.supabase
184
+ .from('connected_accounts')
185
+ .select('*')
186
+ .eq('user_id', userId)
187
+ .eq('is_primary', true)
188
+ .eq('is_active', true)
189
+ .single();
190
+
191
+ if (error || !data) return null;
192
+ return this.mapToConnectedAccount(data);
193
+ }
194
+
195
+ /**
196
+ * Set primary connected account
197
+ * UPDATE all user accounts is_primary = false, then UPDATE specific account is_primary = true
198
+ * @param userId - User identifier
199
+ * @param accountId - Account identifier to set as primary
200
+ */
201
+ async setPrimary(userId: string, accountId: string): Promise<void> {
202
+ // First, unset all primary accounts for user
203
+ await this.supabase
204
+ .from('connected_accounts')
205
+ .update({ is_primary: false, updated_at: new Date() })
206
+ .eq('user_id', userId);
207
+
208
+ // Then set the specified account as primary
209
+ const { error } = await this.supabase
210
+ .from('connected_accounts')
211
+ .update({ is_primary: true, updated_at: new Date() })
212
+ .eq('id', accountId)
213
+ .eq('user_id', userId);
214
+
215
+ if (error) throw new Error(`Failed to set primary account: ${error.message}`);
216
+ }
217
+
218
+ /**
219
+ * Link account to user
220
+ * INSERT new connected account and emit account.linked event
221
+ * @param userId - User identifier
222
+ * @param provider - Provider name
223
+ * @param providerData - Provider account data
224
+ * @returns Created connected account
225
+ */
226
+ async linkAccount(userId: string, provider: string, providerData: ConnectedAccount): Promise<ConnectedAccount> {
227
+ const accountData = {
228
+ user_id: userId,
229
+ provider_type: providerData.providerType ?? 'OAUTH',
230
+ provider,
231
+ provider_account_id: providerData.providerAccountId,
232
+ provider_email: providerData.providerEmail,
233
+ provider_username: providerData.providerUsername,
234
+ provider_display_name: providerData.providerDisplayName,
235
+ provider_avatar_url: providerData.providerAvatarUrl,
236
+ provider_metadata: providerData.providerMetadata ?? {},
237
+ is_primary: false,
238
+ is_verified: true,
239
+ is_active: true,
240
+ linked_at: new Date(),
241
+ created_at: new Date(),
242
+ updated_at: new Date()
243
+ };
244
+
245
+ const { data, error } = await this.supabase
246
+ .from('connected_accounts')
247
+ .insert(accountData)
248
+ .select()
249
+ .single();
250
+
251
+ if (error || !data) throw new Error(`Failed to link account: ${error?.message}`);
252
+
253
+ const connectedAccount = this.mapToConnectedAccount(data);
254
+
255
+ // Emit account linked event
256
+ this.emitAccountLinkedEvent(userId, provider, connectedAccount.id);
257
+
258
+ return connectedAccount;
259
+ }
260
+
261
+ /**
262
+ * Unlink account from user
263
+ * DELETE connected account with validation (not last auth method) and emit account.unlinked event
264
+ * @param accountId - Account identifier
265
+ */
266
+ async unlinkAccount(accountId: string): Promise<void> {
267
+ // Get account details
268
+ const account = await this.findById(accountId);
269
+ if (!account) {
270
+ throw new Error('Connected account not found');
271
+ }
272
+
273
+ // Check if this is the last authentication method
274
+ const userAccounts = await this.findByUserId(account.userId);
275
+ if (userAccounts.length <= 1) {
276
+ throw new Error('Cannot unlink the last authentication method');
277
+ }
278
+
279
+ // Delete the account
280
+ await this.delete(accountId);
281
+
282
+ // Emit account unlinked event
283
+ this.emitAccountUnlinkedEvent(account.userId, account.provider, accountId);
284
+ }
285
+
286
+ /**
287
+ * Emit account linked event
288
+ * @param userId - User identifier
289
+ * @param provider - Provider name
290
+ * @param accountId - Account identifier
291
+ * @private
292
+ */
293
+ private emitAccountLinkedEvent(userId: string, provider: string, accountId: string): void {
294
+ // Mock event emission - in real implementation would use event system
295
+ globalThis.console.log('Event: auth.account.linked', {
296
+ userId,
297
+ provider,
298
+ accountId,
299
+ timestamp: new Date()
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Emit account unlinked event
305
+ * @param userId - User identifier
306
+ * @param provider - Provider name
307
+ * @param accountId - Account identifier
308
+ * @private
309
+ */
310
+ private emitAccountUnlinkedEvent(userId: string, provider: string, accountId: string): void {
311
+ // Mock event emission - in real implementation would use event system
312
+ globalThis.console.log('Event: auth.account.unlinked', {
313
+ userId,
314
+ provider,
315
+ accountId,
316
+ timestamp: new Date()
317
+ });
318
+ }
319
+
320
+ /**
321
+ * Transform camelCase DTO to snake_case database format
322
+ * @private
323
+ */
324
+ private transformToDbFormat(data: CreateConnectedAccountData): Record<string, string| undefined | Record<string, unknown> |Date | boolean > {
325
+ return {
326
+ user_id: data.userId,
327
+ provider_type: data.providerType,
328
+ provider: data.provider,
329
+ provider_account_id: data.providerAccountId,
330
+ provider_email: data.providerEmail,
331
+ provider_username: data.providerUsername,
332
+ provider_display_name: data.providerDisplayName,
333
+ provider_avatar_url: data.providerAvatarUrl,
334
+ provider_profile_url: data.providerProfileUrl,
335
+ provider_metadata: data.providerMetadata,
336
+ wallet_address: data.walletAddress,
337
+ chain_id: data.chainId,
338
+ access_token_encrypted: data.accessTokenEncrypted,
339
+ refresh_token_encrypted: data.refreshTokenEncrypted,
340
+ token_expires_at: data.tokenExpiresAt,
341
+ token_scope: data.tokenScope,
342
+ is_primary: data.isPrimary ?? false,
343
+ is_verified: data.isVerified ?? true,
344
+ is_active: data.isActive ?? true,
345
+ linked_ip_address: data.linkedIpAddress,
346
+ linked_user_agent: data.linkedUserAgent,
347
+ };
348
+ }
349
+
350
+ /**
351
+ * Build update data with only defined fields
352
+ * @private
353
+ */
354
+ private buildUpdateData(data: UpdateConnectedAccountData): Record<string, string> {
355
+ const result: Record<string, string> = { updated_at: new Date().toString() };
356
+
357
+ // Only include defined fields
358
+ Object.entries(data).forEach(([key, value]) => {
359
+ if (value !== undefined) {
360
+ const dbKey = this.camelToSnake(key);
361
+ result[dbKey] = value;
362
+ }
363
+ });
364
+
365
+ return result;
366
+ }
367
+
368
+ /**
369
+ * Convert camelCase to snake_case
370
+ * @private
371
+ */
372
+ private camelToSnake(str: string): string {
373
+ return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
374
+ }
375
+
376
+ /**
377
+ * STEP 8: Map database row to ConnectedAccount interface
378
+ * Converts snake_case to camelCase
379
+ *
380
+ * @param data - Raw database row
381
+ * @returns Mapped ConnectedAccount object
382
+ * @private
383
+ */
384
+ private mapToConnectedAccount(data: ConnectedAccount): ConnectedAccount {
385
+ return {
386
+ id: data.id,
387
+ userId: data.userId,
388
+ providerType: data.providerType,
389
+ provider: data.provider,
390
+ providerAccountId: data.providerAccountId,
391
+ providerEmail: data.providerEmail,
392
+ providerUsername: data.providerUsername,
393
+ providerDisplayName: data.providerDisplayName,
394
+ providerAvatarUrl: data.providerAvatarUrl,
395
+ providerProfileUrl: data.providerProfileUrl,
396
+ providerMetadata: data.providerMetadata,
397
+ walletAddress: data.walletAddress,
398
+ chainId: data.chainId,
399
+ accessTokenEncrypted: data.accessTokenEncrypted,
400
+ refreshTokenEncrypted: data.refreshTokenEncrypted,
401
+ tokenExpiresAt: data.tokenExpiresAt ? new Date(data.tokenExpiresAt) : undefined,
402
+ tokenScope: data.tokenScope,
403
+ isPrimary: data.isPrimary,
404
+ isVerified: data.isVerified,
405
+ isActive: data.isActive,
406
+ linkedAt: new Date(data.linkedAt),
407
+ linkedIpAddress: data.lastUsedIpAddress,
408
+ linkedUserAgent: data.linkedUserAgent,
409
+ lastUsedAt: data.lastUsedAt ? new Date(data.lastUsedAt) : undefined,
410
+ lastUsedIpAddress: data.lastUsedIpAddress,
411
+ createdAt: new Date(data.createdAt),
412
+ updatedAt: new Date(data.updatedAt),
413
+ };
414
+ }
415
+ }