@checkstack/auth-backend 0.0.3 → 0.2.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/CHANGELOG.md +174 -0
- package/drizzle/0002_lowly_squirrel_girl.sql +43 -0
- package/drizzle/0003_tranquil_sally_floyd.sql +8 -0
- package/drizzle/0004_lucky_power_man.sql +21 -0
- package/drizzle/meta/0002_snapshot.json +1017 -0
- package/drizzle/meta/0003_snapshot.json +1050 -0
- package/drizzle/meta/0004_snapshot.json +1050 -0
- package/drizzle/meta/_journal.json +21 -0
- package/package.json +1 -1
- package/src/index.ts +176 -162
- package/src/router.test.ts +11 -11
- package/src/router.ts +525 -90
- package/src/schema.ts +125 -18
- package/src/teams.test.ts +1985 -0
- package/src/utils/user.test.ts +65 -46
- package/src/utils/user.ts +21 -13
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,28 +390,38 @@ 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
|
|
|
408
|
+
// Get team memberships for this application
|
|
409
|
+
const appTeams = await db
|
|
410
|
+
.select({ teamId: schema.applicationTeam.teamId })
|
|
411
|
+
.from(schema.applicationTeam)
|
|
412
|
+
.where(
|
|
413
|
+
eq(schema.applicationTeam.applicationId, applicationId)
|
|
414
|
+
);
|
|
415
|
+
const teamIds = appTeams.map((t) => t.teamId);
|
|
416
|
+
|
|
405
417
|
// Return ApplicationUser
|
|
406
418
|
return {
|
|
407
419
|
type: "application" as const,
|
|
408
420
|
id: app.id,
|
|
409
421
|
name: app.name,
|
|
410
422
|
roles: roleIds,
|
|
411
|
-
|
|
423
|
+
accessRulesArray,
|
|
424
|
+
teamIds,
|
|
412
425
|
};
|
|
413
426
|
}
|
|
414
427
|
}
|
|
@@ -539,6 +552,7 @@ export default createBackendPlugin({
|
|
|
539
552
|
}),
|
|
540
553
|
emailAndPassword: {
|
|
541
554
|
enabled: credentialEnabled,
|
|
555
|
+
autoSignIn: true, // Log in user immediately after successful registration
|
|
542
556
|
disableSignUp: !registrationAllowed,
|
|
543
557
|
minPasswordLength: 8,
|
|
544
558
|
maxPasswordLength: 128,
|
|
@@ -624,7 +638,7 @@ export default createBackendPlugin({
|
|
|
624
638
|
logger.info("[auth-backend] ✅ Authentication reloaded successfully");
|
|
625
639
|
};
|
|
626
640
|
|
|
627
|
-
// IMPORTANT: Seed roles BEFORE syncing
|
|
641
|
+
// IMPORTANT: Seed roles BEFORE syncing access rules so default perms can be assigned
|
|
628
642
|
logger.debug("🌱 Checking for initial roles...");
|
|
629
643
|
const adminRole = await database
|
|
630
644
|
.select()
|
|
@@ -639,7 +653,7 @@ export default createBackendPlugin({
|
|
|
639
653
|
logger.info(" -> Created 'admin' role.");
|
|
640
654
|
}
|
|
641
655
|
|
|
642
|
-
// Seed "users" role for default
|
|
656
|
+
// Seed "users" role for default access rules
|
|
643
657
|
const usersRole = await database
|
|
644
658
|
.select()
|
|
645
659
|
.from(schema.role)
|
|
@@ -663,7 +677,7 @@ export default createBackendPlugin({
|
|
|
663
677
|
await database.insert(schema.role).values({
|
|
664
678
|
id: "anonymous",
|
|
665
679
|
name: "Anonymous Users",
|
|
666
|
-
description: "
|
|
680
|
+
description: "Access rules for unauthenticated (anonymous) users",
|
|
667
681
|
isSystem: true,
|
|
668
682
|
});
|
|
669
683
|
logger.info(" -> Created 'anonymous' role.");
|
|
@@ -684,7 +698,7 @@ export default createBackendPlugin({
|
|
|
684
698
|
logger.info(" -> Created 'applications' role.");
|
|
685
699
|
}
|
|
686
700
|
|
|
687
|
-
// Note:
|
|
701
|
+
// Note: Access rule sync happens in afterPluginsReady (when all plugins have registered)
|
|
688
702
|
|
|
689
703
|
// 4. Register oRPC router
|
|
690
704
|
const authRouter = createAuthRouter(
|
|
@@ -692,7 +706,7 @@ export default createBackendPlugin({
|
|
|
692
706
|
strategyRegistry,
|
|
693
707
|
reloadAuth,
|
|
694
708
|
config,
|
|
695
|
-
|
|
709
|
+
accessRuleRegistry
|
|
696
710
|
);
|
|
697
711
|
rpc.registerRouter(authRouter, authContract);
|
|
698
712
|
|
|
@@ -756,7 +770,7 @@ export default createBackendPlugin({
|
|
|
756
770
|
iconName: "Users",
|
|
757
771
|
shortcuts: ["meta+shift+u", "ctrl+shift+u"],
|
|
758
772
|
route: resolveRoute(authRoutes.routes.settings) + "?tab=users",
|
|
759
|
-
|
|
773
|
+
requiredAccessRules: [authAccess.users.read],
|
|
760
774
|
},
|
|
761
775
|
{
|
|
762
776
|
id: "createUser",
|
|
@@ -766,15 +780,15 @@ export default createBackendPlugin({
|
|
|
766
780
|
route:
|
|
767
781
|
resolveRoute(authRoutes.routes.settings) +
|
|
768
782
|
"?tab=users&action=create",
|
|
769
|
-
|
|
783
|
+
requiredAccessRules: [authAccess.users.create],
|
|
770
784
|
},
|
|
771
785
|
{
|
|
772
786
|
id: "roles",
|
|
773
787
|
title: "Manage Roles",
|
|
774
|
-
subtitle: "Manage roles and
|
|
788
|
+
subtitle: "Manage roles and access rules",
|
|
775
789
|
iconName: "Shield",
|
|
776
790
|
route: resolveRoute(authRoutes.routes.settings) + "?tab=roles",
|
|
777
|
-
|
|
791
|
+
requiredAccessRules: [authAccess.roles.read],
|
|
778
792
|
},
|
|
779
793
|
{
|
|
780
794
|
id: "applications",
|
|
@@ -783,82 +797,82 @@ export default createBackendPlugin({
|
|
|
783
797
|
iconName: "Key",
|
|
784
798
|
route:
|
|
785
799
|
resolveRoute(authRoutes.routes.settings) + "?tab=applications",
|
|
786
|
-
|
|
800
|
+
requiredAccessRules: [authAccess.applications],
|
|
787
801
|
},
|
|
788
802
|
],
|
|
789
803
|
});
|
|
790
804
|
|
|
791
805
|
logger.debug("✅ Auth Backend initialized.");
|
|
792
806
|
},
|
|
793
|
-
// Phase 3: After all plugins are ready - sync all
|
|
807
|
+
// Phase 3: After all plugins are ready - sync all access rules including defaults
|
|
794
808
|
afterPluginsReady: async ({ database, logger, onHook }) => {
|
|
795
|
-
// Now that all plugins are ready, sync
|
|
809
|
+
// Now that all plugins are ready, sync access rules including defaults
|
|
796
810
|
// This is critical because during init, other plugins haven't registered yet
|
|
797
|
-
const
|
|
811
|
+
const allAccessRules = accessRuleRegistry.getAccessRules();
|
|
798
812
|
logger.debug(
|
|
799
|
-
`[auth-backend] afterPluginsReady: syncing ${
|
|
813
|
+
`[auth-backend] afterPluginsReady: syncing ${allAccessRules.length} access rules from all plugins`
|
|
800
814
|
);
|
|
801
|
-
await
|
|
815
|
+
await syncAccessRulesToDb({
|
|
802
816
|
database: database as NodePgDatabase<typeof schema>,
|
|
803
817
|
logger,
|
|
804
|
-
|
|
818
|
+
accessRules: allAccessRules,
|
|
805
819
|
fullSync: true,
|
|
806
820
|
});
|
|
807
821
|
|
|
808
|
-
// Subscribe to
|
|
809
|
-
// 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
|
|
810
824
|
onHook(
|
|
811
|
-
coreHooks.
|
|
812
|
-
async ({
|
|
813
|
-
await
|
|
825
|
+
coreHooks.accessRulesRegistered,
|
|
826
|
+
async ({ accessRules }) => {
|
|
827
|
+
await syncAccessRulesToDb({
|
|
814
828
|
database: database as NodePgDatabase<typeof schema>,
|
|
815
829
|
logger,
|
|
816
|
-
|
|
830
|
+
accessRules,
|
|
817
831
|
});
|
|
818
832
|
},
|
|
819
833
|
{
|
|
820
834
|
mode: "work-queue",
|
|
821
|
-
workerGroup: "
|
|
835
|
+
workerGroup: "access-rule-db-sync",
|
|
822
836
|
maxRetries: 5,
|
|
823
837
|
}
|
|
824
838
|
);
|
|
825
839
|
|
|
826
|
-
// Subscribe to plugin deregistered hook for
|
|
827
|
-
// 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
|
|
828
842
|
onHook(
|
|
829
843
|
coreHooks.pluginDeregistered,
|
|
830
844
|
async ({ pluginId }) => {
|
|
831
845
|
logger.debug(
|
|
832
|
-
`[auth-backend] Cleaning up
|
|
846
|
+
`[auth-backend] Cleaning up access rules for deregistered plugin: ${pluginId}`
|
|
833
847
|
);
|
|
834
848
|
|
|
835
|
-
// Delete all
|
|
836
|
-
const
|
|
849
|
+
// Delete all access rules with this plugin's prefix
|
|
850
|
+
const allDbAccessRules = await database
|
|
837
851
|
.select()
|
|
838
|
-
.from(schema.
|
|
839
|
-
const
|
|
852
|
+
.from(schema.accessRule);
|
|
853
|
+
const pluginAccessRules = allDbAccessRules.filter((p) =>
|
|
840
854
|
p.id.startsWith(`${pluginId}.`)
|
|
841
855
|
);
|
|
842
856
|
|
|
843
|
-
for (const perm of
|
|
844
|
-
// Delete
|
|
857
|
+
for (const perm of pluginAccessRules) {
|
|
858
|
+
// Delete role_access_rule entries first
|
|
845
859
|
await database
|
|
846
|
-
.delete(schema.
|
|
847
|
-
.where(eq(schema.
|
|
848
|
-
// Then delete the
|
|
860
|
+
.delete(schema.roleAccessRule)
|
|
861
|
+
.where(eq(schema.roleAccessRule.accessRuleId, perm.id));
|
|
862
|
+
// Then delete the access rule itself
|
|
849
863
|
await database
|
|
850
|
-
.delete(schema.
|
|
851
|
-
.where(eq(schema.
|
|
852
|
-
logger.debug(` -> Removed
|
|
864
|
+
.delete(schema.accessRule)
|
|
865
|
+
.where(eq(schema.accessRule.id, perm.id));
|
|
866
|
+
logger.debug(` -> Removed access rule: ${perm.id}`);
|
|
853
867
|
}
|
|
854
868
|
|
|
855
869
|
logger.debug(
|
|
856
|
-
`[auth-backend] Cleaned up ${
|
|
870
|
+
`[auth-backend] Cleaned up ${pluginAccessRules.length} access rules for ${pluginId}`
|
|
857
871
|
);
|
|
858
872
|
},
|
|
859
873
|
{
|
|
860
874
|
mode: "work-queue",
|
|
861
|
-
workerGroup: "
|
|
875
|
+
workerGroup: "access-rule-cleanup",
|
|
862
876
|
maxRetries: 3,
|
|
863
877
|
}
|
|
864
878
|
);
|