@jskit-ai/users-core 0.1.33 → 0.1.36
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 +16 -245
- package/package.json +7 -7
- package/src/server/UsersCoreServiceProvider.js +4 -28
- package/src/server/UsersWorkspacesServiceProvider.js +44 -0
- package/src/server/common/registerCommonRepositories.js +0 -19
- package/src/server/common/repositories/userSettingsRepository.js +1 -12
- package/src/server/registerUsersBootstrap.js +22 -0
- package/src/server/registerUsersCore.js +30 -0
- package/src/server/registerWorkspaceBootstrap.js +2 -5
- package/src/server/registerWorkspaceCore.js +1 -16
- package/src/server/registerWorkspaceRepositories.js +26 -0
- package/src/server/usersBootstrapContributor.js +248 -0
- package/src/server/workspaceBootstrapContributor.js +63 -257
- package/src/shared/settings.js +1 -2
- package/templates/migrations/users_core_generic_initial.cjs +69 -0
- package/test/registerUsersCore.test.js +42 -0
- package/test/usersBootstrapContributor.test.js +172 -0
- package/test/usersRouteRequestInputValidator.test.js +7 -390
- package/test/workspaceBootstrapContributor.test.js +31 -343
- package/test-support/registerDefaultSettingsFields.js +1 -1
- package/templates/config/roles.js +0 -27
- package/templates/migrations/users_core_initial.cjs +0 -123
- package/templates/migrations/users_core_workspace_settings_single_name_source.cjs +0 -71
- package/templates/migrations/users_core_workspaces_drop_color.cjs +0 -85
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +0 -197
|
@@ -3,12 +3,11 @@ import {
|
|
|
3
3
|
} from "@jskit-ai/kernel/server/actions";
|
|
4
4
|
import { registerRouteVisibilityResolver } from "@jskit-ai/kernel/server/http";
|
|
5
5
|
import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
|
|
6
|
-
import { TENANCY_MODE_WORKSPACES, resolveTenancyProfile } from "../shared/tenancyProfile.js";
|
|
7
6
|
import { createService as createWorkspaceService } from "./common/services/workspaceContextService.js";
|
|
8
|
-
import { createService as createAuthProfileSyncService } from "./common/services/authProfileSyncService.js";
|
|
9
7
|
import { createWorkspaceActionContextContributor } from "./common/contributors/workspaceActionContextContributor.js";
|
|
10
8
|
import { createWorkspaceRouteVisibilityResolver } from "./common/contributors/workspaceRouteVisibilityResolver.js";
|
|
11
9
|
import { createWorkspaceAuthPolicyContextResolver } from "./common/contributors/workspaceAuthPolicyContextResolver.js";
|
|
10
|
+
import { TENANCY_MODE_WORKSPACES } from "../shared/tenancyProfile.js";
|
|
12
11
|
import { resolveWorkspaceInvitationsPolicy } from "./support/workspaceInvitationsPolicy.js";
|
|
13
12
|
import { resolveWorkspaceSurfaceIdsFromAppConfig } from "./support/workspaceActionSurfaces.js";
|
|
14
13
|
|
|
@@ -27,20 +26,6 @@ function registerWorkspaceCore(app) {
|
|
|
27
26
|
workspaceSettingsRepository: scope.make("workspaceSettingsRepository")
|
|
28
27
|
});
|
|
29
28
|
});
|
|
30
|
-
|
|
31
|
-
app.singleton("users.profile.sync.service", (scope) => {
|
|
32
|
-
return createAuthProfileSyncService({
|
|
33
|
-
usersRepository: scope.make("usersRepository"),
|
|
34
|
-
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
35
|
-
workspaceProvisioningService: scope.make("users.workspace.service")
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
app.singleton("users.tenancy.profile", (scope) => {
|
|
40
|
-
const appConfig = resolveAppConfig(scope);
|
|
41
|
-
return resolveTenancyProfile(appConfig);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
29
|
app.singleton("users.workspace.enabled", (scope) => {
|
|
45
30
|
return scope.make("users.tenancy.profile").workspace.enabled === true;
|
|
46
31
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRepository as createWorkspacesRepository } from "./common/repositories/workspacesRepository.js";
|
|
2
|
+
import { createRepository as createWorkspaceMembershipsRepository } from "./common/repositories/workspaceMembershipsRepository.js";
|
|
3
|
+
import { createRepository as createWorkspaceInvitesRepository } from "./common/repositories/workspaceInvitesRepository.js";
|
|
4
|
+
|
|
5
|
+
function registerWorkspaceRepositories(app) {
|
|
6
|
+
if (!app || typeof app.singleton !== "function") {
|
|
7
|
+
throw new Error("registerWorkspaceRepositories requires application singleton().");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
app.singleton("workspacesRepository", (scope) => {
|
|
11
|
+
const knex = scope.make("jskit.database.knex");
|
|
12
|
+
return createWorkspacesRepository(knex);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
app.singleton("workspaceMembershipsRepository", (scope) => {
|
|
16
|
+
const knex = scope.make("jskit.database.knex");
|
|
17
|
+
return createWorkspaceMembershipsRepository(knex);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
app.singleton("workspaceInvitesRepository", (scope) => {
|
|
21
|
+
const knex = scope.make("jskit.database.knex");
|
|
22
|
+
return createWorkspaceInvitesRepository(knex);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { registerWorkspaceRepositories };
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
2
|
+
import { requireServiceMethod } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
3
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
4
|
+
import {
|
|
5
|
+
TENANCY_MODE_NONE,
|
|
6
|
+
TENANCY_MODE_PERSONAL,
|
|
7
|
+
TENANCY_MODE_WORKSPACES,
|
|
8
|
+
WORKSPACE_SLUG_POLICY_NONE,
|
|
9
|
+
WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
|
|
10
|
+
WORKSPACE_SLUG_POLICY_USER_SELECTED,
|
|
11
|
+
resolveTenancyProfile
|
|
12
|
+
} from "../shared/tenancyProfile.js";
|
|
13
|
+
import { accountAvatarFormatter } from "./common/formatters/accountAvatarFormatter.js";
|
|
14
|
+
import { authenticatedUserValidator } from "./common/validators/authenticatedUserValidator.js";
|
|
15
|
+
import { userSettingsFields } from "../shared/resources/userSettingsFields.js";
|
|
16
|
+
|
|
17
|
+
function getOAuthProviderCatalogPayload(authService) {
|
|
18
|
+
if (!authService || typeof authService.getOAuthProviderCatalog !== "function") {
|
|
19
|
+
return {
|
|
20
|
+
oauthProviders: [],
|
|
21
|
+
oauthDefaultProvider: null
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const catalog = authService.getOAuthProviderCatalog();
|
|
26
|
+
const providers = Array.isArray(catalog?.providers)
|
|
27
|
+
? catalog.providers
|
|
28
|
+
.map((provider) => ({
|
|
29
|
+
id: normalizeLowerText(provider?.id),
|
|
30
|
+
label: normalizeText(provider?.label)
|
|
31
|
+
}))
|
|
32
|
+
.filter((provider) => provider.id && provider.label)
|
|
33
|
+
: [];
|
|
34
|
+
const defaultProvider = normalizeLowerText(catalog?.defaultProvider);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
oauthProviders: providers,
|
|
38
|
+
oauthDefaultProvider: providers.some((provider) => provider.id === defaultProvider) ? defaultProvider : null
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeBoolean(value, fallback) {
|
|
43
|
+
if (typeof value === "boolean") {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
if (typeof value === "string") {
|
|
47
|
+
const normalized = normalizeLowerText(value);
|
|
48
|
+
if (normalized === "true") {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (normalized === "false") {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return fallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveAppState(appConfig = {}, { workspaceInvitationsEnabled = false } = {}) {
|
|
59
|
+
const features = {
|
|
60
|
+
workspaceSwitching: normalizeBoolean(appConfig.workspaceSwitching, false),
|
|
61
|
+
workspaceInvites: workspaceInvitationsEnabled === true,
|
|
62
|
+
assistantEnabled: normalizeBoolean(appConfig.assistantEnabled, false),
|
|
63
|
+
assistantRequiredPermission: normalizeText(appConfig.assistantRequiredPermission),
|
|
64
|
+
socialEnabled: normalizeBoolean(appConfig.socialEnabled, false),
|
|
65
|
+
socialFederationEnabled: normalizeBoolean(appConfig.socialFederationEnabled, false)
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
features
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeSlugPolicy(value = "") {
|
|
74
|
+
const normalizedValue = normalizeLowerText(value);
|
|
75
|
+
if (
|
|
76
|
+
normalizedValue === WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME ||
|
|
77
|
+
normalizedValue === WORKSPACE_SLUG_POLICY_USER_SELECTED
|
|
78
|
+
) {
|
|
79
|
+
return normalizedValue;
|
|
80
|
+
}
|
|
81
|
+
return WORKSPACE_SLUG_POLICY_NONE;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isSupportedTenancyMode(value = "") {
|
|
85
|
+
return value === TENANCY_MODE_NONE || value === TENANCY_MODE_PERSONAL || value === TENANCY_MODE_WORKSPACES;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveBootstrapTenancyProfile(tenancyProfile = null, appConfig = {}) {
|
|
89
|
+
const fallback = resolveTenancyProfile(appConfig);
|
|
90
|
+
const source = tenancyProfile && typeof tenancyProfile === "object" ? tenancyProfile : fallback;
|
|
91
|
+
const mode = isSupportedTenancyMode(source?.mode) ? source.mode : fallback.mode;
|
|
92
|
+
const workspace = source?.workspace && typeof source.workspace === "object" ? source.workspace : fallback.workspace;
|
|
93
|
+
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
mode,
|
|
96
|
+
workspace: Object.freeze({
|
|
97
|
+
enabled: workspace.enabled === true,
|
|
98
|
+
autoProvision: workspace.autoProvision === true,
|
|
99
|
+
allowSelfCreate: workspace.allowSelfCreate === true,
|
|
100
|
+
slugPolicy: normalizeSlugPolicy(workspace.slugPolicy)
|
|
101
|
+
})
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function createAnonymousBootstrapPayload({ appState, tenancyProfile }) {
|
|
106
|
+
return {
|
|
107
|
+
session: {
|
|
108
|
+
authenticated: false
|
|
109
|
+
},
|
|
110
|
+
profile: null,
|
|
111
|
+
tenancy: tenancyProfile,
|
|
112
|
+
app: appState,
|
|
113
|
+
workspaces: [],
|
|
114
|
+
pendingInvites: [],
|
|
115
|
+
activeWorkspace: null,
|
|
116
|
+
membership: null,
|
|
117
|
+
requestedWorkspace: null,
|
|
118
|
+
permissions: [],
|
|
119
|
+
surfaceAccess: {
|
|
120
|
+
consoleowner: false
|
|
121
|
+
},
|
|
122
|
+
workspaceSettings: null,
|
|
123
|
+
userSettings: null,
|
|
124
|
+
requestMeta: {
|
|
125
|
+
hasRequest: false
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function mapUserSettingsBootstrap(settings = {}) {
|
|
131
|
+
const source = settings && typeof settings === "object" ? settings : {};
|
|
132
|
+
const mapped = {};
|
|
133
|
+
|
|
134
|
+
for (const field of userSettingsFields) {
|
|
135
|
+
if (field.includeInBootstrap === false) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const rawValue = Object.hasOwn(source, field.key)
|
|
139
|
+
? source[field.key]
|
|
140
|
+
: field.resolveDefault({
|
|
141
|
+
settings: source
|
|
142
|
+
});
|
|
143
|
+
mapped[field.key] = field.normalizeOutput(rawValue, {
|
|
144
|
+
settings: source
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return mapped;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function createUsersBootstrapContributor({
|
|
152
|
+
usersRepository,
|
|
153
|
+
userSettingsRepository,
|
|
154
|
+
appConfig = {},
|
|
155
|
+
tenancyProfile = null,
|
|
156
|
+
authService,
|
|
157
|
+
consoleService = null
|
|
158
|
+
} = {}) {
|
|
159
|
+
const contributorId = "users.bootstrap";
|
|
160
|
+
const appState = resolveAppState(appConfig);
|
|
161
|
+
const resolvedTenancyProfile = resolveBootstrapTenancyProfile(tenancyProfile, appConfig);
|
|
162
|
+
|
|
163
|
+
requireServiceMethod(usersRepository, "findById", contributorId, {
|
|
164
|
+
serviceLabel: "usersRepository"
|
|
165
|
+
});
|
|
166
|
+
requireServiceMethod(userSettingsRepository, "ensureForUserId", contributorId, {
|
|
167
|
+
serviceLabel: "userSettingsRepository"
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return Object.freeze({
|
|
171
|
+
contributorId,
|
|
172
|
+
async contribute({ request = null, reply = null } = {}) {
|
|
173
|
+
const authResult = await request.executeAction({
|
|
174
|
+
actionId: "auth.session.read"
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
if (authResult?.clearSession === true && typeof authService?.clearSessionCookies === "function") {
|
|
178
|
+
authService.clearSessionCookies(reply);
|
|
179
|
+
}
|
|
180
|
+
if (authResult?.session && typeof authService?.writeSessionCookies === "function") {
|
|
181
|
+
authService.writeSessionCookies(reply, authResult.session);
|
|
182
|
+
}
|
|
183
|
+
if (authResult?.transientFailure === true) {
|
|
184
|
+
throw new AppError(503, "Authentication service temporarily unavailable. Please retry.");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const normalizedUser = authenticatedUserValidator.normalize(authResult?.authenticated ? authResult?.profile : null);
|
|
188
|
+
let payload = createAnonymousBootstrapPayload({
|
|
189
|
+
appState,
|
|
190
|
+
tenancyProfile: resolvedTenancyProfile
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (normalizedUser) {
|
|
194
|
+
const latestProfile = (await usersRepository.findById(normalizedUser.id)) || normalizedUser;
|
|
195
|
+
const userSettings = await userSettingsRepository.ensureForUserId(latestProfile.id);
|
|
196
|
+
|
|
197
|
+
let seededConsoleOwnerUserId = 0;
|
|
198
|
+
if (
|
|
199
|
+
consoleService &&
|
|
200
|
+
typeof consoleService.ensureInitialConsoleMember === "function"
|
|
201
|
+
) {
|
|
202
|
+
seededConsoleOwnerUserId = Number(await consoleService.ensureInitialConsoleMember(latestProfile.id));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
payload = {
|
|
206
|
+
session: {
|
|
207
|
+
authenticated: true,
|
|
208
|
+
userId: latestProfile.id
|
|
209
|
+
},
|
|
210
|
+
profile: {
|
|
211
|
+
displayName: latestProfile.displayName,
|
|
212
|
+
email: latestProfile.email,
|
|
213
|
+
avatar: accountAvatarFormatter(latestProfile, userSettings)
|
|
214
|
+
},
|
|
215
|
+
tenancy: resolvedTenancyProfile,
|
|
216
|
+
app: appState,
|
|
217
|
+
workspaces: [],
|
|
218
|
+
pendingInvites: [],
|
|
219
|
+
activeWorkspace: null,
|
|
220
|
+
membership: null,
|
|
221
|
+
requestedWorkspace: null,
|
|
222
|
+
permissions: [],
|
|
223
|
+
surfaceAccess: {
|
|
224
|
+
consoleowner: seededConsoleOwnerUserId > 0 && seededConsoleOwnerUserId === Number(latestProfile.id)
|
|
225
|
+
},
|
|
226
|
+
workspaceSettings: null,
|
|
227
|
+
userSettings: mapUserSettingsBootstrap(userSettings),
|
|
228
|
+
requestMeta: {
|
|
229
|
+
hasRequest: Boolean(request)
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const oauthCatalogPayload = getOAuthProviderCatalogPayload(authService);
|
|
235
|
+
const session = payload?.session && typeof payload.session === "object" ? payload.session : { authenticated: false };
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
...payload,
|
|
239
|
+
session: {
|
|
240
|
+
...session,
|
|
241
|
+
...oauthCatalogPayload
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { createUsersBootstrapContributor };
|