@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,270 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
3
|
+
import {
|
|
4
|
+
TENANCY_MODE_NONE,
|
|
5
|
+
resolveTenancyProfile
|
|
6
|
+
} from "../../../shared/tenancyProfile.js";
|
|
7
|
+
import { coerceWorkspaceColor } from "../../../shared/settings.js";
|
|
8
|
+
import {
|
|
9
|
+
resolveRolePermissions
|
|
10
|
+
} from "../../../shared/roles.js";
|
|
11
|
+
import {
|
|
12
|
+
mapWorkspaceSummary
|
|
13
|
+
} from "../formatters/workspaceFormatter.js";
|
|
14
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
15
|
+
import { authenticatedUserValidator } from "../validators/authenticatedUserValidator.js";
|
|
16
|
+
|
|
17
|
+
function toSlugPart(value) {
|
|
18
|
+
const normalized = normalizeLowerText(value)
|
|
19
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
20
|
+
.replace(/^-+|-+$/g, "")
|
|
21
|
+
.slice(0, 48);
|
|
22
|
+
return normalized || "workspace";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildWorkspaceBaseSlug(user = {}) {
|
|
26
|
+
const username = normalizeLowerText(user.username);
|
|
27
|
+
if (username) {
|
|
28
|
+
return toSlugPart(username);
|
|
29
|
+
}
|
|
30
|
+
const displayName = normalizeText(user.displayName);
|
|
31
|
+
if (displayName) {
|
|
32
|
+
return toSlugPart(displayName);
|
|
33
|
+
}
|
|
34
|
+
const email = normalizeLowerText(user.email);
|
|
35
|
+
if (email.includes("@")) {
|
|
36
|
+
return toSlugPart(email.split("@")[0]);
|
|
37
|
+
}
|
|
38
|
+
return "workspace";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildWorkspaceName(user = {}) {
|
|
42
|
+
const displayName = normalizeText(user.displayName);
|
|
43
|
+
if (displayName) {
|
|
44
|
+
return `${displayName}'s Workspace`;
|
|
45
|
+
}
|
|
46
|
+
const email = normalizeLowerText(user.email);
|
|
47
|
+
if (email) {
|
|
48
|
+
return `${email}'s Workspace`;
|
|
49
|
+
}
|
|
50
|
+
return "Workspace";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildPermissionsFromMembership(membership, appConfig = {}) {
|
|
54
|
+
const roleId = normalizeLowerText(membership?.roleId || "member");
|
|
55
|
+
return resolveRolePermissions(roleId, appConfig);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function hashInviteToken(token) {
|
|
59
|
+
return createHash("sha256").update(normalizeText(token)).digest("hex");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeWorkspaceCreationInput(payload = {}) {
|
|
63
|
+
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
64
|
+
return {
|
|
65
|
+
name: normalizeText(source.name),
|
|
66
|
+
requestedSlug: normalizeLowerText(source.slug)
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function createService({
|
|
71
|
+
appConfig = {},
|
|
72
|
+
workspacesRepository,
|
|
73
|
+
workspaceMembershipsRepository,
|
|
74
|
+
workspaceSettingsRepository
|
|
75
|
+
} = {}) {
|
|
76
|
+
if (
|
|
77
|
+
!workspacesRepository ||
|
|
78
|
+
!workspaceMembershipsRepository ||
|
|
79
|
+
!workspaceSettingsRepository
|
|
80
|
+
) {
|
|
81
|
+
throw new Error("workspaceService requires repositories.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const resolvedTenancyProfile = resolveTenancyProfile(appConfig);
|
|
85
|
+
const resolvedTenancyMode = resolvedTenancyProfile.mode;
|
|
86
|
+
const workspacePolicy = resolvedTenancyProfile.workspace;
|
|
87
|
+
const resolvedWorkspaceColor = coerceWorkspaceColor(appConfig.workspaceColor);
|
|
88
|
+
async function ensureUniqueWorkspaceSlug(baseSlug, options = {}) {
|
|
89
|
+
let suffix = 0;
|
|
90
|
+
while (suffix < 1000) {
|
|
91
|
+
suffix += 1;
|
|
92
|
+
const candidate = suffix === 1 ? toSlugPart(baseSlug) : `${toSlugPart(baseSlug)}-${suffix}`;
|
|
93
|
+
const existing = await workspacesRepository.findBySlug(candidate, options);
|
|
94
|
+
if (!existing) {
|
|
95
|
+
return candidate;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw new AppError(500, "Unable to generate unique workspace slug.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function ensureWorkspaceSettingsForWorkspace(workspace, options = {}) {
|
|
102
|
+
return workspaceSettingsRepository.ensureForWorkspaceId(workspace?.id, {
|
|
103
|
+
...options,
|
|
104
|
+
workspace
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function ensurePersonalWorkspaceForUser(user, options = {}) {
|
|
109
|
+
const normalizedUser = authenticatedUserValidator.normalize(user);
|
|
110
|
+
if (!normalizedUser) {
|
|
111
|
+
throw new AppError(400, "Invalid authenticated user payload.");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const existing = await workspacesRepository.findPersonalByOwnerUserId(normalizedUser.id, options);
|
|
115
|
+
if (existing) {
|
|
116
|
+
await workspaceMembershipsRepository.ensureOwnerMembership(existing.id, normalizedUser.id, options);
|
|
117
|
+
await ensureWorkspaceSettingsForWorkspace(existing, options);
|
|
118
|
+
return existing;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const slug = await ensureUniqueWorkspaceSlug(buildWorkspaceBaseSlug(normalizedUser), options);
|
|
122
|
+
const inserted = await workspacesRepository.insert(
|
|
123
|
+
{
|
|
124
|
+
slug,
|
|
125
|
+
name: buildWorkspaceName(normalizedUser),
|
|
126
|
+
ownerUserId: normalizedUser.id,
|
|
127
|
+
isPersonal: true,
|
|
128
|
+
avatarUrl: "",
|
|
129
|
+
color: resolvedWorkspaceColor
|
|
130
|
+
},
|
|
131
|
+
options
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
await workspaceMembershipsRepository.ensureOwnerMembership(inserted.id, normalizedUser.id, options);
|
|
135
|
+
await ensureWorkspaceSettingsForWorkspace(inserted, options);
|
|
136
|
+
return inserted;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function listWorkspacesForUser(user, options = {}) {
|
|
140
|
+
const normalizedUser = authenticatedUserValidator.normalize(user);
|
|
141
|
+
if (!normalizedUser) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (resolvedTenancyMode === TENANCY_MODE_NONE) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const list = await workspacesRepository.listForUserId(normalizedUser.id, options);
|
|
150
|
+
const accessible = list
|
|
151
|
+
.map((entry) => mapWorkspaceSummary(entry, { roleId: entry.roleId, status: entry.membershipStatus }))
|
|
152
|
+
.filter((entry) => entry.isAccessible);
|
|
153
|
+
|
|
154
|
+
return accessible;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function listWorkspacesForAuthenticatedUser(user, options = {}) {
|
|
158
|
+
return listWorkspacesForUser(user, options);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function provisionWorkspaceForNewUser(user, options = {}) {
|
|
162
|
+
const normalizedUser = authenticatedUserValidator.normalize(user);
|
|
163
|
+
if (!normalizedUser) {
|
|
164
|
+
throw new AppError(400, "Invalid authenticated user payload.");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (workspacePolicy.autoProvision !== true) {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return ensurePersonalWorkspaceForUser(normalizedUser, options);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function createWorkspaceForAuthenticatedUser(user, payload = {}, options = {}) {
|
|
175
|
+
const normalizedUser = authenticatedUserValidator.normalize(user);
|
|
176
|
+
if (!normalizedUser) {
|
|
177
|
+
throw new AppError(401, "Authentication required.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (workspacePolicy.allowSelfCreate !== true) {
|
|
181
|
+
throw new AppError(403, "Workspace creation is disabled for this tenancy mode.");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const createInput = normalizeWorkspaceCreationInput(payload);
|
|
185
|
+
if (!createInput.name) {
|
|
186
|
+
throw new AppError(400, "Workspace name is required.");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const slugBase = createInput.requestedSlug || toSlugPart(createInput.name);
|
|
190
|
+
const slug = await ensureUniqueWorkspaceSlug(slugBase, options);
|
|
191
|
+
const inserted = await workspacesRepository.insert(
|
|
192
|
+
{
|
|
193
|
+
slug,
|
|
194
|
+
name: createInput.name,
|
|
195
|
+
ownerUserId: normalizedUser.id,
|
|
196
|
+
isPersonal: false,
|
|
197
|
+
avatarUrl: "",
|
|
198
|
+
color: resolvedWorkspaceColor
|
|
199
|
+
},
|
|
200
|
+
options
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
await workspaceMembershipsRepository.ensureOwnerMembership(inserted.id, normalizedUser.id, options);
|
|
204
|
+
await ensureWorkspaceSettingsForWorkspace(inserted, options);
|
|
205
|
+
return inserted;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function resolveWorkspaceContextForUserBySlug(user, workspaceSlug, options = {}) {
|
|
209
|
+
const normalizedUser = authenticatedUserValidator.normalize(user);
|
|
210
|
+
if (!normalizedUser) {
|
|
211
|
+
throw new AppError(401, "Authentication required.");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (resolvedTenancyMode === TENANCY_MODE_NONE) {
|
|
215
|
+
throw new AppError(403, "Workspace context is disabled.");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const normalizedWorkspaceSlug = normalizeLowerText(workspaceSlug);
|
|
219
|
+
if (!normalizedWorkspaceSlug) {
|
|
220
|
+
throw new AppError(400, "workspaceSlug is required.");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const workspace = await workspacesRepository.findBySlug(normalizedWorkspaceSlug, options);
|
|
224
|
+
|
|
225
|
+
if (!workspace) {
|
|
226
|
+
throw new AppError(404, "Workspace not found.");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let membership = await workspaceMembershipsRepository.findByWorkspaceIdAndUserId(
|
|
230
|
+
workspace.id,
|
|
231
|
+
normalizedUser.id,
|
|
232
|
+
options
|
|
233
|
+
);
|
|
234
|
+
const actorOwnsWorkspace = Number(workspace.ownerUserId) === Number(normalizedUser.id);
|
|
235
|
+
const membershipIsActive = normalizeLowerText(membership?.status) === "active";
|
|
236
|
+
|
|
237
|
+
if (!membershipIsActive && actorOwnsWorkspace) {
|
|
238
|
+
membership = await workspaceMembershipsRepository.ensureOwnerMembership(workspace.id, normalizedUser.id, options);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (!membership || normalizeLowerText(membership.status) !== "active") {
|
|
242
|
+
throw new AppError(403, "You do not have access to this workspace.");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const workspaceSettings = await ensureWorkspaceSettingsForWorkspace(workspace, options);
|
|
246
|
+
const permissions = buildPermissionsFromMembership(membership, appConfig);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
workspace,
|
|
250
|
+
membership,
|
|
251
|
+
permissions,
|
|
252
|
+
workspaceSettings
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return Object.freeze({
|
|
257
|
+
toSlugPart,
|
|
258
|
+
buildWorkspaceName,
|
|
259
|
+
buildWorkspaceBaseSlug,
|
|
260
|
+
hashInviteToken,
|
|
261
|
+
ensurePersonalWorkspaceForUser,
|
|
262
|
+
provisionWorkspaceForNewUser,
|
|
263
|
+
createWorkspaceForAuthenticatedUser,
|
|
264
|
+
listWorkspacesForUser,
|
|
265
|
+
listWorkspacesForAuthenticatedUser,
|
|
266
|
+
resolveWorkspaceContextForUserBySlug
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export { createService };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
function deepFreeze(value, seen = new WeakSet()) {
|
|
2
|
+
if (!value || typeof value !== "object") {
|
|
3
|
+
return value;
|
|
4
|
+
}
|
|
5
|
+
if (seen.has(value)) {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
seen.add(value);
|
|
10
|
+
for (const key of Object.keys(value)) {
|
|
11
|
+
deepFreeze(value[key], seen);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return Object.freeze(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { deepFreeze };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ACCOUNT_SETTINGS_CHANGED_EVENT,
|
|
3
|
+
USERS_BOOTSTRAP_CHANGED_EVENT
|
|
4
|
+
} from "../../../shared/events/usersEvents.js";
|
|
5
|
+
import { deepFreeze } from "./deepFreeze.js";
|
|
6
|
+
|
|
7
|
+
function resolveActorScopedEntityId({ options } = {}) {
|
|
8
|
+
return Number(options?.context?.actor?.id || 0);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function resolveWorkspaceSlugPayload({ args } = {}) {
|
|
12
|
+
return {
|
|
13
|
+
workspaceSlug: String(args?.[0]?.slug || "").trim()
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS = deepFreeze([
|
|
18
|
+
{
|
|
19
|
+
type: "entity.changed",
|
|
20
|
+
source: "account",
|
|
21
|
+
entity: "settings",
|
|
22
|
+
operation: "updated",
|
|
23
|
+
entityId: resolveActorScopedEntityId,
|
|
24
|
+
realtime: {
|
|
25
|
+
event: ACCOUNT_SETTINGS_CHANGED_EVENT,
|
|
26
|
+
audience: "actor_user"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: "entity.changed",
|
|
31
|
+
source: "users",
|
|
32
|
+
entity: "bootstrap",
|
|
33
|
+
operation: "updated",
|
|
34
|
+
entityId: resolveActorScopedEntityId,
|
|
35
|
+
realtime: {
|
|
36
|
+
event: USERS_BOOTSTRAP_CHANGED_EVENT,
|
|
37
|
+
audience: "actor_user"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
function createWorkspaceEntityAndBootstrapEvents({
|
|
43
|
+
workspaceEntity,
|
|
44
|
+
workspaceOperation,
|
|
45
|
+
workspaceRealtimeEvent,
|
|
46
|
+
workspaceEntityId = ({ args }) => args?.[0]?.id,
|
|
47
|
+
bootstrapEntityId = ({ args }) => args?.[0]?.id,
|
|
48
|
+
bootstrapAudience = "event_scope"
|
|
49
|
+
} = {}) {
|
|
50
|
+
const normalizedWorkspaceEntity = String(workspaceEntity || "").trim();
|
|
51
|
+
const normalizedWorkspaceOperation = String(workspaceOperation || "")
|
|
52
|
+
.trim()
|
|
53
|
+
.toLowerCase();
|
|
54
|
+
const normalizedWorkspaceRealtimeEvent = String(workspaceRealtimeEvent || "").trim();
|
|
55
|
+
if (!normalizedWorkspaceEntity || !normalizedWorkspaceOperation || !normalizedWorkspaceRealtimeEvent) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"createWorkspaceEntityAndBootstrapEvents requires workspaceEntity, workspaceOperation, and workspaceRealtimeEvent."
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
if (typeof workspaceEntityId !== "function") {
|
|
61
|
+
throw new Error("createWorkspaceEntityAndBootstrapEvents requires workspaceEntityId to be a function.");
|
|
62
|
+
}
|
|
63
|
+
if (typeof bootstrapEntityId !== "function") {
|
|
64
|
+
throw new Error("createWorkspaceEntityAndBootstrapEvents requires bootstrapEntityId to be a function.");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return deepFreeze([
|
|
68
|
+
{
|
|
69
|
+
type: "entity.changed",
|
|
70
|
+
source: "workspace",
|
|
71
|
+
entity: normalizedWorkspaceEntity,
|
|
72
|
+
operation: normalizedWorkspaceOperation,
|
|
73
|
+
entityId: workspaceEntityId,
|
|
74
|
+
realtime: {
|
|
75
|
+
event: normalizedWorkspaceRealtimeEvent,
|
|
76
|
+
payload: resolveWorkspaceSlugPayload,
|
|
77
|
+
audience: "event_scope"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
type: "entity.changed",
|
|
82
|
+
source: "users",
|
|
83
|
+
entity: "bootstrap",
|
|
84
|
+
operation: "updated",
|
|
85
|
+
entityId: bootstrapEntityId,
|
|
86
|
+
realtime: {
|
|
87
|
+
event: USERS_BOOTSTRAP_CHANGED_EVENT,
|
|
88
|
+
audience: bootstrapAudience
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS, createWorkspaceEntityAndBootstrapEvents };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
function normalizeObject(value) {
|
|
2
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function resolveActionUser(context, input) {
|
|
6
|
+
const payload = normalizeObject(input);
|
|
7
|
+
const request = context?.requestMeta?.request || null;
|
|
8
|
+
return payload.user || request?.user || context?.actor || null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export { resolveActionUser };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
USERS_WORKSPACE_API_BASE_PATH,
|
|
3
|
+
normalizeApiRelativePath
|
|
4
|
+
} from "../../../shared/support/usersApiPaths.js";
|
|
5
|
+
|
|
6
|
+
const USERS_WORKSPACE_ROUTE_BASE_PATH = USERS_WORKSPACE_API_BASE_PATH;
|
|
7
|
+
|
|
8
|
+
function resolveWorkspaceRoutePath(relativePath = "/") {
|
|
9
|
+
const normalizedRelativePath = normalizeApiRelativePath(relativePath);
|
|
10
|
+
if (normalizedRelativePath === "/") {
|
|
11
|
+
return USERS_WORKSPACE_ROUTE_BASE_PATH;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return `${USERS_WORKSPACE_ROUTE_BASE_PATH}${normalizedRelativePath}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { USERS_WORKSPACE_ROUTE_BASE_PATH, resolveWorkspaceRoutePath };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# `validators/`
|
|
2
|
+
|
|
3
|
+
Put shared validators here when they are reused by multiple server slices/adapters.
|
|
4
|
+
|
|
5
|
+
Allowed:
|
|
6
|
+
- validator objects (`schema` + `normalize`)
|
|
7
|
+
- transport-level reusable validators
|
|
8
|
+
|
|
9
|
+
Not allowed:
|
|
10
|
+
- feature-only validators used by one slice
|
|
11
|
+
- business/domain decision logic
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
2
|
+
import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
|
|
3
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
4
|
+
|
|
5
|
+
function normalizeAuthenticatedUser(input = {}) {
|
|
6
|
+
const source = normalizeObjectInput(input);
|
|
7
|
+
const id = Number(source.id);
|
|
8
|
+
if (!Number.isInteger(id) || id < 1) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const email = normalizeLowerText(source.email);
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
email,
|
|
16
|
+
username: normalizeLowerText(source.username),
|
|
17
|
+
displayName: normalizeText(source.displayName) || email || `User ${id}`,
|
|
18
|
+
authProvider: normalizeLowerText(source.authProvider),
|
|
19
|
+
authProviderUserId: normalizeText(source.authProviderUserId),
|
|
20
|
+
avatarStorageKey: source.avatarStorageKey ? normalizeText(source.avatarStorageKey) : null,
|
|
21
|
+
avatarVersion: source.avatarVersion == null ? null : String(source.avatarVersion)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const authenticatedUserValidator = Object.freeze({
|
|
26
|
+
schema: Type.Object(
|
|
27
|
+
{
|
|
28
|
+
id: Type.Integer({ minimum: 1 }),
|
|
29
|
+
email: Type.String({ minLength: 1 }),
|
|
30
|
+
username: Type.Optional(Type.String()),
|
|
31
|
+
displayName: Type.Optional(Type.String()),
|
|
32
|
+
authProvider: Type.Optional(Type.String()),
|
|
33
|
+
authProviderUserId: Type.Optional(Type.String()),
|
|
34
|
+
avatarStorageKey: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
35
|
+
avatarVersion: Type.Optional(Type.Union([Type.String(), Type.Number(), Type.Null()]))
|
|
36
|
+
},
|
|
37
|
+
{ additionalProperties: true }
|
|
38
|
+
),
|
|
39
|
+
normalize: normalizeAuthenticatedUser
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export { authenticatedUserValidator };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Type } from "@fastify/type-provider-typebox";
|
|
2
|
+
import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
|
|
3
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
|
+
|
|
5
|
+
function normalizeRouteParams(input = {}) {
|
|
6
|
+
const source = normalizeObjectInput(input);
|
|
7
|
+
const normalized = {};
|
|
8
|
+
|
|
9
|
+
if (Object.hasOwn(source, "workspaceSlug")) {
|
|
10
|
+
normalized.workspaceSlug = normalizeText(source.workspaceSlug).toLowerCase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (Object.hasOwn(source, "memberUserId")) {
|
|
14
|
+
normalized.memberUserId = normalizeText(source.memberUserId);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (Object.hasOwn(source, "inviteId")) {
|
|
18
|
+
normalized.inviteId = normalizeText(source.inviteId);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (Object.hasOwn(source, "provider")) {
|
|
22
|
+
normalized.provider = normalizeText(source.provider);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return normalized;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeWorkspaceSlugParams(input = {}) {
|
|
29
|
+
const source = normalizeObjectInput(input);
|
|
30
|
+
const normalized = {};
|
|
31
|
+
|
|
32
|
+
if (Object.hasOwn(source, "workspaceSlug")) {
|
|
33
|
+
normalized.workspaceSlug = normalizeText(source.workspaceSlug).toLowerCase();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return normalized;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const routeParamsValidator = Object.freeze({
|
|
40
|
+
schema: Type.Object(
|
|
41
|
+
{
|
|
42
|
+
workspaceSlug: Type.Optional(Type.String({ minLength: 1 })),
|
|
43
|
+
memberUserId: Type.Optional(Type.String({ minLength: 1 })),
|
|
44
|
+
inviteId: Type.Optional(Type.String({ minLength: 1 })),
|
|
45
|
+
provider: Type.Optional(Type.String({ minLength: 1 }))
|
|
46
|
+
},
|
|
47
|
+
{ additionalProperties: false }
|
|
48
|
+
),
|
|
49
|
+
normalize: normalizeRouteParams
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const workspaceSlugParamsValidator = Object.freeze({
|
|
53
|
+
schema: Type.Object(
|
|
54
|
+
{
|
|
55
|
+
workspaceSlug: Type.Optional(Type.String({ minLength: 1 }))
|
|
56
|
+
},
|
|
57
|
+
{ additionalProperties: false }
|
|
58
|
+
),
|
|
59
|
+
normalize: normalizeWorkspaceSlugParams
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export { routeParamsValidator, workspaceSlugParamsValidator };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
3
|
+
import { consoleSettingsResource } from "../../shared/resources/consoleSettingsResource.js";
|
|
4
|
+
|
|
5
|
+
function bootConsoleSettingsRoutes(app) {
|
|
6
|
+
if (!app || typeof app.make !== "function") {
|
|
7
|
+
throw new Error("bootConsoleSettingsRoutes requires application make().");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
11
|
+
|
|
12
|
+
router.register(
|
|
13
|
+
"GET",
|
|
14
|
+
"/api/console/settings",
|
|
15
|
+
{
|
|
16
|
+
auth: "required",
|
|
17
|
+
surface: "console",
|
|
18
|
+
meta: {
|
|
19
|
+
tags: ["console", "settings"],
|
|
20
|
+
summary: "Get console settings"
|
|
21
|
+
},
|
|
22
|
+
responseValidators: withStandardErrorResponses({
|
|
23
|
+
200: consoleSettingsResource.operations.view.outputValidator
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
async function (request, reply) {
|
|
27
|
+
const response = await request.executeAction({
|
|
28
|
+
actionId: "console.settings.read"
|
|
29
|
+
});
|
|
30
|
+
reply.code(200).send(response);
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
router.register(
|
|
35
|
+
"PATCH",
|
|
36
|
+
"/api/console/settings",
|
|
37
|
+
{
|
|
38
|
+
auth: "required",
|
|
39
|
+
surface: "console",
|
|
40
|
+
meta: {
|
|
41
|
+
tags: ["console", "settings"],
|
|
42
|
+
summary: "Update console settings"
|
|
43
|
+
},
|
|
44
|
+
bodyValidator: consoleSettingsResource.operations.replace.bodyValidator,
|
|
45
|
+
responseValidators: withStandardErrorResponses(
|
|
46
|
+
{
|
|
47
|
+
200: consoleSettingsResource.operations.view.outputValidator
|
|
48
|
+
},
|
|
49
|
+
{ includeValidation400: true }
|
|
50
|
+
)
|
|
51
|
+
},
|
|
52
|
+
async function (request, reply) {
|
|
53
|
+
const response = await request.executeAction({
|
|
54
|
+
actionId: "console.settings.update",
|
|
55
|
+
input: {
|
|
56
|
+
payload: request.input.body
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
reply.code(200).send(response);
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { bootConsoleSettingsRoutes };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
|
|
3
|
+
|
|
4
|
+
function createService({ consoleSettingsRepository } = {}) {
|
|
5
|
+
if (!consoleSettingsRepository || typeof consoleSettingsRepository.ensureOwnerUserId !== "function") {
|
|
6
|
+
throw new Error("consoleService requires consoleSettingsRepository.ensureOwnerUserId().");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
async function ensureInitialConsoleMember(userId, options = {}) {
|
|
10
|
+
const normalizedUserId = parsePositiveInteger(userId);
|
|
11
|
+
if (!normalizedUserId) {
|
|
12
|
+
throw new AppError(400, "Invalid console user.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return consoleSettingsRepository.ensureOwnerUserId(normalizedUserId, options);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function requireConsoleOwner(context = {}, options = {}) {
|
|
19
|
+
const actorUserId = parsePositiveInteger(context?.actor?.id);
|
|
20
|
+
if (!actorUserId) {
|
|
21
|
+
throw new AppError(401, "Authentication required.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const ownerUserId = await ensureInitialConsoleMember(actorUserId, options);
|
|
25
|
+
if (actorUserId !== ownerUserId) {
|
|
26
|
+
throw new AppError(403, "Forbidden.");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return Object.freeze({
|
|
31
|
+
ensureInitialConsoleMember,
|
|
32
|
+
requireConsoleOwner
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { createService };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_INPUT_VALIDATOR
|
|
3
|
+
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
4
|
+
import { consoleSettingsResource } from "../../shared/resources/consoleSettingsResource.js";
|
|
5
|
+
|
|
6
|
+
const consoleSettingsActions = Object.freeze([
|
|
7
|
+
{
|
|
8
|
+
id: "console.settings.read",
|
|
9
|
+
version: 1,
|
|
10
|
+
kind: "query",
|
|
11
|
+
channels: ["api", "automation", "internal"],
|
|
12
|
+
surfacesFrom: "console",
|
|
13
|
+
permission: {
|
|
14
|
+
require: "authenticated"
|
|
15
|
+
},
|
|
16
|
+
inputValidator: EMPTY_INPUT_VALIDATOR,
|
|
17
|
+
outputValidator: consoleSettingsResource.operations.view.outputValidator,
|
|
18
|
+
idempotency: "none",
|
|
19
|
+
audit: {
|
|
20
|
+
actionName: "console.settings.read"
|
|
21
|
+
},
|
|
22
|
+
observability: {},
|
|
23
|
+
async execute(_input, context, deps) {
|
|
24
|
+
return deps.consoleSettingsService.getSettings({
|
|
25
|
+
context
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "console.settings.update",
|
|
31
|
+
version: 1,
|
|
32
|
+
kind: "command",
|
|
33
|
+
channels: ["api", "automation", "internal"],
|
|
34
|
+
surfacesFrom: "console",
|
|
35
|
+
permission: {
|
|
36
|
+
require: "authenticated"
|
|
37
|
+
},
|
|
38
|
+
inputValidator: {
|
|
39
|
+
payload: consoleSettingsResource.operations.replace.bodyValidator
|
|
40
|
+
},
|
|
41
|
+
outputValidator: consoleSettingsResource.operations.replace.outputValidator,
|
|
42
|
+
idempotency: "optional",
|
|
43
|
+
audit: {
|
|
44
|
+
actionName: "console.settings.update"
|
|
45
|
+
},
|
|
46
|
+
observability: {},
|
|
47
|
+
async execute(input, context, deps) {
|
|
48
|
+
return deps.consoleSettingsService.updateSettings(input.payload, {
|
|
49
|
+
context
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
export { consoleSettingsActions };
|