@jskit-ai/users-core 0.1.19 → 0.1.21
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 +6 -6
- package/package.json +5 -5
- package/src/server/common/repositories/userProfilesRepository.js +10 -10
- package/src/server/common/repositories/workspaceMembershipsRepository.js +1 -1
- package/src/server/registerWorkspaceCore.js +2 -2
- package/src/server/workspaceBootstrapContributor.js +2 -2
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +1 -1
- package/src/shared/index.js +6 -6
- package/src/shared/support/usersApiPaths.js +11 -2
- package/src/shared/tenancyMode.js +3 -3
- package/src/shared/tenancyProfile.js +5 -5
- package/templates/migrations/users_core_initial.cjs +9 -9
- package/templates/migrations/users_core_profile_username.cjs +13 -13
- package/test/tenancyProfile.test.js +10 -10
- package/test/usersApiPaths.test.js +18 -0
- package/test/usersRouteRequestInputValidator.test.js +2 -2
- package/test/workspaceBootstrapContributor.test.js +5 -5
- package/test/workspaceService.test.js +5 -5
package/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/users-core",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.21",
|
|
5
5
|
description: "Users/workspace domain runtime plus HTTP routes for workspace, account, and console features.",
|
|
6
6
|
dependsOn: [
|
|
7
7
|
"@jskit-ai/auth-core",
|
|
@@ -203,10 +203,10 @@ export default Object.freeze({
|
|
|
203
203
|
mutations: {
|
|
204
204
|
dependencies: {
|
|
205
205
|
runtime: {
|
|
206
|
-
"@jskit-ai/auth-core": "0.1.
|
|
207
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
208
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
209
|
-
"@jskit-ai/kernel": "0.1.
|
|
206
|
+
"@jskit-ai/auth-core": "0.1.15",
|
|
207
|
+
"@jskit-ai/database-runtime": "0.1.16",
|
|
208
|
+
"@jskit-ai/http-runtime": "0.1.15",
|
|
209
|
+
"@jskit-ai/kernel": "0.1.16",
|
|
210
210
|
"@fastify/multipart": "^9.4.0",
|
|
211
211
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
212
212
|
"typebox": "^1.0.81"
|
|
@@ -397,7 +397,7 @@ export default Object.freeze({
|
|
|
397
397
|
id: "users-core-surface-config-workspace",
|
|
398
398
|
when: {
|
|
399
399
|
config: "tenancyMode",
|
|
400
|
-
in: ["personal", "
|
|
400
|
+
in: ["personal", "workspaces"]
|
|
401
401
|
}
|
|
402
402
|
},
|
|
403
403
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -24,10 +24,10 @@
|
|
|
24
24
|
"./shared/resources/consoleSettingsFields": "./src/shared/resources/consoleSettingsFields.js"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@jskit-ai/auth-core": "0.1.
|
|
28
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
29
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
30
|
-
"@jskit-ai/kernel": "0.1.
|
|
27
|
+
"@jskit-ai/auth-core": "0.1.15",
|
|
28
|
+
"@jskit-ai/database-runtime": "0.1.16",
|
|
29
|
+
"@jskit-ai/http-runtime": "0.1.15",
|
|
30
|
+
"@jskit-ai/kernel": "0.1.16",
|
|
31
31
|
"@fastify/multipart": "^9.4.0",
|
|
32
32
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
33
33
|
"typebox": "^1.0.81"
|
|
@@ -93,7 +93,7 @@ function createDuplicateEmailConflictError() {
|
|
|
93
93
|
async function resolveUniqueUsername(client, baseUsername, { excludeUserId = 0 } = {}) {
|
|
94
94
|
for (let suffix = 0; suffix < 1000; suffix += 1) {
|
|
95
95
|
const candidate = buildUsernameCandidate(baseUsername, suffix);
|
|
96
|
-
const existing = await client("
|
|
96
|
+
const existing = await client("users").where({ username: candidate }).first();
|
|
97
97
|
if (!existing || Number(existing.id) === Number(excludeUserId || 0)) {
|
|
98
98
|
return candidate;
|
|
99
99
|
}
|
|
@@ -109,7 +109,7 @@ function createRepository(knex) {
|
|
|
109
109
|
|
|
110
110
|
async function findById(userId, options = {}) {
|
|
111
111
|
const client = options?.trx || knex;
|
|
112
|
-
const row = await client("
|
|
112
|
+
const row = await client("users").where({ id: userId }).first();
|
|
113
113
|
return mapProfileRow(row);
|
|
114
114
|
}
|
|
115
115
|
|
|
@@ -120,7 +120,7 @@ function createRepository(knex) {
|
|
|
120
120
|
return null;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
const row = await client("
|
|
123
|
+
const row = await client("users")
|
|
124
124
|
.where({
|
|
125
125
|
auth_provider: identity.provider,
|
|
126
126
|
auth_provider_user_id: identity.providerUserId
|
|
@@ -131,7 +131,7 @@ function createRepository(knex) {
|
|
|
131
131
|
|
|
132
132
|
async function updateDisplayNameById(userId, displayName, options = {}) {
|
|
133
133
|
const client = options?.trx || knex;
|
|
134
|
-
await client("
|
|
134
|
+
await client("users")
|
|
135
135
|
.where({ id: userId })
|
|
136
136
|
.update({
|
|
137
137
|
display_name: normalizeText(displayName)
|
|
@@ -141,7 +141,7 @@ function createRepository(knex) {
|
|
|
141
141
|
|
|
142
142
|
async function updateAvatarById(userId, avatar = {}, options = {}) {
|
|
143
143
|
const client = options?.trx || knex;
|
|
144
|
-
await client("
|
|
144
|
+
await client("users")
|
|
145
145
|
.where({ id: userId })
|
|
146
146
|
.update({
|
|
147
147
|
avatar_storage_key: avatar.avatarStorageKey || null,
|
|
@@ -154,7 +154,7 @@ function createRepository(knex) {
|
|
|
154
154
|
|
|
155
155
|
async function clearAvatarById(userId, options = {}) {
|
|
156
156
|
const client = options?.trx || knex;
|
|
157
|
-
await client("
|
|
157
|
+
await client("users")
|
|
158
158
|
.where({ id: userId })
|
|
159
159
|
.update({
|
|
160
160
|
avatar_storage_key: null,
|
|
@@ -183,7 +183,7 @@ function createRepository(knex) {
|
|
|
183
183
|
auth_provider: identity.provider,
|
|
184
184
|
auth_provider_user_id: identity.providerUserId
|
|
185
185
|
};
|
|
186
|
-
const existing = await trx("
|
|
186
|
+
const existing = await trx("users").where(where).first();
|
|
187
187
|
|
|
188
188
|
try {
|
|
189
189
|
if (existing) {
|
|
@@ -191,14 +191,14 @@ function createRepository(knex) {
|
|
|
191
191
|
const username = existingUsername || (await resolveUniqueUsername(trx, requestedUsername || usernameBaseFromEmail(email), {
|
|
192
192
|
excludeUserId: existing.id
|
|
193
193
|
}));
|
|
194
|
-
await trx("
|
|
194
|
+
await trx("users").where({ id: existing.id }).update({
|
|
195
195
|
email,
|
|
196
196
|
display_name: displayName,
|
|
197
197
|
username
|
|
198
198
|
});
|
|
199
199
|
} else {
|
|
200
200
|
const username = await resolveUniqueUsername(trx, requestedUsername || usernameBaseFromEmail(email));
|
|
201
|
-
await trx("
|
|
201
|
+
await trx("users").insert({
|
|
202
202
|
auth_provider: identity.provider,
|
|
203
203
|
auth_provider_user_id: identity.providerUserId,
|
|
204
204
|
email,
|
|
@@ -218,7 +218,7 @@ function createRepository(knex) {
|
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
const reloaded = await trx("
|
|
221
|
+
const reloaded = await trx("users").where(where).first();
|
|
222
222
|
return mapProfileRow(reloaded);
|
|
223
223
|
};
|
|
224
224
|
|
|
@@ -116,7 +116,7 @@ function createRepository(knex) {
|
|
|
116
116
|
async function listActiveByWorkspaceId(workspaceId, options = {}) {
|
|
117
117
|
const client = options?.trx || knex;
|
|
118
118
|
const rows = await client("workspace_memberships as wm")
|
|
119
|
-
.join("
|
|
119
|
+
.join("users as up", "up.id", "wm.user_id")
|
|
120
120
|
.where({ "wm.workspace_id": Number(workspaceId), "wm.status": "active" })
|
|
121
121
|
.orderBy("up.display_name", "asc")
|
|
122
122
|
.select([
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "@jskit-ai/kernel/server/actions";
|
|
5
5
|
import { registerRouteVisibilityResolver } from "@jskit-ai/kernel/server/http";
|
|
6
6
|
import { resolveAppConfig } from "@jskit-ai/kernel/server/support";
|
|
7
|
-
import {
|
|
7
|
+
import { TENANCY_MODE_WORKSPACES, resolveTenancyProfile } from "../shared/tenancyProfile.js";
|
|
8
8
|
import { createService as createWorkspaceService } from "./common/services/workspaceContextService.js";
|
|
9
9
|
import { createService as createAuthProfileSyncService } from "./common/services/authProfileSyncService.js";
|
|
10
10
|
import { createWorkspaceActionContextContributor } from "./common/contributors/workspaceActionContextContributor.js";
|
|
@@ -59,7 +59,7 @@ function registerWorkspaceCore(app) {
|
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
app.singleton(USERS_WORKSPACE_TENANCY_ENABLED_TOKEN, (scope) => {
|
|
62
|
-
return scope.make(USERS_TENANCY_PROFILE_TOKEN).mode ===
|
|
62
|
+
return scope.make(USERS_TENANCY_PROFILE_TOKEN).mode === TENANCY_MODE_WORKSPACES;
|
|
63
63
|
});
|
|
64
64
|
|
|
65
65
|
app.singleton(USERS_WORKSPACE_INVITATIONS_ENABLED_TOKEN, (scope) => {
|
|
@@ -4,7 +4,7 @@ import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actio
|
|
|
4
4
|
import {
|
|
5
5
|
TENANCY_MODE_NONE,
|
|
6
6
|
TENANCY_MODE_PERSONAL,
|
|
7
|
-
|
|
7
|
+
TENANCY_MODE_WORKSPACES,
|
|
8
8
|
WORKSPACE_SLUG_POLICY_NONE,
|
|
9
9
|
WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
|
|
10
10
|
WORKSPACE_SLUG_POLICY_USER_SELECTED,
|
|
@@ -164,7 +164,7 @@ function normalizeSlugPolicy(value = "") {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
function isSupportedTenancyMode(value = "") {
|
|
167
|
-
return value === TENANCY_MODE_NONE || value === TENANCY_MODE_PERSONAL || value ===
|
|
167
|
+
return value === TENANCY_MODE_NONE || value === TENANCY_MODE_PERSONAL || value === TENANCY_MODE_WORKSPACES;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
170
|
function resolveBootstrapTenancyProfile(tenancyProfile = null, appConfig = {}) {
|
|
@@ -35,7 +35,7 @@ const INVITE_RECIPIENT_BOOTSTRAP_AUDIENCE = Object.freeze({
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
const row = await knex("workspace_invites as wi")
|
|
38
|
-
.join("
|
|
38
|
+
.join("users as up", "up.email", "wi.email")
|
|
39
39
|
.where("wi.id", inviteId)
|
|
40
40
|
.first("up.id as user_id");
|
|
41
41
|
|
package/src/shared/index.js
CHANGED
|
@@ -28,13 +28,13 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
TENANCY_MODE_NONE,
|
|
30
30
|
TENANCY_MODE_PERSONAL,
|
|
31
|
-
|
|
31
|
+
TENANCY_MODE_WORKSPACES,
|
|
32
32
|
normalizeTenancyMode,
|
|
33
33
|
WORKSPACE_SLUG_POLICY_NONE,
|
|
34
34
|
WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
|
|
35
35
|
WORKSPACE_SLUG_POLICY_USER_SELECTED,
|
|
36
36
|
resolveTenancyProfile,
|
|
37
|
-
|
|
37
|
+
isWorkspacesTenancyMode
|
|
38
38
|
} from "./tenancyProfile.js";
|
|
39
39
|
import {
|
|
40
40
|
ACCOUNT_SETTINGS_CHANGED_EVENT,
|
|
@@ -71,13 +71,13 @@ const USERS_SHARED_API = Object.freeze({
|
|
|
71
71
|
resolveWorkspaceThemePalette,
|
|
72
72
|
TENANCY_MODE_NONE,
|
|
73
73
|
TENANCY_MODE_PERSONAL,
|
|
74
|
-
|
|
74
|
+
TENANCY_MODE_WORKSPACES,
|
|
75
75
|
normalizeTenancyMode,
|
|
76
76
|
WORKSPACE_SLUG_POLICY_NONE,
|
|
77
77
|
WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
|
|
78
78
|
WORKSPACE_SLUG_POLICY_USER_SELECTED,
|
|
79
79
|
resolveTenancyProfile,
|
|
80
|
-
|
|
80
|
+
isWorkspacesTenancyMode,
|
|
81
81
|
ACCOUNT_SETTINGS_CHANGED_EVENT,
|
|
82
82
|
CONSOLE_SETTINGS_CHANGED_EVENT,
|
|
83
83
|
WORKSPACE_SETTINGS_CHANGED_EVENT,
|
|
@@ -112,13 +112,13 @@ export {
|
|
|
112
112
|
resolveWorkspaceThemePalette,
|
|
113
113
|
TENANCY_MODE_NONE,
|
|
114
114
|
TENANCY_MODE_PERSONAL,
|
|
115
|
-
|
|
115
|
+
TENANCY_MODE_WORKSPACES,
|
|
116
116
|
normalizeTenancyMode,
|
|
117
117
|
WORKSPACE_SLUG_POLICY_NONE,
|
|
118
118
|
WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
|
|
119
119
|
WORKSPACE_SLUG_POLICY_USER_SELECTED,
|
|
120
120
|
resolveTenancyProfile,
|
|
121
|
-
|
|
121
|
+
isWorkspacesTenancyMode,
|
|
122
122
|
ACCOUNT_SETTINGS_CHANGED_EVENT,
|
|
123
123
|
CONSOLE_SETTINGS_CHANGED_EVENT,
|
|
124
124
|
WORKSPACE_SETTINGS_CHANGED_EVENT,
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { normalizePathname } from "@jskit-ai/kernel/shared/surface/paths";
|
|
2
|
+
import { splitPathQueryAndHash } from "@jskit-ai/kernel/shared/support";
|
|
2
3
|
|
|
3
4
|
const USERS_PUBLIC_API_BASE_PATH = "/api";
|
|
4
5
|
const USERS_WORKSPACE_API_BASE_PATH = "/api/w/:workspaceSlug/workspace";
|
|
5
6
|
|
|
6
7
|
function normalizeApiRelativePath(relativePath = "/") {
|
|
7
|
-
const
|
|
8
|
-
|
|
8
|
+
const { pathname, queryString, hash } = splitPathQueryAndHash(relativePath);
|
|
9
|
+
const normalizedPath = normalizePathname(pathname || "/") || "/";
|
|
10
|
+
const normalizedQueryString = String(queryString || "").trim().replace(/^\?+/, "");
|
|
11
|
+
const normalizedHash = String(hash || "").trim();
|
|
12
|
+
const querySuffix = normalizedQueryString ? `?${normalizedQueryString}` : "";
|
|
13
|
+
return `${normalizedPath}${querySuffix}${normalizedHash}`;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
function normalizeSurfaceWorkspaceRequirement(value = false) {
|
|
@@ -22,6 +27,10 @@ function resolveApiBasePath({ surfaceRequiresWorkspace = false, relativePath = "
|
|
|
22
27
|
return basePath;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
if (normalizedRelativePath.startsWith("/?") || normalizedRelativePath.startsWith("/#")) {
|
|
31
|
+
return `${basePath}${normalizedRelativePath.slice(1)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
25
34
|
return `${basePath}${normalizedRelativePath}`;
|
|
26
35
|
}
|
|
27
36
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const TENANCY_MODE_NONE = "none";
|
|
2
2
|
const TENANCY_MODE_PERSONAL = "personal";
|
|
3
|
-
const
|
|
3
|
+
const TENANCY_MODE_WORKSPACES = "workspaces";
|
|
4
4
|
|
|
5
5
|
const TENANCY_MODES = Object.freeze([
|
|
6
6
|
TENANCY_MODE_NONE,
|
|
7
7
|
TENANCY_MODE_PERSONAL,
|
|
8
|
-
|
|
8
|
+
TENANCY_MODE_WORKSPACES
|
|
9
9
|
]);
|
|
10
10
|
|
|
11
11
|
function normalizeTenancyMode(value = "") {
|
|
@@ -28,7 +28,7 @@ function isTenancyMode(value = "") {
|
|
|
28
28
|
export {
|
|
29
29
|
TENANCY_MODE_NONE,
|
|
30
30
|
TENANCY_MODE_PERSONAL,
|
|
31
|
-
|
|
31
|
+
TENANCY_MODE_WORKSPACES,
|
|
32
32
|
TENANCY_MODES,
|
|
33
33
|
normalizeTenancyMode,
|
|
34
34
|
isTenancyMode
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
TENANCY_MODE_NONE,
|
|
3
3
|
TENANCY_MODE_PERSONAL,
|
|
4
|
-
|
|
4
|
+
TENANCY_MODE_WORKSPACES,
|
|
5
5
|
normalizeTenancyMode
|
|
6
6
|
} from "./tenancyMode.js";
|
|
7
7
|
import { isRecord } from "@jskit-ai/kernel/shared/support/normalize";
|
|
@@ -56,18 +56,18 @@ function resolveTenancyProfile(appConfig = {}) {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
function
|
|
60
|
-
return normalizeTenancyMode(value) ===
|
|
59
|
+
function isWorkspacesTenancyMode(value = "") {
|
|
60
|
+
return normalizeTenancyMode(value) === TENANCY_MODE_WORKSPACES;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
export {
|
|
64
64
|
TENANCY_MODE_NONE,
|
|
65
65
|
TENANCY_MODE_PERSONAL,
|
|
66
|
-
|
|
66
|
+
TENANCY_MODE_WORKSPACES,
|
|
67
67
|
normalizeTenancyMode,
|
|
68
68
|
WORKSPACE_SLUG_POLICY_NONE,
|
|
69
69
|
WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
|
|
70
70
|
WORKSPACE_SLUG_POLICY_USER_SELECTED,
|
|
71
71
|
resolveTenancyProfile,
|
|
72
|
-
|
|
72
|
+
isWorkspacesTenancyMode
|
|
73
73
|
};
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @param {import('knex').Knex} knex
|
|
3
3
|
*/
|
|
4
4
|
exports.up = async function up(knex) {
|
|
5
|
-
await knex.schema.createTable("
|
|
5
|
+
await knex.schema.createTable("users", (table) => {
|
|
6
6
|
table.increments("id").primary();
|
|
7
7
|
table.string("auth_provider", 64).notNullable();
|
|
8
8
|
table.string("auth_provider_user_id", 191).notNullable();
|
|
@@ -13,16 +13,16 @@ exports.up = async function up(knex) {
|
|
|
13
13
|
table.string("avatar_version", 64).nullable();
|
|
14
14
|
table.timestamp("avatar_updated_at", { useTz: false }).nullable();
|
|
15
15
|
table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
16
|
-
table.unique(["auth_provider", "auth_provider_user_id"], "
|
|
17
|
-
table.unique(["email"], "
|
|
18
|
-
table.unique(["username"], "
|
|
16
|
+
table.unique(["auth_provider", "auth_provider_user_id"], "uq_users_identity");
|
|
17
|
+
table.unique(["email"], "uq_users_email");
|
|
18
|
+
table.unique(["username"], "uq_users_username");
|
|
19
19
|
});
|
|
20
20
|
|
|
21
21
|
await knex.schema.createTable("workspaces", (table) => {
|
|
22
22
|
table.increments("id").primary();
|
|
23
23
|
table.string("slug", 120).notNullable().unique();
|
|
24
24
|
table.string("name", 160).notNullable();
|
|
25
|
-
table.integer("owner_user_id").unsigned().notNullable().references("id").inTable("
|
|
25
|
+
table.integer("owner_user_id").unsigned().notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
26
26
|
table.boolean("is_personal").notNullable().defaultTo(true);
|
|
27
27
|
table.string("avatar_url", 512).notNullable().defaultTo("");
|
|
28
28
|
table.string("color", 7).notNullable().defaultTo("#1867C0");
|
|
@@ -34,7 +34,7 @@ exports.up = async function up(knex) {
|
|
|
34
34
|
await knex.schema.createTable("workspace_memberships", (table) => {
|
|
35
35
|
table.increments("id").primary();
|
|
36
36
|
table.integer("workspace_id").unsigned().notNullable().references("id").inTable("workspaces").onDelete("CASCADE");
|
|
37
|
-
table.integer("user_id").unsigned().notNullable().references("id").inTable("
|
|
37
|
+
table.integer("user_id").unsigned().notNullable().references("id").inTable("users").onDelete("CASCADE");
|
|
38
38
|
table.string("role_id", 64).notNullable().defaultTo("member");
|
|
39
39
|
table.string("status", 32).notNullable().defaultTo("active");
|
|
40
40
|
table.timestamp("created_at", { useTz: false }).notNullable().defaultTo(knex.fn.now());
|
|
@@ -66,7 +66,7 @@ exports.up = async function up(knex) {
|
|
|
66
66
|
table.string("role_id", 64).notNullable().defaultTo("member");
|
|
67
67
|
table.string("status", 32).notNullable().defaultTo("pending");
|
|
68
68
|
table.string("token_hash", 191).notNullable();
|
|
69
|
-
table.integer("invited_by_user_id").unsigned().nullable().references("id").inTable("
|
|
69
|
+
table.integer("invited_by_user_id").unsigned().nullable().references("id").inTable("users").onDelete("SET NULL");
|
|
70
70
|
table.timestamp("expires_at", { useTz: false }).nullable();
|
|
71
71
|
table.timestamp("accepted_at", { useTz: false }).nullable();
|
|
72
72
|
table.timestamp("revoked_at", { useTz: false }).nullable();
|
|
@@ -77,7 +77,7 @@ exports.up = async function up(knex) {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
await knex.schema.createTable("user_settings", (table) => {
|
|
80
|
-
table.integer("user_id").unsigned().primary().references("id").inTable("
|
|
80
|
+
table.integer("user_id").unsigned().primary().references("id").inTable("users").onDelete("CASCADE");
|
|
81
81
|
table.integer("last_active_workspace_id").unsigned().nullable().references("id").inTable("workspaces").onDelete("SET NULL");
|
|
82
82
|
table.string("theme", 32).notNullable().defaultTo("system");
|
|
83
83
|
table.string("locale", 24).notNullable().defaultTo("en");
|
|
@@ -119,5 +119,5 @@ exports.down = async function down(knex) {
|
|
|
119
119
|
await knex.schema.dropTableIfExists("workspace_settings");
|
|
120
120
|
await knex.schema.dropTableIfExists("workspace_memberships");
|
|
121
121
|
await knex.schema.dropTableIfExists("workspaces");
|
|
122
|
-
await knex.schema.dropTableIfExists("
|
|
122
|
+
await knex.schema.dropTableIfExists("users");
|
|
123
123
|
};
|
|
@@ -42,37 +42,37 @@ function resolveUniqueUsername(baseUsername, usedUsernames) {
|
|
|
42
42
|
* @param {import('knex').Knex} knex
|
|
43
43
|
*/
|
|
44
44
|
exports.up = async function up(knex) {
|
|
45
|
-
const
|
|
46
|
-
if (!
|
|
45
|
+
const hasUsersTable = await knex.schema.hasTable("users");
|
|
46
|
+
if (!hasUsersTable) {
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
const hasUsername = await knex.schema.hasColumn("
|
|
50
|
+
const hasUsername = await knex.schema.hasColumn("users", "username");
|
|
51
51
|
if (hasUsername) {
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
await knex.schema.alterTable("
|
|
55
|
+
await knex.schema.alterTable("users", (table) => {
|
|
56
56
|
table.string("username", USERNAME_MAX_LENGTH).nullable();
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
-
const profiles = await knex("
|
|
59
|
+
const profiles = await knex("users").select(["id", "email"]).orderBy("id", "asc");
|
|
60
60
|
const usedUsernames = new Set();
|
|
61
61
|
|
|
62
62
|
for (const profile of profiles) {
|
|
63
63
|
const nextUsername = resolveUniqueUsername(usernameBaseFromEmail(profile.email), usedUsernames);
|
|
64
64
|
usedUsernames.add(nextUsername);
|
|
65
|
-
await knex("
|
|
65
|
+
await knex("users").where({ id: Number(profile.id) }).update({
|
|
66
66
|
username: nextUsername
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
await knex.schema.alterTable("
|
|
70
|
+
await knex.schema.alterTable("users", (table) => {
|
|
71
71
|
table.string("username", USERNAME_MAX_LENGTH).notNullable().alter();
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
-
await knex.schema.alterTable("
|
|
75
|
-
table.unique(["username"], "
|
|
74
|
+
await knex.schema.alterTable("users", (table) => {
|
|
75
|
+
table.unique(["username"], "uq_users_username");
|
|
76
76
|
});
|
|
77
77
|
};
|
|
78
78
|
|
|
@@ -80,17 +80,17 @@ exports.up = async function up(knex) {
|
|
|
80
80
|
* @param {import('knex').Knex} knex
|
|
81
81
|
*/
|
|
82
82
|
exports.down = async function down(knex) {
|
|
83
|
-
const
|
|
84
|
-
if (!
|
|
83
|
+
const hasUsersTable = await knex.schema.hasTable("users");
|
|
84
|
+
if (!hasUsersTable) {
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
const hasUsername = await knex.schema.hasColumn("
|
|
88
|
+
const hasUsername = await knex.schema.hasColumn("users", "username");
|
|
89
89
|
if (!hasUsername) {
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
await knex.schema.alterTable("
|
|
93
|
+
await knex.schema.alterTable("users", (table) => {
|
|
94
94
|
table.dropColumn("username");
|
|
95
95
|
});
|
|
96
96
|
};
|
|
@@ -3,12 +3,12 @@ import test from "node:test";
|
|
|
3
3
|
import {
|
|
4
4
|
TENANCY_MODE_NONE,
|
|
5
5
|
TENANCY_MODE_PERSONAL,
|
|
6
|
-
|
|
6
|
+
TENANCY_MODE_WORKSPACES,
|
|
7
7
|
WORKSPACE_SLUG_POLICY_NONE,
|
|
8
8
|
WORKSPACE_SLUG_POLICY_IMMUTABLE_USERNAME,
|
|
9
9
|
WORKSPACE_SLUG_POLICY_USER_SELECTED,
|
|
10
10
|
resolveTenancyProfile,
|
|
11
|
-
|
|
11
|
+
isWorkspacesTenancyMode
|
|
12
12
|
} from "../src/shared/tenancyProfile.js";
|
|
13
13
|
|
|
14
14
|
test("resolveTenancyProfile returns mode-specific workspace policy matrix", () => {
|
|
@@ -34,9 +34,9 @@ test("resolveTenancyProfile returns mode-specific workspace policy matrix", () =
|
|
|
34
34
|
}
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
const workspaceProfile = resolveTenancyProfile({ tenancyMode:
|
|
37
|
+
const workspaceProfile = resolveTenancyProfile({ tenancyMode: TENANCY_MODE_WORKSPACES });
|
|
38
38
|
assert.deepEqual(workspaceProfile, {
|
|
39
|
-
mode:
|
|
39
|
+
mode: TENANCY_MODE_WORKSPACES,
|
|
40
40
|
workspace: {
|
|
41
41
|
enabled: true,
|
|
42
42
|
autoProvision: false,
|
|
@@ -46,15 +46,15 @@ test("resolveTenancyProfile returns mode-specific workspace policy matrix", () =
|
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
|
|
49
|
-
test("
|
|
50
|
-
assert.equal(
|
|
51
|
-
assert.equal(
|
|
52
|
-
assert.equal(
|
|
49
|
+
test("isWorkspacesTenancyMode is true only for workspace mode", () => {
|
|
50
|
+
assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_WORKSPACES), true);
|
|
51
|
+
assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_PERSONAL), false);
|
|
52
|
+
assert.equal(isWorkspacesTenancyMode(TENANCY_MODE_NONE), false);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
test("resolveTenancyProfile allows explicit workspace self-create policy override", () => {
|
|
56
56
|
const workspaceProfile = resolveTenancyProfile({
|
|
57
|
-
tenancyMode:
|
|
57
|
+
tenancyMode: TENANCY_MODE_WORKSPACES,
|
|
58
58
|
tenancyPolicy: {
|
|
59
59
|
workspace: {
|
|
60
60
|
allowSelfCreate: true
|
|
@@ -62,6 +62,6 @@ test("resolveTenancyProfile allows explicit workspace self-create policy overrid
|
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
-
assert.equal(workspaceProfile.mode,
|
|
65
|
+
assert.equal(workspaceProfile.mode, TENANCY_MODE_WORKSPACES);
|
|
66
66
|
assert.equal(workspaceProfile.workspace.allowSelfCreate, true);
|
|
67
67
|
});
|
|
@@ -29,3 +29,21 @@ test("resolveApiBasePath resolves workspace and non-workspace API base paths", (
|
|
|
29
29
|
"/api/customers"
|
|
30
30
|
);
|
|
31
31
|
});
|
|
32
|
+
|
|
33
|
+
test("resolveApiBasePath preserves query strings and hash fragments", () => {
|
|
34
|
+
assert.equal(
|
|
35
|
+
resolveApiBasePath({
|
|
36
|
+
surfaceRequiresWorkspace: true,
|
|
37
|
+
relativePath: "/customers?search=buddy#top"
|
|
38
|
+
}),
|
|
39
|
+
"/api/w/:workspaceSlug/workspace/customers?search=buddy#top"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
assert.equal(
|
|
43
|
+
resolveApiBasePath({
|
|
44
|
+
surfaceRequiresWorkspace: false,
|
|
45
|
+
relativePath: "/?cursor=2"
|
|
46
|
+
}),
|
|
47
|
+
"/api?cursor=2"
|
|
48
|
+
);
|
|
49
|
+
});
|
|
@@ -229,10 +229,10 @@ test("users-core route registration follows tenancy mode matrix", async () => {
|
|
|
229
229
|
tenancyMode: "personal"
|
|
230
230
|
});
|
|
231
231
|
const workspaceRoutes = await registerRoutesForMode({
|
|
232
|
-
tenancyMode: "
|
|
232
|
+
tenancyMode: "workspaces"
|
|
233
233
|
});
|
|
234
234
|
const workspaceSelfCreateRoutes = await registerRoutesForMode({
|
|
235
|
-
tenancyMode: "
|
|
235
|
+
tenancyMode: "workspaces",
|
|
236
236
|
tenancyPolicy: {
|
|
237
237
|
workspace: {
|
|
238
238
|
allowSelfCreate: true
|
|
@@ -62,7 +62,7 @@ test("workspace bootstrap contributor passes actor context to pending invites se
|
|
|
62
62
|
},
|
|
63
63
|
workspaceInvitationsEnabled: true,
|
|
64
64
|
appConfig: {
|
|
65
|
-
tenancyMode: "
|
|
65
|
+
tenancyMode: "workspaces"
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
|
|
@@ -251,7 +251,7 @@ test("workspace bootstrap contributor resolves workspace slug from bootstrap que
|
|
|
251
251
|
},
|
|
252
252
|
workspaceInvitationsEnabled: true,
|
|
253
253
|
appConfig: {
|
|
254
|
-
tenancyMode: "
|
|
254
|
+
tenancyMode: "workspaces"
|
|
255
255
|
}
|
|
256
256
|
});
|
|
257
257
|
|
|
@@ -314,7 +314,7 @@ test("workspace bootstrap contributor returns global payload with requestedWorks
|
|
|
314
314
|
},
|
|
315
315
|
workspaceInvitationsEnabled: true,
|
|
316
316
|
appConfig: {
|
|
317
|
-
tenancyMode: "
|
|
317
|
+
tenancyMode: "workspaces"
|
|
318
318
|
}
|
|
319
319
|
});
|
|
320
320
|
|
|
@@ -386,7 +386,7 @@ test("workspace bootstrap contributor returns requestedWorkspace=not_found when
|
|
|
386
386
|
},
|
|
387
387
|
workspaceInvitationsEnabled: false,
|
|
388
388
|
appConfig: {
|
|
389
|
-
tenancyMode: "
|
|
389
|
+
tenancyMode: "workspaces"
|
|
390
390
|
}
|
|
391
391
|
});
|
|
392
392
|
|
|
@@ -439,7 +439,7 @@ test("workspace bootstrap contributor returns requestedWorkspace=unauthenticated
|
|
|
439
439
|
},
|
|
440
440
|
workspaceInvitationsEnabled: false,
|
|
441
441
|
appConfig: {
|
|
442
|
-
tenancyMode: "
|
|
442
|
+
tenancyMode: "workspaces"
|
|
443
443
|
}
|
|
444
444
|
});
|
|
445
445
|
|
|
@@ -19,7 +19,7 @@ function createWorkspaceRoles() {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
function createWorkspaceServiceFixture({
|
|
22
|
-
tenancyMode = "
|
|
22
|
+
tenancyMode = "workspaces",
|
|
23
23
|
tenancyPolicy = {},
|
|
24
24
|
workspaceRoles = createWorkspaceRoles(),
|
|
25
25
|
additionalWorkspaces = [],
|
|
@@ -177,7 +177,7 @@ test("workspaceService.listWorkspacesForUser returns only accessible workspaces"
|
|
|
177
177
|
|
|
178
178
|
test("workspaceService.listWorkspacesForUser no longer provisions personal workspace in workspace mode", async () => {
|
|
179
179
|
const { service, calls } = createWorkspaceServiceFixture({
|
|
180
|
-
tenancyMode: "
|
|
180
|
+
tenancyMode: "workspaces",
|
|
181
181
|
personalWorkspace: null
|
|
182
182
|
});
|
|
183
183
|
|
|
@@ -260,7 +260,7 @@ test("workspaceService.provisionWorkspaceForNewUser provisions personal workspac
|
|
|
260
260
|
|
|
261
261
|
test("workspaceService.provisionWorkspaceForNewUser is a no-op outside personal tenancy", async () => {
|
|
262
262
|
const { service, calls } = createWorkspaceServiceFixture({
|
|
263
|
-
tenancyMode: "
|
|
263
|
+
tenancyMode: "workspaces"
|
|
264
264
|
});
|
|
265
265
|
|
|
266
266
|
const result = await service.provisionWorkspaceForNewUser({
|
|
@@ -275,7 +275,7 @@ test("workspaceService.provisionWorkspaceForNewUser is a no-op outside personal
|
|
|
275
275
|
|
|
276
276
|
test("workspaceService.createWorkspaceForAuthenticatedUser creates non-personal workspace in workspace tenancy", async () => {
|
|
277
277
|
const { service, calls, insertedPayloads } = createWorkspaceServiceFixture({
|
|
278
|
-
tenancyMode: "
|
|
278
|
+
tenancyMode: "workspaces",
|
|
279
279
|
tenancyPolicy: {
|
|
280
280
|
workspace: {
|
|
281
281
|
allowSelfCreate: true
|
|
@@ -304,7 +304,7 @@ test("workspaceService.createWorkspaceForAuthenticatedUser creates non-personal
|
|
|
304
304
|
|
|
305
305
|
test("workspaceService.createWorkspaceForAuthenticatedUser rejects creation when self-create policy is disabled", async () => {
|
|
306
306
|
const { service } = createWorkspaceServiceFixture({
|
|
307
|
-
tenancyMode: "
|
|
307
|
+
tenancyMode: "workspaces"
|
|
308
308
|
});
|
|
309
309
|
|
|
310
310
|
await assert.rejects(
|