@jskit-ai/users-core 0.1.30 → 0.1.32
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 +8 -6
- package/package.json +6 -6
- package/src/server/UsersCoreServiceProvider.js +1 -3
- package/src/server/accountProfile/avatarService.js +18 -59
- package/src/server/accountProfile/avatarStorageService.js +14 -95
- package/src/server/accountProfile/bootAccountProfileRoutes.js +10 -14
- package/src/server/common/formatters/workspaceFormatter.js +2 -2
- package/src/server/common/repositories/userProfilesRepository.js +6 -6
- package/src/server/common/repositories/workspaceInvitesRepository.js +2 -2
- package/src/server/common/repositories/workspaceMembershipsRepository.js +9 -9
- package/src/server/common/repositories/workspacesRepository.js +2 -2
- package/src/server/common/services/authProfileSyncService.js +5 -5
- package/src/server/common/services/workspaceContextService.js +3 -3
- package/src/server/common/validators/authenticatedUserValidator.js +2 -2
- package/src/server/workspaceBootstrapContributor.js +2 -2
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +2 -2
- package/src/server/workspaceMembers/workspaceMembersActions.js +2 -2
- package/src/server/workspaceMembers/workspaceMembersService.js +11 -11
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +1 -1
- package/src/shared/resources/workspaceMembersResource.js +11 -11
- package/src/shared/resources/workspacePendingInvitationsResource.js +2 -2
- package/src/shared/resources/workspaceResource.js +2 -2
- package/src/shared/roles.js +8 -8
- package/templates/migrations/users_core_initial.cjs +5 -5
- package/test/authProfileSyncService.test.js +5 -5
- package/test/avatarService.test.js +4 -4
- package/test/usersRouteRequestInputValidator.test.js +4 -4
- package/test/workspaceActionContextContributor.test.js +9 -9
- package/test/workspaceAuthPolicyContextResolver.test.js +2 -2
- package/test/workspaceBootstrapContributor.test.js +1 -1
- package/test/workspaceInvitesRepository.test.js +3 -3
- package/test/workspaceMembersService.test.js +10 -10
- package/test/workspacePendingInvitationsResource.test.js +2 -2
- package/test/workspacePendingInvitationsService.test.js +3 -3
- package/test/workspaceService.test.js +10 -10
- package/src/server/accountProfile/registerAvatarMultipartSupport.js +0 -40
- package/test/registerAvatarMultipartSupport.test.js +0 -63
package/package.descriptor.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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.32",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Users/workspace domain runtime plus HTTP routes for workspace, account, and console features.",
|
|
7
7
|
dependsOn: [
|
|
8
8
|
"@jskit-ai/auth-core",
|
|
9
9
|
"@jskit-ai/database-runtime",
|
|
10
10
|
"@jskit-ai/http-runtime",
|
|
11
|
+
"@jskit-ai/uploads-runtime",
|
|
11
12
|
"@jskit-ai/storage-runtime"
|
|
12
13
|
],
|
|
13
14
|
capabilities: {
|
|
@@ -19,6 +20,7 @@ export default Object.freeze({
|
|
|
19
20
|
"runtime.actions",
|
|
20
21
|
"runtime.database",
|
|
21
22
|
"runtime.storage",
|
|
23
|
+
"runtime.uploads",
|
|
22
24
|
"auth.provider",
|
|
23
25
|
"auth.policy"
|
|
24
26
|
]
|
|
@@ -196,11 +198,11 @@ export default Object.freeze({
|
|
|
196
198
|
mutations: {
|
|
197
199
|
dependencies: {
|
|
198
200
|
runtime: {
|
|
199
|
-
"@jskit-ai/auth-core": "0.1.
|
|
200
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
201
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
202
|
-
"@jskit-ai/kernel": "0.1.
|
|
203
|
-
"@
|
|
201
|
+
"@jskit-ai/auth-core": "0.1.22",
|
|
202
|
+
"@jskit-ai/database-runtime": "0.1.23",
|
|
203
|
+
"@jskit-ai/http-runtime": "0.1.22",
|
|
204
|
+
"@jskit-ai/kernel": "0.1.23",
|
|
205
|
+
"@jskit-ai/uploads-runtime": "0.1.1",
|
|
204
206
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
205
207
|
"typebox": "^1.0.81"
|
|
206
208
|
},
|
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.32",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -24,11 +24,11 @@
|
|
|
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.
|
|
31
|
-
"@
|
|
27
|
+
"@jskit-ai/auth-core": "0.1.22",
|
|
28
|
+
"@jskit-ai/database-runtime": "0.1.23",
|
|
29
|
+
"@jskit-ai/http-runtime": "0.1.22",
|
|
30
|
+
"@jskit-ai/kernel": "0.1.23",
|
|
31
|
+
"@jskit-ai/uploads-runtime": "0.1.1",
|
|
32
32
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
33
33
|
"typebox": "^1.0.81"
|
|
34
34
|
}
|
|
@@ -23,13 +23,12 @@ import { registerAccountNotifications } from "./accountNotifications/registerAcc
|
|
|
23
23
|
import { registerAccountProfile } from "./accountProfile/registerAccountProfile.js";
|
|
24
24
|
import { registerAccountSecurity } from "./accountSecurity/registerAccountSecurity.js";
|
|
25
25
|
import { registerConsoleSettings } from "./consoleSettings/registerConsoleSettings.js";
|
|
26
|
-
import { registerAvatarMultipartSupport } from "./accountProfile/registerAvatarMultipartSupport.js";
|
|
27
26
|
import { registerUsersCoreActionSurfaceSources } from "./support/workspaceActionSurfaces.js";
|
|
28
27
|
|
|
29
28
|
class UsersCoreServiceProvider {
|
|
30
29
|
static id = "users.core";
|
|
31
30
|
|
|
32
|
-
static dependsOn = ["runtime.server", "runtime.actions", "runtime.database", "runtime.storage", "auth.provider"];
|
|
31
|
+
static dependsOn = ["runtime.server", "runtime.actions", "runtime.database", "runtime.storage", "auth.provider", "runtime.uploads"];
|
|
33
32
|
|
|
34
33
|
register(app) {
|
|
35
34
|
registerUsersCoreActionSurfaceSources(app);
|
|
@@ -58,7 +57,6 @@ class UsersCoreServiceProvider {
|
|
|
58
57
|
bootWorkspaceSettings(app);
|
|
59
58
|
bootWorkspaceMembers(app);
|
|
60
59
|
}
|
|
61
|
-
await registerAvatarMultipartSupport(app);
|
|
62
60
|
bootAccountProfileRoutes(app);
|
|
63
61
|
bootAccountPreferencesRoutes(app);
|
|
64
62
|
bootAccountNotificationsRoutes(app);
|
|
@@ -1,63 +1,24 @@
|
|
|
1
|
-
import { AppError
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { DEFAULT_IMAGE_UPLOAD_POLICY } from "@jskit-ai/uploads-runtime/shared";
|
|
3
|
+
import {
|
|
4
|
+
normalizeUploadPolicy,
|
|
5
|
+
readUploadBuffer,
|
|
6
|
+
validateUploadMimeType
|
|
7
|
+
} from "@jskit-ai/uploads-runtime/server/policy/uploadPolicy";
|
|
2
8
|
import { resolveUserProfile } from "../common/services/accountContextService.js";
|
|
3
9
|
|
|
4
|
-
const DEFAULT_AVATAR_POLICY =
|
|
5
|
-
allowedMimeTypes: Object.freeze(["image/jpeg", "image/png", "image/webp"]),
|
|
6
|
-
maxUploadBytes: 5 * 1024 * 1024
|
|
7
|
-
});
|
|
10
|
+
const DEFAULT_AVATAR_POLICY = DEFAULT_IMAGE_UPLOAD_POLICY;
|
|
8
11
|
|
|
9
12
|
function resolveAvatarPolicy(policy = {}) {
|
|
10
|
-
|
|
11
|
-
const allowedMimeTypes =
|
|
12
|
-
Array.isArray(source.allowedMimeTypes) && source.allowedMimeTypes.length > 0
|
|
13
|
-
? source.allowedMimeTypes
|
|
14
|
-
.map((value) => String(value || "").trim().toLowerCase())
|
|
15
|
-
.filter((value) => value.length > 0)
|
|
16
|
-
: [...DEFAULT_AVATAR_POLICY.allowedMimeTypes];
|
|
17
|
-
const normalizedMaxUploadBytes = Number(source.maxUploadBytes);
|
|
18
|
-
const maxUploadBytes =
|
|
19
|
-
Number.isInteger(normalizedMaxUploadBytes) && normalizedMaxUploadBytes > 0
|
|
20
|
-
? normalizedMaxUploadBytes
|
|
21
|
-
: DEFAULT_AVATAR_POLICY.maxUploadBytes;
|
|
22
|
-
|
|
23
|
-
return Object.freeze({
|
|
24
|
-
allowedMimeTypes: Object.freeze(allowedMimeTypes),
|
|
25
|
-
maxUploadBytes
|
|
26
|
-
});
|
|
13
|
+
return normalizeUploadPolicy(policy, DEFAULT_AVATAR_POLICY);
|
|
27
14
|
}
|
|
28
15
|
|
|
29
16
|
async function readAvatarBuffer(stream, { maxBytes = DEFAULT_AVATAR_POLICY.maxUploadBytes } = {}) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let total = 0;
|
|
36
|
-
|
|
37
|
-
for await (const chunk of stream) {
|
|
38
|
-
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
39
|
-
total += bufferChunk.length;
|
|
40
|
-
|
|
41
|
-
if (total > maxBytes) {
|
|
42
|
-
throw createValidationError({
|
|
43
|
-
avatar: `Avatar file is too large. Maximum allowed size is ${Math.floor(maxBytes / (1024 * 1024))}MB.`
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
chunks.push(bufferChunk);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (chunks.length === 0) {
|
|
51
|
-
throw createValidationError({
|
|
52
|
-
avatar: "Avatar file is empty."
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return Buffer.concat(chunks);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function normalizeMimeType(value) {
|
|
60
|
-
return String(value || "").trim().toLowerCase();
|
|
17
|
+
return readUploadBuffer(stream, {
|
|
18
|
+
maxBytes,
|
|
19
|
+
fieldName: "avatar",
|
|
20
|
+
label: "Avatar"
|
|
21
|
+
});
|
|
61
22
|
}
|
|
62
23
|
|
|
63
24
|
function createService({ userProfilesRepository, avatarStorageService, avatarPolicy } = {}) {
|
|
@@ -80,12 +41,10 @@ function createService({ userProfilesRepository, avatarStorageService, avatarPol
|
|
|
80
41
|
|
|
81
42
|
async function uploadForUser(user, payload = {}) {
|
|
82
43
|
const profile = await resolveProfile(user);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
88
|
-
}
|
|
44
|
+
validateUploadMimeType(payload?.mimeType, resolvedAvatarPolicy, {
|
|
45
|
+
fieldName: "avatar",
|
|
46
|
+
label: "Avatar"
|
|
47
|
+
});
|
|
89
48
|
|
|
90
49
|
const buffer = await readAvatarBuffer(payload.stream, {
|
|
91
50
|
maxBytes: resolvedAvatarPolicy.maxUploadBytes
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
|
|
2
|
+
import {
|
|
3
|
+
createUploadStorageService,
|
|
4
|
+
detectCommonMimeTypeFromBuffer
|
|
5
|
+
} from "@jskit-ai/uploads-runtime/server/storage/createUploadStorageService";
|
|
2
6
|
|
|
3
7
|
const AVATAR_STORAGE_PREFIX = "users/avatars";
|
|
4
|
-
const AVATAR_MIME_TYPE_JPEG = "image/jpeg";
|
|
5
|
-
const AVATAR_MIME_TYPE_PNG = "image/png";
|
|
6
|
-
const AVATAR_MIME_TYPE_WEBP = "image/webp";
|
|
7
|
-
const AVATAR_MIME_TYPE_FALLBACK = "application/octet-stream";
|
|
8
8
|
|
|
9
9
|
function buildAvatarStorageKey(userId) {
|
|
10
10
|
const normalizedUserId = parsePositiveInteger(userId);
|
|
@@ -15,106 +15,25 @@ function buildAvatarStorageKey(userId) {
|
|
|
15
15
|
return `${AVATAR_STORAGE_PREFIX}/${normalizedUserId}/avatar`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function normalizeStorageKey(value) {
|
|
19
|
-
const normalized = String(value || "").trim();
|
|
20
|
-
if (!normalized) {
|
|
21
|
-
return "";
|
|
22
|
-
}
|
|
23
|
-
if (normalized.startsWith("/") || normalized.includes("..")) {
|
|
24
|
-
return "";
|
|
25
|
-
}
|
|
26
|
-
return normalized;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function detectAvatarMimeTypeFromBuffer(buffer) {
|
|
30
|
-
if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
|
|
31
|
-
return AVATAR_MIME_TYPE_FALLBACK;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
buffer.length >= 3 &&
|
|
36
|
-
buffer[0] === 0xff &&
|
|
37
|
-
buffer[1] === 0xd8 &&
|
|
38
|
-
buffer[2] === 0xff
|
|
39
|
-
) {
|
|
40
|
-
return AVATAR_MIME_TYPE_JPEG;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
buffer.length >= 8 &&
|
|
45
|
-
buffer[0] === 0x89 &&
|
|
46
|
-
buffer[1] === 0x50 &&
|
|
47
|
-
buffer[2] === 0x4e &&
|
|
48
|
-
buffer[3] === 0x47 &&
|
|
49
|
-
buffer[4] === 0x0d &&
|
|
50
|
-
buffer[5] === 0x0a &&
|
|
51
|
-
buffer[6] === 0x1a &&
|
|
52
|
-
buffer[7] === 0x0a
|
|
53
|
-
) {
|
|
54
|
-
return AVATAR_MIME_TYPE_PNG;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
buffer.length >= 12 &&
|
|
59
|
-
buffer[0] === 0x52 &&
|
|
60
|
-
buffer[1] === 0x49 &&
|
|
61
|
-
buffer[2] === 0x46 &&
|
|
62
|
-
buffer[3] === 0x46 &&
|
|
63
|
-
buffer[8] === 0x57 &&
|
|
64
|
-
buffer[9] === 0x45 &&
|
|
65
|
-
buffer[10] === 0x42 &&
|
|
66
|
-
buffer[11] === 0x50
|
|
67
|
-
) {
|
|
68
|
-
return AVATAR_MIME_TYPE_WEBP;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return AVATAR_MIME_TYPE_FALLBACK;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
18
|
function createService({ storage } = {}) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
19
|
+
const uploadStorageService = createUploadStorageService({
|
|
20
|
+
storage,
|
|
21
|
+
mimeTypeDetector: detectCommonMimeTypeFromBuffer
|
|
22
|
+
});
|
|
78
23
|
|
|
79
24
|
async function saveAvatar({ userId, buffer }) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const storageKey = buildAvatarStorageKey(userId);
|
|
85
|
-
await storage.setItemRaw(storageKey, buffer);
|
|
86
|
-
|
|
87
|
-
return Object.freeze({
|
|
88
|
-
storageKey
|
|
25
|
+
return uploadStorageService.saveFile({
|
|
26
|
+
storageKey: buildAvatarStorageKey(userId),
|
|
27
|
+
buffer
|
|
89
28
|
});
|
|
90
29
|
}
|
|
91
30
|
|
|
92
31
|
async function readAvatar(storageKey) {
|
|
93
|
-
|
|
94
|
-
if (!normalizedStorageKey) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const value = await storage.getItemRaw(normalizedStorageKey);
|
|
99
|
-
if (value == null) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
104
|
-
return Object.freeze({
|
|
105
|
-
storageKey: normalizedStorageKey,
|
|
106
|
-
buffer,
|
|
107
|
-
mimeType: detectAvatarMimeTypeFromBuffer(buffer)
|
|
108
|
-
});
|
|
32
|
+
return uploadStorageService.readFile(storageKey);
|
|
109
33
|
}
|
|
110
34
|
|
|
111
35
|
async function deleteAvatar(storageKey) {
|
|
112
|
-
|
|
113
|
-
if (!normalizedStorageKey || typeof storage.removeItem !== "function") {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
await storage.removeItem(normalizedStorageKey);
|
|
36
|
+
await uploadStorageService.deleteFile(storageKey);
|
|
118
37
|
}
|
|
119
38
|
|
|
120
39
|
return Object.freeze({
|
|
@@ -126,7 +45,7 @@ function createService({ storage } = {}) {
|
|
|
126
45
|
|
|
127
46
|
const __testables = Object.freeze({
|
|
128
47
|
buildAvatarStorageKey,
|
|
129
|
-
detectAvatarMimeTypeFromBuffer
|
|
48
|
+
detectAvatarMimeTypeFromBuffer: detectCommonMimeTypeFromBuffer
|
|
130
49
|
});
|
|
131
50
|
|
|
132
51
|
export { createService, __testables };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
1
|
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
+
import { readSingleMultipartFile } from "@jskit-ai/uploads-runtime/server/multipart/readSingleMultipartFile";
|
|
3
3
|
import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
|
|
4
4
|
import { userProfileResource } from "../../shared/resources/userProfileResource.js";
|
|
5
5
|
|
|
@@ -113,24 +113,20 @@ function bootAccountProfileRoutes(app) {
|
|
|
113
113
|
)
|
|
114
114
|
},
|
|
115
115
|
async function (request, reply) {
|
|
116
|
-
const filePart = await request
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
116
|
+
const filePart = await readSingleMultipartFile(request, {
|
|
117
|
+
fieldNames: ["avatar"],
|
|
118
|
+
required: true,
|
|
119
|
+
fieldErrorKey: "avatar",
|
|
120
|
+
label: "Avatar"
|
|
121
|
+
});
|
|
126
122
|
|
|
127
123
|
const uploadDimension = filePart.fields?.uploadDimension?.value;
|
|
128
124
|
const response = await request.executeAction({
|
|
129
125
|
actionId: "settings.profile.avatar.upload",
|
|
130
126
|
input: {
|
|
131
|
-
stream: filePart.
|
|
132
|
-
mimeType: filePart.
|
|
133
|
-
fileName: filePart.
|
|
127
|
+
stream: filePart.stream,
|
|
128
|
+
mimeType: filePart.mimeType,
|
|
129
|
+
fileName: filePart.fileName,
|
|
134
130
|
uploadDimension
|
|
135
131
|
}
|
|
136
132
|
});
|
|
@@ -7,7 +7,7 @@ function mapWorkspaceSummary(workspace, membership) {
|
|
|
7
7
|
slug: normalizeText(workspace.slug),
|
|
8
8
|
name: normalizeText(workspace.name),
|
|
9
9
|
avatarUrl: normalizeText(workspace.avatarUrl),
|
|
10
|
-
|
|
10
|
+
roleSid: normalizeLowerText(membership?.roleSid || "member") || "member",
|
|
11
11
|
isAccessible: normalizeLowerText(membership?.status || "active") === "active"
|
|
12
12
|
};
|
|
13
13
|
}
|
|
@@ -40,7 +40,7 @@ function mapMembershipSummary(membership, workspace) {
|
|
|
40
40
|
|
|
41
41
|
return {
|
|
42
42
|
workspaceId: Number(workspace?.id || membership.workspaceId),
|
|
43
|
-
|
|
43
|
+
roleSid: normalizeLowerText(membership.roleSid || "member") || "member",
|
|
44
44
|
status: normalizeLowerText(membership.status || "active") || "active"
|
|
45
45
|
};
|
|
46
46
|
}
|
|
@@ -13,7 +13,7 @@ const USERNAME_MAX_LENGTH = 120;
|
|
|
13
13
|
function normalizeIdentity(identityLike) {
|
|
14
14
|
const source = identityLike && typeof identityLike === "object" ? identityLike : {};
|
|
15
15
|
const provider = normalizeLowerText(source.provider || source.authProvider);
|
|
16
|
-
const providerUserId = normalizeText(source.providerUserId || source.
|
|
16
|
+
const providerUserId = normalizeText(source.providerUserId || source.authProviderUserSid);
|
|
17
17
|
if (!provider || !providerUserId) {
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
@@ -57,7 +57,7 @@ function mapProfileRow(row) {
|
|
|
57
57
|
return {
|
|
58
58
|
id: Number(row.id),
|
|
59
59
|
authProvider: normalizeLowerText(row.auth_provider),
|
|
60
|
-
|
|
60
|
+
authProviderUserSid: normalizeText(row.auth_provider_user_sid),
|
|
61
61
|
email: normalizeLowerText(row.email),
|
|
62
62
|
username: normalizeLowerText(row.username),
|
|
63
63
|
displayName: normalizeText(row.display_name),
|
|
@@ -123,7 +123,7 @@ function createRepository(knex) {
|
|
|
123
123
|
const row = await client("users")
|
|
124
124
|
.where({
|
|
125
125
|
auth_provider: identity.provider,
|
|
126
|
-
|
|
126
|
+
auth_provider_user_sid: identity.providerUserId
|
|
127
127
|
})
|
|
128
128
|
.first();
|
|
129
129
|
return mapProfileRow(row);
|
|
@@ -168,7 +168,7 @@ function createRepository(knex) {
|
|
|
168
168
|
const client = options?.trx || knex;
|
|
169
169
|
const identity = normalizeIdentity(profileLike);
|
|
170
170
|
if (!identity) {
|
|
171
|
-
throw new TypeError("upsert requires provider/authProvider and providerUserId/
|
|
171
|
+
throw new TypeError("upsert requires provider/authProvider and providerUserId/authProviderUserSid.");
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
const email = normalizeLowerText(profileLike.email);
|
|
@@ -181,7 +181,7 @@ function createRepository(knex) {
|
|
|
181
181
|
const executeUpsert = async (trx) => {
|
|
182
182
|
const where = {
|
|
183
183
|
auth_provider: identity.provider,
|
|
184
|
-
|
|
184
|
+
auth_provider_user_sid: identity.providerUserId
|
|
185
185
|
};
|
|
186
186
|
const existing = await trx("users").where(where).first();
|
|
187
187
|
|
|
@@ -200,7 +200,7 @@ function createRepository(knex) {
|
|
|
200
200
|
const username = await resolveUniqueUsername(trx, requestedUsername || usernameBaseFromEmail(email));
|
|
201
201
|
await trx("users").insert({
|
|
202
202
|
auth_provider: identity.provider,
|
|
203
|
-
|
|
203
|
+
auth_provider_user_sid: identity.providerUserId,
|
|
204
204
|
email,
|
|
205
205
|
display_name: displayName,
|
|
206
206
|
username
|
|
@@ -17,7 +17,7 @@ function mapRow(row) {
|
|
|
17
17
|
id: Number(row.id),
|
|
18
18
|
workspaceId: Number(row.workspace_id),
|
|
19
19
|
email: normalizeLowerText(row.email),
|
|
20
|
-
|
|
20
|
+
roleSid: normalizeLowerText(row.role_sid || "member") || "member",
|
|
21
21
|
status: normalizeLowerText(row.status || "pending") || "pending",
|
|
22
22
|
tokenHash: normalizeText(row.token_hash),
|
|
23
23
|
invitedByUserId: row.invited_by_user_id == null ? null : Number(row.invited_by_user_id),
|
|
@@ -86,7 +86,7 @@ function createRepository(knex) {
|
|
|
86
86
|
const insertPayload = {
|
|
87
87
|
workspace_id: Number(source.workspaceId),
|
|
88
88
|
email: normalizeLowerText(source.email),
|
|
89
|
-
|
|
89
|
+
role_sid: normalizeLowerText(source.roleSid || "member") || "member",
|
|
90
90
|
status: normalizeLowerText(source.status || "pending") || "pending",
|
|
91
91
|
token_hash: normalizeText(source.tokenHash),
|
|
92
92
|
invited_by_user_id: source.invitedByUserId == null ? null : Number(source.invitedByUserId),
|
|
@@ -16,7 +16,7 @@ function mapRow(row) {
|
|
|
16
16
|
id: Number(row.id),
|
|
17
17
|
workspaceId: Number(row.workspace_id),
|
|
18
18
|
userId: Number(row.user_id),
|
|
19
|
-
|
|
19
|
+
roleSid: normalizeLowerText(row.role_sid || "member") || "member",
|
|
20
20
|
status: normalizeLowerText(row.status || "active") || "active",
|
|
21
21
|
createdAt: toIsoString(row.created_at),
|
|
22
22
|
updatedAt: toIsoString(row.updated_at)
|
|
@@ -30,7 +30,7 @@ function mapMemberSummaryRow(row) {
|
|
|
30
30
|
|
|
31
31
|
return {
|
|
32
32
|
userId: Number(row.user_id),
|
|
33
|
-
|
|
33
|
+
roleSid: normalizeLowerText(row.role_sid || "member") || "member",
|
|
34
34
|
status: normalizeLowerText(row.status || "active") || "active",
|
|
35
35
|
displayName: normalizeText(row.display_name),
|
|
36
36
|
email: normalizeLowerText(row.email)
|
|
@@ -54,11 +54,11 @@ function createRepository(knex) {
|
|
|
54
54
|
const client = options?.trx || knex;
|
|
55
55
|
const existing = await findByWorkspaceIdAndUserId(workspaceId, userId, { trx: client });
|
|
56
56
|
if (existing) {
|
|
57
|
-
if (existing.
|
|
57
|
+
if (existing.roleSid !== OWNER_ROLE_ID || existing.status !== "active") {
|
|
58
58
|
await client("workspace_memberships")
|
|
59
59
|
.where({ workspace_id: Number(workspaceId), user_id: Number(userId) })
|
|
60
60
|
.update({
|
|
61
|
-
|
|
61
|
+
role_sid: OWNER_ROLE_ID,
|
|
62
62
|
status: "active",
|
|
63
63
|
updated_at: nowDb()
|
|
64
64
|
});
|
|
@@ -70,7 +70,7 @@ function createRepository(knex) {
|
|
|
70
70
|
await client("workspace_memberships").insert({
|
|
71
71
|
workspace_id: Number(workspaceId),
|
|
72
72
|
user_id: Number(userId),
|
|
73
|
-
|
|
73
|
+
role_sid: OWNER_ROLE_ID,
|
|
74
74
|
status: "active",
|
|
75
75
|
created_at: nowDb(),
|
|
76
76
|
updated_at: nowDb()
|
|
@@ -87,14 +87,14 @@ function createRepository(knex) {
|
|
|
87
87
|
async function upsertMembership(workspaceId, userId, patch = {}, options = {}) {
|
|
88
88
|
const client = options?.trx || knex;
|
|
89
89
|
const existing = await findByWorkspaceIdAndUserId(workspaceId, userId, { trx: client });
|
|
90
|
-
const
|
|
90
|
+
const roleSid = normalizeLowerText(patch.roleSid || existing?.roleSid || "member") || "member";
|
|
91
91
|
const status = normalizeLowerText(patch.status || existing?.status || "active") || "active";
|
|
92
92
|
|
|
93
93
|
if (!existing) {
|
|
94
94
|
await client("workspace_memberships").insert({
|
|
95
95
|
workspace_id: Number(workspaceId),
|
|
96
96
|
user_id: Number(userId),
|
|
97
|
-
|
|
97
|
+
role_sid: roleSid,
|
|
98
98
|
status,
|
|
99
99
|
created_at: nowDb(),
|
|
100
100
|
updated_at: nowDb()
|
|
@@ -105,7 +105,7 @@ function createRepository(knex) {
|
|
|
105
105
|
await client("workspace_memberships")
|
|
106
106
|
.where({ workspace_id: Number(workspaceId), user_id: Number(userId) })
|
|
107
107
|
.update({
|
|
108
|
-
|
|
108
|
+
role_sid: roleSid,
|
|
109
109
|
status,
|
|
110
110
|
updated_at: nowDb()
|
|
111
111
|
});
|
|
@@ -121,7 +121,7 @@ function createRepository(knex) {
|
|
|
121
121
|
.orderBy("up.display_name", "asc")
|
|
122
122
|
.select([
|
|
123
123
|
"wm.user_id",
|
|
124
|
-
"wm.
|
|
124
|
+
"wm.role_sid",
|
|
125
125
|
"wm.status",
|
|
126
126
|
"up.display_name",
|
|
127
127
|
"up.email"
|
|
@@ -32,7 +32,7 @@ function mapMembershipWorkspaceRow(row) {
|
|
|
32
32
|
|
|
33
33
|
return {
|
|
34
34
|
...mapRow(row),
|
|
35
|
-
|
|
35
|
+
roleSid: normalizeLowerText(row.role_sid || "member"),
|
|
36
36
|
membershipStatus: normalizeLowerText(row.membership_status || "active") || "active"
|
|
37
37
|
};
|
|
38
38
|
}
|
|
@@ -55,7 +55,7 @@ function createRepository(knex) {
|
|
|
55
55
|
"w.deleted_at"
|
|
56
56
|
];
|
|
57
57
|
if (includeMembership) {
|
|
58
|
-
columns.push("wm.
|
|
58
|
+
columns.push("wm.role_sid", "wm.status as membership_status");
|
|
59
59
|
}
|
|
60
60
|
return columns;
|
|
61
61
|
}
|
|
@@ -9,7 +9,7 @@ function buildNormalizedIdentityKey(identityLike) {
|
|
|
9
9
|
|
|
10
10
|
return {
|
|
11
11
|
authProvider: identity.provider,
|
|
12
|
-
|
|
12
|
+
authProviderUserSid: identity.providerUserId
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -25,7 +25,7 @@ function buildNormalizedIdentityProfile(profileLike) {
|
|
|
25
25
|
|
|
26
26
|
return {
|
|
27
27
|
authProvider: identity.authProvider,
|
|
28
|
-
|
|
28
|
+
authProviderUserSid: identity.authProviderUserSid,
|
|
29
29
|
email,
|
|
30
30
|
displayName,
|
|
31
31
|
username: normalizeLowerText(source.username)
|
|
@@ -41,7 +41,7 @@ function profileNeedsUpdate(existing, nextProfile) {
|
|
|
41
41
|
existing.email !== nextProfile.email ||
|
|
42
42
|
existing.displayName !== nextProfile.displayName ||
|
|
43
43
|
existing.authProvider !== nextProfile.authProvider ||
|
|
44
|
-
existing.
|
|
44
|
+
existing.authProviderUserSid !== nextProfile.authProviderUserSid
|
|
45
45
|
);
|
|
46
46
|
}
|
|
47
47
|
|
|
@@ -69,7 +69,7 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
|
|
|
69
69
|
return userProfilesRepository.findByIdentity(
|
|
70
70
|
{
|
|
71
71
|
provider: normalized.authProvider,
|
|
72
|
-
providerUserId: normalized.
|
|
72
|
+
providerUserId: normalized.authProviderUserSid
|
|
73
73
|
},
|
|
74
74
|
options
|
|
75
75
|
);
|
|
@@ -80,7 +80,7 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
|
|
|
80
80
|
return userProfilesRepository.upsert(
|
|
81
81
|
{
|
|
82
82
|
authProvider: normalized.authProvider,
|
|
83
|
-
|
|
83
|
+
authProviderUserSid: normalized.authProviderUserSid,
|
|
84
84
|
email: normalized.email,
|
|
85
85
|
displayName: normalized.displayName,
|
|
86
86
|
username: normalized.username
|
|
@@ -50,8 +50,8 @@ function buildWorkspaceName(user = {}) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function buildPermissionsFromMembership(membership, appConfig = {}) {
|
|
53
|
-
const
|
|
54
|
-
return resolveRolePermissions(
|
|
53
|
+
const roleSid = normalizeLowerText(membership?.roleSid || "member");
|
|
54
|
+
return resolveRolePermissions(roleSid, appConfig);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
function hashInviteToken(token) {
|
|
@@ -145,7 +145,7 @@ function createService({
|
|
|
145
145
|
|
|
146
146
|
const list = await workspacesRepository.listForUserId(normalizedUser.id, options);
|
|
147
147
|
const accessible = list
|
|
148
|
-
.map((entry) => mapWorkspaceSummary(entry, {
|
|
148
|
+
.map((entry) => mapWorkspaceSummary(entry, { roleSid: entry.roleSid, status: entry.membershipStatus }))
|
|
149
149
|
.filter((entry) => entry.isAccessible);
|
|
150
150
|
|
|
151
151
|
return accessible;
|
|
@@ -16,7 +16,7 @@ function normalizeAuthenticatedUser(input = {}) {
|
|
|
16
16
|
username: normalizeLowerText(source.username),
|
|
17
17
|
displayName: normalizeText(source.displayName) || email || `User ${id}`,
|
|
18
18
|
authProvider: normalizeLowerText(source.authProvider),
|
|
19
|
-
|
|
19
|
+
authProviderUserSid: normalizeText(source.authProviderUserSid),
|
|
20
20
|
avatarStorageKey: source.avatarStorageKey ? normalizeText(source.avatarStorageKey) : null,
|
|
21
21
|
avatarVersion: source.avatarVersion == null ? null : String(source.avatarVersion)
|
|
22
22
|
};
|
|
@@ -30,7 +30,7 @@ const authenticatedUserValidator = Object.freeze({
|
|
|
30
30
|
username: Type.Optional(Type.String()),
|
|
31
31
|
displayName: Type.Optional(Type.String()),
|
|
32
32
|
authProvider: Type.Optional(Type.String()),
|
|
33
|
-
|
|
33
|
+
authProviderUserSid: Type.Optional(Type.String()),
|
|
34
34
|
avatarStorageKey: Type.Optional(Type.Union([Type.String(), Type.Null()])),
|
|
35
35
|
avatarVersion: Type.Optional(Type.Union([Type.String(), Type.Number(), Type.Null()]))
|
|
36
36
|
},
|
|
@@ -319,7 +319,7 @@ function createWorkspaceBootstrapContributor({
|
|
|
319
319
|
const latestProfile =
|
|
320
320
|
(await userProfilesRepository.findByIdentity({
|
|
321
321
|
provider: normalizedUser.authProvider,
|
|
322
|
-
providerUserId: normalizedUser.
|
|
322
|
+
providerUserId: normalizedUser.authProviderUserSid
|
|
323
323
|
})) || normalizedUser;
|
|
324
324
|
|
|
325
325
|
const workspaces = await workspaceService.listWorkspacesForUser(latestProfile, { request });
|
|
@@ -362,7 +362,7 @@ function createWorkspaceBootstrapContributor({
|
|
|
362
362
|
pendingInvites,
|
|
363
363
|
activeWorkspace: workspaceContext
|
|
364
364
|
? mapWorkspaceSummary(workspaceContext.workspace, {
|
|
365
|
-
|
|
365
|
+
roleSid: workspaceContext.membership?.roleSid,
|
|
366
366
|
status: workspaceContext.membership?.status
|
|
367
367
|
})
|
|
368
368
|
: null,
|