@jskit-ai/users-core 0.1.31 → 0.1.33
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/package.descriptor.mjs +21 -19
- package/package.json +6 -6
- package/src/server/UsersCoreServiceProvider.js +1 -3
- package/src/server/accountNotifications/accountNotificationsService.js +3 -3
- package/src/server/accountNotifications/registerAccountNotifications.js +1 -1
- package/src/server/accountPreferences/accountPreferencesService.js +3 -3
- package/src/server/accountPreferences/registerAccountPreferences.js +1 -1
- package/src/server/accountProfile/accountProfileActions.js +8 -2
- package/src/server/accountProfile/accountProfileService.js +10 -10
- package/src/server/accountProfile/avatarService.js +26 -67
- package/src/server/accountProfile/avatarStorageService.js +14 -95
- package/src/server/accountProfile/bootAccountProfileRoutes.js +13 -15
- package/src/server/accountProfile/registerAccountProfile.js +2 -2
- package/src/server/accountSecurity/accountSecurityService.js +3 -3
- package/src/server/accountSecurity/registerAccountSecurity.js +1 -1
- package/src/server/common/contributors/workspaceActionContextContributor.js +24 -17
- package/src/server/common/formatters/workspaceFormatter.js +2 -2
- package/src/server/common/registerCommonRepositories.js +3 -3
- package/src/server/common/repositories/{userProfilesRepository.js → usersRepository.js} +7 -7
- package/src/server/common/repositories/workspaceInvitesRepository.js +2 -2
- package/src/server/common/repositories/workspaceMembershipsRepository.js +9 -9
- package/src/server/common/repositories/workspacesRepository.js +2 -2
- package/src/server/common/services/accountContextService.js +4 -4
- package/src/server/common/services/authProfileSyncService.js +15 -15
- package/src/server/common/services/workspaceContextService.js +3 -3
- package/src/server/common/validators/authenticatedUserValidator.js +2 -2
- package/src/server/registerWorkspaceBootstrap.js +1 -1
- package/src/server/registerWorkspaceCore.js +5 -2
- package/src/server/workspaceBootstrapContributor.js +6 -6
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +2 -2
- package/src/server/workspaceMembers/workspaceMembersActions.js +2 -2
- package/src/server/workspaceMembers/workspaceMembersService.js +11 -11
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +1 -1
- package/src/shared/resources/workspaceMembersResource.js +11 -11
- package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
- package/src/shared/resources/workspaceResource.js +2 -2
- package/src/shared/roles.js +37 -12
- package/templates/config/roles.js +27 -0
- package/templates/migrations/users_core_initial.cjs +5 -5
- package/test/authProfileSyncService.test.js +8 -8
- package/test/avatarService.test.js +6 -6
- package/test/roles.test.js +90 -5
- package/test/usersRouteRequestInputValidator.test.js +4 -4
- package/test/workspaceActionContextContributor.test.js +107 -14
- package/test/workspaceAuthPolicyContextResolver.test.js +2 -2
- package/test/workspaceBootstrapContributor.test.js +8 -8
- package/test/workspaceInvitesRepository.test.js +3 -3
- package/test/workspaceMembersService.test.js +14 -12
- package/test/workspacePendingInvitationsResource.test.js +2 -2
- package/test/workspacePendingInvitationsService.test.js +3 -3
- package/test/workspaceService.test.js +22 -18
- package/test/workspaceSettingsResource.test.js +4 -2
- package/src/server/accountProfile/registerAvatarMultipartSupport.js +0 -40
- package/templates/config/workspaceRoles.js +0 -30
- package/test/registerAvatarMultipartSupport.test.js +0 -63
|
@@ -19,7 +19,7 @@ const workspaceSummaryOutputSchema = Type.Object(
|
|
|
19
19
|
const memberSummaryOutputSchema = Type.Object(
|
|
20
20
|
{
|
|
21
21
|
userId: Type.Integer({ minimum: 1 }),
|
|
22
|
-
|
|
22
|
+
roleSid: Type.String({ minLength: 1 }),
|
|
23
23
|
status: Type.String({ minLength: 1 }),
|
|
24
24
|
displayName: Type.String(),
|
|
25
25
|
email: Type.String({ minLength: 1 }),
|
|
@@ -32,7 +32,7 @@ const inviteSummaryOutputSchema = Type.Object(
|
|
|
32
32
|
{
|
|
33
33
|
id: Type.Integer({ minimum: 1 }),
|
|
34
34
|
email: Type.String({ minLength: 3, format: "email" }),
|
|
35
|
-
|
|
35
|
+
roleSid: Type.String({ minLength: 1 }),
|
|
36
36
|
status: Type.String({ minLength: 1 }),
|
|
37
37
|
expiresAt: Type.String({ minLength: 1 }),
|
|
38
38
|
invitedByUserId: Type.Union([Type.Integer({ minimum: 1 }), Type.Null()])
|
|
@@ -57,13 +57,13 @@ function normalizeMemberSummary(member, workspace) {
|
|
|
57
57
|
|
|
58
58
|
return {
|
|
59
59
|
userId: Number(source.userId),
|
|
60
|
-
|
|
60
|
+
roleSid: normalizeLowerText(source.roleSid || "member") || "member",
|
|
61
61
|
status: normalizeLowerText(source.status || "active") || "active",
|
|
62
62
|
displayName: normalizeText(source.displayName),
|
|
63
63
|
email: normalizeLowerText(source.email),
|
|
64
64
|
isOwner:
|
|
65
65
|
Number(source.userId) === Number(workspace.ownerUserId) ||
|
|
66
|
-
normalizeLowerText(source.
|
|
66
|
+
normalizeLowerText(source.roleSid) === OWNER_ROLE_ID
|
|
67
67
|
};
|
|
68
68
|
}
|
|
69
69
|
|
|
@@ -73,7 +73,7 @@ function normalizeInviteSummary(invite) {
|
|
|
73
73
|
return {
|
|
74
74
|
id: Number(source.id),
|
|
75
75
|
email: normalizeLowerText(source.email),
|
|
76
|
-
|
|
76
|
+
roleSid: normalizeLowerText(source.roleSid || "member") || "member",
|
|
77
77
|
status: normalizeLowerText(source.status || "pending") || "pending",
|
|
78
78
|
expiresAt: source.expiresAt,
|
|
79
79
|
invitedByUserId: source.invitedByUserId == null ? null : Number(source.invitedByUserId)
|
|
@@ -160,7 +160,7 @@ const workspaceInvitesOutputValidator = Object.freeze({
|
|
|
160
160
|
const updateMemberRoleBodyValidator = Object.freeze({
|
|
161
161
|
schema: Type.Object(
|
|
162
162
|
{
|
|
163
|
-
|
|
163
|
+
roleSid: Type.String({ minLength: 1 })
|
|
164
164
|
},
|
|
165
165
|
{ additionalProperties: false }
|
|
166
166
|
),
|
|
@@ -168,7 +168,7 @@ const updateMemberRoleBodyValidator = Object.freeze({
|
|
|
168
168
|
const source = normalizeObjectInput(payload);
|
|
169
169
|
|
|
170
170
|
return {
|
|
171
|
-
|
|
171
|
+
roleSid: normalizeLowerText(source.roleSid)
|
|
172
172
|
};
|
|
173
173
|
}
|
|
174
174
|
});
|
|
@@ -177,7 +177,7 @@ const updateMemberRoleInputValidator = Object.freeze({
|
|
|
177
177
|
schema: Type.Object(
|
|
178
178
|
{
|
|
179
179
|
memberUserId: Type.Integer({ minimum: 1 }),
|
|
180
|
-
|
|
180
|
+
roleSid: Type.String({ minLength: 1 })
|
|
181
181
|
},
|
|
182
182
|
{ additionalProperties: false }
|
|
183
183
|
),
|
|
@@ -186,7 +186,7 @@ const updateMemberRoleInputValidator = Object.freeze({
|
|
|
186
186
|
|
|
187
187
|
return {
|
|
188
188
|
memberUserId: normalizePositiveInteger(source.memberUserId),
|
|
189
|
-
|
|
189
|
+
roleSid: normalizeLowerText(source.roleSid)
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
192
|
});
|
|
@@ -211,7 +211,7 @@ const createInviteBodyValidator = Object.freeze({
|
|
|
211
211
|
schema: Type.Object(
|
|
212
212
|
{
|
|
213
213
|
email: Type.String({ minLength: 3, format: "email" }),
|
|
214
|
-
|
|
214
|
+
roleSid: Type.String({ minLength: 1 })
|
|
215
215
|
},
|
|
216
216
|
{ additionalProperties: false }
|
|
217
217
|
),
|
|
@@ -220,7 +220,7 @@ const createInviteBodyValidator = Object.freeze({
|
|
|
220
220
|
|
|
221
221
|
return {
|
|
222
222
|
email: normalizeLowerText(source.email),
|
|
223
|
-
|
|
223
|
+
roleSid: normalizeLowerText(source.roleSid || "member") || "member"
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
226
|
});
|
|
@@ -25,7 +25,7 @@ function normalizePendingInvite(invite) {
|
|
|
25
25
|
workspaceSlug: normalizeText(invite?.workspaceSlug),
|
|
26
26
|
workspaceName: normalizeText(invite?.workspaceName || invite?.workspaceSlug),
|
|
27
27
|
workspaceAvatarUrl: normalizeText(invite?.workspaceAvatarUrl),
|
|
28
|
-
|
|
28
|
+
roleSid: normalizeLowerText(invite?.roleSid || "member") || "member",
|
|
29
29
|
status: normalizeLowerText(invite?.status || "pending") || "pending",
|
|
30
30
|
expiresAt: invite?.expiresAt || null,
|
|
31
31
|
token: encodeInviteTokenHash(tokenHash)
|
|
@@ -44,7 +44,7 @@ const pendingInviteRecordValidator = Object.freeze({
|
|
|
44
44
|
workspaceSlug: Type.String({ minLength: 1 }),
|
|
45
45
|
workspaceName: Type.String({ minLength: 1 }),
|
|
46
46
|
workspaceAvatarUrl: Type.String(),
|
|
47
|
-
|
|
47
|
+
roleSid: Type.String({ minLength: 1 }),
|
|
48
48
|
status: Type.String({ minLength: 1 }),
|
|
49
49
|
expiresAt: Type.Union([Type.String({ minLength: 1 }), Type.Null()]),
|
|
50
50
|
token: Type.String({ minLength: 1 })
|
|
@@ -63,7 +63,7 @@ function normalizeWorkspaceListItemOutput(payload = {}) {
|
|
|
63
63
|
slug: normalizeLowerText(source.slug),
|
|
64
64
|
name: normalizeText(source.name),
|
|
65
65
|
avatarUrl: normalizeText(source.avatarUrl),
|
|
66
|
-
|
|
66
|
+
roleSid: normalizeLowerText(source.roleSid || "member") || "member",
|
|
67
67
|
isAccessible: source.isAccessible !== false
|
|
68
68
|
};
|
|
69
69
|
}
|
|
@@ -85,7 +85,7 @@ const listItemSchema = Type.Object(
|
|
|
85
85
|
slug: Type.String({ minLength: 1 }),
|
|
86
86
|
name: Type.String({ minLength: 1, maxLength: 160 }),
|
|
87
87
|
avatarUrl: Type.String(),
|
|
88
|
-
|
|
88
|
+
roleSid: Type.String({ minLength: 1 }),
|
|
89
89
|
isAccessible: Type.Boolean()
|
|
90
90
|
},
|
|
91
91
|
{ additionalProperties: false }
|
package/src/shared/roles.js
CHANGED
|
@@ -20,13 +20,38 @@ function normalizeRoleId(value) {
|
|
|
20
20
|
.toLowerCase();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function
|
|
23
|
+
function resolveInheritedRolePermissions(roleSid, configuredRoles = {}, seenRoleIds = new Set()) {
|
|
24
|
+
if (seenRoleIds.has(roleSid)) {
|
|
25
|
+
throw new TypeError(`roleCatalog role "${roleSid}" has circular inheritance.`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const source = asRecord(configuredRoles[roleSid]);
|
|
29
|
+
const inheritedRoleId = normalizeRoleId(source.inherits);
|
|
30
|
+
const directPermissions = normalizePermissionList(source.permissions);
|
|
31
|
+
if (!inheritedRoleId) {
|
|
32
|
+
return directPermissions;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!Object.hasOwn(configuredRoles, inheritedRoleId)) {
|
|
36
|
+
throw new TypeError(`roleCatalog role "${roleSid}" inherits unknown role "${inheritedRoleId}".`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const nextSeenRoleIds = new Set(seenRoleIds);
|
|
40
|
+
nextSeenRoleIds.add(roleSid);
|
|
41
|
+
|
|
42
|
+
return normalizePermissionList([
|
|
43
|
+
...resolveInheritedRolePermissions(inheritedRoleId, configuredRoles, nextSeenRoleIds),
|
|
44
|
+
...directPermissions
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createRoleDescriptor(roleSid, configuredDefinition, configuredRoles = {}) {
|
|
24
49
|
const source = asRecord(configuredDefinition);
|
|
25
|
-
const assignable =
|
|
26
|
-
const permissions =
|
|
50
|
+
const assignable = roleSid === OWNER_ROLE_ID ? false : source.assignable === true;
|
|
51
|
+
const permissions = resolveInheritedRolePermissions(roleSid, configuredRoles);
|
|
27
52
|
|
|
28
53
|
return Object.freeze({
|
|
29
|
-
id:
|
|
54
|
+
id: roleSid,
|
|
30
55
|
assignable,
|
|
31
56
|
permissions: Object.freeze([...permissions])
|
|
32
57
|
});
|
|
@@ -38,16 +63,16 @@ function listConfiguredRoleIds(appConfig = {}) {
|
|
|
38
63
|
}
|
|
39
64
|
|
|
40
65
|
function resolveConfiguredDefaultInviteRole(appConfig = {}) {
|
|
41
|
-
return normalizeRoleId(appConfig?.
|
|
66
|
+
return normalizeRoleId(appConfig?.roleCatalog?.workspace?.defaultInviteRole);
|
|
42
67
|
}
|
|
43
68
|
|
|
44
69
|
function normalizeConfiguredRoles(appConfig = {}) {
|
|
45
|
-
const
|
|
46
|
-
const configuredRoles = asRecord(
|
|
70
|
+
const roleCatalog = asRecord(appConfig?.roleCatalog);
|
|
71
|
+
const configuredRoles = asRecord(roleCatalog.roles);
|
|
47
72
|
const normalizedRoles = {};
|
|
48
73
|
|
|
49
|
-
for (const [
|
|
50
|
-
const normalizedRoleId = normalizeRoleId(
|
|
74
|
+
for (const [roleSid, roleDefinition] of Object.entries(configuredRoles)) {
|
|
75
|
+
const normalizedRoleId = normalizeRoleId(roleSid);
|
|
51
76
|
if (!normalizedRoleId) {
|
|
52
77
|
continue;
|
|
53
78
|
}
|
|
@@ -60,7 +85,7 @@ function normalizeConfiguredRoles(appConfig = {}) {
|
|
|
60
85
|
function createWorkspaceRoleCatalog(appConfig = {}) {
|
|
61
86
|
const configuredRoles = normalizeConfiguredRoles(appConfig);
|
|
62
87
|
const roleIds = listConfiguredRoleIds(appConfig);
|
|
63
|
-
const roles = roleIds.map((
|
|
88
|
+
const roles = roleIds.map((roleSid) => createRoleDescriptor(roleSid, configuredRoles[roleSid], configuredRoles));
|
|
64
89
|
const assignableRoleIds = roles.filter((role) => role.assignable).map((role) => role.id);
|
|
65
90
|
const configuredDefaultInviteRole = resolveConfiguredDefaultInviteRole(appConfig);
|
|
66
91
|
const defaultInviteRole = assignableRoleIds.includes(configuredDefaultInviteRole)
|
|
@@ -109,8 +134,8 @@ function listRoleDescriptors(appConfig = {}) {
|
|
|
109
134
|
}));
|
|
110
135
|
}
|
|
111
136
|
|
|
112
|
-
function resolveRolePermissions(
|
|
113
|
-
const normalizedRoleId = normalizeRoleId(
|
|
137
|
+
function resolveRolePermissions(roleSid, appConfig = {}) {
|
|
138
|
+
const normalizedRoleId = normalizeRoleId(roleSid);
|
|
114
139
|
if (!normalizedRoleId) {
|
|
115
140
|
return [];
|
|
116
141
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const roleCatalog = {
|
|
2
|
+
workspace: {
|
|
3
|
+
defaultInviteRole: "member"
|
|
4
|
+
},
|
|
5
|
+
roles: {
|
|
6
|
+
owner: {
|
|
7
|
+
assignable: false,
|
|
8
|
+
permissions: ["*"]
|
|
9
|
+
},
|
|
10
|
+
admin: {
|
|
11
|
+
assignable: true,
|
|
12
|
+
inherits: "member",
|
|
13
|
+
permissions: [
|
|
14
|
+
"workspace.roles.view",
|
|
15
|
+
"workspace.settings.update",
|
|
16
|
+
"workspace.members.view",
|
|
17
|
+
"workspace.members.invite",
|
|
18
|
+
"workspace.members.manage",
|
|
19
|
+
"workspace.invites.revoke"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
member: {
|
|
23
|
+
assignable: true,
|
|
24
|
+
permissions: ["workspace.settings.view"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -5,7 +5,7 @@ exports.up = async function up(knex) {
|
|
|
5
5
|
await knex.schema.createTable("users", (table) => {
|
|
6
6
|
table.increments("id").primary();
|
|
7
7
|
table.string("auth_provider", 64).notNullable();
|
|
8
|
-
table.string("
|
|
8
|
+
table.string("auth_provider_user_sid", 191).notNullable();
|
|
9
9
|
table.string("email", 255).notNullable();
|
|
10
10
|
table.string("username", 120).notNullable();
|
|
11
11
|
table.string("display_name", 160).notNullable();
|
|
@@ -13,7 +13,7 @@ exports.up = async function up(knex) {
|
|
|
13
13
|
table.string("avatar_version", 64).nullable();
|
|
14
14
|
table.timestamp("avatar_updated_at", { useTz: false }).nullable();
|
|
15
15
|
table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
16
|
-
table.unique(["auth_provider", "
|
|
16
|
+
table.unique(["auth_provider", "auth_provider_user_sid"], "uq_users_identity");
|
|
17
17
|
table.unique(["email"], "uq_users_email");
|
|
18
18
|
table.unique(["username"], "uq_users_username");
|
|
19
19
|
});
|
|
@@ -35,7 +35,7 @@ exports.up = async function up(knex) {
|
|
|
35
35
|
table.increments("id").primary();
|
|
36
36
|
table.integer("workspace_id").unsigned().notNullable().references("id").inTable("workspaces").onDelete("CASCADE");
|
|
37
37
|
table.integer("user_id").unsigned().notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
38
|
-
table.string("
|
|
38
|
+
table.string("role_sid", 64).notNullable().defaultTo("member");
|
|
39
39
|
table.string("status", 32).notNullable().defaultTo("active");
|
|
40
40
|
table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
41
41
|
table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
@@ -63,7 +63,7 @@ exports.up = async function up(knex) {
|
|
|
63
63
|
table.increments("id").primary();
|
|
64
64
|
table.integer("workspace_id").unsigned().notNullable().references("id").inTable("workspaces").onDelete("CASCADE");
|
|
65
65
|
table.string("email", 255).notNullable();
|
|
66
|
-
table.string("
|
|
66
|
+
table.string("role_sid", 64).notNullable().defaultTo("member");
|
|
67
67
|
table.string("status", 32).notNullable().defaultTo("pending");
|
|
68
68
|
table.string("token_hash", 191).notNullable();
|
|
69
69
|
table.integer("invited_by_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
|
|
@@ -97,7 +97,7 @@ exports.up = async function up(knex) {
|
|
|
97
97
|
|
|
98
98
|
await knex.schema.createTable("console_settings", (table) => {
|
|
99
99
|
table.integer("id").primary();
|
|
100
|
-
table.integer("owner_user_id").unsigned().nullable();
|
|
100
|
+
table.integer("owner_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
|
|
101
101
|
table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
102
102
|
table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
103
103
|
});
|
|
@@ -7,7 +7,7 @@ test("authProfileSyncService.syncIdentityProfile uses shared transaction for pro
|
|
|
7
7
|
const transaction = { trxId: "tx-1" };
|
|
8
8
|
|
|
9
9
|
const service = createService({
|
|
10
|
-
|
|
10
|
+
usersRepository: {
|
|
11
11
|
async findByIdentity(_identity, options = {}) {
|
|
12
12
|
calls.push({ step: "find", trx: options.trx || null });
|
|
13
13
|
return null;
|
|
@@ -17,7 +17,7 @@ test("authProfileSyncService.syncIdentityProfile uses shared transaction for pro
|
|
|
17
17
|
return {
|
|
18
18
|
id: 13,
|
|
19
19
|
authProvider: payload.authProvider,
|
|
20
|
-
|
|
20
|
+
authProviderUserSid: payload.authProviderUserSid,
|
|
21
21
|
email: payload.email,
|
|
22
22
|
displayName: payload.displayName
|
|
23
23
|
};
|
|
@@ -42,7 +42,7 @@ test("authProfileSyncService.syncIdentityProfile uses shared transaction for pro
|
|
|
42
42
|
|
|
43
43
|
const profile = await service.syncIdentityProfile({
|
|
44
44
|
authProvider: "supabase",
|
|
45
|
-
|
|
45
|
+
authProviderUserSid: "abc-1",
|
|
46
46
|
email: "tony@example.com",
|
|
47
47
|
displayName: "Tony"
|
|
48
48
|
});
|
|
@@ -64,12 +64,12 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
|
|
|
64
64
|
let provisionCalls = 0;
|
|
65
65
|
|
|
66
66
|
const service = createService({
|
|
67
|
-
|
|
67
|
+
usersRepository: {
|
|
68
68
|
async findByIdentity() {
|
|
69
69
|
return {
|
|
70
70
|
id: 7,
|
|
71
71
|
authProvider: "supabase",
|
|
72
|
-
|
|
72
|
+
authProviderUserSid: "abc-7",
|
|
73
73
|
email: "tony@example.com",
|
|
74
74
|
displayName: "Tony"
|
|
75
75
|
};
|
|
@@ -96,7 +96,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
|
|
|
96
96
|
|
|
97
97
|
const profile = await service.syncIdentityProfile({
|
|
98
98
|
authProvider: "supabase",
|
|
99
|
-
|
|
99
|
+
authProviderUserSid: "abc-7",
|
|
100
100
|
email: "tony@example.com",
|
|
101
101
|
displayName: "Tony"
|
|
102
102
|
});
|
|
@@ -109,7 +109,7 @@ test("authProfileSyncService.syncIdentityProfile skips write path when profile i
|
|
|
109
109
|
test("authProfileSyncService.findByIdentity normalizes provider identity input", async () => {
|
|
110
110
|
let capturedIdentity = null;
|
|
111
111
|
const service = createService({
|
|
112
|
-
|
|
112
|
+
usersRepository: {
|
|
113
113
|
async findByIdentity(identity) {
|
|
114
114
|
capturedIdentity = identity;
|
|
115
115
|
return null;
|
|
@@ -127,7 +127,7 @@ test("authProfileSyncService.findByIdentity normalizes provider identity input",
|
|
|
127
127
|
|
|
128
128
|
await service.findByIdentity({
|
|
129
129
|
authProvider: " SUPABASE ",
|
|
130
|
-
|
|
130
|
+
authProviderUserSid: " user-1 "
|
|
131
131
|
});
|
|
132
132
|
|
|
133
133
|
assert.deepEqual(capturedIdentity, {
|
|
@@ -41,7 +41,7 @@ test("avatarService uploadForUser stores bytes and updates profile avatar fields
|
|
|
41
41
|
const repository = createRepositoryDouble({
|
|
42
42
|
id: 7,
|
|
43
43
|
authProvider: "local",
|
|
44
|
-
|
|
44
|
+
authProviderUserSid: "u-7",
|
|
45
45
|
email: "test@example.com",
|
|
46
46
|
displayName: "Tester",
|
|
47
47
|
avatarStorageKey: null,
|
|
@@ -59,13 +59,13 @@ test("avatarService uploadForUser stores bytes and updates profile avatar fields
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
const avatarService = createService({
|
|
62
|
-
|
|
62
|
+
usersRepository: repository,
|
|
63
63
|
avatarStorageService
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
const user = {
|
|
67
67
|
authProvider: "local",
|
|
68
|
-
|
|
68
|
+
authProviderUserSid: "u-7"
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
const result = await avatarService.uploadForUser(user, {
|
|
@@ -84,7 +84,7 @@ test("avatarService clearForUser removes stored avatar and clears profile fields
|
|
|
84
84
|
const repository = createRepositoryDouble({
|
|
85
85
|
id: 7,
|
|
86
86
|
authProvider: "local",
|
|
87
|
-
|
|
87
|
+
authProviderUserSid: "u-7",
|
|
88
88
|
email: "test@example.com",
|
|
89
89
|
displayName: "Tester",
|
|
90
90
|
avatarStorageKey: "users/avatars/7/avatar",
|
|
@@ -99,13 +99,13 @@ test("avatarService clearForUser removes stored avatar and clears profile fields
|
|
|
99
99
|
};
|
|
100
100
|
|
|
101
101
|
const avatarService = createService({
|
|
102
|
-
|
|
102
|
+
usersRepository: repository,
|
|
103
103
|
avatarStorageService
|
|
104
104
|
});
|
|
105
105
|
|
|
106
106
|
const profile = await avatarService.clearForUser({
|
|
107
107
|
authProvider: "local",
|
|
108
|
-
|
|
108
|
+
authProviderUserSid: "u-7"
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
assert.deepEqual(deletedKeys, ["users/avatars/7/avatar"]);
|
package/test/roles.test.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
hasPermission
|
|
8
8
|
} from "../src/shared/roles.js";
|
|
9
9
|
|
|
10
|
-
test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.
|
|
10
|
+
test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.roleCatalog", () => {
|
|
11
11
|
const emptyCatalog = createWorkspaceRoleCatalog();
|
|
12
12
|
assert.deepEqual(emptyCatalog.roles, []);
|
|
13
13
|
assert.deepEqual(emptyCatalog.assignableRoleIds, []);
|
|
@@ -15,8 +15,10 @@ test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.w
|
|
|
15
15
|
assert.equal(emptyCatalog.collaborationEnabled, false);
|
|
16
16
|
|
|
17
17
|
const appConfig = {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
roleCatalog: {
|
|
19
|
+
workspace: {
|
|
20
|
+
defaultInviteRole: "editor"
|
|
21
|
+
},
|
|
20
22
|
roles: {
|
|
21
23
|
owner: {
|
|
22
24
|
assignable: false,
|
|
@@ -24,7 +26,7 @@ test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.w
|
|
|
24
26
|
},
|
|
25
27
|
editor: {
|
|
26
28
|
assignable: true,
|
|
27
|
-
permissions: ["
|
|
29
|
+
permissions: ["crud.contacts.*"]
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
}
|
|
@@ -35,7 +37,90 @@ test("createWorkspaceRoleCatalog resolves role descriptors only from appConfig.w
|
|
|
35
37
|
assert.equal(roleCatalog.defaultInviteRole, "editor");
|
|
36
38
|
assert.equal(roleCatalog.assignableRoleIds.includes("editor"), true);
|
|
37
39
|
assert.deepEqual(resolveRolePermissions("owner", appConfig), ["workspace.settings.update"]);
|
|
38
|
-
assert.equal(hasPermission(editorRole?.permissions, "
|
|
40
|
+
assert.equal(hasPermission(editorRole?.permissions, "crud.contacts.update"), true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("createWorkspaceRoleCatalog resolves inherited role permissions with parent permissions first", () => {
|
|
44
|
+
const appConfig = {
|
|
45
|
+
roleCatalog: {
|
|
46
|
+
workspace: {
|
|
47
|
+
defaultInviteRole: "member"
|
|
48
|
+
},
|
|
49
|
+
roles: {
|
|
50
|
+
member: {
|
|
51
|
+
assignable: true,
|
|
52
|
+
permissions: [
|
|
53
|
+
"workspace.settings.view",
|
|
54
|
+
"crud.contacts.list"
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
admin: {
|
|
58
|
+
assignable: true,
|
|
59
|
+
inherits: "member",
|
|
60
|
+
permissions: [
|
|
61
|
+
"workspace.settings.update",
|
|
62
|
+
"workspace.members.manage",
|
|
63
|
+
"workspace.settings.view"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const roleCatalog = createWorkspaceRoleCatalog(appConfig);
|
|
71
|
+
const adminRole = roleCatalog.roles.find((role) => role.id === "admin");
|
|
72
|
+
|
|
73
|
+
assert.deepEqual(adminRole, {
|
|
74
|
+
id: "admin",
|
|
75
|
+
assignable: true,
|
|
76
|
+
permissions: [
|
|
77
|
+
"workspace.settings.view",
|
|
78
|
+
"crud.contacts.list",
|
|
79
|
+
"workspace.settings.update",
|
|
80
|
+
"workspace.members.manage"
|
|
81
|
+
]
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("createWorkspaceRoleCatalog rejects unknown inherited roles", () => {
|
|
86
|
+
assert.throws(
|
|
87
|
+
() =>
|
|
88
|
+
createWorkspaceRoleCatalog({
|
|
89
|
+
roleCatalog: {
|
|
90
|
+
roles: {
|
|
91
|
+
admin: {
|
|
92
|
+
assignable: true,
|
|
93
|
+
inherits: "member",
|
|
94
|
+
permissions: []
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
/inherits unknown role "member"/
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("createWorkspaceRoleCatalog rejects circular inherited roles", () => {
|
|
104
|
+
assert.throws(
|
|
105
|
+
() =>
|
|
106
|
+
createWorkspaceRoleCatalog({
|
|
107
|
+
roleCatalog: {
|
|
108
|
+
roles: {
|
|
109
|
+
member: {
|
|
110
|
+
assignable: true,
|
|
111
|
+
inherits: "admin",
|
|
112
|
+
permissions: []
|
|
113
|
+
},
|
|
114
|
+
admin: {
|
|
115
|
+
assignable: true,
|
|
116
|
+
inherits: "member",
|
|
117
|
+
permissions: []
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}),
|
|
122
|
+
/circular inheritance/
|
|
123
|
+
);
|
|
39
124
|
});
|
|
40
125
|
|
|
41
126
|
test("cloneWorkspaceRoleCatalog normalizes role ids and returns detached arrays", () => {
|
|
@@ -379,7 +379,7 @@ test("workspace invite and member handlers build action input from request.input
|
|
|
379
379
|
createActionRequest({
|
|
380
380
|
input: {
|
|
381
381
|
params: { workspaceSlug: "acme", memberUserId: "12" },
|
|
382
|
-
body: {
|
|
382
|
+
body: { roleSid: "admin" }
|
|
383
383
|
},
|
|
384
384
|
executeAction
|
|
385
385
|
}),
|
|
@@ -389,7 +389,7 @@ test("workspace invite and member handlers build action input from request.input
|
|
|
389
389
|
createActionRequest({
|
|
390
390
|
input: {
|
|
391
391
|
params: { workspaceSlug: "acme" },
|
|
392
|
-
body: { email: "user@example.com",
|
|
392
|
+
body: { email: "user@example.com", roleSid: "member" }
|
|
393
393
|
},
|
|
394
394
|
executeAction
|
|
395
395
|
}),
|
|
@@ -419,8 +419,8 @@ test("workspace invite and member handlers build action input from request.input
|
|
|
419
419
|
input: { name: "Operations", slug: "operations" }
|
|
420
420
|
});
|
|
421
421
|
assert.deepEqual(calls[1].input, { payload: { token: "token-1", decision: "accept" } });
|
|
422
|
-
assert.deepEqual(calls[2].input, { workspaceSlug: "acme", memberUserId: "12",
|
|
423
|
-
assert.deepEqual(calls[3].input, { workspaceSlug: "acme", email: "user@example.com",
|
|
422
|
+
assert.deepEqual(calls[2].input, { workspaceSlug: "acme", memberUserId: "12", roleSid: "admin" });
|
|
423
|
+
assert.deepEqual(calls[3].input, { workspaceSlug: "acme", email: "user@example.com", roleSid: "member" });
|
|
424
424
|
assert.deepEqual(calls[4].input, { workspaceSlug: "acme", memberUserId: "44" });
|
|
425
425
|
assert.deepEqual(calls[5].input, { workspaceSlug: "acme", inviteId: "55" });
|
|
426
426
|
});
|