@checkstack/auth-backend 0.1.0 → 0.2.1
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/CHANGELOG.md +103 -0
- package/drizzle/0004_lucky_power_man.sql +21 -0
- package/drizzle/meta/0004_snapshot.json +1050 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +1 -1
- package/src/index.ts +166 -162
- package/src/router.test.ts +11 -11
- package/src/router.ts +98 -98
- package/src/schema.ts +20 -20
- package/src/teams.test.ts +836 -81
- package/src/utils/user.test.ts +10 -10
- package/src/utils/user.ts +13 -13
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
} from "@checkstack/backend-api";
|
|
11
11
|
import {
|
|
12
12
|
pluginMetadata,
|
|
13
|
-
|
|
13
|
+
authAccessRules,
|
|
14
|
+
authAccess,
|
|
14
15
|
authContract,
|
|
15
16
|
authRoutes,
|
|
16
|
-
permissions,
|
|
17
17
|
} from "@checkstack/auth-common";
|
|
18
18
|
import { NotificationApi } from "@checkstack/notification-common";
|
|
19
19
|
import * as schema from "./schema";
|
|
@@ -47,145 +47,148 @@ export const betterAuthExtensionPoint =
|
|
|
47
47
|
);
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Sync
|
|
50
|
+
* Sync access rules to database and assign to admin role.
|
|
51
51
|
* @param fullSync - If true, also performs orphan cleanup and default role sync.
|
|
52
|
-
* Should only be true when syncing ALL
|
|
52
|
+
* Should only be true when syncing ALL access rules (not per-plugin hooks).
|
|
53
53
|
*/
|
|
54
|
-
async function
|
|
54
|
+
async function syncAccessRulesToDb({
|
|
55
55
|
database,
|
|
56
56
|
logger,
|
|
57
|
-
|
|
57
|
+
accessRules,
|
|
58
58
|
fullSync = false,
|
|
59
59
|
}: {
|
|
60
60
|
database: NodePgDatabase<typeof schema>;
|
|
61
61
|
logger: { debug: (msg: string) => void };
|
|
62
|
-
|
|
62
|
+
accessRules: {
|
|
63
63
|
id: string;
|
|
64
64
|
description?: string;
|
|
65
|
-
|
|
66
|
-
|
|
65
|
+
isDefault?: boolean;
|
|
66
|
+
isPublic?: boolean;
|
|
67
67
|
}[];
|
|
68
68
|
fullSync?: boolean;
|
|
69
69
|
}) {
|
|
70
|
-
logger.debug(`🔑 Syncing ${
|
|
71
|
-
|
|
72
|
-
for (const
|
|
70
|
+
logger.debug(`🔑 Syncing ${accessRules.length} access rules to database...`);
|
|
71
|
+
|
|
72
|
+
for (const rule of accessRules) {
|
|
73
|
+
// Map AccessRule fields to DB fields
|
|
74
|
+
const dbRecord = {
|
|
75
|
+
id: rule.id,
|
|
76
|
+
description: rule.description,
|
|
77
|
+
isAuthenticatedDefault: rule.isDefault,
|
|
78
|
+
isPublicDefault: rule.isPublic,
|
|
79
|
+
};
|
|
73
80
|
const existing = await database
|
|
74
81
|
.select()
|
|
75
|
-
.from(schema.
|
|
76
|
-
.where(eq(schema.
|
|
82
|
+
.from(schema.accessRule)
|
|
83
|
+
.where(eq(schema.accessRule.id, rule.id));
|
|
77
84
|
|
|
78
85
|
if (existing.length === 0) {
|
|
79
|
-
await database.insert(schema.
|
|
80
|
-
logger.debug(` -> Created
|
|
86
|
+
await database.insert(schema.accessRule).values(dbRecord);
|
|
87
|
+
logger.debug(` -> Created access rule: ${rule.id}`);
|
|
81
88
|
} else {
|
|
82
89
|
await database
|
|
83
|
-
.update(schema.
|
|
84
|
-
.set({ description:
|
|
85
|
-
.where(eq(schema.
|
|
90
|
+
.update(schema.accessRule)
|
|
91
|
+
.set({ description: rule.description })
|
|
92
|
+
.where(eq(schema.accessRule.id, rule.id));
|
|
86
93
|
}
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
// Assign all
|
|
90
|
-
const
|
|
96
|
+
// Assign all access rules to admin role
|
|
97
|
+
const adminRoleAccessRules = await database
|
|
91
98
|
.select()
|
|
92
|
-
.from(schema.
|
|
93
|
-
.where(eq(schema.
|
|
99
|
+
.from(schema.roleAccessRule)
|
|
100
|
+
.where(eq(schema.roleAccessRule.roleId, "admin"));
|
|
94
101
|
|
|
95
|
-
for (const
|
|
96
|
-
const
|
|
97
|
-
(rp) => rp.
|
|
102
|
+
for (const rule of accessRules) {
|
|
103
|
+
const hasAccess = adminRoleAccessRules.some(
|
|
104
|
+
(rp) => rp.accessRuleId === rule.id
|
|
98
105
|
);
|
|
99
106
|
|
|
100
|
-
if (!
|
|
107
|
+
if (!hasAccess) {
|
|
101
108
|
await database
|
|
102
|
-
.insert(schema.
|
|
109
|
+
.insert(schema.roleAccessRule)
|
|
103
110
|
.values({
|
|
104
111
|
roleId: "admin",
|
|
105
|
-
|
|
112
|
+
accessRuleId: rule.id,
|
|
106
113
|
})
|
|
107
114
|
.onConflictDoNothing();
|
|
108
|
-
logger.debug(` -> Assigned
|
|
115
|
+
logger.debug(` -> Assigned access rule ${rule.id} to admin role`);
|
|
109
116
|
}
|
|
110
117
|
}
|
|
111
118
|
|
|
112
119
|
// Only perform orphan cleanup and default sync when doing a full sync
|
|
113
|
-
// (i.e., when we have ALL
|
|
120
|
+
// (i.e., when we have ALL access rules, not just one plugin's access rules from a hook)
|
|
114
121
|
if (!fullSync) {
|
|
115
122
|
return;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
// Cleanup orphan
|
|
119
|
-
const registeredIds = new Set(
|
|
120
|
-
const
|
|
121
|
-
const
|
|
125
|
+
// Cleanup orphan access rules (no longer registered by any plugin)
|
|
126
|
+
const registeredIds = new Set(accessRules.map((r) => r.id));
|
|
127
|
+
const allDbAccessRules = await database.select().from(schema.accessRule);
|
|
128
|
+
const orphanAccessRules = allDbAccessRules.filter(
|
|
122
129
|
(p) => !registeredIds.has(p.id)
|
|
123
130
|
);
|
|
124
131
|
|
|
125
|
-
if (
|
|
132
|
+
if (orphanAccessRules.length > 0) {
|
|
126
133
|
logger.debug(
|
|
127
|
-
`🧹 Removing ${
|
|
134
|
+
`🧹 Removing ${orphanAccessRules.length} orphan access rule(s)...`
|
|
128
135
|
);
|
|
129
|
-
for (const orphan of
|
|
130
|
-
// Delete
|
|
136
|
+
for (const orphan of orphanAccessRules) {
|
|
137
|
+
// Delete role_access_rule entries first (FK doesn't cascade)
|
|
131
138
|
await database
|
|
132
|
-
.delete(schema.
|
|
133
|
-
.where(eq(schema.
|
|
134
|
-
// Then delete the
|
|
139
|
+
.delete(schema.roleAccessRule)
|
|
140
|
+
.where(eq(schema.roleAccessRule.accessRuleId, orphan.id));
|
|
141
|
+
// Then delete the access rule itself
|
|
135
142
|
await database
|
|
136
|
-
.delete(schema.
|
|
137
|
-
.where(eq(schema.
|
|
138
|
-
logger.debug(` -> Removed orphan
|
|
143
|
+
.delete(schema.accessRule)
|
|
144
|
+
.where(eq(schema.accessRule.id, orphan.id));
|
|
145
|
+
logger.debug(` -> Removed orphan access rule: ${orphan.id}`);
|
|
139
146
|
}
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
// Sync authenticated default
|
|
143
|
-
await
|
|
149
|
+
// Sync authenticated default access rules to users role
|
|
150
|
+
await syncAuthenticatedDefaultAccessRulesToUsersRole({
|
|
144
151
|
database,
|
|
145
152
|
logger,
|
|
146
|
-
|
|
153
|
+
accessRules,
|
|
147
154
|
});
|
|
148
155
|
|
|
149
|
-
// Sync public default
|
|
150
|
-
await
|
|
156
|
+
// Sync public default access rules to anonymous role
|
|
157
|
+
await syncPublicDefaultAccessRulesToAnonymousRole({
|
|
151
158
|
database,
|
|
152
159
|
logger,
|
|
153
|
-
|
|
160
|
+
accessRules,
|
|
154
161
|
});
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
/**
|
|
158
|
-
* Sync authenticated default
|
|
159
|
-
* Respects admin-disabled defaults stored in
|
|
165
|
+
* Sync authenticated default access rules (isAuthenticatedDefault=true) to the "users" role.
|
|
166
|
+
* Respects admin-disabled defaults stored in disabled_default_access_rule table.
|
|
160
167
|
*/
|
|
161
|
-
async function
|
|
168
|
+
async function syncAuthenticatedDefaultAccessRulesToUsersRole({
|
|
162
169
|
database,
|
|
163
170
|
logger,
|
|
164
|
-
|
|
171
|
+
accessRules,
|
|
165
172
|
}: {
|
|
166
173
|
database: NodePgDatabase<typeof schema>;
|
|
167
174
|
logger: { debug: (msg: string) => void };
|
|
168
|
-
|
|
175
|
+
accessRules: { id: string; isDefault?: boolean }[];
|
|
169
176
|
}) {
|
|
170
|
-
// Debug: log all
|
|
177
|
+
// Debug: log all access rules with their isDefault status
|
|
171
178
|
logger.debug(
|
|
172
|
-
`[DEBUG] All
|
|
179
|
+
`[DEBUG] All access rules received (${accessRules.length} total):`
|
|
173
180
|
);
|
|
174
|
-
for (const
|
|
175
|
-
logger.debug(
|
|
176
|
-
` -> ${p.id}: isAuthenticatedDefault=${p.isAuthenticatedDefault}`
|
|
177
|
-
);
|
|
181
|
+
for (const r of accessRules) {
|
|
182
|
+
logger.debug(` -> ${r.id}: isDefault=${r.isDefault}`);
|
|
178
183
|
}
|
|
179
184
|
|
|
180
|
-
const
|
|
181
|
-
(p) => p.isAuthenticatedDefault
|
|
182
|
-
);
|
|
185
|
+
const defaultRules = accessRules.filter((r) => r.isDefault);
|
|
183
186
|
logger.debug(
|
|
184
|
-
`👥 Found ${
|
|
187
|
+
`👥 Found ${defaultRules.length} authenticated default access rules to sync to users role`
|
|
185
188
|
);
|
|
186
|
-
if (
|
|
189
|
+
if (defaultRules.length === 0) {
|
|
187
190
|
logger.debug(
|
|
188
|
-
` -> No authenticated default
|
|
191
|
+
` -> No authenticated default access rules found, skipping sync`
|
|
189
192
|
);
|
|
190
193
|
return;
|
|
191
194
|
}
|
|
@@ -193,90 +196,90 @@ async function syncAuthenticatedDefaultPermissionsToUsersRole({
|
|
|
193
196
|
// Get already disabled defaults (admin has removed them)
|
|
194
197
|
const disabledDefaults = await database
|
|
195
198
|
.select()
|
|
196
|
-
.from(schema.
|
|
197
|
-
const disabledIds = new Set(disabledDefaults.map((d) => d.
|
|
199
|
+
.from(schema.disabledDefaultAccessRule);
|
|
200
|
+
const disabledIds = new Set(disabledDefaults.map((d) => d.accessRuleId));
|
|
198
201
|
|
|
199
|
-
// Get current users role
|
|
200
|
-
const
|
|
202
|
+
// Get current users role access rules
|
|
203
|
+
const usersRoleAccessRules = await database
|
|
201
204
|
.select()
|
|
202
|
-
.from(schema.
|
|
203
|
-
.where(eq(schema.
|
|
205
|
+
.from(schema.roleAccessRule)
|
|
206
|
+
.where(eq(schema.roleAccessRule.roleId, "users"));
|
|
204
207
|
|
|
205
|
-
for (const
|
|
208
|
+
for (const rule of defaultRules) {
|
|
206
209
|
// Skip if admin has disabled this default
|
|
207
|
-
if (disabledIds.has(
|
|
208
|
-
logger.debug(` -> Skipping disabled authenticated default: ${
|
|
210
|
+
if (disabledIds.has(rule.id)) {
|
|
211
|
+
logger.debug(` -> Skipping disabled authenticated default: ${rule.id}`);
|
|
209
212
|
continue;
|
|
210
213
|
}
|
|
211
214
|
|
|
212
|
-
const
|
|
213
|
-
(rp) => rp.
|
|
215
|
+
const hasAccess = usersRoleAccessRules.some(
|
|
216
|
+
(rp) => rp.accessRuleId === rule.id
|
|
214
217
|
);
|
|
215
218
|
|
|
216
|
-
if (!
|
|
217
|
-
await database.insert(schema.
|
|
219
|
+
if (!hasAccess) {
|
|
220
|
+
await database.insert(schema.roleAccessRule).values({
|
|
218
221
|
roleId: "users",
|
|
219
|
-
|
|
222
|
+
accessRuleId: rule.id,
|
|
220
223
|
});
|
|
221
224
|
logger.debug(
|
|
222
|
-
` -> Assigned authenticated default
|
|
225
|
+
` -> Assigned authenticated default access rule ${rule.id} to users role`
|
|
223
226
|
);
|
|
224
227
|
}
|
|
225
228
|
}
|
|
226
229
|
}
|
|
227
230
|
|
|
228
231
|
/**
|
|
229
|
-
* Sync public default
|
|
230
|
-
* Respects admin-disabled defaults stored in
|
|
232
|
+
* Sync public default access rules (isPublic=true) to the "anonymous" role.
|
|
233
|
+
* Respects admin-disabled defaults stored in disabled_public_default_access_rule table.
|
|
231
234
|
*/
|
|
232
|
-
async function
|
|
235
|
+
async function syncPublicDefaultAccessRulesToAnonymousRole({
|
|
233
236
|
database,
|
|
234
237
|
logger,
|
|
235
|
-
|
|
238
|
+
accessRules,
|
|
236
239
|
}: {
|
|
237
240
|
database: NodePgDatabase<typeof schema>;
|
|
238
241
|
logger: { debug: (msg: string) => void };
|
|
239
|
-
|
|
242
|
+
accessRules: { id: string; isPublic?: boolean }[];
|
|
240
243
|
}) {
|
|
241
|
-
const publicDefaults =
|
|
244
|
+
const publicDefaults = accessRules.filter((r) => r.isPublic);
|
|
242
245
|
logger.debug(
|
|
243
|
-
`🌐 Found ${publicDefaults.length} public default
|
|
246
|
+
`🌐 Found ${publicDefaults.length} public default access rules to sync to anonymous role`
|
|
244
247
|
);
|
|
245
248
|
if (publicDefaults.length === 0) {
|
|
246
|
-
logger.debug(` -> No public default
|
|
249
|
+
logger.debug(` -> No public default access rules found, skipping sync`);
|
|
247
250
|
return;
|
|
248
251
|
}
|
|
249
252
|
|
|
250
253
|
// Get already disabled public defaults (admin has removed them)
|
|
251
254
|
const disabledDefaults = await database
|
|
252
255
|
.select()
|
|
253
|
-
.from(schema.
|
|
254
|
-
const disabledIds = new Set(disabledDefaults.map((d) => d.
|
|
256
|
+
.from(schema.disabledPublicDefaultAccessRule);
|
|
257
|
+
const disabledIds = new Set(disabledDefaults.map((d) => d.accessRuleId));
|
|
255
258
|
|
|
256
|
-
// Get current anonymous role
|
|
257
|
-
const
|
|
259
|
+
// Get current anonymous role access rules
|
|
260
|
+
const anonymousRoleAccessRules = await database
|
|
258
261
|
.select()
|
|
259
|
-
.from(schema.
|
|
260
|
-
.where(eq(schema.
|
|
262
|
+
.from(schema.roleAccessRule)
|
|
263
|
+
.where(eq(schema.roleAccessRule.roleId, "anonymous"));
|
|
261
264
|
|
|
262
|
-
for (const
|
|
265
|
+
for (const rule of publicDefaults) {
|
|
263
266
|
// Skip if admin has disabled this public default
|
|
264
|
-
if (disabledIds.has(
|
|
265
|
-
logger.debug(` -> Skipping disabled public default: ${
|
|
267
|
+
if (disabledIds.has(rule.id)) {
|
|
268
|
+
logger.debug(` -> Skipping disabled public default: ${rule.id}`);
|
|
266
269
|
continue;
|
|
267
270
|
}
|
|
268
271
|
|
|
269
|
-
const
|
|
270
|
-
(rp) => rp.
|
|
272
|
+
const hasAccess = anonymousRoleAccessRules.some(
|
|
273
|
+
(rp) => rp.accessRuleId === rule.id
|
|
271
274
|
);
|
|
272
275
|
|
|
273
|
-
if (!
|
|
274
|
-
await database.insert(schema.
|
|
276
|
+
if (!hasAccess) {
|
|
277
|
+
await database.insert(schema.roleAccessRule).values({
|
|
275
278
|
roleId: "anonymous",
|
|
276
|
-
|
|
279
|
+
accessRuleId: rule.id,
|
|
277
280
|
});
|
|
278
281
|
logger.debug(
|
|
279
|
-
` -> Assigned public default
|
|
282
|
+
` -> Assigned public default access rule ${rule.id} to anonymous role`
|
|
280
283
|
);
|
|
281
284
|
}
|
|
282
285
|
}
|
|
@@ -295,15 +298,15 @@ export default createBackendPlugin({
|
|
|
295
298
|
getStrategies: () => strategies,
|
|
296
299
|
};
|
|
297
300
|
|
|
298
|
-
//
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
// Get all
|
|
302
|
-
return env.pluginManager.
|
|
301
|
+
// Access rule registry - gets all access rules from PluginManager
|
|
302
|
+
const accessRuleRegistry = {
|
|
303
|
+
getAccessRules: () => {
|
|
304
|
+
// Get all access rules from the central PluginManager registry
|
|
305
|
+
return env.pluginManager.getAllAccessRules();
|
|
303
306
|
},
|
|
304
307
|
};
|
|
305
308
|
|
|
306
|
-
env.
|
|
309
|
+
env.registerAccessRules(authAccessRules);
|
|
307
310
|
|
|
308
311
|
env.registerExtensionPoint(betterAuthExtensionPoint, {
|
|
309
312
|
addStrategy: (s) => {
|
|
@@ -321,7 +324,7 @@ export default createBackendPlugin({
|
|
|
321
324
|
},
|
|
322
325
|
});
|
|
323
326
|
|
|
324
|
-
// Helper to fetch
|
|
327
|
+
// Helper to fetch access rules
|
|
325
328
|
const enrichUserLocal = async (user: User) => {
|
|
326
329
|
if (!db) return user;
|
|
327
330
|
return enrichUser(user, db);
|
|
@@ -377,7 +380,7 @@ export default createBackendPlugin({
|
|
|
377
380
|
// Ignore errors from lastUsedAt update
|
|
378
381
|
});
|
|
379
382
|
|
|
380
|
-
// Fetch roles and compute
|
|
383
|
+
// Fetch roles and compute access rules for the application
|
|
381
384
|
const appRoles = await db
|
|
382
385
|
.select({ roleId: schema.applicationRole.roleId })
|
|
383
386
|
.from(schema.applicationRole)
|
|
@@ -387,18 +390,18 @@ export default createBackendPlugin({
|
|
|
387
390
|
|
|
388
391
|
const roleIds = appRoles.map((r) => r.roleId);
|
|
389
392
|
|
|
390
|
-
// Get
|
|
391
|
-
let
|
|
393
|
+
// Get access rules for these roles
|
|
394
|
+
let accessRulesArray: string[] = [];
|
|
392
395
|
if (roleIds.length > 0) {
|
|
393
396
|
const rolePerms = await db
|
|
394
397
|
.select({
|
|
395
|
-
|
|
398
|
+
accessRuleId: schema.roleAccessRule.accessRuleId,
|
|
396
399
|
})
|
|
397
|
-
.from(schema.
|
|
398
|
-
.where(inArray(schema.
|
|
400
|
+
.from(schema.roleAccessRule)
|
|
401
|
+
.where(inArray(schema.roleAccessRule.roleId, roleIds));
|
|
399
402
|
|
|
400
|
-
|
|
401
|
-
...new Set(rolePerms.map((rp) => rp.
|
|
403
|
+
accessRulesArray = [
|
|
404
|
+
...new Set(rolePerms.map((rp) => rp.accessRuleId)),
|
|
402
405
|
];
|
|
403
406
|
}
|
|
404
407
|
|
|
@@ -417,7 +420,7 @@ export default createBackendPlugin({
|
|
|
417
420
|
id: app.id,
|
|
418
421
|
name: app.name,
|
|
419
422
|
roles: roleIds,
|
|
420
|
-
|
|
423
|
+
accessRulesArray,
|
|
421
424
|
teamIds,
|
|
422
425
|
};
|
|
423
426
|
}
|
|
@@ -549,6 +552,7 @@ export default createBackendPlugin({
|
|
|
549
552
|
}),
|
|
550
553
|
emailAndPassword: {
|
|
551
554
|
enabled: credentialEnabled,
|
|
555
|
+
autoSignIn: true, // Log in user immediately after successful registration
|
|
552
556
|
disableSignUp: !registrationAllowed,
|
|
553
557
|
minPasswordLength: 8,
|
|
554
558
|
maxPasswordLength: 128,
|
|
@@ -634,7 +638,7 @@ export default createBackendPlugin({
|
|
|
634
638
|
logger.info("[auth-backend] ✅ Authentication reloaded successfully");
|
|
635
639
|
};
|
|
636
640
|
|
|
637
|
-
// IMPORTANT: Seed roles BEFORE syncing
|
|
641
|
+
// IMPORTANT: Seed roles BEFORE syncing access rules so default perms can be assigned
|
|
638
642
|
logger.debug("🌱 Checking for initial roles...");
|
|
639
643
|
const adminRole = await database
|
|
640
644
|
.select()
|
|
@@ -649,7 +653,7 @@ export default createBackendPlugin({
|
|
|
649
653
|
logger.info(" -> Created 'admin' role.");
|
|
650
654
|
}
|
|
651
655
|
|
|
652
|
-
// Seed "users" role for default
|
|
656
|
+
// Seed "users" role for default access rules
|
|
653
657
|
const usersRole = await database
|
|
654
658
|
.select()
|
|
655
659
|
.from(schema.role)
|
|
@@ -673,7 +677,7 @@ export default createBackendPlugin({
|
|
|
673
677
|
await database.insert(schema.role).values({
|
|
674
678
|
id: "anonymous",
|
|
675
679
|
name: "Anonymous Users",
|
|
676
|
-
description: "
|
|
680
|
+
description: "Access rules for unauthenticated (anonymous) users",
|
|
677
681
|
isSystem: true,
|
|
678
682
|
});
|
|
679
683
|
logger.info(" -> Created 'anonymous' role.");
|
|
@@ -694,7 +698,7 @@ export default createBackendPlugin({
|
|
|
694
698
|
logger.info(" -> Created 'applications' role.");
|
|
695
699
|
}
|
|
696
700
|
|
|
697
|
-
// Note:
|
|
701
|
+
// Note: Access rule sync happens in afterPluginsReady (when all plugins have registered)
|
|
698
702
|
|
|
699
703
|
// 4. Register oRPC router
|
|
700
704
|
const authRouter = createAuthRouter(
|
|
@@ -702,7 +706,7 @@ export default createBackendPlugin({
|
|
|
702
706
|
strategyRegistry,
|
|
703
707
|
reloadAuth,
|
|
704
708
|
config,
|
|
705
|
-
|
|
709
|
+
accessRuleRegistry
|
|
706
710
|
);
|
|
707
711
|
rpc.registerRouter(authRouter, authContract);
|
|
708
712
|
|
|
@@ -766,7 +770,7 @@ export default createBackendPlugin({
|
|
|
766
770
|
iconName: "Users",
|
|
767
771
|
shortcuts: ["meta+shift+u", "ctrl+shift+u"],
|
|
768
772
|
route: resolveRoute(authRoutes.routes.settings) + "?tab=users",
|
|
769
|
-
|
|
773
|
+
requiredAccessRules: [authAccess.users.read],
|
|
770
774
|
},
|
|
771
775
|
{
|
|
772
776
|
id: "createUser",
|
|
@@ -776,15 +780,15 @@ export default createBackendPlugin({
|
|
|
776
780
|
route:
|
|
777
781
|
resolveRoute(authRoutes.routes.settings) +
|
|
778
782
|
"?tab=users&action=create",
|
|
779
|
-
|
|
783
|
+
requiredAccessRules: [authAccess.users.create],
|
|
780
784
|
},
|
|
781
785
|
{
|
|
782
786
|
id: "roles",
|
|
783
787
|
title: "Manage Roles",
|
|
784
|
-
subtitle: "Manage roles and
|
|
788
|
+
subtitle: "Manage roles and access rules",
|
|
785
789
|
iconName: "Shield",
|
|
786
790
|
route: resolveRoute(authRoutes.routes.settings) + "?tab=roles",
|
|
787
|
-
|
|
791
|
+
requiredAccessRules: [authAccess.roles.read],
|
|
788
792
|
},
|
|
789
793
|
{
|
|
790
794
|
id: "applications",
|
|
@@ -793,82 +797,82 @@ export default createBackendPlugin({
|
|
|
793
797
|
iconName: "Key",
|
|
794
798
|
route:
|
|
795
799
|
resolveRoute(authRoutes.routes.settings) + "?tab=applications",
|
|
796
|
-
|
|
800
|
+
requiredAccessRules: [authAccess.applications],
|
|
797
801
|
},
|
|
798
802
|
],
|
|
799
803
|
});
|
|
800
804
|
|
|
801
805
|
logger.debug("✅ Auth Backend initialized.");
|
|
802
806
|
},
|
|
803
|
-
// Phase 3: After all plugins are ready - sync all
|
|
807
|
+
// Phase 3: After all plugins are ready - sync all access rules including defaults
|
|
804
808
|
afterPluginsReady: async ({ database, logger, onHook }) => {
|
|
805
|
-
// Now that all plugins are ready, sync
|
|
809
|
+
// Now that all plugins are ready, sync access rules including defaults
|
|
806
810
|
// This is critical because during init, other plugins haven't registered yet
|
|
807
|
-
const
|
|
811
|
+
const allAccessRules = accessRuleRegistry.getAccessRules();
|
|
808
812
|
logger.debug(
|
|
809
|
-
`[auth-backend] afterPluginsReady: syncing ${
|
|
813
|
+
`[auth-backend] afterPluginsReady: syncing ${allAccessRules.length} access rules from all plugins`
|
|
810
814
|
);
|
|
811
|
-
await
|
|
815
|
+
await syncAccessRulesToDb({
|
|
812
816
|
database: database as NodePgDatabase<typeof schema>,
|
|
813
817
|
logger,
|
|
814
|
-
|
|
818
|
+
accessRules: allAccessRules,
|
|
815
819
|
fullSync: true,
|
|
816
820
|
});
|
|
817
821
|
|
|
818
|
-
// Subscribe to
|
|
819
|
-
// This syncs new
|
|
822
|
+
// Subscribe to access rule registration hook for future registrations
|
|
823
|
+
// This syncs new access rules when other plugins register them dynamically
|
|
820
824
|
onHook(
|
|
821
|
-
coreHooks.
|
|
822
|
-
async ({
|
|
823
|
-
await
|
|
825
|
+
coreHooks.accessRulesRegistered,
|
|
826
|
+
async ({ accessRules }) => {
|
|
827
|
+
await syncAccessRulesToDb({
|
|
824
828
|
database: database as NodePgDatabase<typeof schema>,
|
|
825
829
|
logger,
|
|
826
|
-
|
|
830
|
+
accessRules,
|
|
827
831
|
});
|
|
828
832
|
},
|
|
829
833
|
{
|
|
830
834
|
mode: "work-queue",
|
|
831
|
-
workerGroup: "
|
|
835
|
+
workerGroup: "access-rule-db-sync",
|
|
832
836
|
maxRetries: 5,
|
|
833
837
|
}
|
|
834
838
|
);
|
|
835
839
|
|
|
836
|
-
// Subscribe to plugin deregistered hook for
|
|
837
|
-
// When a plugin is removed at runtime, delete its
|
|
840
|
+
// Subscribe to plugin deregistered hook for access rule cleanup
|
|
841
|
+
// When a plugin is removed at runtime, delete its access rules from DB
|
|
838
842
|
onHook(
|
|
839
843
|
coreHooks.pluginDeregistered,
|
|
840
844
|
async ({ pluginId }) => {
|
|
841
845
|
logger.debug(
|
|
842
|
-
`[auth-backend] Cleaning up
|
|
846
|
+
`[auth-backend] Cleaning up access rules for deregistered plugin: ${pluginId}`
|
|
843
847
|
);
|
|
844
848
|
|
|
845
|
-
// Delete all
|
|
846
|
-
const
|
|
849
|
+
// Delete all access rules with this plugin's prefix
|
|
850
|
+
const allDbAccessRules = await database
|
|
847
851
|
.select()
|
|
848
|
-
.from(schema.
|
|
849
|
-
const
|
|
852
|
+
.from(schema.accessRule);
|
|
853
|
+
const pluginAccessRules = allDbAccessRules.filter((p) =>
|
|
850
854
|
p.id.startsWith(`${pluginId}.`)
|
|
851
855
|
);
|
|
852
856
|
|
|
853
|
-
for (const perm of
|
|
854
|
-
// Delete
|
|
857
|
+
for (const perm of pluginAccessRules) {
|
|
858
|
+
// Delete role_access_rule entries first
|
|
855
859
|
await database
|
|
856
|
-
.delete(schema.
|
|
857
|
-
.where(eq(schema.
|
|
858
|
-
// Then delete the
|
|
860
|
+
.delete(schema.roleAccessRule)
|
|
861
|
+
.where(eq(schema.roleAccessRule.accessRuleId, perm.id));
|
|
862
|
+
// Then delete the access rule itself
|
|
859
863
|
await database
|
|
860
|
-
.delete(schema.
|
|
861
|
-
.where(eq(schema.
|
|
862
|
-
logger.debug(` -> Removed
|
|
864
|
+
.delete(schema.accessRule)
|
|
865
|
+
.where(eq(schema.accessRule.id, perm.id));
|
|
866
|
+
logger.debug(` -> Removed access rule: ${perm.id}`);
|
|
863
867
|
}
|
|
864
868
|
|
|
865
869
|
logger.debug(
|
|
866
|
-
`[auth-backend] Cleaned up ${
|
|
870
|
+
`[auth-backend] Cleaned up ${pluginAccessRules.length} access rules for ${pluginId}`
|
|
867
871
|
);
|
|
868
872
|
},
|
|
869
873
|
{
|
|
870
874
|
mode: "work-queue",
|
|
871
|
-
workerGroup: "
|
|
875
|
+
workerGroup: "access-rule-cleanup",
|
|
872
876
|
maxRetries: 3,
|
|
873
877
|
}
|
|
874
878
|
);
|