@plyaz/auth 1.0.0 → 1.0.2
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/commits.txt +3 -3
- package/dist/common/index.cjs +3 -1
- package/dist/common/index.cjs.map +1 -1
- package/dist/common/index.mjs +3 -1
- package/dist/common/index.mjs.map +1 -1
- package/dist/index.cjs +424 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +421 -152
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/release_message.txt +28 -0
- package/src/adapters/auth-adapter-factory.ts +4 -3
- package/src/adapters/auth-adapter.mapper.ts +2 -2
- package/src/adapters/base-auth.adapter.ts +17 -9
- package/src/adapters/clerk/clerk.adapter.ts +9 -12
- package/src/adapters/custom/custom.adapter.ts +19 -10
- package/src/adapters/index.ts +0 -1
- package/src/adapters/next-auth/authOptions.ts +20 -16
- package/src/adapters/next-auth/next-auth.adapter.ts +13 -15
- package/src/api/client.ts +4 -6
- package/src/audit/audit.logger.ts +19 -10
- package/src/client/components/ProtectedRoute.tsx +15 -11
- package/src/client/hooks/useAuth.ts +23 -21
- package/src/client/hooks/useConnectedAccounts.ts +57 -45
- package/src/client/hooks/usePermissions.ts +1 -1
- package/src/client/hooks/useRBAC.ts +6 -6
- package/src/client/hooks/useSession.ts +5 -5
- package/src/client/providers/AuthProvider.tsx +23 -17
- package/src/client/store/auth.store.ts +71 -62
- package/src/client/utils/storage.ts +45 -18
- package/src/common/constants/oauth-providers.ts +10 -7
- package/src/common/errors/auth.errors.ts +4 -4
- package/src/common/errors/specific-auth-errors.ts +5 -9
- package/src/common/regex/index.ts +6 -4
- package/src/common/types/auth.types.ts +47 -38
- package/src/common/types/index.ts +12 -6
- package/src/common/utils/index.ts +15 -11
- package/src/core/blacklist/token.blacklist.ts +13 -7
- package/src/core/index.ts +2 -2
- package/src/core/jwt/jwt.manager.ts +47 -22
- package/src/core/session/session.manager.ts +17 -14
- package/src/db/repositories/connected-account.repository.ts +120 -78
- package/src/db/repositories/role.repository.ts +41 -26
- package/src/db/repositories/session.repository.ts +9 -10
- package/src/db/repositories/user.repository.ts +105 -91
- package/src/flows/index.ts +2 -2
- package/src/flows/sign-in.flow.ts +28 -14
- package/src/flows/sign-up.flow.ts +31 -20
- package/src/index.ts +36 -37
- package/src/libs/clerk.helper.ts +6 -7
- package/src/libs/supabase.helper.ts +79 -61
- package/src/libs/supabaseClient.ts +3 -3
- package/src/providers/base/auth-provider.interface.ts +13 -11
- package/src/providers/base/index.ts +1 -1
- package/src/providers/index.ts +1 -1
- package/src/providers/oauth/facebook.provider.ts +63 -39
- package/src/providers/oauth/github.provider.ts +14 -10
- package/src/providers/oauth/google.provider.ts +39 -28
- package/src/providers/oauth/index.ts +1 -1
- package/src/rbac/dynamic-roles.ts +88 -54
- package/src/rbac/index.ts +4 -4
- package/src/rbac/permission-checker.ts +147 -75
- package/src/rbac/role-hierarchy.ts +8 -8
- package/src/rbac/role.manager.ts +11 -8
- package/src/security/csrf/csrf.protection.ts +9 -7
- package/src/security/index.ts +2 -2
- package/src/security/rate-limiting/auth/auth.controller.ts +2 -4
- package/src/security/rate-limiting/auth/rate-limiting.interface.ts +26 -6
- package/src/security/rate-limiting/auth.module.ts +1 -2
- package/src/server/auth.module.ts +55 -52
- package/src/server/decorators/auth.decorator.ts +9 -11
- package/src/server/decorators/auth.decorators.ts +8 -9
- package/src/server/decorators/current-user.decorator.ts +6 -6
- package/src/server/decorators/permission.decorator.ts +17 -9
- package/src/server/guards/auth.guard.ts +21 -16
- package/src/server/guards/custom-throttler.guard.ts +4 -9
- package/src/server/guards/permissions.guard.ts +32 -23
- package/src/server/guards/roles.guard.ts +14 -12
- package/src/server/middleware/auth.middleware.ts +4 -4
- package/src/server/middleware/session.middleware.ts +4 -4
- package/src/server/services/account.service.ts +96 -48
- package/src/server/services/auth.service.ts +57 -28
- package/src/server/services/brute-force.service.ts +24 -19
- package/src/server/services/index.ts +1 -1
- package/src/server/services/rate-limiter.service.ts +9 -4
- package/src/server/services/session.service.ts +84 -48
- package/src/server/services/token.service.ts +71 -51
- package/src/session/cookie-store.ts +47 -34
- package/src/session/enhanced-session-manager.ts +69 -48
- package/src/session/index.ts +5 -5
- package/src/session/memory-store.ts +37 -30
- package/src/session/redis-store.ts +105 -72
- package/src/strategies/oauth.strategy.ts +10 -9
- package/src/strategies/traditional-auth.strategy.ts +41 -29
- package/src/tokens/index.ts +4 -4
- package/src/tokens/refresh-token-manager.ts +70 -55
- package/src/tokens/token-validator.ts +109 -53
- package/vitest.setup.d.ts +2 -2
- package/vitest.setup.ts +1 -1
|
@@ -1,13 +1,18 @@
|
|
|
1
|
-
import { createClient } from
|
|
2
|
-
import type {
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
import type {
|
|
3
|
+
ConnectedAccount,
|
|
4
|
+
ConnectedAccountRepository as IConnectedAccountRepository,
|
|
5
|
+
CreateConnectedAccountData,
|
|
6
|
+
UpdateConnectedAccountData,
|
|
7
|
+
} from "@/common/types/auth.types";
|
|
3
8
|
|
|
4
9
|
/**
|
|
5
10
|
* Repository for managing connected accounts (provider account linking)
|
|
6
|
-
*
|
|
11
|
+
*
|
|
7
12
|
* @description
|
|
8
13
|
* Handles CRUD operations for external provider accounts linked to users.
|
|
9
14
|
* Supports OAuth providers (Clerk, Google, Facebook, etc.) and stores tokens.
|
|
10
|
-
*
|
|
15
|
+
*
|
|
11
16
|
* @example
|
|
12
17
|
* ```typescript
|
|
13
18
|
* const repo = new ConnectedAccountRepository(supabaseUrl, supabaseKey);
|
|
@@ -26,42 +31,39 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
26
31
|
this.supabase = createClient(supabaseUrl, supabaseKey);
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
34
|
/**
|
|
34
35
|
* STEP 2: Create a new connected account
|
|
35
36
|
* Links an external provider account to a user
|
|
36
|
-
*
|
|
37
|
+
*
|
|
37
38
|
* @param data - Connected account creation data
|
|
38
39
|
* @returns Promise resolving to created ConnectedAccount
|
|
39
40
|
* @throws Error if creation fails
|
|
40
41
|
*/
|
|
41
42
|
async create(data: CreateConnectedAccountData): Promise<ConnectedAccount> {
|
|
42
43
|
const insertData = this.transformToDbFormat(data);
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
const { data: accountData, error } = await this.supabase
|
|
45
|
-
.from(
|
|
46
|
+
.from("connected_accounts")
|
|
46
47
|
.insert(insertData)
|
|
47
48
|
.select()
|
|
48
49
|
.single();
|
|
49
50
|
|
|
50
|
-
if (error || !accountData)
|
|
51
|
+
if (error || !accountData)
|
|
52
|
+
throw new Error(`Failed to create connected account: ${error?.message}`);
|
|
51
53
|
return this.mapToConnectedAccount(accountData);
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
/**
|
|
55
57
|
* STEP 3: Find connected account by ID
|
|
56
|
-
*
|
|
58
|
+
*
|
|
57
59
|
* @param id - Connected account UUID
|
|
58
60
|
* @returns Promise resolving to ConnectedAccount or null if not found
|
|
59
61
|
*/
|
|
60
62
|
async findById(id: string): Promise<ConnectedAccount | null> {
|
|
61
63
|
const { data, error } = await this.supabase
|
|
62
|
-
.from(
|
|
63
|
-
.select(
|
|
64
|
-
.eq(
|
|
64
|
+
.from("connected_accounts")
|
|
65
|
+
.select("*")
|
|
66
|
+
.eq("id", id)
|
|
65
67
|
.single();
|
|
66
68
|
|
|
67
69
|
if (error || !data) return null;
|
|
@@ -71,16 +73,16 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
71
73
|
/**
|
|
72
74
|
* STEP 4: Find all connected accounts for a user
|
|
73
75
|
* Returns accounts ordered by creation date (newest first)
|
|
74
|
-
*
|
|
76
|
+
*
|
|
75
77
|
* @param userId - User UUID
|
|
76
78
|
* @returns Promise resolving to array of ConnectedAccount
|
|
77
79
|
*/
|
|
78
80
|
async findByUserId(userId: string): Promise<ConnectedAccount[]> {
|
|
79
81
|
const { data, error } = await this.supabase
|
|
80
|
-
.from(
|
|
81
|
-
.select(
|
|
82
|
-
.eq(
|
|
83
|
-
.order(
|
|
82
|
+
.from("connected_accounts")
|
|
83
|
+
.select("*")
|
|
84
|
+
.eq("user_id", userId)
|
|
85
|
+
.order("created_at", { ascending: false });
|
|
84
86
|
|
|
85
87
|
if (error || !data) return [];
|
|
86
88
|
return data.map(this.mapToConnectedAccount);
|
|
@@ -89,17 +91,20 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
89
91
|
/**
|
|
90
92
|
* STEP 5: Find connected account by provider credentials
|
|
91
93
|
* Used for authentication - matches (provider, provider_account_id)
|
|
92
|
-
*
|
|
94
|
+
*
|
|
93
95
|
* @param provider - Provider name (e.g., 'clerk', 'google')
|
|
94
96
|
* @param providerAccountId - Provider's user ID
|
|
95
97
|
* @returns Promise resolving to ConnectedAccount or null if not found
|
|
96
98
|
*/
|
|
97
|
-
async findByProvider(
|
|
99
|
+
async findByProvider(
|
|
100
|
+
provider: string,
|
|
101
|
+
providerAccountId: string,
|
|
102
|
+
): Promise<ConnectedAccount | null> {
|
|
98
103
|
const { data, error } = await this.supabase
|
|
99
|
-
.from(
|
|
100
|
-
.select(
|
|
101
|
-
.eq(
|
|
102
|
-
.eq(
|
|
104
|
+
.from("connected_accounts")
|
|
105
|
+
.select("*")
|
|
106
|
+
.eq("provider", provider)
|
|
107
|
+
.eq("provider_account_id", providerAccountId)
|
|
103
108
|
.single();
|
|
104
109
|
|
|
105
110
|
if (error || !data) return null;
|
|
@@ -109,28 +114,35 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
109
114
|
/**
|
|
110
115
|
* Find connected account by provider and ID (alias for compatibility)
|
|
111
116
|
*/
|
|
112
|
-
async findByProviderAndId(
|
|
117
|
+
async findByProviderAndId(
|
|
118
|
+
provider: string,
|
|
119
|
+
providerAccountId: string,
|
|
120
|
+
): Promise<ConnectedAccount | null> {
|
|
113
121
|
return this.findByProvider(provider, providerAccountId);
|
|
114
122
|
}
|
|
115
123
|
|
|
116
124
|
/**
|
|
117
125
|
* Update tokens for connected account
|
|
118
126
|
*/
|
|
119
|
-
async updateTokens(
|
|
127
|
+
async updateTokens(
|
|
128
|
+
accountId: string,
|
|
129
|
+
accessToken: string,
|
|
130
|
+
refreshToken?: string,
|
|
131
|
+
): Promise<void> {
|
|
120
132
|
const updateData = {
|
|
121
133
|
access_token_encrypted: accessToken,
|
|
122
134
|
updated_at: new Date(),
|
|
123
|
-
refresh_token_encrypted:refreshToken
|
|
135
|
+
refresh_token_encrypted: refreshToken,
|
|
124
136
|
};
|
|
125
|
-
|
|
137
|
+
|
|
126
138
|
if (refreshToken) {
|
|
127
139
|
updateData.refresh_token_encrypted = refreshToken;
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
const { error } = await this.supabase
|
|
131
|
-
.from(
|
|
143
|
+
.from("connected_accounts")
|
|
132
144
|
.update(updateData)
|
|
133
|
-
.eq(
|
|
145
|
+
.eq("id", accountId);
|
|
134
146
|
|
|
135
147
|
if (error) throw new Error(`Failed to update tokens: ${error.message}`);
|
|
136
148
|
}
|
|
@@ -138,39 +150,44 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
138
150
|
/**
|
|
139
151
|
* STEP 6: Update connected account
|
|
140
152
|
* Updates tokens, profile info, or metadata
|
|
141
|
-
*
|
|
153
|
+
*
|
|
142
154
|
* @param id - Connected account UUID
|
|
143
155
|
* @param data - Partial update data
|
|
144
156
|
* @returns Promise resolving to updated ConnectedAccount
|
|
145
157
|
* @throws Error if update fails
|
|
146
158
|
*/
|
|
147
|
-
async update(
|
|
159
|
+
async update(
|
|
160
|
+
id: string,
|
|
161
|
+
data: UpdateConnectedAccountData,
|
|
162
|
+
): Promise<ConnectedAccount> {
|
|
148
163
|
const updateData = this.buildUpdateData(data);
|
|
149
164
|
|
|
150
165
|
const { data: accountData, error } = await this.supabase
|
|
151
|
-
.from(
|
|
166
|
+
.from("connected_accounts")
|
|
152
167
|
.update(updateData)
|
|
153
|
-
.eq(
|
|
168
|
+
.eq("id", id)
|
|
154
169
|
.select()
|
|
155
170
|
.single();
|
|
156
171
|
|
|
157
|
-
if (error || !accountData)
|
|
172
|
+
if (error || !accountData)
|
|
173
|
+
throw new Error(`Failed to update connected account: ${error?.message}`);
|
|
158
174
|
return this.mapToConnectedAccount(accountData);
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
/**
|
|
162
178
|
* STEP 7: Delete connected account (unlink provider)
|
|
163
|
-
*
|
|
179
|
+
*
|
|
164
180
|
* @param id - Connected account UUID
|
|
165
181
|
* @throws Error if deletion fails
|
|
166
182
|
*/
|
|
167
183
|
async delete(id: string): Promise<void> {
|
|
168
184
|
const { error } = await this.supabase
|
|
169
|
-
.from(
|
|
185
|
+
.from("connected_accounts")
|
|
170
186
|
.delete()
|
|
171
|
-
.eq(
|
|
187
|
+
.eq("id", id);
|
|
172
188
|
|
|
173
|
-
if (error)
|
|
189
|
+
if (error)
|
|
190
|
+
throw new Error(`Failed to delete connected account: ${error.message}`);
|
|
174
191
|
}
|
|
175
192
|
|
|
176
193
|
/**
|
|
@@ -181,11 +198,11 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
181
198
|
*/
|
|
182
199
|
async findPrimary(userId: string): Promise<ConnectedAccount | null> {
|
|
183
200
|
const { data, error } = await this.supabase
|
|
184
|
-
.from(
|
|
185
|
-
.select(
|
|
186
|
-
.eq(
|
|
187
|
-
.eq(
|
|
188
|
-
.eq(
|
|
201
|
+
.from("connected_accounts")
|
|
202
|
+
.select("*")
|
|
203
|
+
.eq("user_id", userId)
|
|
204
|
+
.eq("is_primary", true)
|
|
205
|
+
.eq("is_active", true)
|
|
189
206
|
.single();
|
|
190
207
|
|
|
191
208
|
if (error || !data) return null;
|
|
@@ -201,18 +218,19 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
201
218
|
async setPrimary(userId: string, accountId: string): Promise<void> {
|
|
202
219
|
// First, unset all primary accounts for user
|
|
203
220
|
await this.supabase
|
|
204
|
-
.from(
|
|
221
|
+
.from("connected_accounts")
|
|
205
222
|
.update({ is_primary: false, updated_at: new Date() })
|
|
206
|
-
.eq(
|
|
223
|
+
.eq("user_id", userId);
|
|
207
224
|
|
|
208
225
|
// Then set the specified account as primary
|
|
209
226
|
const { error } = await this.supabase
|
|
210
|
-
.from(
|
|
227
|
+
.from("connected_accounts")
|
|
211
228
|
.update({ is_primary: true, updated_at: new Date() })
|
|
212
|
-
.eq(
|
|
213
|
-
.eq(
|
|
229
|
+
.eq("id", accountId)
|
|
230
|
+
.eq("user_id", userId);
|
|
214
231
|
|
|
215
|
-
if (error)
|
|
232
|
+
if (error)
|
|
233
|
+
throw new Error(`Failed to set primary account: ${error.message}`);
|
|
216
234
|
}
|
|
217
235
|
|
|
218
236
|
/**
|
|
@@ -223,10 +241,14 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
223
241
|
* @param providerData - Provider account data
|
|
224
242
|
* @returns Created connected account
|
|
225
243
|
*/
|
|
226
|
-
async linkAccount(
|
|
244
|
+
async linkAccount(
|
|
245
|
+
userId: string,
|
|
246
|
+
provider: string,
|
|
247
|
+
providerData: ConnectedAccount,
|
|
248
|
+
): Promise<ConnectedAccount> {
|
|
227
249
|
const accountData = {
|
|
228
250
|
user_id: userId,
|
|
229
|
-
provider_type: providerData.providerType ??
|
|
251
|
+
provider_type: providerData.providerType ?? "OAUTH",
|
|
230
252
|
provider,
|
|
231
253
|
provider_account_id: providerData.providerAccountId,
|
|
232
254
|
provider_email: providerData.providerEmail,
|
|
@@ -239,22 +261,23 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
239
261
|
is_active: true,
|
|
240
262
|
linked_at: new Date(),
|
|
241
263
|
created_at: new Date(),
|
|
242
|
-
updated_at: new Date()
|
|
264
|
+
updated_at: new Date(),
|
|
243
265
|
};
|
|
244
266
|
|
|
245
267
|
const { data, error } = await this.supabase
|
|
246
|
-
.from(
|
|
268
|
+
.from("connected_accounts")
|
|
247
269
|
.insert(accountData)
|
|
248
270
|
.select()
|
|
249
271
|
.single();
|
|
250
272
|
|
|
251
|
-
if (error || !data)
|
|
252
|
-
|
|
273
|
+
if (error || !data)
|
|
274
|
+
throw new Error(`Failed to link account: ${error?.message}`);
|
|
275
|
+
|
|
253
276
|
const connectedAccount = this.mapToConnectedAccount(data);
|
|
254
|
-
|
|
277
|
+
|
|
255
278
|
// Emit account linked event
|
|
256
279
|
this.emitAccountLinkedEvent(userId, provider, connectedAccount.id);
|
|
257
|
-
|
|
280
|
+
|
|
258
281
|
return connectedAccount;
|
|
259
282
|
}
|
|
260
283
|
|
|
@@ -267,18 +290,18 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
267
290
|
// Get account details
|
|
268
291
|
const account = await this.findById(accountId);
|
|
269
292
|
if (!account) {
|
|
270
|
-
throw new Error(
|
|
293
|
+
throw new Error("Connected account not found");
|
|
271
294
|
}
|
|
272
295
|
|
|
273
296
|
// Check if this is the last authentication method
|
|
274
297
|
const userAccounts = await this.findByUserId(account.userId);
|
|
275
298
|
if (userAccounts.length <= 1) {
|
|
276
|
-
throw new Error(
|
|
299
|
+
throw new Error("Cannot unlink the last authentication method");
|
|
277
300
|
}
|
|
278
301
|
|
|
279
302
|
// Delete the account
|
|
280
303
|
await this.delete(accountId);
|
|
281
|
-
|
|
304
|
+
|
|
282
305
|
// Emit account unlinked event
|
|
283
306
|
this.emitAccountUnlinkedEvent(account.userId, account.provider, accountId);
|
|
284
307
|
}
|
|
@@ -290,13 +313,17 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
290
313
|
* @param accountId - Account identifier
|
|
291
314
|
* @private
|
|
292
315
|
*/
|
|
293
|
-
private emitAccountLinkedEvent(
|
|
316
|
+
private emitAccountLinkedEvent(
|
|
317
|
+
userId: string,
|
|
318
|
+
provider: string,
|
|
319
|
+
accountId: string,
|
|
320
|
+
): void {
|
|
294
321
|
// Mock event emission - in real implementation would use event system
|
|
295
|
-
globalThis.console.log(
|
|
322
|
+
globalThis.console.log("Event: auth.account.linked", {
|
|
296
323
|
userId,
|
|
297
324
|
provider,
|
|
298
325
|
accountId,
|
|
299
|
-
timestamp: new Date()
|
|
326
|
+
timestamp: new Date(),
|
|
300
327
|
});
|
|
301
328
|
}
|
|
302
329
|
|
|
@@ -307,13 +334,17 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
307
334
|
* @param accountId - Account identifier
|
|
308
335
|
* @private
|
|
309
336
|
*/
|
|
310
|
-
private emitAccountUnlinkedEvent(
|
|
337
|
+
private emitAccountUnlinkedEvent(
|
|
338
|
+
userId: string,
|
|
339
|
+
provider: string,
|
|
340
|
+
accountId: string,
|
|
341
|
+
): void {
|
|
311
342
|
// Mock event emission - in real implementation would use event system
|
|
312
|
-
globalThis.console.log(
|
|
343
|
+
globalThis.console.log("Event: auth.account.unlinked", {
|
|
313
344
|
userId,
|
|
314
345
|
provider,
|
|
315
346
|
accountId,
|
|
316
|
-
timestamp: new Date()
|
|
347
|
+
timestamp: new Date(),
|
|
317
348
|
});
|
|
318
349
|
}
|
|
319
350
|
|
|
@@ -321,7 +352,12 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
321
352
|
* Transform camelCase DTO to snake_case database format
|
|
322
353
|
* @private
|
|
323
354
|
*/
|
|
324
|
-
private transformToDbFormat(
|
|
355
|
+
private transformToDbFormat(
|
|
356
|
+
data: CreateConnectedAccountData,
|
|
357
|
+
): Record<
|
|
358
|
+
string,
|
|
359
|
+
string | undefined | Record<string, unknown> | Date | boolean
|
|
360
|
+
> {
|
|
325
361
|
return {
|
|
326
362
|
user_id: data.userId,
|
|
327
363
|
provider_type: data.providerType,
|
|
@@ -351,9 +387,13 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
351
387
|
* Build update data with only defined fields
|
|
352
388
|
* @private
|
|
353
389
|
*/
|
|
354
|
-
private buildUpdateData(
|
|
355
|
-
|
|
356
|
-
|
|
390
|
+
private buildUpdateData(
|
|
391
|
+
data: UpdateConnectedAccountData,
|
|
392
|
+
): Record<string, string> {
|
|
393
|
+
const result: Record<string, string> = {
|
|
394
|
+
updated_at: new Date().toString(),
|
|
395
|
+
};
|
|
396
|
+
|
|
357
397
|
// Only include defined fields
|
|
358
398
|
Object.entries(data).forEach(([key, value]) => {
|
|
359
399
|
if (value !== undefined) {
|
|
@@ -361,7 +401,7 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
361
401
|
result[dbKey] = value;
|
|
362
402
|
}
|
|
363
403
|
});
|
|
364
|
-
|
|
404
|
+
|
|
365
405
|
return result;
|
|
366
406
|
}
|
|
367
407
|
|
|
@@ -370,13 +410,13 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
370
410
|
* @private
|
|
371
411
|
*/
|
|
372
412
|
private camelToSnake(str: string): string {
|
|
373
|
-
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
413
|
+
return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
|
|
374
414
|
}
|
|
375
415
|
|
|
376
416
|
/**
|
|
377
417
|
* STEP 8: Map database row to ConnectedAccount interface
|
|
378
418
|
* Converts snake_case to camelCase
|
|
379
|
-
*
|
|
419
|
+
*
|
|
380
420
|
* @param data - Raw database row
|
|
381
421
|
* @returns Mapped ConnectedAccount object
|
|
382
422
|
* @private
|
|
@@ -398,7 +438,9 @@ export class ConnectedAccountRepository implements IConnectedAccountRepository {
|
|
|
398
438
|
chainId: data.chainId,
|
|
399
439
|
accessTokenEncrypted: data.accessTokenEncrypted,
|
|
400
440
|
refreshTokenEncrypted: data.refreshTokenEncrypted,
|
|
401
|
-
tokenExpiresAt: data.tokenExpiresAt
|
|
441
|
+
tokenExpiresAt: data.tokenExpiresAt
|
|
442
|
+
? new Date(data.tokenExpiresAt)
|
|
443
|
+
: undefined,
|
|
402
444
|
tokenScope: data.tokenScope,
|
|
403
445
|
isPrimary: data.isPrimary,
|
|
404
446
|
isVerified: data.isVerified,
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Role hierarchy manager for @plyaz/auth
|
|
3
3
|
* @module @plyaz/auth/rbac/role-hierarchy
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
5
|
* @description
|
|
6
6
|
* Manages role hierarchy and inheritance for role-based access control.
|
|
7
7
|
* Handles role precedence, permission inheritance, and hierarchical
|
|
8
8
|
* role validation. Enables complex organizational structures with
|
|
9
9
|
* inherited permissions and role-based delegation.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
11
|
* @example
|
|
12
12
|
* ```typescript
|
|
13
13
|
* import { RoleHierarchy } from '@plyaz/auth';
|
|
14
|
-
*
|
|
14
|
+
*
|
|
15
15
|
* const hierarchy = new RoleHierarchy();
|
|
16
16
|
* hierarchy.addRole({ id: 'admin', name: 'Admin', permissions: ['*'] });
|
|
17
17
|
* hierarchy.addRole({ id: 'moderator', name: 'Moderator', permissions: ['read', 'write'] });
|
|
18
18
|
* hierarchy.addRoleRelationship('moderator', 'admin');
|
|
19
|
-
*
|
|
19
|
+
*
|
|
20
20
|
* const hasAdminAccess = hierarchy.hasPermission('moderator', 'delete');
|
|
21
21
|
* console.log(hierarchy.getHierarchyTree());
|
|
22
22
|
* ```
|
|
@@ -29,9 +29,9 @@ import type { Role, RoleHierarchyConfig, RoleNode } from "@plyaz/types";
|
|
|
29
29
|
* Manages role relationships and permission inheritance
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
interface AddRole extends Role {
|
|
33
|
+
permissions?: [];
|
|
34
|
+
}
|
|
35
35
|
|
|
36
36
|
export class RoleHierarchy {
|
|
37
37
|
private readonly config: RoleHierarchyConfig;
|
|
@@ -44,7 +44,7 @@ export class RoleHierarchy {
|
|
|
44
44
|
maxDepth: 10,
|
|
45
45
|
detectCircular: true,
|
|
46
46
|
cacheInheritance: true,
|
|
47
|
-
...config
|
|
47
|
+
...config,
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -57,11 +57,11 @@ export class RoleHierarchy {
|
|
|
57
57
|
role,
|
|
58
58
|
parents: new Set(),
|
|
59
59
|
children: new Set(),
|
|
60
|
-
permissions: new Set(role.permissions ?? [])
|
|
60
|
+
permissions: new Set(role.permissions ?? []),
|
|
61
61
|
};
|
|
62
62
|
|
|
63
63
|
this.roles.set(role.id, roleNode);
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
// Clear cache when hierarchy changes
|
|
66
66
|
if (this.config.cacheInheritance) {
|
|
67
67
|
this.hierarchyCache.clear();
|
|
@@ -73,7 +73,7 @@ export class RoleHierarchy {
|
|
|
73
73
|
*/
|
|
74
74
|
removeRole(roleId: string): void {
|
|
75
75
|
const roleNode = this.roles.get(roleId);
|
|
76
|
-
|
|
76
|
+
|
|
77
77
|
if (!roleNode) {
|
|
78
78
|
return;
|
|
79
79
|
}
|
|
@@ -94,7 +94,7 @@ export class RoleHierarchy {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
this.roles.delete(roleId);
|
|
97
|
-
|
|
97
|
+
|
|
98
98
|
// Clear cache when hierarchy changes
|
|
99
99
|
if (this.config.cacheInheritance) {
|
|
100
100
|
this.hierarchyCache.clear();
|
|
@@ -109,18 +109,23 @@ export class RoleHierarchy {
|
|
|
109
109
|
const parentRole = this.roles.get(parentRoleId);
|
|
110
110
|
|
|
111
111
|
if (!childRole || !parentRole) {
|
|
112
|
-
throw new Error(
|
|
112
|
+
throw new Error("Role not found");
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// Check for circular dependencies
|
|
116
|
-
if (
|
|
117
|
-
|
|
116
|
+
if (
|
|
117
|
+
this.config.detectCircular &&
|
|
118
|
+
this.inheritsFrom(parentRoleId, childRoleId)
|
|
119
|
+
) {
|
|
120
|
+
throw new Error("Adding relationship would create circular dependency");
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
// Check hierarchy depth
|
|
121
124
|
const depth = this.calculateDepth(parentRoleId);
|
|
122
125
|
if (depth >= this.config.maxDepth) {
|
|
123
|
-
throw new Error(
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Maximum hierarchy depth exceeded: ${depth}/${this.config.maxDepth}`,
|
|
128
|
+
);
|
|
124
129
|
}
|
|
125
130
|
|
|
126
131
|
// Add relationship
|
|
@@ -290,7 +295,6 @@ export class RoleHierarchy {
|
|
|
290
295
|
return effectivePermissions;
|
|
291
296
|
}
|
|
292
297
|
|
|
293
|
-
|
|
294
298
|
/**
|
|
295
299
|
* Validate hierarchy integrity
|
|
296
300
|
*/
|
|
@@ -313,7 +317,9 @@ export class RoleHierarchy {
|
|
|
313
317
|
for (const roleId of this.roles.keys()) {
|
|
314
318
|
const depth = this.calculateDepth(roleId);
|
|
315
319
|
if (depth > this.config.maxDepth) {
|
|
316
|
-
issues.push(
|
|
320
|
+
issues.push(
|
|
321
|
+
`Role ${roleId} exceeds maximum hierarchy depth: ${depth}/${this.config.maxDepth}`,
|
|
322
|
+
);
|
|
317
323
|
}
|
|
318
324
|
}
|
|
319
325
|
|
|
@@ -321,20 +327,24 @@ export class RoleHierarchy {
|
|
|
321
327
|
for (const [roleId, roleNode] of this.roles.entries()) {
|
|
322
328
|
for (const parentId of roleNode.parents) {
|
|
323
329
|
if (!this.roles.has(parentId)) {
|
|
324
|
-
issues.push(
|
|
330
|
+
issues.push(
|
|
331
|
+
`Role ${roleId} references non-existent parent: ${parentId}`,
|
|
332
|
+
);
|
|
325
333
|
}
|
|
326
334
|
}
|
|
327
335
|
|
|
328
336
|
for (const childId of roleNode.children) {
|
|
329
337
|
if (!this.roles.has(childId)) {
|
|
330
|
-
issues.push(
|
|
338
|
+
issues.push(
|
|
339
|
+
`Role ${roleId} references non-existent child: ${childId}`,
|
|
340
|
+
);
|
|
331
341
|
}
|
|
332
342
|
}
|
|
333
343
|
}
|
|
334
344
|
|
|
335
345
|
return {
|
|
336
346
|
valid: issues.length === 0,
|
|
337
|
-
issues
|
|
347
|
+
issues,
|
|
338
348
|
};
|
|
339
349
|
}
|
|
340
350
|
|
|
@@ -363,7 +373,7 @@ export class RoleHierarchy {
|
|
|
363
373
|
for (const [roleId, roleNode] of this.roles.entries()) {
|
|
364
374
|
if (roleNode.parents.size === 0) rootRoles++;
|
|
365
375
|
if (roleNode.children.size === 0) leafRoles++;
|
|
366
|
-
|
|
376
|
+
|
|
367
377
|
const depth = this.calculateDepth(roleId);
|
|
368
378
|
maxDepth = Math.max(maxDepth, depth);
|
|
369
379
|
totalPermissions += roleNode.permissions.size;
|
|
@@ -374,7 +384,10 @@ export class RoleHierarchy {
|
|
|
374
384
|
maxDepth,
|
|
375
385
|
rootRoles,
|
|
376
386
|
leafRoles,
|
|
377
|
-
avgPermissions:
|
|
387
|
+
avgPermissions:
|
|
388
|
+
this.roles.size > 0
|
|
389
|
+
? Math.round(totalPermissions / this.roles.size)
|
|
390
|
+
: 0,
|
|
378
391
|
};
|
|
379
392
|
}
|
|
380
393
|
|
|
@@ -461,7 +474,7 @@ export class RoleHierarchy {
|
|
|
461
474
|
private detectCircularRecursive(
|
|
462
475
|
roleId: string,
|
|
463
476
|
visited: Set<string>,
|
|
464
|
-
recursionStack: Set<string
|
|
477
|
+
recursionStack: Set<string>,
|
|
465
478
|
): boolean {
|
|
466
479
|
visited.add(roleId);
|
|
467
480
|
recursionStack.add(roleId);
|
|
@@ -496,7 +509,9 @@ export class RoleHierarchy {
|
|
|
496
509
|
/**
|
|
497
510
|
* Build role tree recursively
|
|
498
511
|
*/
|
|
499
|
-
private buildRoleTree(
|
|
512
|
+
private buildRoleTree(
|
|
513
|
+
roleId: string,
|
|
514
|
+
): { role: Role; children: Record<string, unknown> } | null {
|
|
500
515
|
const roleNode = this.roles.get(roleId);
|
|
501
516
|
if (!roleNode) {
|
|
502
517
|
return null;
|
|
@@ -504,7 +519,7 @@ export class RoleHierarchy {
|
|
|
504
519
|
|
|
505
520
|
const tree: { role: Role; children: Record<string, unknown> } = {
|
|
506
521
|
role: roleNode.role,
|
|
507
|
-
children: {}
|
|
522
|
+
children: {},
|
|
508
523
|
};
|
|
509
524
|
|
|
510
525
|
for (const childId of roleNode.children) {
|