@jskit-ai/users-core 0.1.4
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 +464 -0
- package/package.json +35 -0
- package/src/server/UsersCoreServiceProvider.js +74 -0
- package/src/server/accountNotifications/accountNotificationsActions.js +39 -0
- package/src/server/accountNotifications/accountNotificationsService.js +41 -0
- package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +41 -0
- package/src/server/accountNotifications/registerAccountNotifications.js +39 -0
- package/src/server/accountPreferences/accountPreferencesActions.js +39 -0
- package/src/server/accountPreferences/accountPreferencesService.js +41 -0
- package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +41 -0
- package/src/server/accountPreferences/registerAccountPreferences.js +39 -0
- package/src/server/accountProfile/accountProfileActions.js +137 -0
- package/src/server/accountProfile/accountProfileService.js +124 -0
- package/src/server/accountProfile/avatarService.js +141 -0
- package/src/server/accountProfile/avatarStorageService.js +132 -0
- package/src/server/accountProfile/bootAccountProfileRoutes.js +166 -0
- package/src/server/accountProfile/registerAccountProfile.js +62 -0
- package/src/server/accountProfile/registerAvatarMultipartSupport.js +43 -0
- package/src/server/accountSecurity/accountSecurityActions.js +144 -0
- package/src/server/accountSecurity/accountSecurityService.js +103 -0
- package/src/server/accountSecurity/bootAccountSecurityRoutes.js +183 -0
- package/src/server/accountSecurity/registerAccountSecurity.js +31 -0
- package/src/server/common/README.md +21 -0
- package/src/server/common/contributors/README.md +11 -0
- package/src/server/common/contributors/workspaceActionContextContributor.js +79 -0
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +79 -0
- package/src/server/common/diTokens.js +21 -0
- package/src/server/common/formatters/README.md +11 -0
- package/src/server/common/formatters/accountAvatarFormatter.js +42 -0
- package/src/server/common/formatters/accountSecurityStatusFormatter.js +71 -0
- package/src/server/common/formatters/accountSettingsResponseFormatter.js +62 -0
- package/src/server/common/formatters/workspaceFormatter.js +46 -0
- package/src/server/common/registerCommonRepositories.js +45 -0
- package/src/server/common/registerSharedApi.js +9 -0
- package/src/server/common/repositories/README.md +24 -0
- package/src/server/common/repositories/repositoryUtils.js +50 -0
- package/src/server/common/repositories/userProfilesRepository.js +251 -0
- package/src/server/common/repositories/userSettingsRepository.js +179 -0
- package/src/server/common/repositories/workspaceInvitesRepository.js +172 -0
- package/src/server/common/repositories/workspaceMembershipsRepository.js +157 -0
- package/src/server/common/repositories/workspacesRepository.js +183 -0
- package/src/server/common/routes/README.md +11 -0
- package/src/server/common/services/README.md +12 -0
- package/src/server/common/services/accountContextService.js +31 -0
- package/src/server/common/services/authProfileSyncService.js +128 -0
- package/src/server/common/services/workspaceContextService.js +270 -0
- package/src/server/common/support/deepFreeze.js +17 -0
- package/src/server/common/support/realtimeServiceEvents.js +94 -0
- package/src/server/common/support/resolveActionUser.js +11 -0
- package/src/server/common/support/workspaceRoutePaths.js +17 -0
- package/src/server/common/validators/README.md +11 -0
- package/src/server/common/validators/authenticatedUserValidator.js +42 -0
- package/src/server/common/validators/routeParamsValidator.js +62 -0
- package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +64 -0
- package/src/server/consoleSettings/consoleService.js +36 -0
- package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
- package/src/server/consoleSettings/consoleSettingsRepository.js +111 -0
- package/src/server/consoleSettings/consoleSettingsService.js +40 -0
- package/src/server/consoleSettings/registerConsoleSettings.js +57 -0
- package/src/server/registerWorkspaceBootstrap.js +36 -0
- package/src/server/registerWorkspaceCore.js +95 -0
- package/src/server/support/resolveWorkspace.js +16 -0
- package/src/server/support/workspaceActionSurfaces.js +135 -0
- package/src/server/support/workspaceInvitationsPolicy.js +45 -0
- package/src/server/support/workspaceRouteInput.js +22 -0
- package/src/server/workspaceBootstrapContributor.js +401 -0
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +73 -0
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +65 -0
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +238 -0
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +112 -0
- package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
- package/src/server/workspaceMembers/workspaceMembersService.js +210 -0
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +63 -0
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +128 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +137 -0
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +77 -0
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +67 -0
- package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +135 -0
- package/src/server/workspaceSettings/workspaceSettingsService.js +65 -0
- package/src/shared/events/usersEvents.js +19 -0
- package/src/shared/index.js +91 -0
- package/src/shared/operationMessages.js +16 -0
- package/src/shared/resources/consoleSettingsFields.js +55 -0
- package/src/shared/resources/consoleSettingsResource.js +139 -0
- package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
- package/src/shared/resources/userProfileResource.js +148 -0
- package/src/shared/resources/userSettingsFields.js +71 -0
- package/src/shared/resources/userSettingsResource.js +416 -0
- package/src/shared/resources/workspaceMembersResource.js +352 -0
- package/src/shared/resources/workspacePendingInvitationsResource.js +87 -0
- package/src/shared/resources/workspaceResource.js +149 -0
- package/src/shared/resources/workspaceSettingsFields.js +60 -0
- package/src/shared/resources/workspaceSettingsResource.js +178 -0
- package/src/shared/roles.js +136 -0
- package/src/shared/settings.js +31 -0
- package/src/shared/support/usersApiPaths.js +34 -0
- package/src/shared/support/usersVisibility.js +45 -0
- package/src/shared/support/workspacePathModel.js +145 -0
- package/src/shared/tenancyMode.js +35 -0
- package/src/shared/tenancyProfile.js +73 -0
- package/templates/config/workspaceRoles.js +30 -0
- package/templates/migrations/users_core_console_owner.cjs +39 -0
- package/templates/migrations/users_core_initial.cjs +118 -0
- package/templates/migrations/users_core_profile_username.cjs +98 -0
- package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
- package/templates/packages/main/src/shared/resources/userSettingsFields.js +138 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +105 -0
- package/test/authProfileSyncService.test.js +119 -0
- package/test/avatarService.test.js +114 -0
- package/test/avatarStorageService.test.js +61 -0
- package/test/consoleService.test.js +57 -0
- package/test/consoleSettingsService.test.js +86 -0
- package/test/exportsContract.test.js +38 -0
- package/test/registerAvatarMultipartSupport.test.js +64 -0
- package/test/registerServiceRealtimeEvents.test.js +160 -0
- package/test/registerWorkspaceDirectory.test.js +26 -0
- package/test/registerWorkspaceSettings.test.js +44 -0
- package/test/resourcesCanonical.test.js +90 -0
- package/test/roles.test.js +74 -0
- package/test/settingsFieldRegistriesSingleton.test.js +24 -0
- package/test/tenancyProfile.test.js +67 -0
- package/test/userSettingsResource.test.js +31 -0
- package/test/usersApiPaths.test.js +31 -0
- package/test/usersRouteRequestInputValidator.test.js +556 -0
- package/test/usersRouteResources.test.js +113 -0
- package/test/usersRouteValidators.test.js +49 -0
- package/test/usersVisibility.test.js +22 -0
- package/test/workspaceActionContextContributor.test.js +251 -0
- package/test/workspaceActionSurfaces.test.js +105 -0
- package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
- package/test/workspaceBootstrapContributor.test.js +466 -0
- package/test/workspaceInvitationsPolicy.test.js +71 -0
- package/test/workspaceInvitesRepository.test.js +111 -0
- package/test/workspaceMembersService.test.js +400 -0
- package/test/workspacePathModel.test.js +93 -0
- package/test/workspacePendingInvitationsResource.test.js +38 -0
- package/test/workspacePendingInvitationsService.test.js +151 -0
- package/test/workspaceRouteVisibilityResolver.test.js +83 -0
- package/test/workspaceService.test.js +480 -0
- package/test/workspaceSettingsActions.test.js +42 -0
- package/test/workspaceSettingsRepository.test.js +156 -0
- package/test/workspaceSettingsResource.test.js +156 -0
- package/test/workspaceSettingsService.test.js +120 -0
- package/test-support/registerDefaultSettingsFields.js +3 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createService } from "../src/server/workspaceMembers/workspaceMembersService.js";
|
|
4
|
+
import { createWorkspaceRoleCatalog } from "../src/shared/roles.js";
|
|
5
|
+
|
|
6
|
+
function authorizedOptions(permissions = []) {
|
|
7
|
+
return {
|
|
8
|
+
context: {
|
|
9
|
+
actor: {
|
|
10
|
+
id: 1
|
|
11
|
+
},
|
|
12
|
+
permissions
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function createRoleCatalog() {
|
|
18
|
+
return createWorkspaceRoleCatalog({
|
|
19
|
+
workspaceRoles: {
|
|
20
|
+
defaultInviteRole: "member",
|
|
21
|
+
roles: {
|
|
22
|
+
owner: {
|
|
23
|
+
assignable: false,
|
|
24
|
+
permissions: ["*"]
|
|
25
|
+
},
|
|
26
|
+
admin: {
|
|
27
|
+
assignable: true,
|
|
28
|
+
permissions: ["workspace.members.manage"]
|
|
29
|
+
},
|
|
30
|
+
member: {
|
|
31
|
+
assignable: true,
|
|
32
|
+
permissions: ["workspace.members.view", "workspace.members.invite"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function createFixture() {
|
|
40
|
+
const workspace = {
|
|
41
|
+
id: 7,
|
|
42
|
+
slug: "tonymobily3",
|
|
43
|
+
name: "TonyMobily3",
|
|
44
|
+
ownerUserId: 9,
|
|
45
|
+
avatarUrl: "",
|
|
46
|
+
color: "#0F6B54"
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const service = createService({
|
|
50
|
+
workspaceMembershipsRepository: {
|
|
51
|
+
async listActiveByWorkspaceId(workspaceId) {
|
|
52
|
+
assert.equal(Number(workspaceId), 7);
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
userId: 11,
|
|
56
|
+
roleId: "member",
|
|
57
|
+
status: "active",
|
|
58
|
+
displayName: "Alice",
|
|
59
|
+
email: "alice@example.com"
|
|
60
|
+
}
|
|
61
|
+
];
|
|
62
|
+
},
|
|
63
|
+
async findByWorkspaceIdAndUserId(workspaceId, userId) {
|
|
64
|
+
assert.equal(Number(workspaceId), 7);
|
|
65
|
+
assert.equal(Number(userId), 11);
|
|
66
|
+
return {
|
|
67
|
+
workspaceId: 7,
|
|
68
|
+
userId: 11,
|
|
69
|
+
roleId: "member",
|
|
70
|
+
status: "active"
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
async upsertMembership(workspaceId, userId, patch) {
|
|
74
|
+
assert.equal(Number(workspaceId), 7);
|
|
75
|
+
assert.equal(Number(userId), 11);
|
|
76
|
+
assert.deepEqual(patch, {
|
|
77
|
+
roleId: "admin",
|
|
78
|
+
status: "active"
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
workspaceInvitesRepository: {
|
|
83
|
+
async listPendingByWorkspaceIdWithWorkspace(workspaceId) {
|
|
84
|
+
assert.equal(Number(workspaceId), 7);
|
|
85
|
+
return [];
|
|
86
|
+
},
|
|
87
|
+
async expirePendingByWorkspaceIdAndEmail() {},
|
|
88
|
+
async insert() {},
|
|
89
|
+
async findPendingByIdForWorkspace() {
|
|
90
|
+
return null;
|
|
91
|
+
},
|
|
92
|
+
async revokeById() {}
|
|
93
|
+
},
|
|
94
|
+
inviteExpiresInMs: 7 * 24 * 60 * 60 * 1000,
|
|
95
|
+
roleCatalog: createRoleCatalog()
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return { service, workspace };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
test("workspaceMembersService.createInvite uses configured inviteExpiresInMs", async () => {
|
|
102
|
+
const expiresAtValues = [];
|
|
103
|
+
const service = createService({
|
|
104
|
+
workspaceMembershipsRepository: {
|
|
105
|
+
async listActiveByWorkspaceId() {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
workspaceInvitesRepository: {
|
|
110
|
+
async expirePendingByWorkspaceIdAndEmail() {},
|
|
111
|
+
async insert(payload) {
|
|
112
|
+
expiresAtValues.push(payload.expiresAt);
|
|
113
|
+
return {
|
|
114
|
+
id: 31
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
async listPendingByWorkspaceIdWithWorkspace() {
|
|
118
|
+
return [];
|
|
119
|
+
},
|
|
120
|
+
async findPendingByIdForWorkspace() {
|
|
121
|
+
return null;
|
|
122
|
+
},
|
|
123
|
+
async revokeById() {}
|
|
124
|
+
},
|
|
125
|
+
inviteExpiresInMs: 30 * 60 * 1000,
|
|
126
|
+
roleCatalog: createRoleCatalog()
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const before = Date.now();
|
|
130
|
+
const response = await service.createInvite(
|
|
131
|
+
{
|
|
132
|
+
id: 7,
|
|
133
|
+
ownerUserId: 9
|
|
134
|
+
},
|
|
135
|
+
{ id: 11 },
|
|
136
|
+
{
|
|
137
|
+
email: "alice@example.com",
|
|
138
|
+
roleId: "member"
|
|
139
|
+
},
|
|
140
|
+
authorizedOptions(["workspace.members.invite"])
|
|
141
|
+
);
|
|
142
|
+
const after = Date.now();
|
|
143
|
+
|
|
144
|
+
assert.equal(expiresAtValues.length, 1);
|
|
145
|
+
const expiresAt = new Date(expiresAtValues[0]).getTime();
|
|
146
|
+
assert.ok(expiresAt >= before + 30 * 60 * 1000);
|
|
147
|
+
assert.ok(expiresAt <= after + 30 * 60 * 1000);
|
|
148
|
+
assert.equal(response.createdInviteId, 31);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("workspaceMembersService.revokeInvite returns the revoked invite id", async () => {
|
|
152
|
+
let revokedInviteId = 0;
|
|
153
|
+
const service = createService({
|
|
154
|
+
workspaceMembershipsRepository: {
|
|
155
|
+
async listActiveByWorkspaceId() {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
workspaceInvitesRepository: {
|
|
160
|
+
async listPendingByWorkspaceIdWithWorkspace() {
|
|
161
|
+
return [];
|
|
162
|
+
},
|
|
163
|
+
async expirePendingByWorkspaceIdAndEmail() {},
|
|
164
|
+
async insert() {
|
|
165
|
+
return {
|
|
166
|
+
id: 1
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
async findPendingByIdForWorkspace(inviteId, workspaceId) {
|
|
170
|
+
assert.equal(Number(inviteId), 47);
|
|
171
|
+
assert.equal(Number(workspaceId), 7);
|
|
172
|
+
return {
|
|
173
|
+
id: 47,
|
|
174
|
+
workspaceId: 7,
|
|
175
|
+
status: "pending"
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
async revokeById(inviteId) {
|
|
179
|
+
revokedInviteId = Number(inviteId);
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
inviteExpiresInMs: 30 * 60 * 1000,
|
|
183
|
+
roleCatalog: createRoleCatalog()
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const response = await service.revokeInvite(
|
|
187
|
+
{
|
|
188
|
+
id: 7,
|
|
189
|
+
ownerUserId: 9
|
|
190
|
+
},
|
|
191
|
+
47,
|
|
192
|
+
authorizedOptions(["workspace.invites.revoke"])
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
assert.equal(revokedInviteId, 47);
|
|
196
|
+
assert.equal(response.revokedInviteId, 47);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("workspaceMembersService rejects invite operations when invitations are disabled", async () => {
|
|
200
|
+
const service = createService({
|
|
201
|
+
workspaceMembershipsRepository: {
|
|
202
|
+
async listActiveByWorkspaceId() {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
},
|
|
206
|
+
workspaceInvitesRepository: {
|
|
207
|
+
async listPendingByWorkspaceIdWithWorkspace() {
|
|
208
|
+
throw new Error("invite repository should not be called when invitations are disabled");
|
|
209
|
+
},
|
|
210
|
+
async expirePendingByWorkspaceIdAndEmail() {
|
|
211
|
+
throw new Error("invite repository should not be called when invitations are disabled");
|
|
212
|
+
},
|
|
213
|
+
async insert() {
|
|
214
|
+
throw new Error("invite repository should not be called when invitations are disabled");
|
|
215
|
+
},
|
|
216
|
+
async findPendingByIdForWorkspace() {
|
|
217
|
+
throw new Error("invite repository should not be called when invitations are disabled");
|
|
218
|
+
},
|
|
219
|
+
async revokeById() {
|
|
220
|
+
throw new Error("invite repository should not be called when invitations are disabled");
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
inviteExpiresInMs: 30 * 60 * 1000,
|
|
224
|
+
roleCatalog: createRoleCatalog(),
|
|
225
|
+
workspaceInvitationsEnabled: false
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await assert.rejects(
|
|
229
|
+
() =>
|
|
230
|
+
service.listInvites(
|
|
231
|
+
{
|
|
232
|
+
id: 7,
|
|
233
|
+
ownerUserId: 9
|
|
234
|
+
},
|
|
235
|
+
authorizedOptions(["workspace.members.view"])
|
|
236
|
+
),
|
|
237
|
+
/Workspace invitations are disabled/
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("workspaceMembersService.listMembers uses the resolved workspace directly", async () => {
|
|
242
|
+
const { service, workspace } = createFixture();
|
|
243
|
+
|
|
244
|
+
const response = await service.listMembers(workspace, authorizedOptions(["workspace.members.view"]));
|
|
245
|
+
|
|
246
|
+
assert.deepEqual(response.workspace, {
|
|
247
|
+
id: 7,
|
|
248
|
+
slug: "tonymobily3",
|
|
249
|
+
name: "TonyMobily3",
|
|
250
|
+
ownerUserId: 9,
|
|
251
|
+
avatarUrl: "",
|
|
252
|
+
color: "#0F6B54"
|
|
253
|
+
});
|
|
254
|
+
assert.equal(response.members.length, 1);
|
|
255
|
+
assert.equal(response.members[0].displayName, "Alice");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("workspaceMembersService.updateMemberRole returns the refreshed member list without re-fetching the workspace", async () => {
|
|
259
|
+
const { service, workspace } = createFixture();
|
|
260
|
+
|
|
261
|
+
const response = await service.updateMemberRole(
|
|
262
|
+
workspace,
|
|
263
|
+
{
|
|
264
|
+
memberUserId: 11,
|
|
265
|
+
roleId: "admin"
|
|
266
|
+
},
|
|
267
|
+
authorizedOptions(["workspace.members.manage"])
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
assert.equal(response.members.length, 1);
|
|
271
|
+
assert.equal(response.members[0].roleId, "member");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("workspaceMembersService.removeMember marks membership revoked and returns refreshed members", async () => {
|
|
275
|
+
let removed = false;
|
|
276
|
+
const workspace = {
|
|
277
|
+
id: 7,
|
|
278
|
+
slug: "tonymobily3",
|
|
279
|
+
name: "TonyMobily3",
|
|
280
|
+
ownerUserId: 9,
|
|
281
|
+
avatarUrl: "",
|
|
282
|
+
color: "#0F6B54"
|
|
283
|
+
};
|
|
284
|
+
const service = createService({
|
|
285
|
+
workspaceMembershipsRepository: {
|
|
286
|
+
async listActiveByWorkspaceId(workspaceId) {
|
|
287
|
+
assert.equal(Number(workspaceId), 7);
|
|
288
|
+
return removed
|
|
289
|
+
? []
|
|
290
|
+
: [
|
|
291
|
+
{
|
|
292
|
+
userId: 11,
|
|
293
|
+
roleId: "member",
|
|
294
|
+
status: "active",
|
|
295
|
+
displayName: "Alice",
|
|
296
|
+
email: "alice@example.com"
|
|
297
|
+
}
|
|
298
|
+
];
|
|
299
|
+
},
|
|
300
|
+
async findByWorkspaceIdAndUserId(workspaceId, userId) {
|
|
301
|
+
assert.equal(Number(workspaceId), 7);
|
|
302
|
+
assert.equal(Number(userId), 11);
|
|
303
|
+
return {
|
|
304
|
+
workspaceId: 7,
|
|
305
|
+
userId: 11,
|
|
306
|
+
roleId: "member",
|
|
307
|
+
status: "active"
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
async upsertMembership(workspaceId, userId, patch) {
|
|
311
|
+
assert.equal(Number(workspaceId), 7);
|
|
312
|
+
assert.equal(Number(userId), 11);
|
|
313
|
+
assert.deepEqual(patch, {
|
|
314
|
+
roleId: "member",
|
|
315
|
+
status: "revoked"
|
|
316
|
+
});
|
|
317
|
+
removed = true;
|
|
318
|
+
}
|
|
319
|
+
},
|
|
320
|
+
workspaceInvitesRepository: {
|
|
321
|
+
async listPendingByWorkspaceIdWithWorkspace() {
|
|
322
|
+
return [];
|
|
323
|
+
},
|
|
324
|
+
async expirePendingByWorkspaceIdAndEmail() {},
|
|
325
|
+
async insert() {},
|
|
326
|
+
async findPendingByIdForWorkspace() {
|
|
327
|
+
return null;
|
|
328
|
+
},
|
|
329
|
+
async revokeById() {}
|
|
330
|
+
},
|
|
331
|
+
inviteExpiresInMs: 7 * 24 * 60 * 60 * 1000,
|
|
332
|
+
roleCatalog: createRoleCatalog()
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const response = await service.removeMember(
|
|
336
|
+
workspace,
|
|
337
|
+
{
|
|
338
|
+
memberUserId: 11
|
|
339
|
+
},
|
|
340
|
+
authorizedOptions(["workspace.members.manage"])
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
assert.equal(response.members.length, 0);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("workspaceMembersService.removeMember rejects removing the owner", async () => {
|
|
347
|
+
const workspace = {
|
|
348
|
+
id: 7,
|
|
349
|
+
slug: "tonymobily3",
|
|
350
|
+
name: "TonyMobily3",
|
|
351
|
+
ownerUserId: 9,
|
|
352
|
+
avatarUrl: "",
|
|
353
|
+
color: "#0F6B54"
|
|
354
|
+
};
|
|
355
|
+
const service = createService({
|
|
356
|
+
workspaceMembershipsRepository: {
|
|
357
|
+
async listActiveByWorkspaceId() {
|
|
358
|
+
return [];
|
|
359
|
+
},
|
|
360
|
+
async findByWorkspaceIdAndUserId(workspaceId, userId) {
|
|
361
|
+
assert.equal(Number(workspaceId), 7);
|
|
362
|
+
assert.equal(Number(userId), 9);
|
|
363
|
+
return {
|
|
364
|
+
workspaceId: 7,
|
|
365
|
+
userId: 9,
|
|
366
|
+
roleId: "owner",
|
|
367
|
+
status: "active"
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
async upsertMembership() {
|
|
371
|
+
throw new Error("remove owner should not update membership");
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
workspaceInvitesRepository: {
|
|
375
|
+
async listPendingByWorkspaceIdWithWorkspace() {
|
|
376
|
+
return [];
|
|
377
|
+
},
|
|
378
|
+
async expirePendingByWorkspaceIdAndEmail() {},
|
|
379
|
+
async insert() {},
|
|
380
|
+
async findPendingByIdForWorkspace() {
|
|
381
|
+
return null;
|
|
382
|
+
},
|
|
383
|
+
async revokeById() {}
|
|
384
|
+
},
|
|
385
|
+
inviteExpiresInMs: 7 * 24 * 60 * 60 * 1000,
|
|
386
|
+
roleCatalog: createRoleCatalog()
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
await assert.rejects(
|
|
390
|
+
() =>
|
|
391
|
+
service.removeMember(
|
|
392
|
+
workspace,
|
|
393
|
+
{
|
|
394
|
+
memberUserId: 9
|
|
395
|
+
},
|
|
396
|
+
authorizedOptions(["workspace.members.manage"])
|
|
397
|
+
),
|
|
398
|
+
/Cannot remove workspace owner/
|
|
399
|
+
);
|
|
400
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { normalizePathname as normalizeKernelPathname } from "@jskit-ai/kernel/shared/surface/paths";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
normalizePathname,
|
|
7
|
+
normalizeSurfaceSegmentFromRouteBase,
|
|
8
|
+
parseWorkspacePathname,
|
|
9
|
+
resolveDefaultWorkspaceSurfaceId,
|
|
10
|
+
resolveWorkspaceSurfaceIdFromSuffixSegments
|
|
11
|
+
} from "../src/shared/support/workspacePathModel.js";
|
|
12
|
+
|
|
13
|
+
test("normalizePathname reuses kernel surface path helper", () => {
|
|
14
|
+
assert.equal(normalizePathname, normalizeKernelPathname);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("normalizePathname trims query/hash and trailing slashes", () => {
|
|
18
|
+
assert.equal(normalizePathname("w/acme/admin/?a=1#x"), "/w/acme/admin");
|
|
19
|
+
assert.equal(normalizePathname("///w//acme///"), "/w/acme");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("normalizeSurfaceSegmentFromRouteBase resolves workspace-aware segment", () => {
|
|
23
|
+
assert.equal(normalizeSurfaceSegmentFromRouteBase("/w/:workspaceSlug/admin"), "admin");
|
|
24
|
+
assert.equal(normalizeSurfaceSegmentFromRouteBase("/w/:workspaceSlug"), "");
|
|
25
|
+
assert.equal(normalizeSurfaceSegmentFromRouteBase("/"), "");
|
|
26
|
+
assert.equal(normalizeSurfaceSegmentFromRouteBase(""), "");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("parseWorkspacePathname reads workspace slug and suffix segments", () => {
|
|
30
|
+
assert.deepEqual(parseWorkspacePathname("/w/acme/admin/contacts"), {
|
|
31
|
+
workspaceSlug: "acme",
|
|
32
|
+
suffixSegments: ["admin", "contacts"]
|
|
33
|
+
});
|
|
34
|
+
assert.equal(parseWorkspacePathname("/admin/settings"), null);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("resolveDefaultWorkspaceSurfaceId keeps workspace default or first workspace fallback", () => {
|
|
38
|
+
assert.equal(
|
|
39
|
+
resolveDefaultWorkspaceSurfaceId({
|
|
40
|
+
defaultSurfaceId: "app",
|
|
41
|
+
workspaceSurfaceIds: ["admin", "app"],
|
|
42
|
+
surfaceRequiresWorkspace: (surfaceId) => surfaceId === "app"
|
|
43
|
+
}),
|
|
44
|
+
"app"
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
assert.equal(
|
|
48
|
+
resolveDefaultWorkspaceSurfaceId({
|
|
49
|
+
defaultSurfaceId: "app",
|
|
50
|
+
workspaceSurfaceIds: ["admin", "app"],
|
|
51
|
+
surfaceRequiresWorkspace: (surfaceId) => surfaceId === "admin"
|
|
52
|
+
}),
|
|
53
|
+
"admin"
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("resolveWorkspaceSurfaceIdFromSuffixSegments resolves prefixed workspace surfaces", () => {
|
|
58
|
+
assert.equal(
|
|
59
|
+
resolveWorkspaceSurfaceIdFromSuffixSegments({
|
|
60
|
+
suffixSegments: [],
|
|
61
|
+
defaultWorkspaceSurfaceId: "app",
|
|
62
|
+
workspaceSurfaces: [
|
|
63
|
+
{ id: "app", routeBase: "/w/:workspaceSlug" },
|
|
64
|
+
{ id: "admin", routeBase: "/w/:workspaceSlug/admin" }
|
|
65
|
+
]
|
|
66
|
+
}),
|
|
67
|
+
"app"
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
assert.equal(
|
|
71
|
+
resolveWorkspaceSurfaceIdFromSuffixSegments({
|
|
72
|
+
suffixSegments: ["admin", "contacts"],
|
|
73
|
+
defaultWorkspaceSurfaceId: "app",
|
|
74
|
+
workspaceSurfaces: [
|
|
75
|
+
{ id: "app", routeBase: "/w/:workspaceSlug" },
|
|
76
|
+
{ id: "admin", routeBase: "/w/:workspaceSlug/admin" }
|
|
77
|
+
]
|
|
78
|
+
}),
|
|
79
|
+
"admin"
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
assert.equal(
|
|
83
|
+
resolveWorkspaceSurfaceIdFromSuffixSegments({
|
|
84
|
+
suffixSegments: ["projects", "123"],
|
|
85
|
+
defaultWorkspaceSurfaceId: "app",
|
|
86
|
+
workspaceSurfaces: [
|
|
87
|
+
{ id: "app", routeBase: "/w/:workspaceSlug" },
|
|
88
|
+
{ id: "admin", routeBase: "/w/:workspaceSlug/admin" }
|
|
89
|
+
]
|
|
90
|
+
}),
|
|
91
|
+
"app"
|
|
92
|
+
);
|
|
93
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { encodeInviteTokenHash } from "@jskit-ai/auth-core/shared/inviteTokens";
|
|
4
|
+
import { workspacePendingInvitationsResource } from "../src/shared/resources/workspacePendingInvitationsResource.js";
|
|
5
|
+
|
|
6
|
+
test("workspacePendingInvitationsResource output normalizer shapes raw invite rows", () => {
|
|
7
|
+
const tokenHash = "a".repeat(64);
|
|
8
|
+
|
|
9
|
+
const result = workspacePendingInvitationsResource.operations.list.outputValidator.normalize({
|
|
10
|
+
pendingInvites: [
|
|
11
|
+
{
|
|
12
|
+
id: 10,
|
|
13
|
+
workspaceId: 3,
|
|
14
|
+
workspaceSlug: "tonymobily3",
|
|
15
|
+
workspaceName: "",
|
|
16
|
+
workspaceAvatarUrl: "",
|
|
17
|
+
roleId: "Member",
|
|
18
|
+
status: "Pending",
|
|
19
|
+
expiresAt: "2030-01-01T00:00:00.000Z",
|
|
20
|
+
tokenHash
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}).pendingInvites;
|
|
24
|
+
|
|
25
|
+
assert.deepEqual(result, [
|
|
26
|
+
{
|
|
27
|
+
id: 10,
|
|
28
|
+
workspaceId: 3,
|
|
29
|
+
workspaceSlug: "tonymobily3",
|
|
30
|
+
workspaceName: "tonymobily3",
|
|
31
|
+
workspaceAvatarUrl: "",
|
|
32
|
+
roleId: "member",
|
|
33
|
+
status: "pending",
|
|
34
|
+
expiresAt: "2030-01-01T00:00:00.000Z",
|
|
35
|
+
token: encodeInviteTokenHash(tokenHash)
|
|
36
|
+
}
|
|
37
|
+
]);
|
|
38
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { encodeInviteTokenHash } from "@jskit-ai/auth-core/shared/inviteTokens";
|
|
4
|
+
import { createService } from "../src/server/workspacePendingInvitations/workspacePendingInvitationsService.js";
|
|
5
|
+
|
|
6
|
+
function createFixture({
|
|
7
|
+
pendingInvitesByEmail = [],
|
|
8
|
+
inviteByTokenHash = null
|
|
9
|
+
} = {}) {
|
|
10
|
+
const tokenHashCalls = [];
|
|
11
|
+
const upsertCalls = [];
|
|
12
|
+
const revokeCalls = [];
|
|
13
|
+
const acceptCalls = [];
|
|
14
|
+
|
|
15
|
+
const service = createService({
|
|
16
|
+
workspaceInvitesRepository: {
|
|
17
|
+
async listPendingByEmail() {
|
|
18
|
+
return Array.isArray(pendingInvitesByEmail) ? [...pendingInvitesByEmail] : [];
|
|
19
|
+
},
|
|
20
|
+
async findPendingByTokenHash(tokenHash) {
|
|
21
|
+
tokenHashCalls.push(String(tokenHash || ""));
|
|
22
|
+
if (!inviteByTokenHash || typeof inviteByTokenHash !== "object") {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return inviteByTokenHash[String(tokenHash || "")] || null;
|
|
27
|
+
},
|
|
28
|
+
async revokeById(inviteId) {
|
|
29
|
+
revokeCalls.push(Number(inviteId));
|
|
30
|
+
},
|
|
31
|
+
async markAcceptedById(inviteId) {
|
|
32
|
+
acceptCalls.push(Number(inviteId));
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
workspaceMembershipsRepository: {
|
|
36
|
+
async upsertMembership(workspaceId, userId, payload) {
|
|
37
|
+
upsertCalls.push({
|
|
38
|
+
workspaceId: Number(workspaceId),
|
|
39
|
+
userId: Number(userId),
|
|
40
|
+
payload: payload && typeof payload === "object" ? { ...payload } : payload
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
service,
|
|
48
|
+
calls: {
|
|
49
|
+
tokenHashCalls,
|
|
50
|
+
upsertCalls,
|
|
51
|
+
revokeCalls,
|
|
52
|
+
acceptCalls
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
test("listPendingInvitesForUser returns raw pending invite rows for the action layer to shape", async () => {
|
|
58
|
+
const tokenHash = "a".repeat(64);
|
|
59
|
+
const { service } = createFixture({
|
|
60
|
+
pendingInvitesByEmail: [
|
|
61
|
+
{
|
|
62
|
+
id: 10,
|
|
63
|
+
workspaceId: 1,
|
|
64
|
+
workspaceSlug: "tonymobily3",
|
|
65
|
+
workspaceName: "TonyMobily3",
|
|
66
|
+
workspaceAvatarUrl: "",
|
|
67
|
+
roleId: "member",
|
|
68
|
+
status: "pending",
|
|
69
|
+
expiresAt: "2030-01-01T00:00:00.000Z",
|
|
70
|
+
tokenHash
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const pendingInvites = await service.listPendingInvitesForUser({
|
|
76
|
+
id: 7,
|
|
77
|
+
email: "chiaramobily@gmail.com"
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
assert.equal(pendingInvites.length, 1);
|
|
81
|
+
assert.equal(pendingInvites[0].tokenHash, tokenHash);
|
|
82
|
+
assert.equal(pendingInvites[0].workspaceName, "TonyMobily3");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("acceptInviteByToken accepts opaque invite token and resolves invite by decoded hash", async () => {
|
|
86
|
+
const tokenHash = "b".repeat(64);
|
|
87
|
+
const encodedToken = encodeInviteTokenHash(tokenHash);
|
|
88
|
+
const { service, calls } = createFixture({
|
|
89
|
+
inviteByTokenHash: {
|
|
90
|
+
[tokenHash]: {
|
|
91
|
+
id: 44,
|
|
92
|
+
workspaceId: 1,
|
|
93
|
+
email: "chiaramobily@gmail.com",
|
|
94
|
+
roleId: "member",
|
|
95
|
+
status: "pending",
|
|
96
|
+
tokenHash,
|
|
97
|
+
expiresAt: "2030-01-01T00:00:00.000Z"
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const response = await service.acceptInviteByToken({
|
|
103
|
+
user: {
|
|
104
|
+
id: 7,
|
|
105
|
+
email: "chiaramobily@gmail.com",
|
|
106
|
+
displayName: "Chiara"
|
|
107
|
+
},
|
|
108
|
+
token: encodedToken
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(calls.tokenHashCalls, [tokenHash]);
|
|
112
|
+
assert.equal(calls.upsertCalls.length, 1);
|
|
113
|
+
assert.deepEqual(calls.acceptCalls, [44]);
|
|
114
|
+
assert.deepEqual(calls.revokeCalls, []);
|
|
115
|
+
assert.equal(response.decision, "accepted");
|
|
116
|
+
assert.equal(response.workspaceId, 1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("refuseInviteByToken revokes the invite and returns refused", async () => {
|
|
120
|
+
const tokenHash = "c".repeat(64);
|
|
121
|
+
const encodedToken = encodeInviteTokenHash(tokenHash);
|
|
122
|
+
const { service, calls } = createFixture({
|
|
123
|
+
inviteByTokenHash: {
|
|
124
|
+
[tokenHash]: {
|
|
125
|
+
id: 45,
|
|
126
|
+
workspaceId: 1,
|
|
127
|
+
email: "chiaramobily@gmail.com",
|
|
128
|
+
roleId: "member",
|
|
129
|
+
status: "pending",
|
|
130
|
+
tokenHash,
|
|
131
|
+
expiresAt: "2030-01-01T00:00:00.000Z"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const response = await service.refuseInviteByToken({
|
|
137
|
+
user: {
|
|
138
|
+
id: 7,
|
|
139
|
+
email: "chiaramobily@gmail.com",
|
|
140
|
+
displayName: "Chiara"
|
|
141
|
+
},
|
|
142
|
+
token: encodedToken
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
assert.deepEqual(calls.tokenHashCalls, [tokenHash]);
|
|
146
|
+
assert.deepEqual(calls.acceptCalls, []);
|
|
147
|
+
assert.deepEqual(calls.revokeCalls, [45]);
|
|
148
|
+
assert.equal(calls.upsertCalls.length, 0);
|
|
149
|
+
assert.equal(response.decision, "refused");
|
|
150
|
+
assert.equal(response.workspaceId, 1);
|
|
151
|
+
});
|