@jskit-ai/users-core 0.1.31 → 0.1.33
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 +21 -19
- package/package.json +6 -6
- package/src/server/UsersCoreServiceProvider.js +1 -3
- package/src/server/accountNotifications/accountNotificationsService.js +3 -3
- package/src/server/accountNotifications/registerAccountNotifications.js +1 -1
- package/src/server/accountPreferences/accountPreferencesService.js +3 -3
- package/src/server/accountPreferences/registerAccountPreferences.js +1 -1
- package/src/server/accountProfile/accountProfileActions.js +8 -2
- package/src/server/accountProfile/accountProfileService.js +10 -10
- package/src/server/accountProfile/avatarService.js +26 -67
- package/src/server/accountProfile/avatarStorageService.js +14 -95
- package/src/server/accountProfile/bootAccountProfileRoutes.js +13 -15
- package/src/server/accountProfile/registerAccountProfile.js +2 -2
- package/src/server/accountSecurity/accountSecurityService.js +3 -3
- package/src/server/accountSecurity/registerAccountSecurity.js +1 -1
- package/src/server/common/contributors/workspaceActionContextContributor.js +24 -17
- package/src/server/common/formatters/workspaceFormatter.js +2 -2
- package/src/server/common/registerCommonRepositories.js +3 -3
- package/src/server/common/repositories/{userProfilesRepository.js → usersRepository.js} +7 -7
- 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/accountContextService.js +4 -4
- package/src/server/common/services/authProfileSyncService.js +15 -15
- package/src/server/common/services/workspaceContextService.js +3 -3
- package/src/server/common/validators/authenticatedUserValidator.js +2 -2
- package/src/server/registerWorkspaceBootstrap.js +1 -1
- package/src/server/registerWorkspaceCore.js +5 -2
- package/src/server/workspaceBootstrapContributor.js +6 -6
- 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 +37 -12
- package/templates/config/roles.js +27 -0
- package/templates/migrations/users_core_initial.cjs +5 -5
- package/test/authProfileSyncService.test.js +8 -8
- package/test/avatarService.test.js +6 -6
- package/test/roles.test.js +90 -5
- package/test/usersRouteRequestInputValidator.test.js +4 -4
- package/test/workspaceActionContextContributor.test.js +107 -14
- package/test/workspaceAuthPolicyContextResolver.test.js +2 -2
- package/test/workspaceBootstrapContributor.test.js +8 -8
- package/test/workspaceInvitesRepository.test.js +3 -3
- package/test/workspaceMembersService.test.js +14 -12
- package/test/workspacePendingInvitationsResource.test.js +2 -2
- package/test/workspacePendingInvitationsService.test.js +3 -3
- package/test/workspaceService.test.js +22 -18
- package/test/workspaceSettingsResource.test.js +4 -2
- package/src/server/accountProfile/registerAvatarMultipartSupport.js +0 -40
- package/templates/config/workspaceRoles.js +0 -30
- package/test/registerAvatarMultipartSupport.test.js +0 -63
|
@@ -11,7 +11,7 @@ function registerAccountSecurity(app) {
|
|
|
11
11
|
const authService = scope.has("authService") ? scope.make("authService") : null;
|
|
12
12
|
return createAccountSecurityService({
|
|
13
13
|
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
14
|
-
|
|
14
|
+
usersRepository: scope.make("usersRepository"),
|
|
15
15
|
authService
|
|
16
16
|
});
|
|
17
17
|
});
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
normalizeObject,
|
|
3
3
|
requireServiceMethod
|
|
4
4
|
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
5
|
+
import { normalizeSurfaceId } from "@jskit-ai/kernel/shared/surface/registry";
|
|
5
6
|
import {
|
|
6
7
|
checkRouteVisibility,
|
|
7
8
|
USERS_ROUTE_VISIBILITY_PUBLIC,
|
|
@@ -9,45 +10,51 @@ import {
|
|
|
9
10
|
USERS_ROUTE_VISIBILITY_WORKSPACE_USER
|
|
10
11
|
} from "../../../shared/support/usersVisibility.js";
|
|
11
12
|
import { resolveActionUser } from "../support/resolveActionUser.js";
|
|
12
|
-
|
|
13
|
-
const WORKSPACE_CONTEXT_ACTION_IDS = Object.freeze([
|
|
14
|
-
"workspace.roles.list",
|
|
15
|
-
"workspace.settings.read",
|
|
16
|
-
"workspace.settings.update",
|
|
17
|
-
"workspace.members.list",
|
|
18
|
-
"workspace.member.role.update",
|
|
19
|
-
"workspace.member.remove",
|
|
20
|
-
"workspace.invites.list",
|
|
21
|
-
"workspace.invite.create",
|
|
22
|
-
"workspace.invite.revoke"
|
|
23
|
-
]);
|
|
24
13
|
const WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET = new Set([
|
|
25
14
|
USERS_ROUTE_VISIBILITY_WORKSPACE,
|
|
26
15
|
USERS_ROUTE_VISIBILITY_WORKSPACE_USER
|
|
27
16
|
]);
|
|
28
17
|
|
|
29
|
-
function
|
|
18
|
+
function normalizeWorkspaceSurfaceIds(surfaceIds = []) {
|
|
19
|
+
const source = Array.isArray(surfaceIds) ? surfaceIds : [];
|
|
20
|
+
const normalized = new Set();
|
|
21
|
+
|
|
22
|
+
for (const entry of source) {
|
|
23
|
+
const surfaceId = normalizeSurfaceId(entry);
|
|
24
|
+
if (!surfaceId) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
normalized.add(surfaceId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return normalized;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createWorkspaceActionContextContributor({ workspaceService, workspaceSurfaceIds = [] } = {}) {
|
|
30
34
|
const contributorId = "users.workspace.context";
|
|
35
|
+
const workspaceSurfaceIdSet = normalizeWorkspaceSurfaceIds(workspaceSurfaceIds);
|
|
31
36
|
|
|
32
37
|
requireServiceMethod(workspaceService, "resolveWorkspaceContextForUserBySlug", contributorId);
|
|
33
38
|
|
|
34
39
|
return Object.freeze({
|
|
35
40
|
contributorId,
|
|
36
|
-
async contribute({
|
|
41
|
+
async contribute({ definition = null, input, context, request } = {}) {
|
|
37
42
|
const payload = normalizeObject(input);
|
|
38
43
|
if (!Object.hasOwn(payload, "workspaceSlug")) {
|
|
39
44
|
return {};
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
const
|
|
43
|
-
const
|
|
47
|
+
const actionSurfaces = Array.isArray(definition?.surfaces) ? definition.surfaces : [];
|
|
48
|
+
const hasWorkspaceActionSurface = actionSurfaces.some((surfaceId) => workspaceSurfaceIdSet.has(surfaceId));
|
|
49
|
+
const routeSurfaceId = normalizeSurfaceId(request?.routeOptions?.config?.surface);
|
|
50
|
+
const hasWorkspaceSurface = workspaceSurfaceIdSet.has(routeSurfaceId);
|
|
44
51
|
const routeVisibilityInput =
|
|
45
52
|
request && request.routeOptions && request.routeOptions.config
|
|
46
53
|
? request.routeOptions.config.visibility
|
|
47
54
|
: USERS_ROUTE_VISIBILITY_PUBLIC;
|
|
48
55
|
const routeVisibility = checkRouteVisibility(routeVisibilityInput);
|
|
49
56
|
const hasWorkspaceRouteVisibility = WORKSPACE_VISIBILITY_ACTION_CONTEXT_SET.has(routeVisibility);
|
|
50
|
-
if (!
|
|
57
|
+
if (!hasWorkspaceActionSurface && !hasWorkspaceRouteVisibility && !hasWorkspaceSurface) {
|
|
51
58
|
return {};
|
|
52
59
|
}
|
|
53
60
|
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createRepository as
|
|
1
|
+
import { createRepository as createUsersRepository } from "./repositories/usersRepository.js";
|
|
2
2
|
import { createRepository as createUserSettingsRepository } from "./repositories/userSettingsRepository.js";
|
|
3
3
|
import { createRepository as createWorkspacesRepository } from "./repositories/workspacesRepository.js";
|
|
4
4
|
import { createRepository as createWorkspaceMembershipsRepository } from "./repositories/workspaceMembershipsRepository.js";
|
|
@@ -10,9 +10,9 @@ function registerCommonRepositories(app) {
|
|
|
10
10
|
throw new Error("registerCommonRepositories requires application singleton().");
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
app.singleton("
|
|
13
|
+
app.singleton("usersRepository", (scope) => {
|
|
14
14
|
const knex = scope.make("jskit.database.knex");
|
|
15
|
-
return
|
|
15
|
+
return createUsersRepository(knex);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
app.singleton("userSettingsRepository", (scope) => {
|
|
@@ -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),
|
|
@@ -104,7 +104,7 @@ async function resolveUniqueUsername(client, baseUsername, { excludeUserId = 0 }
|
|
|
104
104
|
|
|
105
105
|
function createRepository(knex) {
|
|
106
106
|
if (typeof knex !== "function") {
|
|
107
|
-
throw new TypeError("
|
|
107
|
+
throw new TypeError("usersRepository requires knex.");
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
async function findById(userId, options = {}) {
|
|
@@ -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
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { normalizeIdentity } from "../repositories/
|
|
1
|
+
import { normalizeIdentity } from "../repositories/usersRepository.js";
|
|
2
2
|
|
|
3
|
-
async function resolveUserProfile(
|
|
3
|
+
async function resolveUserProfile(usersRepository, user) {
|
|
4
4
|
const identity = normalizeIdentity(user);
|
|
5
5
|
if (identity) {
|
|
6
|
-
const profile = await
|
|
6
|
+
const profile = await usersRepository.findByIdentity(identity);
|
|
7
7
|
if (profile) {
|
|
8
8
|
return profile;
|
|
9
9
|
}
|
|
@@ -11,7 +11,7 @@ async function resolveUserProfile(userProfilesRepository, user) {
|
|
|
11
11
|
|
|
12
12
|
const userId = Number(user?.id);
|
|
13
13
|
if (Number.isInteger(userId) && userId > 0) {
|
|
14
|
-
const profileById = await
|
|
14
|
+
const profileById = await usersRepository.findById(userId);
|
|
15
15
|
if (profileById) {
|
|
16
16
|
return profileById;
|
|
17
17
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
2
|
-
import { normalizeIdentity } from "../repositories/
|
|
2
|
+
import { normalizeIdentity } from "../repositories/usersRepository.js";
|
|
3
3
|
|
|
4
4
|
function buildNormalizedIdentityKey(identityLike) {
|
|
5
5
|
const identity = normalizeIdentity(identityLike);
|
|
@@ -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
|
|
|
@@ -53,12 +53,12 @@ function requireSynchronizedProfile(profile) {
|
|
|
53
53
|
throw new Error("Profile synchronization failed.");
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
function createService({
|
|
57
|
-
if (!
|
|
58
|
-
throw new Error("authProfileSyncService requires
|
|
56
|
+
function createService({ usersRepository, workspaceProvisioningService = null, userSettingsRepository = null } = {}) {
|
|
57
|
+
if (!usersRepository || typeof usersRepository.findByIdentity !== "function") {
|
|
58
|
+
throw new Error("authProfileSyncService requires usersRepository.findByIdentity().");
|
|
59
59
|
}
|
|
60
|
-
if (typeof
|
|
61
|
-
throw new Error("authProfileSyncService requires
|
|
60
|
+
if (typeof usersRepository.upsert !== "function") {
|
|
61
|
+
throw new Error("authProfileSyncService requires usersRepository.upsert().");
|
|
62
62
|
}
|
|
63
63
|
if (!userSettingsRepository || typeof userSettingsRepository.ensureForUserId !== "function") {
|
|
64
64
|
throw new Error("authProfileSyncService requires userSettingsRepository.ensureForUserId().");
|
|
@@ -66,10 +66,10 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
|
|
|
66
66
|
|
|
67
67
|
async function findByIdentity(identityLike, options = {}) {
|
|
68
68
|
const normalized = buildNormalizedIdentityKey(identityLike);
|
|
69
|
-
return
|
|
69
|
+
return usersRepository.findByIdentity(
|
|
70
70
|
{
|
|
71
71
|
provider: normalized.authProvider,
|
|
72
|
-
providerUserId: normalized.
|
|
72
|
+
providerUserId: normalized.authProviderUserSid
|
|
73
73
|
},
|
|
74
74
|
options
|
|
75
75
|
);
|
|
@@ -77,10 +77,10 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
|
|
|
77
77
|
|
|
78
78
|
async function upsertByIdentity(profileLike, options = {}) {
|
|
79
79
|
const normalized = buildNormalizedIdentityProfile(profileLike);
|
|
80
|
-
return
|
|
80
|
+
return usersRepository.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
|
|
@@ -118,8 +118,8 @@ function createService({ userProfilesRepository, workspaceProvisioningService =
|
|
|
118
118
|
if (options?.trx) {
|
|
119
119
|
return runSync(options.trx);
|
|
120
120
|
}
|
|
121
|
-
if (typeof
|
|
122
|
-
return
|
|
121
|
+
if (typeof usersRepository.withTransaction === "function") {
|
|
122
|
+
return usersRepository.withTransaction((trx) => runSync(trx));
|
|
123
123
|
}
|
|
124
124
|
return runSync();
|
|
125
125
|
}
|
|
@@ -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
|
},
|
|
@@ -17,7 +17,7 @@ function registerWorkspaceBootstrap(app) {
|
|
|
17
17
|
? scope.make("users.workspace.pending-invitations.service")
|
|
18
18
|
: null,
|
|
19
19
|
workspaceInvitationsEnabled,
|
|
20
|
-
|
|
20
|
+
usersRepository: scope.make("usersRepository"),
|
|
21
21
|
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
22
22
|
appConfig: resolveAppConfig(scope),
|
|
23
23
|
tenancyProfile: scope.make("users.tenancy.profile"),
|
|
@@ -10,6 +10,7 @@ import { createWorkspaceActionContextContributor } from "./common/contributors/w
|
|
|
10
10
|
import { createWorkspaceRouteVisibilityResolver } from "./common/contributors/workspaceRouteVisibilityResolver.js";
|
|
11
11
|
import { createWorkspaceAuthPolicyContextResolver } from "./common/contributors/workspaceAuthPolicyContextResolver.js";
|
|
12
12
|
import { resolveWorkspaceInvitationsPolicy } from "./support/workspaceInvitationsPolicy.js";
|
|
13
|
+
import { resolveWorkspaceSurfaceIdsFromAppConfig } from "./support/workspaceActionSurfaces.js";
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
function registerWorkspaceCore(app) {
|
|
@@ -29,7 +30,7 @@ function registerWorkspaceCore(app) {
|
|
|
29
30
|
|
|
30
31
|
app.singleton("users.profile.sync.service", (scope) => {
|
|
31
32
|
return createAuthProfileSyncService({
|
|
32
|
-
|
|
33
|
+
usersRepository: scope.make("usersRepository"),
|
|
33
34
|
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
34
35
|
workspaceProvisioningService: scope.make("users.workspace.service")
|
|
35
36
|
});
|
|
@@ -62,8 +63,10 @@ function registerWorkspaceCore(app) {
|
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
registerActionContextContributor(app, "users.core.workspace.actionContextContributor", (scope) => {
|
|
66
|
+
const appConfig = resolveAppConfig(scope);
|
|
65
67
|
return createWorkspaceActionContextContributor({
|
|
66
|
-
workspaceService: scope.make("users.workspace.service")
|
|
68
|
+
workspaceService: scope.make("users.workspace.service"),
|
|
69
|
+
workspaceSurfaceIds: resolveWorkspaceSurfaceIdsFromAppConfig(appConfig)
|
|
67
70
|
});
|
|
68
71
|
});
|
|
69
72
|
|
|
@@ -230,7 +230,7 @@ function mapUserSettingsBootstrap(settings = {}) {
|
|
|
230
230
|
function createWorkspaceBootstrapContributor({
|
|
231
231
|
workspaceService,
|
|
232
232
|
workspacePendingInvitationsService,
|
|
233
|
-
|
|
233
|
+
usersRepository,
|
|
234
234
|
userSettingsRepository,
|
|
235
235
|
workspaceInvitationsEnabled = false,
|
|
236
236
|
appConfig = {},
|
|
@@ -255,8 +255,8 @@ function createWorkspaceBootstrapContributor({
|
|
|
255
255
|
serviceLabel: "workspacePendingInvitationsService"
|
|
256
256
|
});
|
|
257
257
|
}
|
|
258
|
-
requireServiceMethod(
|
|
259
|
-
serviceLabel: "
|
|
258
|
+
requireServiceMethod(usersRepository, "findByIdentity", contributorId, {
|
|
259
|
+
serviceLabel: "usersRepository"
|
|
260
260
|
});
|
|
261
261
|
requireServiceMethod(userSettingsRepository, "ensureForUserId", contributorId, {
|
|
262
262
|
serviceLabel: "userSettingsRepository"
|
|
@@ -317,9 +317,9 @@ function createWorkspaceBootstrapContributor({
|
|
|
317
317
|
|
|
318
318
|
if (normalizedUser) {
|
|
319
319
|
const latestProfile =
|
|
320
|
-
(await
|
|
320
|
+
(await usersRepository.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,
|
|
@@ -103,7 +103,7 @@ function bootWorkspaceMembers(app) {
|
|
|
103
103
|
input: {
|
|
104
104
|
workspaceSlug: request.input.params.workspaceSlug,
|
|
105
105
|
memberUserId: request.input.params.memberUserId,
|
|
106
|
-
|
|
106
|
+
roleSid: request.input.body.roleSid
|
|
107
107
|
}
|
|
108
108
|
});
|
|
109
109
|
reply.code(200).send(response);
|
|
@@ -195,7 +195,7 @@ function bootWorkspaceMembers(app) {
|
|
|
195
195
|
input: {
|
|
196
196
|
workspaceSlug: request.input.params.workspaceSlug,
|
|
197
197
|
email: request.input.body.email,
|
|
198
|
-
|
|
198
|
+
roleSid: request.input.body.roleSid
|
|
199
199
|
}
|
|
200
200
|
});
|
|
201
201
|
reply.code(200).send(response);
|
|
@@ -68,7 +68,7 @@ const workspaceMembersActions = Object.freeze([
|
|
|
68
68
|
async execute(input, context, deps) {
|
|
69
69
|
return deps.workspaceMembersService.updateMemberRole(resolveWorkspace(context, input), {
|
|
70
70
|
memberUserId: input.memberUserId,
|
|
71
|
-
|
|
71
|
+
roleSid: input.roleSid
|
|
72
72
|
}, {
|
|
73
73
|
context
|
|
74
74
|
});
|
|
@@ -150,7 +150,7 @@ const workspaceMembersActions = Object.freeze([
|
|
|
150
150
|
resolveActionUser(context, input),
|
|
151
151
|
{
|
|
152
152
|
email: input.email,
|
|
153
|
-
|
|
153
|
+
roleSid: input.roleSid
|
|
154
154
|
},
|
|
155
155
|
{
|
|
156
156
|
context
|
|
@@ -62,12 +62,12 @@ function createService({
|
|
|
62
62
|
|
|
63
63
|
async function updateMemberRole(workspace, payload = {}, options = {}) {
|
|
64
64
|
const memberUserId = payload.memberUserId;
|
|
65
|
-
const
|
|
66
|
-
if (!assignableRoleIds.includes(
|
|
65
|
+
const roleSid = payload.roleSid;
|
|
66
|
+
if (!assignableRoleIds.includes(roleSid)) {
|
|
67
67
|
throw new AppError(400, "Validation failed.", {
|
|
68
68
|
details: {
|
|
69
69
|
fieldErrors: {
|
|
70
|
-
|
|
70
|
+
roleSid: "Role is not assignable."
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
});
|
|
@@ -77,7 +77,7 @@ function createService({
|
|
|
77
77
|
if (!existingMembership || existingMembership.status !== "active") {
|
|
78
78
|
throw new AppError(404, "Member not found.");
|
|
79
79
|
}
|
|
80
|
-
if (Number(memberUserId) === Number(workspace.ownerUserId) || existingMembership.
|
|
80
|
+
if (Number(memberUserId) === Number(workspace.ownerUserId) || existingMembership.roleSid === OWNER_ROLE_ID) {
|
|
81
81
|
throw new AppError(409, "Cannot change workspace owner role.");
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -85,7 +85,7 @@ function createService({
|
|
|
85
85
|
workspace.id,
|
|
86
86
|
memberUserId,
|
|
87
87
|
{
|
|
88
|
-
|
|
88
|
+
roleSid,
|
|
89
89
|
status: "active"
|
|
90
90
|
},
|
|
91
91
|
options
|
|
@@ -101,7 +101,7 @@ function createService({
|
|
|
101
101
|
if (!existingMembership || existingMembership.status !== "active") {
|
|
102
102
|
throw new AppError(404, "Member not found.");
|
|
103
103
|
}
|
|
104
|
-
if (Number(memberUserId) === Number(workspace.ownerUserId) || existingMembership.
|
|
104
|
+
if (Number(memberUserId) === Number(workspace.ownerUserId) || existingMembership.roleSid === OWNER_ROLE_ID) {
|
|
105
105
|
throw new AppError(409, "Cannot remove workspace owner.");
|
|
106
106
|
}
|
|
107
107
|
|
|
@@ -109,7 +109,7 @@ function createService({
|
|
|
109
109
|
workspace.id,
|
|
110
110
|
memberUserId,
|
|
111
111
|
{
|
|
112
|
-
|
|
112
|
+
roleSid: existingMembership.roleSid,
|
|
113
113
|
status: "revoked"
|
|
114
114
|
},
|
|
115
115
|
options
|
|
@@ -134,12 +134,12 @@ function createService({
|
|
|
134
134
|
|
|
135
135
|
async function createInvite(workspace, user, payload = {}, options = {}) {
|
|
136
136
|
const email = payload.email;
|
|
137
|
-
const
|
|
138
|
-
if (!assignableRoleIds.includes(
|
|
137
|
+
const roleSid = payload.roleSid;
|
|
138
|
+
if (!assignableRoleIds.includes(roleSid)) {
|
|
139
139
|
throw new AppError(400, "Validation failed.", {
|
|
140
140
|
details: {
|
|
141
141
|
fieldErrors: {
|
|
142
|
-
|
|
142
|
+
roleSid: "Role is not assignable."
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
});
|
|
@@ -152,7 +152,7 @@ function createService({
|
|
|
152
152
|
{
|
|
153
153
|
workspaceId: workspace.id,
|
|
154
154
|
email,
|
|
155
|
-
|
|
155
|
+
roleSid,
|
|
156
156
|
status: "pending",
|
|
157
157
|
tokenHash,
|
|
158
158
|
invitedByUserId: Number(user?.id || 0) || null,
|