@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,114 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { createService } from "../src/server/accountProfile/avatarService.js";
|
|
5
|
+
|
|
6
|
+
function createRepositoryDouble(initialProfile) {
|
|
7
|
+
const state = {
|
|
8
|
+
profile: { ...initialProfile }
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
state,
|
|
13
|
+
async findByIdentity() {
|
|
14
|
+
return { ...state.profile };
|
|
15
|
+
},
|
|
16
|
+
async findById() {
|
|
17
|
+
return { ...state.profile };
|
|
18
|
+
},
|
|
19
|
+
async updateAvatarById(userId, avatar = {}) {
|
|
20
|
+
state.profile = {
|
|
21
|
+
...state.profile,
|
|
22
|
+
id: Number(userId),
|
|
23
|
+
avatarStorageKey: avatar.avatarStorageKey || null,
|
|
24
|
+
avatarVersion: avatar.avatarVersion == null ? null : String(avatar.avatarVersion)
|
|
25
|
+
};
|
|
26
|
+
return { ...state.profile };
|
|
27
|
+
},
|
|
28
|
+
async clearAvatarById(userId) {
|
|
29
|
+
state.profile = {
|
|
30
|
+
...state.profile,
|
|
31
|
+
id: Number(userId),
|
|
32
|
+
avatarStorageKey: null,
|
|
33
|
+
avatarVersion: null
|
|
34
|
+
};
|
|
35
|
+
return { ...state.profile };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("avatarService uploadForUser stores bytes and updates profile avatar fields", async () => {
|
|
41
|
+
const repository = createRepositoryDouble({
|
|
42
|
+
id: 7,
|
|
43
|
+
authProvider: "local",
|
|
44
|
+
authProviderUserId: "u-7",
|
|
45
|
+
email: "test@example.com",
|
|
46
|
+
displayName: "Tester",
|
|
47
|
+
avatarStorageKey: null,
|
|
48
|
+
avatarVersion: null
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const savedPayloads = [];
|
|
52
|
+
const avatarStorageService = {
|
|
53
|
+
async saveAvatar(payload) {
|
|
54
|
+
savedPayloads.push(payload);
|
|
55
|
+
return {
|
|
56
|
+
storageKey: "users/avatars/7/avatar"
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const avatarService = createService({
|
|
62
|
+
userProfilesRepository: repository,
|
|
63
|
+
avatarStorageService
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const user = {
|
|
67
|
+
authProvider: "local",
|
|
68
|
+
authProviderUserId: "u-7"
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = await avatarService.uploadForUser(user, {
|
|
72
|
+
mimeType: "image/png",
|
|
73
|
+
stream: Readable.from([Buffer.from([0x89, 0x50, 0x4e, 0x47])])
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
assert.equal(savedPayloads.length, 1);
|
|
77
|
+
assert.equal(savedPayloads[0].userId, 7);
|
|
78
|
+
assert.ok(Buffer.isBuffer(savedPayloads[0].buffer));
|
|
79
|
+
assert.equal(result.profile.avatarStorageKey, "users/avatars/7/avatar");
|
|
80
|
+
assert.equal(typeof result.profile.avatarVersion, "string");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("avatarService clearForUser removes stored avatar and clears profile fields", async () => {
|
|
84
|
+
const repository = createRepositoryDouble({
|
|
85
|
+
id: 7,
|
|
86
|
+
authProvider: "local",
|
|
87
|
+
authProviderUserId: "u-7",
|
|
88
|
+
email: "test@example.com",
|
|
89
|
+
displayName: "Tester",
|
|
90
|
+
avatarStorageKey: "users/avatars/7/avatar",
|
|
91
|
+
avatarVersion: "123"
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const deletedKeys = [];
|
|
95
|
+
const avatarStorageService = {
|
|
96
|
+
async deleteAvatar(storageKey) {
|
|
97
|
+
deletedKeys.push(storageKey);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const avatarService = createService({
|
|
102
|
+
userProfilesRepository: repository,
|
|
103
|
+
avatarStorageService
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const profile = await avatarService.clearForUser({
|
|
107
|
+
authProvider: "local",
|
|
108
|
+
authProviderUserId: "u-7"
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
assert.deepEqual(deletedKeys, ["users/avatars/7/avatar"]);
|
|
112
|
+
assert.equal(profile.avatarStorageKey, null);
|
|
113
|
+
assert.equal(profile.avatarVersion, null);
|
|
114
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createService, __testables } from "../src/server/accountProfile/avatarStorageService.js";
|
|
4
|
+
|
|
5
|
+
function createStorageDouble() {
|
|
6
|
+
const values = new Map();
|
|
7
|
+
return {
|
|
8
|
+
async getItemRaw(key) {
|
|
9
|
+
return values.has(key) ? values.get(key) : null;
|
|
10
|
+
},
|
|
11
|
+
async setItemRaw(key, value) {
|
|
12
|
+
values.set(key, value);
|
|
13
|
+
},
|
|
14
|
+
async removeItem(key) {
|
|
15
|
+
values.delete(key);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
test("avatarStorageService builds stable storage key by user id", () => {
|
|
21
|
+
assert.equal(__testables.buildAvatarStorageKey(7), "users/avatars/7/avatar");
|
|
22
|
+
assert.throws(() => __testables.buildAvatarStorageKey(0), /positive integer user id/);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("avatarStorageService detects common avatar mime types", () => {
|
|
26
|
+
assert.equal(
|
|
27
|
+
__testables.detectAvatarMimeTypeFromBuffer(Buffer.from([0xff, 0xd8, 0xff, 0xdb])),
|
|
28
|
+
"image/jpeg"
|
|
29
|
+
);
|
|
30
|
+
assert.equal(
|
|
31
|
+
__testables.detectAvatarMimeTypeFromBuffer(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])),
|
|
32
|
+
"image/png"
|
|
33
|
+
);
|
|
34
|
+
assert.equal(
|
|
35
|
+
__testables.detectAvatarMimeTypeFromBuffer(
|
|
36
|
+
Buffer.from([0x52, 0x49, 0x46, 0x46, 0x11, 0x22, 0x33, 0x44, 0x57, 0x45, 0x42, 0x50])
|
|
37
|
+
),
|
|
38
|
+
"image/webp"
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("avatarStorageService saves, reads, and deletes avatar bytes", async () => {
|
|
43
|
+
const storage = createStorageDouble();
|
|
44
|
+
const avatarStorageService = createService({ storage });
|
|
45
|
+
const payload = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
46
|
+
|
|
47
|
+
const saved = await avatarStorageService.saveAvatar({
|
|
48
|
+
userId: 42,
|
|
49
|
+
buffer: payload
|
|
50
|
+
});
|
|
51
|
+
assert.equal(saved.storageKey, "users/avatars/42/avatar");
|
|
52
|
+
|
|
53
|
+
const loaded = await avatarStorageService.readAvatar(saved.storageKey);
|
|
54
|
+
assert.equal(loaded?.mimeType, "image/png");
|
|
55
|
+
assert.ok(Buffer.isBuffer(loaded?.buffer));
|
|
56
|
+
assert.equal(loaded?.buffer?.toString("hex"), payload.toString("hex"));
|
|
57
|
+
|
|
58
|
+
await avatarStorageService.deleteAvatar(saved.storageKey);
|
|
59
|
+
const missing = await avatarStorageService.readAvatar(saved.storageKey);
|
|
60
|
+
assert.equal(missing, null);
|
|
61
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createService } from "../src/server/consoleSettings/consoleService.js";
|
|
4
|
+
|
|
5
|
+
function createFixture(initialOwnerUserId = null) {
|
|
6
|
+
const state = {
|
|
7
|
+
ownerUserId: initialOwnerUserId
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const service = createService({
|
|
11
|
+
consoleSettingsRepository: {
|
|
12
|
+
async ensureOwnerUserId(userId) {
|
|
13
|
+
const normalizedUserId = Number(userId);
|
|
14
|
+
if (!state.ownerUserId) {
|
|
15
|
+
state.ownerUserId = normalizedUserId;
|
|
16
|
+
}
|
|
17
|
+
return state.ownerUserId;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return { service, state };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("consoleService seeds the first authenticated user as console owner", async () => {
|
|
26
|
+
const { service, state } = createFixture();
|
|
27
|
+
|
|
28
|
+
const firstOwner = await service.ensureInitialConsoleMember(7);
|
|
29
|
+
const secondAttempt = await service.ensureInitialConsoleMember(9);
|
|
30
|
+
|
|
31
|
+
assert.equal(firstOwner, 7);
|
|
32
|
+
assert.equal(secondAttempt, 7);
|
|
33
|
+
assert.equal(state.ownerUserId, 7);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("consoleService.requireConsoleOwner denies authenticated non-owners", async () => {
|
|
37
|
+
const { service } = createFixture(7);
|
|
38
|
+
|
|
39
|
+
await assert.rejects(
|
|
40
|
+
() =>
|
|
41
|
+
service.requireConsoleOwner({
|
|
42
|
+
actor: {
|
|
43
|
+
id: 9
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
(error) => error?.status === 403
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("consoleService.requireConsoleOwner requires authentication", async () => {
|
|
51
|
+
const { service } = createFixture(7);
|
|
52
|
+
|
|
53
|
+
await assert.rejects(
|
|
54
|
+
() => service.requireConsoleOwner({}),
|
|
55
|
+
(error) => error?.status === 401
|
|
56
|
+
);
|
|
57
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createService } from "../src/server/consoleSettings/consoleSettingsService.js";
|
|
4
|
+
|
|
5
|
+
function createFixture({ deny = false } = {}) {
|
|
6
|
+
const calls = {
|
|
7
|
+
requireConsoleOwner: [],
|
|
8
|
+
updateSingleton: []
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const service = createService({
|
|
12
|
+
consoleService: {
|
|
13
|
+
async requireConsoleOwner(context) {
|
|
14
|
+
calls.requireConsoleOwner.push(context || null);
|
|
15
|
+
if (deny) {
|
|
16
|
+
const error = new Error("Forbidden.");
|
|
17
|
+
error.status = 403;
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
consoleSettingsRepository: {
|
|
23
|
+
async getSingleton() {
|
|
24
|
+
return {};
|
|
25
|
+
},
|
|
26
|
+
async updateSingleton(patch = {}) {
|
|
27
|
+
calls.updateSingleton.push({ ...patch });
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return { service, calls };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("consoleSettingsService.getSettings requires owner access and returns normalized payload", async () => {
|
|
37
|
+
const { service, calls } = createFixture();
|
|
38
|
+
const context = {
|
|
39
|
+
actor: {
|
|
40
|
+
id: 7
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const response = await service.getSettings({ context });
|
|
45
|
+
|
|
46
|
+
assert.deepEqual(calls.requireConsoleOwner, [context]);
|
|
47
|
+
assert.deepEqual(response, {
|
|
48
|
+
settings: {}
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("consoleSettingsService.updateSettings requires owner access before writing", async () => {
|
|
53
|
+
const { service, calls } = createFixture();
|
|
54
|
+
const context = {
|
|
55
|
+
actor: {
|
|
56
|
+
id: 7
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const response = await service.updateSettings(
|
|
61
|
+
{},
|
|
62
|
+
{ context }
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
assert.deepEqual(calls.requireConsoleOwner, [context]);
|
|
66
|
+
assert.deepEqual(calls.updateSingleton, [{}]);
|
|
67
|
+
assert.deepEqual(response, {
|
|
68
|
+
settings: {}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("consoleSettingsService denies access when owner validation fails", async () => {
|
|
73
|
+
const { service } = createFixture({ deny: true });
|
|
74
|
+
|
|
75
|
+
await assert.rejects(
|
|
76
|
+
() =>
|
|
77
|
+
service.getSettings({
|
|
78
|
+
context: {
|
|
79
|
+
actor: {
|
|
80
|
+
id: 9
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}),
|
|
84
|
+
(error) => error?.status === 403
|
|
85
|
+
);
|
|
86
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { evaluatePackageExportsContract } from "../../../tooling/test-support/exportsContract.mjs";
|
|
6
|
+
|
|
7
|
+
const TEST_DIRECTORY = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const REPO_ROOT = path.resolve(TEST_DIRECTORY, "..", "..", "..");
|
|
9
|
+
const PACKAGE_DIR = path.join(REPO_ROOT, "packages", "users-core");
|
|
10
|
+
|
|
11
|
+
test("users-core exports are explicit and aligned with production/template usage", () => {
|
|
12
|
+
const result = evaluatePackageExportsContract({
|
|
13
|
+
repoRoot: REPO_ROOT,
|
|
14
|
+
packageDir: PACKAGE_DIR,
|
|
15
|
+
packageId: "@jskit-ai/users-core"
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
assert.deepEqual(
|
|
19
|
+
result.wildcardExports,
|
|
20
|
+
[],
|
|
21
|
+
`users-core exports must be explicit. Remove wildcard keys: ${result.wildcardExports.join(", ")}`
|
|
22
|
+
);
|
|
23
|
+
assert.deepEqual(
|
|
24
|
+
result.missingRequiredExports,
|
|
25
|
+
[],
|
|
26
|
+
`users-core required exports missing: ${result.missingRequiredExports.join(", ")}`
|
|
27
|
+
);
|
|
28
|
+
assert.deepEqual(
|
|
29
|
+
result.missingExports,
|
|
30
|
+
[],
|
|
31
|
+
`users-core imports missing from package exports:\n${result.missingExports.join("\n")}`
|
|
32
|
+
);
|
|
33
|
+
assert.deepEqual(
|
|
34
|
+
result.staleExports,
|
|
35
|
+
[],
|
|
36
|
+
`Stale users-core exports found. Remove stale keys: ${result.staleExports.join(", ")}`
|
|
37
|
+
);
|
|
38
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
4
|
+
import { registerAvatarMultipartSupport } from "../src/server/accountProfile/registerAvatarMultipartSupport.js";
|
|
5
|
+
|
|
6
|
+
function createAppStub({ hasFastify = true, fastify = null } = {}) {
|
|
7
|
+
const resolvedFastify =
|
|
8
|
+
fastify ||
|
|
9
|
+
{
|
|
10
|
+
register: async () => {},
|
|
11
|
+
hasContentTypeParser: () => false
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
has(token) {
|
|
16
|
+
if (token === KERNEL_TOKENS.Fastify) {
|
|
17
|
+
return hasFastify;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
},
|
|
21
|
+
make(token) {
|
|
22
|
+
if (token === KERNEL_TOKENS.Fastify) {
|
|
23
|
+
return resolvedFastify;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test("registerAvatarMultipartSupport returns early when Fastify is not available", async () => {
|
|
31
|
+
const app = createAppStub({ hasFastify: false });
|
|
32
|
+
await assert.doesNotReject(async () => registerAvatarMultipartSupport(app));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("registerAvatarMultipartSupport registers multipart parser only once", async () => {
|
|
36
|
+
let registerCount = 0;
|
|
37
|
+
const fastify = {
|
|
38
|
+
register: async () => {
|
|
39
|
+
registerCount += 1;
|
|
40
|
+
},
|
|
41
|
+
hasContentTypeParser: () => false
|
|
42
|
+
};
|
|
43
|
+
const app = createAppStub({ fastify });
|
|
44
|
+
|
|
45
|
+
await registerAvatarMultipartSupport(app);
|
|
46
|
+
await registerAvatarMultipartSupport(app);
|
|
47
|
+
|
|
48
|
+
assert.equal(registerCount, 1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("registerAvatarMultipartSupport skips registration when parser already exists", async () => {
|
|
52
|
+
let registerCount = 0;
|
|
53
|
+
const fastify = {
|
|
54
|
+
register: async () => {
|
|
55
|
+
registerCount += 1;
|
|
56
|
+
},
|
|
57
|
+
hasContentTypeParser: (contentType) => String(contentType || "").trim().toLowerCase() === "multipart"
|
|
58
|
+
};
|
|
59
|
+
const app = createAppStub({ fastify });
|
|
60
|
+
|
|
61
|
+
await registerAvatarMultipartSupport(app);
|
|
62
|
+
|
|
63
|
+
assert.equal(registerCount, 0);
|
|
64
|
+
});
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerAccountProfile } from "../src/server/accountProfile/registerAccountProfile.js";
|
|
4
|
+
import { registerAccountPreferences } from "../src/server/accountPreferences/registerAccountPreferences.js";
|
|
5
|
+
import { registerAccountNotifications } from "../src/server/accountNotifications/registerAccountNotifications.js";
|
|
6
|
+
import { registerConsoleSettings } from "../src/server/consoleSettings/registerConsoleSettings.js";
|
|
7
|
+
import { registerWorkspaceMembers } from "../src/server/workspaceMembers/registerWorkspaceMembers.js";
|
|
8
|
+
import { registerWorkspacePendingInvitations } from "../src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js";
|
|
9
|
+
import {
|
|
10
|
+
ACCOUNT_SETTINGS_CHANGED_EVENT,
|
|
11
|
+
CONSOLE_SETTINGS_CHANGED_EVENT,
|
|
12
|
+
USERS_BOOTSTRAP_CHANGED_EVENT,
|
|
13
|
+
WORKSPACE_MEMBERS_CHANGED_EVENT,
|
|
14
|
+
WORKSPACE_INVITES_CHANGED_EVENT,
|
|
15
|
+
WORKSPACES_CHANGED_EVENT,
|
|
16
|
+
WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT
|
|
17
|
+
} from "../src/shared/events/usersEvents.js";
|
|
18
|
+
|
|
19
|
+
function createAppDouble() {
|
|
20
|
+
const serviceCalls = [];
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
serviceCalls,
|
|
24
|
+
app: {
|
|
25
|
+
singleton() {
|
|
26
|
+
return this;
|
|
27
|
+
},
|
|
28
|
+
service(token, factory, metadata) {
|
|
29
|
+
serviceCalls.push({
|
|
30
|
+
token,
|
|
31
|
+
factory,
|
|
32
|
+
metadata
|
|
33
|
+
});
|
|
34
|
+
return this;
|
|
35
|
+
},
|
|
36
|
+
actions() {
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findServiceCall(serviceCalls, token) {
|
|
44
|
+
return serviceCalls.find((entry) => entry.token === token) || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
test("account register functions publish account.settings.changed for update operations", () => {
|
|
48
|
+
const profileApp = createAppDouble();
|
|
49
|
+
registerAccountProfile(profileApp.app);
|
|
50
|
+
const profile = findServiceCall(profileApp.serviceCalls, "users.accountProfile.service");
|
|
51
|
+
assert.equal(profile?.metadata?.events?.updateProfile?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
|
|
52
|
+
assert.equal(profile?.metadata?.events?.updateProfile?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
53
|
+
assert.equal(profile?.metadata?.events?.uploadAvatar?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
|
|
54
|
+
assert.equal(profile?.metadata?.events?.uploadAvatar?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
55
|
+
assert.equal(profile?.metadata?.events?.deleteAvatar?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
|
|
56
|
+
assert.equal(profile?.metadata?.events?.deleteAvatar?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
57
|
+
|
|
58
|
+
const preferencesApp = createAppDouble();
|
|
59
|
+
registerAccountPreferences(preferencesApp.app);
|
|
60
|
+
const preferences = findServiceCall(preferencesApp.serviceCalls, "users.accountPreferences.service");
|
|
61
|
+
assert.equal(preferences?.metadata?.events?.updatePreferences?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
|
|
62
|
+
assert.equal(preferences?.metadata?.events?.updatePreferences?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
63
|
+
|
|
64
|
+
const notificationsApp = createAppDouble();
|
|
65
|
+
registerAccountNotifications(notificationsApp.app);
|
|
66
|
+
const notifications = findServiceCall(notificationsApp.serviceCalls, "users.accountNotifications.service");
|
|
67
|
+
assert.equal(notifications?.metadata?.events?.updateNotifications?.[0]?.realtime?.event, ACCOUNT_SETTINGS_CHANGED_EVENT);
|
|
68
|
+
assert.equal(notifications?.metadata?.events?.updateNotifications?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("console settings register publishes console.settings.changed", () => {
|
|
72
|
+
const payload = createAppDouble();
|
|
73
|
+
registerConsoleSettings(payload.app);
|
|
74
|
+
const consoleSettings = findServiceCall(payload.serviceCalls, "users.console.settings.service");
|
|
75
|
+
assert.equal(consoleSettings?.metadata?.events?.updateSettings?.[0]?.realtime?.event, CONSOLE_SETTINGS_CHANGED_EVENT);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("workspace register functions publish members/invites/workspace-list realtime events", async () => {
|
|
79
|
+
const membersApp = createAppDouble();
|
|
80
|
+
registerWorkspaceMembers(membersApp.app);
|
|
81
|
+
const members = findServiceCall(membersApp.serviceCalls, "users.workspace.members.service");
|
|
82
|
+
assert.equal(members?.metadata?.events?.updateMemberRole?.[0]?.realtime?.event, WORKSPACE_MEMBERS_CHANGED_EVENT);
|
|
83
|
+
assert.equal(members?.metadata?.events?.updateMemberRole?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
84
|
+
assert.equal(members?.metadata?.events?.removeMember?.[0]?.realtime?.event, WORKSPACE_MEMBERS_CHANGED_EVENT);
|
|
85
|
+
assert.equal(members?.metadata?.events?.removeMember?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
86
|
+
assert.equal(members?.metadata?.events?.createInvite?.[0]?.realtime?.event, WORKSPACE_INVITES_CHANGED_EVENT);
|
|
87
|
+
assert.equal(members?.metadata?.events?.createInvite?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
88
|
+
assert.equal(members?.metadata?.events?.createInvite?.[1]?.entityId?.({ result: { createdInviteId: 91 } }), 91);
|
|
89
|
+
assert.equal(members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.preset, "event_scope");
|
|
90
|
+
assert.equal(typeof members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.userQuery, "function");
|
|
91
|
+
const createInviteAudienceQueryResult = await members?.metadata?.events?.createInvite?.[1]?.realtime?.audience?.userQuery({
|
|
92
|
+
knex() {
|
|
93
|
+
return {
|
|
94
|
+
join() {
|
|
95
|
+
return this;
|
|
96
|
+
},
|
|
97
|
+
where(field, value) {
|
|
98
|
+
assert.equal(field, "wi.id");
|
|
99
|
+
assert.equal(value, 91);
|
|
100
|
+
return this;
|
|
101
|
+
},
|
|
102
|
+
async first() {
|
|
103
|
+
return {
|
|
104
|
+
user_id: 55
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
event: {
|
|
110
|
+
entityId: 91
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
assert.deepEqual(createInviteAudienceQueryResult, [{ userId: 55 }]);
|
|
114
|
+
assert.equal(members?.metadata?.events?.revokeInvite?.[0]?.realtime?.event, WORKSPACE_INVITES_CHANGED_EVENT);
|
|
115
|
+
assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
116
|
+
assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.entityId?.({ result: { revokedInviteId: 19 } }), 19);
|
|
117
|
+
assert.equal(members?.metadata?.events?.revokeInvite?.[1]?.realtime?.audience?.preset, "event_scope");
|
|
118
|
+
assert.equal(typeof members?.metadata?.events?.revokeInvite?.[1]?.realtime?.audience?.userQuery, "function");
|
|
119
|
+
|
|
120
|
+
const pendingApp = createAppDouble();
|
|
121
|
+
registerWorkspacePendingInvitations(pendingApp.app);
|
|
122
|
+
const pending = findServiceCall(pendingApp.serviceCalls, "users.workspace.pending-invitations.service");
|
|
123
|
+
const acceptInviteEvents = Array.isArray(pending?.metadata?.events?.acceptInviteByToken)
|
|
124
|
+
? pending.metadata.events.acceptInviteByToken
|
|
125
|
+
: [];
|
|
126
|
+
const acceptInviteRealtimeEvents = acceptInviteEvents.map((entry) => entry?.realtime?.event).filter(Boolean);
|
|
127
|
+
assert.ok(acceptInviteRealtimeEvents.includes(WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT));
|
|
128
|
+
assert.ok(acceptInviteRealtimeEvents.includes(USERS_BOOTSTRAP_CHANGED_EVENT));
|
|
129
|
+
assert.ok(acceptInviteRealtimeEvents.includes(WORKSPACES_CHANGED_EVENT));
|
|
130
|
+
assert.ok(acceptInviteRealtimeEvents.includes(WORKSPACE_MEMBERS_CHANGED_EVENT));
|
|
131
|
+
assert.ok(acceptInviteRealtimeEvents.includes(WORKSPACE_INVITES_CHANGED_EVENT));
|
|
132
|
+
|
|
133
|
+
const acceptedMembersChange = acceptInviteEvents.find(
|
|
134
|
+
(entry) => entry?.realtime?.event === WORKSPACE_MEMBERS_CHANGED_EVENT
|
|
135
|
+
);
|
|
136
|
+
assert.equal(acceptedMembersChange?.entityId?.({ result: { workspaceId: 9 } }), 9);
|
|
137
|
+
assert.deepEqual(
|
|
138
|
+
acceptedMembersChange?.realtime?.audience?.({
|
|
139
|
+
event: {
|
|
140
|
+
entityId: 9
|
|
141
|
+
}
|
|
142
|
+
}),
|
|
143
|
+
{
|
|
144
|
+
workspaceId: 9
|
|
145
|
+
}
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const acceptedInvitesChange = acceptInviteEvents.find(
|
|
149
|
+
(entry) => entry?.realtime?.event === WORKSPACE_INVITES_CHANGED_EVENT
|
|
150
|
+
);
|
|
151
|
+
assert.equal(acceptedInvitesChange?.entityId?.({ result: { workspaceId: 9 } }), 9);
|
|
152
|
+
|
|
153
|
+
const refuseInviteEvents = Array.isArray(pending?.metadata?.events?.refuseInviteByToken)
|
|
154
|
+
? pending.metadata.events.refuseInviteByToken
|
|
155
|
+
: [];
|
|
156
|
+
const refuseInviteRealtimeEvents = refuseInviteEvents.map((entry) => entry?.realtime?.event).filter(Boolean);
|
|
157
|
+
assert.ok(refuseInviteRealtimeEvents.includes(WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT));
|
|
158
|
+
assert.ok(refuseInviteRealtimeEvents.includes(USERS_BOOTSTRAP_CHANGED_EVENT));
|
|
159
|
+
assert.ok(refuseInviteRealtimeEvents.includes(WORKSPACE_INVITES_CHANGED_EVENT));
|
|
160
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerWorkspaceDirectory } from "../src/server/workspaceDirectory/registerWorkspaceDirectory.js";
|
|
4
|
+
|
|
5
|
+
function createAppDouble() {
|
|
6
|
+
const actionBatches = [];
|
|
7
|
+
|
|
8
|
+
return {
|
|
9
|
+
actionBatches,
|
|
10
|
+
singleton() {},
|
|
11
|
+
actions(entries) {
|
|
12
|
+
actionBatches.push(Array.isArray(entries) ? entries : [entries]);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function listActionIds(app) {
|
|
18
|
+
return app.actionBatches.flat().map((entry) => String(entry?.id || ""));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test("registerWorkspaceDirectory registers workspace directory actions without resolving runtime tenancy tokens", () => {
|
|
22
|
+
const app = createAppDouble();
|
|
23
|
+
|
|
24
|
+
registerWorkspaceDirectory(app);
|
|
25
|
+
assert.deepEqual(listActionIds(app), ["workspace.workspaces.create", "workspace.workspaces.list"]);
|
|
26
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerWorkspaceSettings } from "../src/server/workspaceSettings/registerWorkspaceSettings.js";
|
|
4
|
+
import {
|
|
5
|
+
USERS_BOOTSTRAP_CHANGED_EVENT,
|
|
6
|
+
WORKSPACE_SETTINGS_CHANGED_EVENT
|
|
7
|
+
} from "../src/shared/events/usersEvents.js";
|
|
8
|
+
|
|
9
|
+
test("registerWorkspaceSettings registers workspace settings service realtime event metadata", () => {
|
|
10
|
+
const singletonBindings = new Map();
|
|
11
|
+
const actionCalls = [];
|
|
12
|
+
const serviceCalls = [];
|
|
13
|
+
|
|
14
|
+
const app = {
|
|
15
|
+
singleton(token, factory) {
|
|
16
|
+
singletonBindings.set(token, factory);
|
|
17
|
+
return this;
|
|
18
|
+
},
|
|
19
|
+
service(token, factory, metadata) {
|
|
20
|
+
serviceCalls.push({
|
|
21
|
+
token,
|
|
22
|
+
factory,
|
|
23
|
+
metadata
|
|
24
|
+
});
|
|
25
|
+
return this;
|
|
26
|
+
},
|
|
27
|
+
actions(definitions) {
|
|
28
|
+
actionCalls.push(definitions);
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
registerWorkspaceSettings(app);
|
|
34
|
+
|
|
35
|
+
assert.equal(singletonBindings.has("workspaceSettingsRepository"), true);
|
|
36
|
+
assert.equal(serviceCalls.length, 1);
|
|
37
|
+
assert.equal(serviceCalls[0].token, "users.workspace.settings.service");
|
|
38
|
+
assert.equal(typeof serviceCalls[0].factory, "function");
|
|
39
|
+
assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.event, WORKSPACE_SETTINGS_CHANGED_EVENT);
|
|
40
|
+
assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[0]?.realtime?.audience, "event_scope");
|
|
41
|
+
assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.event, USERS_BOOTSTRAP_CHANGED_EVENT);
|
|
42
|
+
assert.equal(serviceCalls[0].metadata?.events?.updateWorkspaceSettings?.[1]?.realtime?.audience, "event_scope");
|
|
43
|
+
assert.equal(actionCalls.length, 1);
|
|
44
|
+
});
|