@jskit-ai/users-core 0.1.33 → 0.1.35
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
|
@@ -1,13 +1,7 @@
|
|
|
1
|
-
import { AppError } from "@jskit-ai/kernel/server/runtime";
|
|
2
1
|
import { requireServiceMethod } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
3
|
-
import { normalizeLowerText
|
|
2
|
+
import { normalizeLowerText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
4
3
|
import {
|
|
5
4
|
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
5
|
resolveTenancyProfile
|
|
12
6
|
} from "../shared/tenancyProfile.js";
|
|
13
7
|
import { workspacePendingInvitationsResource } from "../shared/resources/workspacePendingInvitationsResource.js";
|
|
@@ -16,9 +10,6 @@ import {
|
|
|
16
10
|
mapWorkspaceSettingsPublic,
|
|
17
11
|
mapWorkspaceSummary
|
|
18
12
|
} from "./common/formatters/workspaceFormatter.js";
|
|
19
|
-
import { accountAvatarFormatter } from "./common/formatters/accountAvatarFormatter.js";
|
|
20
|
-
import { authenticatedUserValidator } from "./common/validators/authenticatedUserValidator.js";
|
|
21
|
-
import { userSettingsFields } from "../shared/resources/userSettingsFields.js";
|
|
22
13
|
|
|
23
14
|
const REQUESTED_WORKSPACE_STATUS_RESOLVED = "resolved";
|
|
24
15
|
const REQUESTED_WORKSPACE_STATUS_NOT_FOUND = "not_found";
|
|
@@ -31,47 +22,6 @@ function normalizePendingInvites(invites) {
|
|
|
31
22
|
}).pendingInvites;
|
|
32
23
|
}
|
|
33
24
|
|
|
34
|
-
function getOAuthProviderCatalogPayload(authService) {
|
|
35
|
-
if (!authService || typeof authService.getOAuthProviderCatalog !== "function") {
|
|
36
|
-
return {
|
|
37
|
-
oauthProviders: [],
|
|
38
|
-
oauthDefaultProvider: null
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const catalog = authService.getOAuthProviderCatalog();
|
|
43
|
-
const providers = Array.isArray(catalog?.providers)
|
|
44
|
-
? catalog.providers
|
|
45
|
-
.map((provider) => ({
|
|
46
|
-
id: normalizeLowerText(provider?.id),
|
|
47
|
-
label: normalizeText(provider?.label)
|
|
48
|
-
}))
|
|
49
|
-
.filter((provider) => provider.id && provider.label)
|
|
50
|
-
: [];
|
|
51
|
-
const defaultProvider = normalizeLowerText(catalog?.defaultProvider);
|
|
52
|
-
|
|
53
|
-
return {
|
|
54
|
-
oauthProviders: providers,
|
|
55
|
-
oauthDefaultProvider: providers.some((provider) => provider.id === defaultProvider) ? defaultProvider : null
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function normalizeBoolean(value, fallback) {
|
|
60
|
-
if (typeof value === "boolean") {
|
|
61
|
-
return value;
|
|
62
|
-
}
|
|
63
|
-
if (typeof value === "string") {
|
|
64
|
-
const normalized = normalizeLowerText(value);
|
|
65
|
-
if (normalized === "true") {
|
|
66
|
-
return true;
|
|
67
|
-
}
|
|
68
|
-
if (normalized === "false") {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return fallback;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
25
|
function normalizeQueryPayload(value = {}) {
|
|
76
26
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
77
27
|
return {};
|
|
@@ -137,111 +87,28 @@ function resolveRequestedWorkspaceStatusFromError(error) {
|
|
|
137
87
|
return "";
|
|
138
88
|
}
|
|
139
89
|
|
|
140
|
-
function resolveAppState(appConfig = {}, { workspaceInvitationsEnabled = true } = {}) {
|
|
141
|
-
const features = {
|
|
142
|
-
workspaceSwitching: normalizeBoolean(appConfig.workspaceSwitching, true),
|
|
143
|
-
workspaceInvites: workspaceInvitationsEnabled === true,
|
|
144
|
-
assistantEnabled: normalizeBoolean(appConfig.assistantEnabled, false),
|
|
145
|
-
assistantRequiredPermission: normalizeText(appConfig.assistantRequiredPermission),
|
|
146
|
-
socialEnabled: normalizeBoolean(appConfig.socialEnabled, false),
|
|
147
|
-
socialFederationEnabled: normalizeBoolean(appConfig.socialFederationEnabled, false)
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
return {
|
|
151
|
-
features
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function normalizeSlugPolicy(value = "") {
|
|
156
|
-
const normalizedValue = normalizeLowerText(value);
|
|
157
|
-
if (
|
|
158
|
-
normalizedValue === WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME ||
|
|
159
|
-
normalizedValue === WORKSPACE_SLUG_POLICY_USER_SELECTED
|
|
160
|
-
) {
|
|
161
|
-
return normalizedValue;
|
|
162
|
-
}
|
|
163
|
-
return WORKSPACE_SLUG_POLICY_NONE;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function isSupportedTenancyMode(value = "") {
|
|
167
|
-
return value === TENANCY_MODE_NONE || value === TENANCY_MODE_PERSONAL || value === TENANCY_MODE_WORKSPACES;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
90
|
function resolveBootstrapTenancyProfile(tenancyProfile = null, appConfig = {}) {
|
|
171
91
|
const fallback = resolveTenancyProfile(appConfig);
|
|
172
|
-
const source = tenancyProfile && typeof tenancyProfile === "object" ? tenancyProfile : fallback;
|
|
173
|
-
const mode = isSupportedTenancyMode(source?.mode) ? source.mode : fallback.mode;
|
|
174
|
-
const workspace = source?.workspace && typeof source.workspace === "object" ? source.workspace : fallback.workspace;
|
|
175
|
-
|
|
176
92
|
return Object.freeze({
|
|
177
|
-
mode,
|
|
93
|
+
mode: fallback.mode,
|
|
178
94
|
workspace: Object.freeze({
|
|
179
|
-
enabled: workspace.enabled === true,
|
|
180
|
-
autoProvision: workspace.autoProvision === true,
|
|
181
|
-
allowSelfCreate: workspace.allowSelfCreate === true,
|
|
182
|
-
slugPolicy:
|
|
95
|
+
enabled: fallback.workspace.enabled === true,
|
|
96
|
+
autoProvision: fallback.workspace.autoProvision === true,
|
|
97
|
+
allowSelfCreate: fallback.workspace.allowSelfCreate === true,
|
|
98
|
+
slugPolicy: fallback.workspace.slugPolicy
|
|
183
99
|
})
|
|
184
100
|
});
|
|
185
101
|
}
|
|
186
102
|
|
|
187
|
-
function createAnonymousBootstrapPayload({ appState, tenancyProfile }) {
|
|
188
|
-
return {
|
|
189
|
-
session: {
|
|
190
|
-
authenticated: false
|
|
191
|
-
},
|
|
192
|
-
profile: null,
|
|
193
|
-
tenancy: tenancyProfile,
|
|
194
|
-
app: appState,
|
|
195
|
-
workspaces: [],
|
|
196
|
-
pendingInvites: [],
|
|
197
|
-
activeWorkspace: null,
|
|
198
|
-
membership: null,
|
|
199
|
-
requestedWorkspace: null,
|
|
200
|
-
permissions: [],
|
|
201
|
-
surfaceAccess: {
|
|
202
|
-
consoleowner: false
|
|
203
|
-
},
|
|
204
|
-
workspaceSettings: null,
|
|
205
|
-
userSettings: null
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function mapUserSettingsBootstrap(settings = {}) {
|
|
210
|
-
const source = settings && typeof settings === "object" ? settings : {};
|
|
211
|
-
const mapped = {};
|
|
212
|
-
|
|
213
|
-
for (const field of userSettingsFields) {
|
|
214
|
-
if (field.includeInBootstrap === false) {
|
|
215
|
-
continue;
|
|
216
|
-
}
|
|
217
|
-
const rawValue = Object.hasOwn(source, field.key)
|
|
218
|
-
? source[field.key]
|
|
219
|
-
: field.resolveDefault({
|
|
220
|
-
settings: source
|
|
221
|
-
});
|
|
222
|
-
mapped[field.key] = field.normalizeOutput(rawValue, {
|
|
223
|
-
settings: source
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return mapped;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
103
|
function createWorkspaceBootstrapContributor({
|
|
231
104
|
workspaceService,
|
|
232
105
|
workspacePendingInvitationsService,
|
|
233
106
|
usersRepository,
|
|
234
|
-
userSettingsRepository,
|
|
235
107
|
workspaceInvitationsEnabled = false,
|
|
236
108
|
appConfig = {},
|
|
237
|
-
tenancyProfile = null
|
|
238
|
-
authService,
|
|
239
|
-
consoleService = null
|
|
109
|
+
tenancyProfile = null
|
|
240
110
|
} = {}) {
|
|
241
|
-
const contributorId = "users.bootstrap";
|
|
242
|
-
const appState = resolveAppState(appConfig, {
|
|
243
|
-
workspaceInvitationsEnabled
|
|
244
|
-
});
|
|
111
|
+
const contributorId = "users.workspace.bootstrap";
|
|
245
112
|
const resolvedTenancyProfile = resolveBootstrapTenancyProfile(tenancyProfile, appConfig);
|
|
246
113
|
|
|
247
114
|
requireServiceMethod(workspaceService, "listWorkspacesForUser", contributorId, {
|
|
@@ -255,144 +122,83 @@ function createWorkspaceBootstrapContributor({
|
|
|
255
122
|
serviceLabel: "workspacePendingInvitationsService"
|
|
256
123
|
});
|
|
257
124
|
}
|
|
258
|
-
requireServiceMethod(usersRepository, "
|
|
125
|
+
requireServiceMethod(usersRepository, "findById", contributorId, {
|
|
259
126
|
serviceLabel: "usersRepository"
|
|
260
127
|
});
|
|
261
|
-
requireServiceMethod(userSettingsRepository, "ensureForUserId", contributorId, {
|
|
262
|
-
serviceLabel: "userSettingsRepository"
|
|
263
|
-
});
|
|
264
128
|
|
|
265
129
|
return Object.freeze({
|
|
266
130
|
contributorId,
|
|
267
|
-
async contribute({ request = null,
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
131
|
+
async contribute({ request = null, query = {}, payload = {} } = {}) {
|
|
132
|
+
const normalizedUserId = Number(payload?.session?.authenticated === true ? payload?.session?.userId : 0);
|
|
133
|
+
const normalizedWorkspaceSlug = resolveBootstrapWorkspaceSlug({ query, request });
|
|
134
|
+
if (!normalizedUserId) {
|
|
135
|
+
if (!normalizedWorkspaceSlug || resolvedTenancyProfile.mode === TENANCY_MODE_NONE) {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
271
138
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
if (authResult?.transientFailure === true) {
|
|
279
|
-
throw new AppError(503, "Authentication service temporarily unavailable. Please retry.");
|
|
139
|
+
return {
|
|
140
|
+
requestedWorkspace: createRequestedWorkspacePayload(
|
|
141
|
+
normalizedWorkspaceSlug,
|
|
142
|
+
REQUESTED_WORKSPACE_STATUS_UNAUTHENTICATED
|
|
143
|
+
)
|
|
144
|
+
};
|
|
280
145
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
consoleService &&
|
|
286
|
-
typeof consoleService.ensureInitialConsoleMember === "function"
|
|
287
|
-
) {
|
|
288
|
-
seededConsoleOwnerUserId = Number(await consoleService.ensureInitialConsoleMember(authResult.profile.id));
|
|
146
|
+
|
|
147
|
+
const latestProfile = await usersRepository.findById(normalizedUserId);
|
|
148
|
+
if (!latestProfile) {
|
|
149
|
+
return {};
|
|
289
150
|
}
|
|
290
151
|
|
|
291
|
-
const user = authResult?.authenticated ? authResult.profile : null;
|
|
292
|
-
const normalizedUser = authenticatedUserValidator.normalize(user);
|
|
293
152
|
const pendingInvites =
|
|
294
|
-
workspaceInvitationsEnabled
|
|
153
|
+
workspaceInvitationsEnabled
|
|
295
154
|
? normalizePendingInvites(
|
|
296
|
-
await workspacePendingInvitationsService.listPendingInvitesForUser(
|
|
155
|
+
await workspacePendingInvitationsService.listPendingInvitesForUser(latestProfile, {
|
|
297
156
|
context: {
|
|
298
|
-
actor:
|
|
157
|
+
actor: latestProfile
|
|
299
158
|
}
|
|
300
159
|
})
|
|
301
160
|
)
|
|
302
161
|
: [];
|
|
303
|
-
const
|
|
304
|
-
let
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
...payload,
|
|
311
|
-
requestedWorkspace: createRequestedWorkspacePayload(
|
|
162
|
+
const workspaces = await workspaceService.listWorkspacesForUser(latestProfile, { request });
|
|
163
|
+
let workspaceContext = null;
|
|
164
|
+
let requestedWorkspace = null;
|
|
165
|
+
if (normalizedWorkspaceSlug && resolvedTenancyProfile.mode !== TENANCY_MODE_NONE) {
|
|
166
|
+
try {
|
|
167
|
+
workspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
168
|
+
latestProfile,
|
|
312
169
|
normalizedWorkspaceSlug,
|
|
313
|
-
|
|
314
|
-
)
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
})) || normalizedUser;
|
|
324
|
-
|
|
325
|
-
const workspaces = await workspaceService.listWorkspacesForUser(latestProfile, { request });
|
|
326
|
-
let workspaceContext = null;
|
|
327
|
-
let requestedWorkspace = null;
|
|
328
|
-
if (normalizedWorkspaceSlug && resolvedTenancyProfile.mode !== TENANCY_MODE_NONE) {
|
|
329
|
-
try {
|
|
330
|
-
workspaceContext = await workspaceService.resolveWorkspaceContextForUserBySlug(
|
|
331
|
-
latestProfile,
|
|
332
|
-
normalizedWorkspaceSlug,
|
|
333
|
-
{ request }
|
|
334
|
-
);
|
|
335
|
-
requestedWorkspace = createRequestedWorkspacePayload(
|
|
336
|
-
normalizedWorkspaceSlug,
|
|
337
|
-
REQUESTED_WORKSPACE_STATUS_RESOLVED
|
|
338
|
-
);
|
|
339
|
-
} catch (error) {
|
|
340
|
-
const requestedWorkspaceStatus = resolveRequestedWorkspaceStatusFromError(error);
|
|
341
|
-
if (!requestedWorkspaceStatus) {
|
|
342
|
-
throw error;
|
|
343
|
-
}
|
|
344
|
-
requestedWorkspace = createRequestedWorkspacePayload(normalizedWorkspaceSlug, requestedWorkspaceStatus);
|
|
170
|
+
{ request }
|
|
171
|
+
);
|
|
172
|
+
requestedWorkspace = createRequestedWorkspacePayload(
|
|
173
|
+
normalizedWorkspaceSlug,
|
|
174
|
+
REQUESTED_WORKSPACE_STATUS_RESOLVED
|
|
175
|
+
);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
const requestedWorkspaceStatus = resolveRequestedWorkspaceStatusFromError(error);
|
|
178
|
+
if (!requestedWorkspaceStatus) {
|
|
179
|
+
throw error;
|
|
345
180
|
}
|
|
181
|
+
requestedWorkspace = createRequestedWorkspacePayload(normalizedWorkspaceSlug, requestedWorkspaceStatus);
|
|
346
182
|
}
|
|
347
|
-
|
|
348
|
-
const userSettings = await userSettingsRepository.ensureForUserId(latestProfile.id);
|
|
349
|
-
payload = {
|
|
350
|
-
session: {
|
|
351
|
-
authenticated: true,
|
|
352
|
-
userId: latestProfile.id
|
|
353
|
-
},
|
|
354
|
-
profile: {
|
|
355
|
-
displayName: latestProfile.displayName,
|
|
356
|
-
email: latestProfile.email,
|
|
357
|
-
avatar: accountAvatarFormatter(latestProfile, userSettings)
|
|
358
|
-
},
|
|
359
|
-
tenancy: resolvedTenancyProfile,
|
|
360
|
-
app: appState,
|
|
361
|
-
workspaces: [...workspaces],
|
|
362
|
-
pendingInvites,
|
|
363
|
-
activeWorkspace: workspaceContext
|
|
364
|
-
? mapWorkspaceSummary(workspaceContext.workspace, {
|
|
365
|
-
roleSid: workspaceContext.membership?.roleSid,
|
|
366
|
-
status: workspaceContext.membership?.status
|
|
367
|
-
})
|
|
368
|
-
: null,
|
|
369
|
-
membership: mapMembershipSummary(workspaceContext?.membership, workspaceContext?.workspace),
|
|
370
|
-
requestedWorkspace,
|
|
371
|
-
permissions: workspaceContext ? [...workspaceContext.permissions] : [],
|
|
372
|
-
surfaceAccess: {
|
|
373
|
-
consoleowner: seededConsoleOwnerUserId > 0 && seededConsoleOwnerUserId === Number(latestProfile.id)
|
|
374
|
-
},
|
|
375
|
-
workspaceSettings: workspaceContext
|
|
376
|
-
? mapWorkspaceSettingsPublic(workspaceContext.workspaceSettings, {
|
|
377
|
-
workspaceInvitationsEnabled
|
|
378
|
-
})
|
|
379
|
-
: null,
|
|
380
|
-
userSettings: mapUserSettingsBootstrap(userSettings),
|
|
381
|
-
requestMeta: {
|
|
382
|
-
hasRequest: Boolean(request)
|
|
383
|
-
}
|
|
384
|
-
};
|
|
385
183
|
}
|
|
386
184
|
|
|
387
|
-
const oauthCatalogPayload = getOAuthProviderCatalogPayload(authService);
|
|
388
|
-
const session = payload?.session && typeof payload.session === "object" ? payload.session : { authenticated: false };
|
|
389
|
-
|
|
390
185
|
return {
|
|
391
|
-
...
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
186
|
+
workspaces: [...workspaces],
|
|
187
|
+
pendingInvites,
|
|
188
|
+
activeWorkspace: workspaceContext
|
|
189
|
+
? mapWorkspaceSummary(workspaceContext.workspace, {
|
|
190
|
+
roleSid: workspaceContext.membership?.roleSid,
|
|
191
|
+
status: workspaceContext.membership?.status
|
|
192
|
+
})
|
|
193
|
+
: null,
|
|
194
|
+
membership: mapMembershipSummary(workspaceContext?.membership, workspaceContext?.workspace),
|
|
195
|
+
requestedWorkspace,
|
|
196
|
+
permissions: workspaceContext ? [...workspaceContext.permissions] : [],
|
|
197
|
+
workspaceSettings: workspaceContext
|
|
198
|
+
? mapWorkspaceSettingsPublic(workspaceContext.workspaceSettings, {
|
|
199
|
+
workspaceInvitationsEnabled
|
|
200
|
+
})
|
|
201
|
+
: null
|
|
396
202
|
};
|
|
397
203
|
}
|
|
398
204
|
});
|
package/src/shared/settings.js
CHANGED
|
@@ -30,8 +30,7 @@ const DEFAULT_USER_SETTINGS = Object.freeze({
|
|
|
30
30
|
accountActivity: true,
|
|
31
31
|
securityAlerts: true,
|
|
32
32
|
passwordSignInEnabled: true,
|
|
33
|
-
passwordSetupRequired: false
|
|
34
|
-
lastActiveWorkspaceId: null
|
|
33
|
+
passwordSetupRequired: false
|
|
35
34
|
});
|
|
36
35
|
|
|
37
36
|
function normalizeWorkspaceHexColor(value) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {import('knex').Knex} knex
|
|
3
|
+
*/
|
|
4
|
+
exports.up = async function up(knex) {
|
|
5
|
+
const hasUsersTable = await knex.schema.hasTable("users");
|
|
6
|
+
if (!hasUsersTable) {
|
|
7
|
+
await knex.schema.createTable("users", (table) => {
|
|
8
|
+
table.increments("id").primary();
|
|
9
|
+
table.string("auth_provider", 64).notNullable();
|
|
10
|
+
table.string("auth_provider_user_sid", 191).notNullable();
|
|
11
|
+
table.string("email", 255).notNullable();
|
|
12
|
+
table.string("username", 120).notNullable();
|
|
13
|
+
table.string("display_name", 160).notNullable();
|
|
14
|
+
table.string("avatar_storage_key", 512).nullable();
|
|
15
|
+
table.string("avatar_version", 64).nullable();
|
|
16
|
+
table.timestamp("avatar_updated_at", { useTz: false }).nullable();
|
|
17
|
+
table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
18
|
+
table.unique(["auth_provider", "auth_provider_user_sid"], "uq_users_identity");
|
|
19
|
+
table.unique(["email"], "uq_users_email");
|
|
20
|
+
table.unique(["username"], "uq_users_username");
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const hasUserSettingsTable = await knex.schema.hasTable("user_settings");
|
|
25
|
+
if (!hasUserSettingsTable) {
|
|
26
|
+
await knex.schema.createTable("user_settings", (table) => {
|
|
27
|
+
table.integer("user_id").unsigned().primary().references("id").inTable("users").onDelete("CASCADE");
|
|
28
|
+
table.string("theme", 32).notNullable().defaultTo("system");
|
|
29
|
+
table.string("locale", 24).notNullable().defaultTo("en");
|
|
30
|
+
table.string("time_zone", 64).notNullable().defaultTo("UTC");
|
|
31
|
+
table.string("date_format", 32).notNullable().defaultTo("yyyy-mm-dd");
|
|
32
|
+
table.string("number_format", 32).notNullable().defaultTo("1,234.56");
|
|
33
|
+
table.string("currency_code", 3).notNullable().defaultTo("USD");
|
|
34
|
+
table.integer("avatar_size").notNullable().defaultTo(64);
|
|
35
|
+
table.boolean("password_sign_in_enabled").notNullable().defaultTo(true);
|
|
36
|
+
table.boolean("password_setup_required").notNullable().defaultTo(false);
|
|
37
|
+
table.boolean("notify_product_updates").notNullable().defaultTo(true);
|
|
38
|
+
table.boolean("notify_account_activity").notNullable().defaultTo(true);
|
|
39
|
+
table.boolean("notify_security_alerts").notNullable().defaultTo(true);
|
|
40
|
+
table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
41
|
+
table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const hasConsoleSettingsTable = await knex.schema.hasTable("console_settings");
|
|
46
|
+
if (!hasConsoleSettingsTable) {
|
|
47
|
+
await knex.schema.createTable("console_settings", (table) => {
|
|
48
|
+
table.integer("id").primary();
|
|
49
|
+
table.integer("owner_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
|
|
50
|
+
table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
51
|
+
table.timestamp("updated_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await knex("console_settings").insert({
|
|
55
|
+
id: 1,
|
|
56
|
+
created_at: knex.fn.now(),
|
|
57
|
+
updated_at: knex.fn.now()
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {import('knex').Knex} knex
|
|
64
|
+
*/
|
|
65
|
+
exports.down = async function down(knex) {
|
|
66
|
+
await knex.schema.dropTableIfExists("console_settings");
|
|
67
|
+
await knex.schema.dropTableIfExists("user_settings");
|
|
68
|
+
await knex.schema.dropTableIfExists("users");
|
|
69
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { registerUsersCore } from "../src/server/registerUsersCore.js";
|
|
4
|
+
|
|
5
|
+
test("registerUsersCore registers console and workspace action surface aliases when action runtime is available", () => {
|
|
6
|
+
const calls = [];
|
|
7
|
+
const app = {
|
|
8
|
+
singleton() {
|
|
9
|
+
return this;
|
|
10
|
+
},
|
|
11
|
+
actionSurfaceSource(sourceName, resolver) {
|
|
12
|
+
calls.push({
|
|
13
|
+
sourceName: String(sourceName || ""),
|
|
14
|
+
resolverType: typeof resolver
|
|
15
|
+
});
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
registerUsersCore(app);
|
|
21
|
+
|
|
22
|
+
assert.deepEqual(calls, [
|
|
23
|
+
{
|
|
24
|
+
sourceName: "workspace",
|
|
25
|
+
resolverType: "function"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
sourceName: "console",
|
|
29
|
+
resolverType: "function"
|
|
30
|
+
}
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("registerUsersCore still works when action runtime has not installed actionSurfaceSource yet", () => {
|
|
35
|
+
const app = {
|
|
36
|
+
singleton() {
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
assert.doesNotThrow(() => registerUsersCore(app));
|
|
42
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createUsersBootstrapContributor } from "../src/server/usersBootstrapContributor.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
|
+
authProviderUserSid: "user-7",
|
|
14
|
+
username: "tester",
|
|
15
|
+
displayName: "Test User",
|
|
16
|
+
email: "test@example.com",
|
|
17
|
+
...overrides
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function createUserSettings() {
|
|
22
|
+
return {
|
|
23
|
+
theme: "system",
|
|
24
|
+
locale: "en",
|
|
25
|
+
timeZone: "UTC",
|
|
26
|
+
dateFormat: "YYYY-MM-DD",
|
|
27
|
+
numberFormat: "1,234.56",
|
|
28
|
+
currencyCode: "USD",
|
|
29
|
+
avatarSize: 64,
|
|
30
|
+
productUpdates: true,
|
|
31
|
+
accountActivity: true,
|
|
32
|
+
securityAlerts: true
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("users bootstrap contributor seeds the initial console owner and exposes generic app payload", async () => {
|
|
37
|
+
const profile = createAuthenticatedProfile({ id: 12 });
|
|
38
|
+
const consoleOwnerSeeds = [];
|
|
39
|
+
const writtenSessions = [];
|
|
40
|
+
const contributor = createUsersBootstrapContributor({
|
|
41
|
+
usersRepository: {
|
|
42
|
+
async findById() {
|
|
43
|
+
return profile;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
userSettingsRepository: {
|
|
47
|
+
async ensureForUserId() {
|
|
48
|
+
return createUserSettings();
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
authService: {
|
|
52
|
+
writeSessionCookies(reply, session) {
|
|
53
|
+
writtenSessions.push({ reply, session });
|
|
54
|
+
},
|
|
55
|
+
getOAuthProviderCatalog() {
|
|
56
|
+
return {
|
|
57
|
+
providers: [
|
|
58
|
+
{ id: "google", label: "Google" }
|
|
59
|
+
],
|
|
60
|
+
defaultProvider: "google"
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
consoleService: {
|
|
65
|
+
async ensureInitialConsoleMember(userId) {
|
|
66
|
+
consoleOwnerSeeds.push(Number(userId));
|
|
67
|
+
return Number(userId);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const reply = {};
|
|
73
|
+
const payload = await contributor.contribute({
|
|
74
|
+
request: {
|
|
75
|
+
async executeAction() {
|
|
76
|
+
return {
|
|
77
|
+
authenticated: true,
|
|
78
|
+
profile,
|
|
79
|
+
session: {
|
|
80
|
+
csrfToken: "csrf-1"
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
reply
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.deepEqual(consoleOwnerSeeds, [12]);
|
|
89
|
+
assert.equal(writtenSessions.length, 1);
|
|
90
|
+
assert.equal(writtenSessions[0].reply, reply);
|
|
91
|
+
assert.deepEqual(writtenSessions[0].session, {
|
|
92
|
+
csrfToken: "csrf-1"
|
|
93
|
+
});
|
|
94
|
+
assert.equal(payload.session.authenticated, true);
|
|
95
|
+
assert.equal(payload.session.userId, 12);
|
|
96
|
+
assert.equal(payload.surfaceAccess.consoleowner, true);
|
|
97
|
+
assert.equal(payload.app.features.workspaceSwitching, false);
|
|
98
|
+
assert.deepEqual(payload.session.oauthProviders, [
|
|
99
|
+
{
|
|
100
|
+
id: "google",
|
|
101
|
+
label: "Google"
|
|
102
|
+
}
|
|
103
|
+
]);
|
|
104
|
+
assert.equal(payload.session.oauthDefaultProvider, "google");
|
|
105
|
+
assert.deepEqual(payload.workspaces, []);
|
|
106
|
+
assert.deepEqual(payload.userSettings, {});
|
|
107
|
+
assert.equal(payload.requestMeta.hasRequest, true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("users bootstrap contributor emits canonical tenancy profile for anonymous bootstrap", async () => {
|
|
111
|
+
const contributor = createUsersBootstrapContributor({
|
|
112
|
+
usersRepository: {
|
|
113
|
+
async findById() {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
userSettingsRepository: {
|
|
118
|
+
async ensureForUserId() {
|
|
119
|
+
return createUserSettings();
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
tenancyProfile: {
|
|
123
|
+
mode: TENANCY_MODE_PERSONAL,
|
|
124
|
+
workspace: {
|
|
125
|
+
enabled: true,
|
|
126
|
+
autoProvision: true,
|
|
127
|
+
allowSelfCreate: false,
|
|
128
|
+
slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
appConfig: {
|
|
132
|
+
tenancyMode: "none"
|
|
133
|
+
},
|
|
134
|
+
authService: {
|
|
135
|
+
getOAuthProviderCatalog() {
|
|
136
|
+
return {
|
|
137
|
+
providers: [],
|
|
138
|
+
defaultProvider: null
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const payload = await contributor.contribute({
|
|
145
|
+
request: {
|
|
146
|
+
async executeAction() {
|
|
147
|
+
return {
|
|
148
|
+
authenticated: false
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
reply: {}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
assert.deepEqual(payload.tenancy, {
|
|
156
|
+
mode: TENANCY_MODE_PERSONAL,
|
|
157
|
+
workspace: {
|
|
158
|
+
enabled: true,
|
|
159
|
+
autoProvision: true,
|
|
160
|
+
allowSelfCreate: false,
|
|
161
|
+
slugPolicy: WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
assert.deepEqual(payload.session, {
|
|
165
|
+
authenticated: false,
|
|
166
|
+
oauthProviders: [],
|
|
167
|
+
oauthDefaultProvider: null
|
|
168
|
+
});
|
|
169
|
+
assert.deepEqual(payload.workspaces, []);
|
|
170
|
+
assert.equal(payload.surfaceAccess.consoleowner, false);
|
|
171
|
+
assert.equal(payload.userSettings, null);
|
|
172
|
+
});
|