@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,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
|
+
}
|