@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,466 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createWorkspaceBootstrapContributor } from "../src/server/workspaceBootstrapContributor.js";
|
|
4
|
+
import {
|
|
5
|
+
TENANCY_MODE_PERSONAL,
|
|
6
|
+
WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
|
|
7
|
+
} from "../src/shared/tenancyProfile.js";
|
|
8
|
+
|
|
9
|
+
function createAuthenticatedProfile(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
id: 7,
|
|
12
|
+
authProvider: "local",
|
|
13
|
+
authProviderUserId: "user-7",
|
|
14
|
+
username: "tester",
|
|
15
|
+
displayName: "Test User",
|
|
16
|
+
email: "test@example.com",
|
|
17
|
+
...overrides
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("workspace bootstrap contributor passes actor context to pending invites service", async () => {
|
|
22
|
+
const profile = createAuthenticatedProfile();
|
|
23
|
+
const pendingServiceCalls = [];
|
|
24
|
+
const contributor = createWorkspaceBootstrapContributor({
|
|
25
|
+
workspaceService: {
|
|
26
|
+
async listWorkspacesForUser() {
|
|
27
|
+
return [];
|
|
28
|
+
},
|
|
29
|
+
async resolveWorkspaceContextForUserBySlug() {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
workspacePendingInvitationsService: {
|
|
34
|
+
async listPendingInvitesForUser(user, options = {}) {
|
|
35
|
+
pendingServiceCalls.push({
|
|
36
|
+
user,
|
|
37
|
+
options
|
|
38
|
+
});
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
userProfilesRepository: {
|
|
43
|
+
async findByIdentity() {
|
|
44
|
+
return profile;
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
userSettingsRepository: {
|
|
48
|
+
async ensureForUserId() {
|
|
49
|
+
return {
|
|
50
|
+
theme: "system",
|
|
51
|
+
locale: "en",
|
|
52
|
+
timeZone: "UTC",
|
|
53
|
+
dateFormat: "YYYY-MM-DD",
|
|
54
|
+
numberFormat: "1,234.56",
|
|
55
|
+
currencyCode: "USD",
|
|
56
|
+
avatarSize: 64,
|
|
57
|
+
productUpdates: true,
|
|
58
|
+
accountActivity: true,
|
|
59
|
+
securityAlerts: true
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
workspaceInvitationsEnabled: true,
|
|
64
|
+
appConfig: {
|
|
65
|
+
tenancyMode: "workspace"
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
await contributor.contribute({
|
|
70
|
+
request: {
|
|
71
|
+
async executeAction() {
|
|
72
|
+
return {
|
|
73
|
+
authenticated: true,
|
|
74
|
+
profile
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
reply: {}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
assert.equal(pendingServiceCalls.length, 1);
|
|
82
|
+
assert.equal(pendingServiceCalls[0].user.id, profile.id);
|
|
83
|
+
assert.equal(pendingServiceCalls[0].options?.context?.actor?.id, profile.id);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("workspace bootstrap contributor seeds the initial console owner on authenticated bootstrap", async () => {
|
|
87
|
+
const profile = createAuthenticatedProfile({ id: 12 });
|
|
88
|
+
const consoleOwnerSeeds = [];
|
|
89
|
+
|
|
90
|
+
const contributor = createWorkspaceBootstrapContributor({
|
|
91
|
+
workspaceService: {
|
|
92
|
+
async listWorkspacesForUser() {
|
|
93
|
+
return [];
|
|
94
|
+
},
|
|
95
|
+
async resolveWorkspaceContextForUserBySlug() {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
workspacePendingInvitationsService: {
|
|
100
|
+
async listPendingInvitesForUser() {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
userProfilesRepository: {
|
|
105
|
+
async findByIdentity() {
|
|
106
|
+
return profile;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
userSettingsRepository: {
|
|
110
|
+
async ensureForUserId() {
|
|
111
|
+
return {
|
|
112
|
+
theme: "system",
|
|
113
|
+
locale: "en",
|
|
114
|
+
timeZone: "UTC",
|
|
115
|
+
dateFormat: "YYYY-MM-DD",
|
|
116
|
+
numberFormat: "1,234.56",
|
|
117
|
+
currencyCode: "USD",
|
|
118
|
+
avatarSize: 64,
|
|
119
|
+
productUpdates: true,
|
|
120
|
+
accountActivity: true,
|
|
121
|
+
securityAlerts: true
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
workspaceInvitationsEnabled: false,
|
|
126
|
+
consoleService: {
|
|
127
|
+
async ensureInitialConsoleMember(userId) {
|
|
128
|
+
consoleOwnerSeeds.push(Number(userId));
|
|
129
|
+
return Number(userId);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const payload = await contributor.contribute({
|
|
135
|
+
request: {
|
|
136
|
+
async executeAction() {
|
|
137
|
+
return {
|
|
138
|
+
authenticated: true,
|
|
139
|
+
profile
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
reply: {}
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
assert.deepEqual(consoleOwnerSeeds, [12]);
|
|
147
|
+
assert.equal(payload.surfaceAccess?.consoleowner, true);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("workspace bootstrap contributor emits canonical tenancy profile from users-core", async () => {
|
|
151
|
+
const contributor = createWorkspaceBootstrapContributor({
|
|
152
|
+
workspaceService: {
|
|
153
|
+
async listWorkspacesForUser() {
|
|
154
|
+
return [];
|
|
155
|
+
},
|
|
156
|
+
async resolveWorkspaceContextForUserBySlug() {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
workspacePendingInvitationsService: {
|
|
161
|
+
async listPendingInvitesForUser() {
|
|
162
|
+
return [];
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
userProfilesRepository: {
|
|
166
|
+
async findByIdentity() {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
userSettingsRepository: {
|
|
171
|
+
async ensureForUserId() {
|
|
172
|
+
return {};
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
workspaceInvitationsEnabled: false,
|
|
176
|
+
tenancyProfile: {
|
|
177
|
+
mode: TENANCY_MODE_PERSONAL,
|
|
178
|
+
workspace: {
|
|
179
|
+
enabled: true,
|
|
180
|
+
autoProvision: true,
|
|
181
|
+
allowSelfCreate: false,
|
|
182
|
+
slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
appConfig: {
|
|
186
|
+
tenancyMode: "none"
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const payload = await contributor.contribute({
|
|
191
|
+
request: {
|
|
192
|
+
async executeAction() {
|
|
193
|
+
return {
|
|
194
|
+
authenticated: false
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
reply: {}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
assert.deepEqual(payload.tenancy, {
|
|
202
|
+
mode: TENANCY_MODE_PERSONAL,
|
|
203
|
+
workspace: {
|
|
204
|
+
enabled: true,
|
|
205
|
+
autoProvision: true,
|
|
206
|
+
allowSelfCreate: false,
|
|
207
|
+
slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
assert.equal(payload.app.tenancyMode, undefined);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("workspace bootstrap contributor resolves workspace slug from bootstrap query", async () => {
|
|
214
|
+
const profile = createAuthenticatedProfile();
|
|
215
|
+
const calls = [];
|
|
216
|
+
const contributor = createWorkspaceBootstrapContributor({
|
|
217
|
+
workspaceService: {
|
|
218
|
+
async listWorkspacesForUser() {
|
|
219
|
+
return [];
|
|
220
|
+
},
|
|
221
|
+
async resolveWorkspaceContextForUserBySlug(_user, workspaceSlug) {
|
|
222
|
+
calls.push(workspaceSlug);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
workspacePendingInvitationsService: {
|
|
227
|
+
async listPendingInvitesForUser() {
|
|
228
|
+
return [];
|
|
229
|
+
}
|
|
230
|
+
},
|
|
231
|
+
userProfilesRepository: {
|
|
232
|
+
async findByIdentity() {
|
|
233
|
+
return profile;
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
userSettingsRepository: {
|
|
237
|
+
async ensureForUserId() {
|
|
238
|
+
return {
|
|
239
|
+
theme: "system",
|
|
240
|
+
locale: "en",
|
|
241
|
+
timeZone: "UTC",
|
|
242
|
+
dateFormat: "YYYY-MM-DD",
|
|
243
|
+
numberFormat: "1,234.56",
|
|
244
|
+
currencyCode: "USD",
|
|
245
|
+
avatarSize: 64,
|
|
246
|
+
productUpdates: true,
|
|
247
|
+
accountActivity: true,
|
|
248
|
+
securityAlerts: true
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
workspaceInvitationsEnabled: true,
|
|
253
|
+
appConfig: {
|
|
254
|
+
tenancyMode: "workspace"
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
await contributor.contribute({
|
|
259
|
+
query: {
|
|
260
|
+
workspaceSlug: " AcMe "
|
|
261
|
+
},
|
|
262
|
+
request: {
|
|
263
|
+
async executeAction() {
|
|
264
|
+
return {
|
|
265
|
+
authenticated: true,
|
|
266
|
+
profile
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
reply: {}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
assert.deepEqual(calls, ["acme"]);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("workspace bootstrap contributor returns global payload with requestedWorkspace=forbidden when slug access is denied", async () => {
|
|
277
|
+
const profile = createAuthenticatedProfile();
|
|
278
|
+
const contributor = createWorkspaceBootstrapContributor({
|
|
279
|
+
workspaceService: {
|
|
280
|
+
async listWorkspacesForUser() {
|
|
281
|
+
return [{ id: 3, slug: "chiara", name: "Chiara Workspace" }];
|
|
282
|
+
},
|
|
283
|
+
async resolveWorkspaceContextForUserBySlug() {
|
|
284
|
+
const error = new Error("Forbidden.");
|
|
285
|
+
error.status = 403;
|
|
286
|
+
throw error;
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
workspacePendingInvitationsService: {
|
|
290
|
+
async listPendingInvitesForUser() {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
userProfilesRepository: {
|
|
295
|
+
async findByIdentity() {
|
|
296
|
+
return profile;
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
userSettingsRepository: {
|
|
300
|
+
async ensureForUserId() {
|
|
301
|
+
return {
|
|
302
|
+
theme: "system",
|
|
303
|
+
locale: "en",
|
|
304
|
+
timeZone: "UTC",
|
|
305
|
+
dateFormat: "YYYY-MM-DD",
|
|
306
|
+
numberFormat: "1,234.56",
|
|
307
|
+
currencyCode: "USD",
|
|
308
|
+
avatarSize: 64,
|
|
309
|
+
productUpdates: true,
|
|
310
|
+
accountActivity: true,
|
|
311
|
+
securityAlerts: true
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
},
|
|
315
|
+
workspaceInvitationsEnabled: true,
|
|
316
|
+
appConfig: {
|
|
317
|
+
tenancyMode: "workspace"
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const payload = await contributor.contribute({
|
|
322
|
+
query: {
|
|
323
|
+
workspaceSlug: "tonymobily"
|
|
324
|
+
},
|
|
325
|
+
request: {
|
|
326
|
+
async executeAction() {
|
|
327
|
+
return {
|
|
328
|
+
authenticated: true,
|
|
329
|
+
profile
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
reply: {}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
assert.equal(payload.session.authenticated, true);
|
|
337
|
+
assert.deepEqual(payload.workspaces, [{ id: 3, slug: "chiara", name: "Chiara Workspace" }]);
|
|
338
|
+
assert.deepEqual(payload.requestedWorkspace, {
|
|
339
|
+
slug: "tonymobily",
|
|
340
|
+
status: "forbidden"
|
|
341
|
+
});
|
|
342
|
+
assert.equal(payload.activeWorkspace, null);
|
|
343
|
+
assert.equal(payload.membership, null);
|
|
344
|
+
assert.deepEqual(payload.permissions, []);
|
|
345
|
+
assert.equal(payload.workspaceSettings, null);
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("workspace bootstrap contributor returns requestedWorkspace=not_found when slug does not exist", async () => {
|
|
349
|
+
const profile = createAuthenticatedProfile();
|
|
350
|
+
const contributor = createWorkspaceBootstrapContributor({
|
|
351
|
+
workspaceService: {
|
|
352
|
+
async listWorkspacesForUser() {
|
|
353
|
+
return [{ id: 1, slug: "acme", name: "Acme Workspace" }];
|
|
354
|
+
},
|
|
355
|
+
async resolveWorkspaceContextForUserBySlug() {
|
|
356
|
+
const error = new Error("Workspace not found.");
|
|
357
|
+
error.status = 404;
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
workspacePendingInvitationsService: {
|
|
362
|
+
async listPendingInvitesForUser() {
|
|
363
|
+
return [];
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
userProfilesRepository: {
|
|
367
|
+
async findByIdentity() {
|
|
368
|
+
return profile;
|
|
369
|
+
}
|
|
370
|
+
},
|
|
371
|
+
userSettingsRepository: {
|
|
372
|
+
async ensureForUserId() {
|
|
373
|
+
return {
|
|
374
|
+
theme: "system",
|
|
375
|
+
locale: "en",
|
|
376
|
+
timeZone: "UTC",
|
|
377
|
+
dateFormat: "YYYY-MM-DD",
|
|
378
|
+
numberFormat: "1,234.56",
|
|
379
|
+
currencyCode: "USD",
|
|
380
|
+
avatarSize: 64,
|
|
381
|
+
productUpdates: true,
|
|
382
|
+
accountActivity: true,
|
|
383
|
+
securityAlerts: true
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
workspaceInvitationsEnabled: false,
|
|
388
|
+
appConfig: {
|
|
389
|
+
tenancyMode: "workspace"
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const payload = await contributor.contribute({
|
|
394
|
+
query: {
|
|
395
|
+
workspaceSlug: "missing-workspace"
|
|
396
|
+
},
|
|
397
|
+
request: {
|
|
398
|
+
async executeAction() {
|
|
399
|
+
return {
|
|
400
|
+
authenticated: true,
|
|
401
|
+
profile
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
reply: {}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
assert.deepEqual(payload.requestedWorkspace, {
|
|
409
|
+
slug: "missing-workspace",
|
|
410
|
+
status: "not_found"
|
|
411
|
+
});
|
|
412
|
+
assert.deepEqual(payload.workspaces, [{ id: 1, slug: "acme", name: "Acme Workspace" }]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("workspace bootstrap contributor returns requestedWorkspace=unauthenticated for anonymous workspace slug query", async () => {
|
|
416
|
+
const contributor = createWorkspaceBootstrapContributor({
|
|
417
|
+
workspaceService: {
|
|
418
|
+
async listWorkspacesForUser() {
|
|
419
|
+
return [];
|
|
420
|
+
},
|
|
421
|
+
async resolveWorkspaceContextForUserBySlug() {
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
workspacePendingInvitationsService: {
|
|
426
|
+
async listPendingInvitesForUser() {
|
|
427
|
+
return [];
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
userProfilesRepository: {
|
|
431
|
+
async findByIdentity() {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
},
|
|
435
|
+
userSettingsRepository: {
|
|
436
|
+
async ensureForUserId() {
|
|
437
|
+
return {};
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
workspaceInvitationsEnabled: false,
|
|
441
|
+
appConfig: {
|
|
442
|
+
tenancyMode: "workspace"
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
const payload = await contributor.contribute({
|
|
447
|
+
query: {
|
|
448
|
+
workspaceSlug: "tonymobily"
|
|
449
|
+
},
|
|
450
|
+
request: {
|
|
451
|
+
async executeAction() {
|
|
452
|
+
return {
|
|
453
|
+
authenticated: false
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
},
|
|
457
|
+
reply: {}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
assert.equal(payload.session.authenticated, false);
|
|
461
|
+
assert.deepEqual(payload.requestedWorkspace, {
|
|
462
|
+
slug: "tonymobily",
|
|
463
|
+
status: "unauthenticated"
|
|
464
|
+
});
|
|
465
|
+
assert.deepEqual(payload.workspaces, []);
|
|
466
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { resolveWorkspaceInvitationsPolicy } from "../src/server/support/workspaceInvitationsPolicy.js";
|
|
4
|
+
|
|
5
|
+
test("workspace invitations policy enables invitations by default in personal mode", () => {
|
|
6
|
+
const policy = resolveWorkspaceInvitationsPolicy({
|
|
7
|
+
appConfig: {},
|
|
8
|
+
tenancyProfile: {
|
|
9
|
+
mode: "personal",
|
|
10
|
+
workspace: {
|
|
11
|
+
enabled: true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
assert.equal(policy.enabled, true);
|
|
17
|
+
assert.equal(policy.allowInPersonalMode, true);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("workspace invitations policy disables invitations in personal mode when explicitly configured", () => {
|
|
21
|
+
const policy = resolveWorkspaceInvitationsPolicy({
|
|
22
|
+
appConfig: {
|
|
23
|
+
workspaceInvitations: {
|
|
24
|
+
allowInPersonalMode: false
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
tenancyProfile: {
|
|
28
|
+
mode: "personal",
|
|
29
|
+
workspace: {
|
|
30
|
+
enabled: true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
assert.equal(policy.enabled, false);
|
|
36
|
+
assert.equal(policy.allowInPersonalMode, false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("workspace invitations policy disables invitations when workspace mode is disabled", () => {
|
|
40
|
+
const policy = resolveWorkspaceInvitationsPolicy({
|
|
41
|
+
appConfig: {},
|
|
42
|
+
tenancyProfile: {
|
|
43
|
+
mode: "none",
|
|
44
|
+
workspace: {
|
|
45
|
+
enabled: false
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
assert.equal(policy.enabled, false);
|
|
51
|
+
assert.equal(policy.workspaceEnabled, false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("workspace invitations policy disables invitations when app config disables feature", () => {
|
|
55
|
+
const policy = resolveWorkspaceInvitationsPolicy({
|
|
56
|
+
appConfig: {
|
|
57
|
+
workspaceInvitations: {
|
|
58
|
+
enabled: false,
|
|
59
|
+
allowInPersonalMode: true
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
tenancyProfile: {
|
|
63
|
+
mode: "workspace",
|
|
64
|
+
workspace: {
|
|
65
|
+
enabled: true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
assert.equal(policy.enabled, false);
|
|
71
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createRepository } from "../src/server/common/repositories/workspaceInvitesRepository.js";
|
|
4
|
+
|
|
5
|
+
function createKnexStub() {
|
|
6
|
+
const state = {
|
|
7
|
+
insertPayload: null
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const row = {
|
|
11
|
+
id: 1,
|
|
12
|
+
workspace_id: 1,
|
|
13
|
+
email: "invitee@example.com",
|
|
14
|
+
role_id: "member",
|
|
15
|
+
status: "pending",
|
|
16
|
+
token_hash: "hash",
|
|
17
|
+
invited_by_user_id: 1,
|
|
18
|
+
expires_at: "2026-03-16 00:26:35.709",
|
|
19
|
+
accepted_at: null,
|
|
20
|
+
revoked_at: null,
|
|
21
|
+
created_at: "2026-03-09 00:26:35.710",
|
|
22
|
+
updated_at: "2026-03-09 00:26:35.710"
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function tableBuilder(tableName) {
|
|
26
|
+
assert.equal(tableName, "workspace_invites");
|
|
27
|
+
return {
|
|
28
|
+
insert(payload) {
|
|
29
|
+
state.insertPayload = payload;
|
|
30
|
+
return Promise.resolve([1]);
|
|
31
|
+
},
|
|
32
|
+
where(criteria) {
|
|
33
|
+
assert.equal(typeof criteria, "object");
|
|
34
|
+
return {
|
|
35
|
+
first() {
|
|
36
|
+
return Promise.resolve({ ...row });
|
|
37
|
+
},
|
|
38
|
+
orderBy() {
|
|
39
|
+
return {
|
|
40
|
+
first() {
|
|
41
|
+
return Promise.resolve({ ...row });
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { knexStub: tableBuilder, state };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
test("workspaceInvitesRepository.insert normalizes expiresAt ISO input to database datetime", async () => {
|
|
54
|
+
const { knexStub, state } = createKnexStub();
|
|
55
|
+
const repository = createRepository(knexStub);
|
|
56
|
+
|
|
57
|
+
await repository.insert({
|
|
58
|
+
workspaceId: 1,
|
|
59
|
+
email: "invitee@example.com",
|
|
60
|
+
roleId: "member",
|
|
61
|
+
status: "pending",
|
|
62
|
+
tokenHash: "hash",
|
|
63
|
+
invitedByUserId: 1,
|
|
64
|
+
expiresAt: "2026-03-16T00:26:35.709Z"
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.equal(state.insertPayload.expires_at, "2026-03-16 00:26:35.709");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("workspaceInvitesRepository.findPendingByTokenHash reads from invites table without workspace join", async () => {
|
|
71
|
+
const calls = {
|
|
72
|
+
tableName: "",
|
|
73
|
+
whereCriteria: null
|
|
74
|
+
};
|
|
75
|
+
const row = {
|
|
76
|
+
id: 44,
|
|
77
|
+
workspace_id: 9,
|
|
78
|
+
email: "invitee@example.com",
|
|
79
|
+
role_id: "member",
|
|
80
|
+
status: "pending",
|
|
81
|
+
token_hash: "hash-token",
|
|
82
|
+
invited_by_user_id: 1,
|
|
83
|
+
expires_at: "2030-01-01 00:00:00.000",
|
|
84
|
+
accepted_at: null,
|
|
85
|
+
revoked_at: null,
|
|
86
|
+
created_at: "2026-03-09 00:26:35.710",
|
|
87
|
+
updated_at: "2026-03-09 00:26:35.710"
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const repository = createRepository((tableName) => {
|
|
91
|
+
calls.tableName = String(tableName || "");
|
|
92
|
+
return {
|
|
93
|
+
where(criteria) {
|
|
94
|
+
calls.whereCriteria = criteria;
|
|
95
|
+
return this;
|
|
96
|
+
},
|
|
97
|
+
first() {
|
|
98
|
+
return Promise.resolve({ ...row });
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const invite = await repository.findPendingByTokenHash("hash-token");
|
|
104
|
+
assert.equal(calls.tableName, "workspace_invites");
|
|
105
|
+
assert.deepEqual(calls.whereCriteria, {
|
|
106
|
+
token_hash: "hash-token",
|
|
107
|
+
status: "pending"
|
|
108
|
+
});
|
|
109
|
+
assert.equal(invite?.workspaceId, 9);
|
|
110
|
+
assert.equal(invite?.workspaceSlug, undefined);
|
|
111
|
+
});
|