@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/utils/user.test.ts
CHANGED
|
@@ -3,37 +3,38 @@ import { enrichUser } from "./user";
|
|
|
3
3
|
import { User } from "better-auth/types";
|
|
4
4
|
|
|
5
5
|
// Mock Drizzle DB
|
|
6
|
-
const createMockDb = (data: {
|
|
7
|
-
|
|
6
|
+
const createMockDb = (data: {
|
|
7
|
+
roles?: unknown[];
|
|
8
|
+
accessRules?: unknown[];
|
|
9
|
+
teams?: unknown[];
|
|
10
|
+
}) => {
|
|
11
|
+
const mockDb: unknown = {
|
|
8
12
|
select: mock(() => mockDb),
|
|
9
13
|
from: mock(() => mockDb),
|
|
10
14
|
innerJoin: mock(() => mockDb),
|
|
11
15
|
where: mock(() => mockDb),
|
|
12
16
|
};
|
|
13
17
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
// We need to look at the schema name or some identifier.
|
|
22
|
-
// Since we are mocking the schema as well, we can check equality.
|
|
23
|
-
// However, for a simple mock, we can just alternate or use a counter.
|
|
24
|
-
|
|
25
|
-
// In enrichUser:
|
|
26
|
-
// 1. select from userRole (inner join role)
|
|
27
|
-
// 2. select from rolePermission (inner join permission) -> for each role
|
|
28
|
-
|
|
29
|
-
// Let's use a simpler approach: track call count for this specific mock instance
|
|
30
|
-
if (!mockDb._callCount) mockDb._callCount = 0;
|
|
31
|
-
mockDb._callCount++;
|
|
18
|
+
// Track call count for sequential responses
|
|
19
|
+
// Call order in enrichUser: 1=roles, 2+=access rules per role, final=teams
|
|
20
|
+
let callCount = 0;
|
|
21
|
+
const nonAdminRoles = (data.roles || []).filter(
|
|
22
|
+
(r) => (r as { roleId: string }).roleId !== "admin"
|
|
23
|
+
);
|
|
32
24
|
|
|
33
|
-
|
|
25
|
+
// eslint-disable-next-line unicorn/no-thenable
|
|
26
|
+
(mockDb as { then: unknown }).then = (resolve: (arg0: unknown) => void) => {
|
|
27
|
+
callCount++;
|
|
28
|
+
if (callCount === 1) {
|
|
29
|
+
// First call: get roles
|
|
34
30
|
return resolve(data.roles || []);
|
|
35
31
|
}
|
|
36
|
-
|
|
32
|
+
if (callCount <= 1 + nonAdminRoles.length && nonAdminRoles.length > 0) {
|
|
33
|
+
// Access rule calls for each non-admin role
|
|
34
|
+
return resolve(data.accessRules || []);
|
|
35
|
+
}
|
|
36
|
+
// Team memberships (final call)
|
|
37
|
+
return resolve(data.teams || []);
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
return mockDb;
|
|
@@ -49,51 +50,69 @@ describe("enrichUser", () => {
|
|
|
49
50
|
updatedAt: new Date(),
|
|
50
51
|
};
|
|
51
52
|
|
|
52
|
-
it("should enrich user with admin role and wildcard
|
|
53
|
+
it("should enrich user with admin role and wildcard access", async () => {
|
|
53
54
|
const mockDb = createMockDb({
|
|
54
55
|
roles: [{ roleId: "admin" }],
|
|
56
|
+
teams: [{ teamId: "team-1" }],
|
|
55
57
|
});
|
|
56
58
|
|
|
57
|
-
const result = await enrichUser(
|
|
59
|
+
const result = await enrichUser(
|
|
60
|
+
baseUser,
|
|
61
|
+
mockDb as Parameters<typeof enrichUser>[1]
|
|
62
|
+
);
|
|
58
63
|
|
|
59
64
|
expect(result.roles).toContain("admin");
|
|
60
|
-
expect(result.
|
|
65
|
+
expect(result.accessRules).toContain("*");
|
|
66
|
+
expect(result.teamIds).toEqual(["team-1"]);
|
|
61
67
|
});
|
|
62
68
|
|
|
63
|
-
it("should enrich user with custom roles and
|
|
69
|
+
it("should enrich user with custom roles and access rules", async () => {
|
|
64
70
|
const mockDb = createMockDb({
|
|
65
|
-
roles: [{ roleId: "editor" }
|
|
66
|
-
|
|
71
|
+
roles: [{ roleId: "editor" }],
|
|
72
|
+
accessRules: [{ accessRuleId: "blog.edit" }],
|
|
73
|
+
teams: [],
|
|
67
74
|
});
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
let callCount = 0;
|
|
75
|
-
// eslint-disable-next-line unicorn/no-thenable
|
|
76
|
-
mockDb.then = (resolve: (arg0: unknown) => void) => {
|
|
77
|
-
callCount++;
|
|
78
|
-
if (callCount === 1) return resolve([{ roleId: "editor" }]);
|
|
79
|
-
if (callCount === 2) return resolve([{ permissionId: "blog.edit" }]);
|
|
80
|
-
return resolve([]);
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const result = await enrichUser(baseUser, mockDb);
|
|
76
|
+
const result = await enrichUser(
|
|
77
|
+
baseUser,
|
|
78
|
+
mockDb as Parameters<typeof enrichUser>[1]
|
|
79
|
+
);
|
|
84
80
|
|
|
85
81
|
expect(result.roles).toContain("editor");
|
|
86
|
-
expect(result.
|
|
82
|
+
expect(result.accessRules).toContain("blog.edit");
|
|
83
|
+
expect(result.teamIds).toEqual([]);
|
|
87
84
|
});
|
|
88
85
|
|
|
89
86
|
it("should handle user with no roles", async () => {
|
|
90
87
|
const mockDb = createMockDb({
|
|
91
88
|
roles: [],
|
|
89
|
+
teams: [],
|
|
92
90
|
});
|
|
93
91
|
|
|
94
|
-
const result = await enrichUser(
|
|
92
|
+
const result = await enrichUser(
|
|
93
|
+
baseUser,
|
|
94
|
+
mockDb as Parameters<typeof enrichUser>[1]
|
|
95
|
+
);
|
|
95
96
|
|
|
96
97
|
expect(result.roles).toEqual([]);
|
|
97
|
-
expect(result.
|
|
98
|
+
expect(result.accessRules).toEqual([]);
|
|
99
|
+
expect(result.teamIds).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("should include multiple team memberships", async () => {
|
|
103
|
+
const mockDb = createMockDb({
|
|
104
|
+
roles: [{ roleId: "admin" }],
|
|
105
|
+
teams: [{ teamId: "team-1" }, { teamId: "team-2" }, { teamId: "team-3" }],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const result = await enrichUser(
|
|
109
|
+
baseUser,
|
|
110
|
+
mockDb as Parameters<typeof enrichUser>[1]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(result.teamIds).toHaveLength(3);
|
|
114
|
+
expect(result.teamIds).toContain("team-1");
|
|
115
|
+
expect(result.teamIds).toContain("team-2");
|
|
116
|
+
expect(result.teamIds).toContain("team-3");
|
|
98
117
|
});
|
|
99
118
|
});
|
package/src/utils/user.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { RealUser } from "@checkstack/backend-api";
|
|
|
5
5
|
import * as schema from "../schema";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* Enriches a better-auth User with roles and
|
|
8
|
+
* Enriches a better-auth User with roles, access rules, and team memberships from the database.
|
|
9
9
|
* Returns a RealUser type for use in the RPC context.
|
|
10
10
|
*/
|
|
11
11
|
export const enrichUser = async (
|
|
@@ -23,31 +23,38 @@ export const enrichUser = async (
|
|
|
23
23
|
.where(eq(schema.userRole.userId, user.id));
|
|
24
24
|
|
|
25
25
|
const roles = userRoles.map((r) => r.roleId);
|
|
26
|
-
const
|
|
26
|
+
const accessRulesSet = new Set<string>();
|
|
27
27
|
|
|
28
|
-
// 2. Get
|
|
28
|
+
// 2. Get access rules for each role
|
|
29
29
|
for (const roleId of roles) {
|
|
30
30
|
if (roleId === "admin") {
|
|
31
|
-
|
|
31
|
+
accessRulesSet.add("*");
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const roleAccessRules = await db
|
|
36
36
|
.select({
|
|
37
|
-
|
|
37
|
+
accessRuleId: schema.accessRule.id,
|
|
38
38
|
})
|
|
39
|
-
.from(schema.
|
|
39
|
+
.from(schema.roleAccessRule)
|
|
40
40
|
.innerJoin(
|
|
41
|
-
schema.
|
|
42
|
-
eq(schema.
|
|
41
|
+
schema.accessRule,
|
|
42
|
+
eq(schema.accessRule.id, schema.roleAccessRule.accessRuleId)
|
|
43
43
|
)
|
|
44
|
-
.where(eq(schema.
|
|
44
|
+
.where(eq(schema.roleAccessRule.roleId, roleId));
|
|
45
45
|
|
|
46
|
-
for (const p of
|
|
47
|
-
|
|
46
|
+
for (const p of roleAccessRules) {
|
|
47
|
+
accessRulesSet.add(p.accessRuleId);
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// 3. Get Team memberships
|
|
52
|
+
const userTeams = await db
|
|
53
|
+
.select({ teamId: schema.userTeam.teamId })
|
|
54
|
+
.from(schema.userTeam)
|
|
55
|
+
.where(eq(schema.userTeam.userId, user.id));
|
|
56
|
+
const teamIds = userTeams.map((t) => t.teamId);
|
|
57
|
+
|
|
51
58
|
return {
|
|
52
59
|
// Spread user first to preserve additional properties
|
|
53
60
|
...user,
|
|
@@ -57,6 +64,7 @@ export const enrichUser = async (
|
|
|
57
64
|
email: user.email,
|
|
58
65
|
name: user.name,
|
|
59
66
|
roles,
|
|
60
|
-
|
|
67
|
+
accessRules: [...accessRulesSet],
|
|
68
|
+
teamIds,
|
|
61
69
|
};
|
|
62
70
|
};
|