@rebasepro/server-core 0.2.4 → 0.2.5
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/dist/common/src/collections/default-collections.d.ts +5 -8
- package/dist/index.es.js +150 -169
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +150 -169
- package/dist/index.umd.js.map +1 -1
- package/dist/types/src/controllers/auth.d.ts +2 -2
- package/dist/types/src/controllers/client.d.ts +25 -40
- package/dist/types/src/controllers/data.d.ts +4 -0
- package/dist/types/src/controllers/data_driver.d.ts +5 -0
- package/dist/types/src/types/auth_adapter.d.ts +3 -56
- package/dist/types/src/types/backend.d.ts +2 -2
- package/dist/types/src/types/backend_hooks.d.ts +2 -17
- package/dist/types/src/types/properties.d.ts +7 -5
- package/dist/types/src/types/user_management_delegate.d.ts +16 -53
- package/dist/types/src/users/index.d.ts +0 -1
- package/dist/types/src/users/user.d.ts +0 -1
- package/package.json +5 -5
- package/src/auth/admin-routes.ts +2 -91
- package/src/auth/builtin-auth-adapter.ts +5 -55
- package/src/auth/custom-auth-adapter.ts +1 -1
- package/src/init.ts +19 -1
- package/test/admin-routes.test.ts +0 -169
- package/test/backend-hooks-admin.test.ts +0 -25
- package/test/custom-auth-adapter.test.ts +2 -10
- package/dist/types/src/users/roles.d.ts +0 -14
|
@@ -15,13 +15,10 @@ import type {
|
|
|
15
15
|
AuthenticatedUser,
|
|
16
16
|
AuthAdapterCapabilities,
|
|
17
17
|
UserManagementAdapter,
|
|
18
|
-
RoleManagementAdapter,
|
|
19
18
|
AuthUserListOptions,
|
|
20
19
|
AuthUserListResult,
|
|
21
20
|
AuthUserData,
|
|
22
21
|
AuthCreateUserData,
|
|
23
|
-
AuthRoleData,
|
|
24
|
-
AuthCreateRoleData,
|
|
25
22
|
BootstrappedAuth,
|
|
26
23
|
BackendHooks,
|
|
27
24
|
} from "@rebasepro/types";
|
|
@@ -131,8 +128,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
131
128
|
// Resolve roles from the repository
|
|
132
129
|
let roles: string[] = payload.roles || [];
|
|
133
130
|
try {
|
|
134
|
-
|
|
135
|
-
roles = userRoles.map((r) => r.id);
|
|
131
|
+
roles = await authRepository.getUserRoleIds(payload.userId);
|
|
136
132
|
} catch {
|
|
137
133
|
// Fall back to token roles if repository lookup fails
|
|
138
134
|
}
|
|
@@ -174,8 +170,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
174
170
|
|
|
175
171
|
let roles: string[] = payload.roles || [];
|
|
176
172
|
try {
|
|
177
|
-
|
|
178
|
-
roles = userRoles.map((r) => r.id);
|
|
173
|
+
roles = await authRepository.getUserRoleIds(payload.userId);
|
|
179
174
|
} catch {
|
|
180
175
|
// Fall back to token roles if repository lookup fails
|
|
181
176
|
}
|
|
@@ -194,7 +189,7 @@ export function createBuiltinAuthAdapter(config: BuiltinAuthAdapterConfig): Auth
|
|
|
194
189
|
|
|
195
190
|
userManagement: createUserManagementFromRepo(authRepository, resolvedOps, authHooks),
|
|
196
191
|
|
|
197
|
-
|
|
192
|
+
|
|
198
193
|
|
|
199
194
|
createAuthRoutes(): Hono<HonoEnv> | undefined {
|
|
200
195
|
return createAuthRoutes({
|
|
@@ -327,9 +322,8 @@ function createUserManagementFromRepo(repo: AuthRepository, resolvedOps: Resolve
|
|
|
327
322
|
}
|
|
328
323
|
},
|
|
329
324
|
|
|
330
|
-
async getUserRoles(userId: string): Promise<
|
|
331
|
-
|
|
332
|
-
return roles.map(toAuthRoleData);
|
|
325
|
+
async getUserRoles(userId: string): Promise<string[]> {
|
|
326
|
+
return repo.getUserRoleIds(userId);
|
|
333
327
|
},
|
|
334
328
|
|
|
335
329
|
async setUserRoles(userId: string, roleIds: string[]): Promise<void> {
|
|
@@ -338,40 +332,6 @@ function createUserManagementFromRepo(repo: AuthRepository, resolvedOps: Resolve
|
|
|
338
332
|
};
|
|
339
333
|
}
|
|
340
334
|
|
|
341
|
-
function createRoleManagementFromRepo(repo: AuthRepository): RoleManagementAdapter {
|
|
342
|
-
return {
|
|
343
|
-
async listRoles(): Promise<AuthRoleData[]> {
|
|
344
|
-
const roles = await repo.listRoles();
|
|
345
|
-
return roles.map(toAuthRoleData);
|
|
346
|
-
},
|
|
347
|
-
|
|
348
|
-
async getRoleById(id: string): Promise<AuthRoleData | null> {
|
|
349
|
-
const role = await repo.getRoleById(id);
|
|
350
|
-
return role ? toAuthRoleData(role) : null;
|
|
351
|
-
},
|
|
352
|
-
|
|
353
|
-
async createRole(data: AuthCreateRoleData): Promise<AuthRoleData> {
|
|
354
|
-
const role = await repo.createRole({
|
|
355
|
-
id: data.id,
|
|
356
|
-
name: data.name,
|
|
357
|
-
isAdmin: data.isAdmin,
|
|
358
|
-
defaultPermissions: data.defaultPermissions,
|
|
359
|
-
collectionPermissions: data.collectionPermissions,
|
|
360
|
-
});
|
|
361
|
-
return toAuthRoleData(role);
|
|
362
|
-
},
|
|
363
|
-
|
|
364
|
-
async updateRole(id: string, data: Partial<AuthRoleData>): Promise<AuthRoleData | null> {
|
|
365
|
-
const role = await repo.updateRole(id, data);
|
|
366
|
-
return role ? toAuthRoleData(role) : null;
|
|
367
|
-
},
|
|
368
|
-
|
|
369
|
-
async deleteRole(id: string): Promise<void> {
|
|
370
|
-
await repo.deleteRole(id);
|
|
371
|
-
},
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
|
|
375
335
|
function toAuthUserData(user: { id: string; email: string; displayName?: string | null; photoUrl?: string | null; emailVerified?: boolean; metadata?: Record<string, unknown>; createdAt?: Date; updatedAt?: Date }): AuthUserData {
|
|
376
336
|
return {
|
|
377
337
|
id: user.id,
|
|
@@ -384,13 +344,3 @@ function toAuthUserData(user: { id: string; email: string; displayName?: string
|
|
|
384
344
|
updatedAt: user.updatedAt,
|
|
385
345
|
};
|
|
386
346
|
}
|
|
387
|
-
|
|
388
|
-
function toAuthRoleData(role: { id: string; name: string; isAdmin: boolean; defaultPermissions?: unknown; collectionPermissions?: unknown }): AuthRoleData {
|
|
389
|
-
return {
|
|
390
|
-
id: role.id,
|
|
391
|
-
name: role.name,
|
|
392
|
-
isAdmin: role.isAdmin,
|
|
393
|
-
defaultPermissions: role.defaultPermissions as AuthRoleData["defaultPermissions"],
|
|
394
|
-
collectionPermissions: role.collectionPermissions as AuthRoleData["collectionPermissions"],
|
|
395
|
-
};
|
|
396
|
-
}
|
package/src/init.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { createApiKeyRoutes } from "./auth/api-keys/api-key-routes";
|
|
|
23
23
|
import type { ApiKeyStore } from "./auth/api-keys/api-key-store";
|
|
24
24
|
import { createApiKeyRateLimiter } from "./auth/rate-limiter";
|
|
25
25
|
import { createRebaseClient } from "@rebasepro/client";
|
|
26
|
+
import { defaultUsersCollection } from "@rebasepro/common";
|
|
26
27
|
import { createHistoryRoutes } from "./history";
|
|
27
28
|
import { EmailConfig, createEmailService } from "./email";
|
|
28
29
|
import type { EmailService } from "./email";
|
|
@@ -292,6 +293,13 @@ async function _initializeRebaseBackend(config: RebaseBackendConfig): Promise<Re
|
|
|
292
293
|
dir: config.collectionsDir });
|
|
293
294
|
}
|
|
294
295
|
|
|
296
|
+
if (config.auth) {
|
|
297
|
+
// Prepend defaultUsersCollection if not overridden by the developer's collections
|
|
298
|
+
activeCollections = Array.from(
|
|
299
|
+
new Map([defaultUsersCollection, ...activeCollections].map(c => [c.slug, c])).values()
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
295
303
|
const realtimeServices: Record<string, RealtimeProvider> = {};
|
|
296
304
|
const delegates: Record<string, DataDriver> = {};
|
|
297
305
|
|
|
@@ -378,7 +386,6 @@ collectionRegistry });
|
|
|
378
386
|
// (the return type still exposes `auth?: BootstrappedAuth`)
|
|
379
387
|
authConfigResult = {
|
|
380
388
|
userService: authAdapter.userManagement ?? {},
|
|
381
|
-
roleService: authAdapter.roleManagement ?? {},
|
|
382
389
|
};
|
|
383
390
|
} else {
|
|
384
391
|
// ── RebaseAuthConfig — wrap in built-in adapter ──
|
|
@@ -792,6 +799,17 @@ collectionRegistry });
|
|
|
792
799
|
logger.info("Email service attached to singleton", { configured: emailService.isConfigured() });
|
|
793
800
|
}
|
|
794
801
|
|
|
802
|
+
// Attach raw SQL capability when the driver supports it (Postgres, MySQL).
|
|
803
|
+
// Document databases (MongoDB, Firestore) won't have this.
|
|
804
|
+
const driverAdmin = defaultBootstrapper.getAdmin?.(defaultDriverResult);
|
|
805
|
+
if (isSQLAdmin(driverAdmin)) {
|
|
806
|
+
Object.assign(serverClient, {
|
|
807
|
+
sql: (query: string, options?: { database?: string; role?: string }) =>
|
|
808
|
+
driverAdmin.executeSql(query, options)
|
|
809
|
+
});
|
|
810
|
+
logger.info("SQL capability attached to singleton");
|
|
811
|
+
}
|
|
812
|
+
|
|
795
813
|
_initRebase(serverClient);
|
|
796
814
|
logger.info("Rebase singleton initialized");
|
|
797
815
|
|
|
@@ -540,174 +540,5 @@ displayName: "Updated" }),
|
|
|
540
540
|
});
|
|
541
541
|
});
|
|
542
542
|
|
|
543
|
-
// ── Role CRUD ───────────────────────────────────────────────────────
|
|
544
|
-
describe("Role Management", () => {
|
|
545
|
-
describe("GET /admin/roles", () => {
|
|
546
|
-
it("returns list of roles", async () => {
|
|
547
|
-
const app = createApp();
|
|
548
|
-
mockAuthRepo.listRoles.mockResolvedValueOnce([
|
|
549
|
-
mockRole("admin", true),
|
|
550
|
-
mockRole("editor"),
|
|
551
|
-
mockRole("viewer")
|
|
552
|
-
]);
|
|
553
|
-
|
|
554
|
-
const res = await app.request("/admin/roles", { headers: { ...adminAuth() } });
|
|
555
|
-
expect(res.status).toBe(200);
|
|
556
|
-
const body = await res.json() as any;
|
|
557
|
-
expect(body.roles).toHaveLength(3);
|
|
558
|
-
expect(body.roles[0].isAdmin).toBe(true);
|
|
559
|
-
});
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
describe("GET /admin/roles/:roleId", () => {
|
|
563
|
-
it("returns role by ID", async () => {
|
|
564
|
-
const app = createApp();
|
|
565
|
-
mockAuthRepo.getRoleById.mockResolvedValueOnce(mockRole("admin", true));
|
|
566
|
-
|
|
567
|
-
const res = await app.request("/admin/roles/admin", { headers: { ...adminAuth() } });
|
|
568
|
-
expect(res.status).toBe(200);
|
|
569
|
-
const body = await res.json() as any;
|
|
570
|
-
expect(body.role.id).toBe("admin");
|
|
571
|
-
expect(body.role.isAdmin).toBe(true);
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
it("returns 404 for non-existent role", async () => {
|
|
575
|
-
const app = createApp();
|
|
576
|
-
mockAuthRepo.getRoleById.mockResolvedValueOnce(null);
|
|
577
|
-
|
|
578
|
-
const res = await app.request("/admin/roles/missing", { headers: { ...adminAuth() } });
|
|
579
|
-
expect(res.status).toBe(404);
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
describe("POST /admin/roles", () => {
|
|
584
|
-
it("creates a new role", async () => {
|
|
585
|
-
const app = createApp();
|
|
586
|
-
|
|
587
|
-
const res = await app.request("/admin/roles", {
|
|
588
|
-
...json({ id: "custom",
|
|
589
|
-
name: "Custom Role" }),
|
|
590
|
-
headers: { ...json({}).headers,
|
|
591
|
-
...adminAuth() }
|
|
592
|
-
});
|
|
593
|
-
expect(res.status).toBe(201);
|
|
594
|
-
const body = await res.json() as any;
|
|
595
|
-
expect(body.role.id).toBe("custom");
|
|
596
|
-
});
|
|
597
543
|
|
|
598
|
-
it("returns 400 for missing id or name", async () => {
|
|
599
|
-
const app = createApp();
|
|
600
|
-
|
|
601
|
-
const res = await app.request("/admin/roles", {
|
|
602
|
-
...json({ id: "nope" }),
|
|
603
|
-
headers: { ...json({}).headers,
|
|
604
|
-
...adminAuth() }
|
|
605
|
-
});
|
|
606
|
-
expect(res.status).toBe(400);
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
it("returns 409 when role already exists", async () => {
|
|
610
|
-
const app = createApp();
|
|
611
|
-
mockAuthRepo.getRoleById.mockResolvedValueOnce(mockRole("custom"));
|
|
612
|
-
|
|
613
|
-
const res = await app.request("/admin/roles", {
|
|
614
|
-
...json({ id: "custom",
|
|
615
|
-
name: "Dup" }),
|
|
616
|
-
headers: { ...json({}).headers,
|
|
617
|
-
...adminAuth() }
|
|
618
|
-
});
|
|
619
|
-
expect(res.status).toBe(409);
|
|
620
|
-
const body = await res.json() as any;
|
|
621
|
-
expect(body.error.code).toBe("ROLE_EXISTS");
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
describe("PUT /admin/roles/:roleId", () => {
|
|
626
|
-
it("updates an existing role", async () => {
|
|
627
|
-
const app = createApp();
|
|
628
|
-
mockAuthRepo.getRoleById.mockResolvedValueOnce(mockRole("editor"));
|
|
629
|
-
|
|
630
|
-
const res = await app.request("/admin/roles/editor", {
|
|
631
|
-
method: "PUT",
|
|
632
|
-
headers: { "Content-Type": "application/json",
|
|
633
|
-
...adminAuth() },
|
|
634
|
-
body: JSON.stringify({ name: "Super Editor" })
|
|
635
|
-
});
|
|
636
|
-
expect(res.status).toBe(200);
|
|
637
|
-
expect(mockAuthRepo.updateRole).toHaveBeenCalledWith("editor", expect.objectContaining({
|
|
638
|
-
name: "Super Editor"
|
|
639
|
-
}));
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
it("returns 404 for non-existent role", async () => {
|
|
643
|
-
const app = createApp();
|
|
644
|
-
mockAuthRepo.getRoleById.mockResolvedValueOnce(null);
|
|
645
|
-
|
|
646
|
-
const res = await app.request("/admin/roles/missing", {
|
|
647
|
-
method: "PUT",
|
|
648
|
-
headers: { "Content-Type": "application/json",
|
|
649
|
-
...adminAuth() },
|
|
650
|
-
body: JSON.stringify({ name: "Nope" })
|
|
651
|
-
});
|
|
652
|
-
expect(res.status).toBe(404);
|
|
653
|
-
});
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
describe("DELETE /admin/roles/:roleId", () => {
|
|
657
|
-
it("deletes a custom role", async () => {
|
|
658
|
-
const app = createApp();
|
|
659
|
-
mockAuthRepo.getRoleById.mockResolvedValueOnce(mockRole("custom"));
|
|
660
|
-
|
|
661
|
-
const res = await app.request("/admin/roles/custom", {
|
|
662
|
-
method: "DELETE",
|
|
663
|
-
headers: { ...adminAuth() }
|
|
664
|
-
});
|
|
665
|
-
expect(res.status).toBe(200);
|
|
666
|
-
expect(mockAuthRepo.deleteRole).toHaveBeenCalledWith("custom");
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it("prevents deletion of built-in admin role", async () => {
|
|
670
|
-
const app = createApp();
|
|
671
|
-
|
|
672
|
-
const res = await app.request("/admin/roles/admin", {
|
|
673
|
-
method: "DELETE",
|
|
674
|
-
headers: { ...adminAuth() }
|
|
675
|
-
});
|
|
676
|
-
expect(res.status).toBe(400);
|
|
677
|
-
const body = await res.json() as any;
|
|
678
|
-
expect(body.error.code).toBe("BUILTIN_ROLE");
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
it("prevents deletion of built-in editor role", async () => {
|
|
682
|
-
const app = createApp();
|
|
683
|
-
|
|
684
|
-
const res = await app.request("/admin/roles/editor", {
|
|
685
|
-
method: "DELETE",
|
|
686
|
-
headers: { ...adminAuth() }
|
|
687
|
-
});
|
|
688
|
-
expect(res.status).toBe(400);
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
it("prevents deletion of built-in viewer role", async () => {
|
|
692
|
-
const app = createApp();
|
|
693
|
-
|
|
694
|
-
const res = await app.request("/admin/roles/viewer", {
|
|
695
|
-
method: "DELETE",
|
|
696
|
-
headers: { ...adminAuth() }
|
|
697
|
-
});
|
|
698
|
-
expect(res.status).toBe(400);
|
|
699
|
-
});
|
|
700
|
-
|
|
701
|
-
it("returns 404 for non-existent role", async () => {
|
|
702
|
-
const app = createApp();
|
|
703
|
-
mockAuthRepo.getRoleById.mockResolvedValueOnce(null);
|
|
704
|
-
|
|
705
|
-
const res = await app.request("/admin/roles/ghost", {
|
|
706
|
-
method: "DELETE",
|
|
707
|
-
headers: { ...adminAuth() }
|
|
708
|
-
});
|
|
709
|
-
expect(res.status).toBe(404);
|
|
710
|
-
});
|
|
711
|
-
});
|
|
712
|
-
});
|
|
713
544
|
});
|
|
@@ -362,32 +362,7 @@ describe("BackendHooks — Admin Routes", () => {
|
|
|
362
362
|
});
|
|
363
363
|
});
|
|
364
364
|
|
|
365
|
-
// ── roles.afterRead ─────────────────────────────────────────────────
|
|
366
|
-
describe("roles.afterRead", () => {
|
|
367
|
-
it("filters out roles from GET /admin/roles", async () => {
|
|
368
|
-
const hooks: BackendHooks = {
|
|
369
|
-
roles: {
|
|
370
|
-
afterRead(role) {
|
|
371
|
-
// Hide internal roles
|
|
372
|
-
if (role.id === "internal") return null;
|
|
373
|
-
return role;
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
};
|
|
377
|
-
const app = createApp(hooks);
|
|
378
|
-
mockAuthRepo.listRoles.mockResolvedValueOnce([
|
|
379
|
-
mockRole("admin", true),
|
|
380
|
-
mockRole("internal"),
|
|
381
|
-
mockRole("editor")
|
|
382
|
-
]);
|
|
383
365
|
|
|
384
|
-
const res = await app.request("/admin/roles", { headers: { ...adminAuth() } });
|
|
385
|
-
expect(res.status).toBe(200);
|
|
386
|
-
const body = await res.json() as any;
|
|
387
|
-
expect(body.roles).toHaveLength(2);
|
|
388
|
-
expect(body.roles.map((r: any) => r.id)).toEqual(["admin", "editor"]);
|
|
389
|
-
});
|
|
390
|
-
});
|
|
391
366
|
|
|
392
367
|
// ── no hooks (passthrough) ──────────────────────────────────────────
|
|
393
368
|
describe("no hooks configured", () => {
|
|
@@ -143,7 +143,7 @@ describe("createCustomAuthAdapter", () => {
|
|
|
143
143
|
expect(adapter.serviceKey).toBe("sk_live_123");
|
|
144
144
|
});
|
|
145
145
|
|
|
146
|
-
it("passes through userManagement
|
|
146
|
+
it("passes through userManagement when provided", () => {
|
|
147
147
|
const userMgmt = {
|
|
148
148
|
getUser: jest.fn(),
|
|
149
149
|
listUsers: jest.fn(),
|
|
@@ -151,27 +151,19 @@ describe("createCustomAuthAdapter", () => {
|
|
|
151
151
|
updateUser: jest.fn(),
|
|
152
152
|
deleteUser: jest.fn(),
|
|
153
153
|
};
|
|
154
|
-
const roleMgmt = {
|
|
155
|
-
listRoles: jest.fn(),
|
|
156
|
-
getUserRoles: jest.fn(),
|
|
157
|
-
setUserRoles: jest.fn(),
|
|
158
|
-
};
|
|
159
154
|
|
|
160
155
|
const adapter = createCustomAuthAdapter({
|
|
161
156
|
verifyRequest: async () => null,
|
|
162
157
|
userManagement: userMgmt as unknown as CustomAuthAdapterOptions["userManagement"],
|
|
163
|
-
roleManagement: roleMgmt as unknown as CustomAuthAdapterOptions["roleManagement"],
|
|
164
158
|
});
|
|
165
159
|
|
|
166
160
|
expect(adapter.userManagement).toBe(userMgmt);
|
|
167
|
-
expect(adapter.roleManagement).toBe(roleMgmt);
|
|
168
161
|
});
|
|
169
162
|
|
|
170
|
-
it("omits userManagement
|
|
163
|
+
it("omits userManagement when not provided", () => {
|
|
171
164
|
const adapter = createCustomAuthAdapter({
|
|
172
165
|
verifyRequest: async () => null,
|
|
173
166
|
});
|
|
174
167
|
expect(adapter.userManagement).toBeUndefined();
|
|
175
|
-
expect(adapter.roleManagement).toBeUndefined();
|
|
176
168
|
});
|
|
177
169
|
});
|