@jskit-ai/users-core 0.1.4
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 +464 -0
- package/package.json +35 -0
- package/src/server/UsersCoreServiceProvider.js +74 -0
- package/src/server/accountNotifications/accountNotificationsActions.js +39 -0
- package/src/server/accountNotifications/accountNotificationsService.js +41 -0
- package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +41 -0
- package/src/server/accountNotifications/registerAccountNotifications.js +39 -0
- package/src/server/accountPreferences/accountPreferencesActions.js +39 -0
- package/src/server/accountPreferences/accountPreferencesService.js +41 -0
- package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +41 -0
- package/src/server/accountPreferences/registerAccountPreferences.js +39 -0
- package/src/server/accountProfile/accountProfileActions.js +137 -0
- package/src/server/accountProfile/accountProfileService.js +124 -0
- package/src/server/accountProfile/avatarService.js +141 -0
- package/src/server/accountProfile/avatarStorageService.js +132 -0
- package/src/server/accountProfile/bootAccountProfileRoutes.js +166 -0
- package/src/server/accountProfile/registerAccountProfile.js +62 -0
- package/src/server/accountProfile/registerAvatarMultipartSupport.js +43 -0
- package/src/server/accountSecurity/accountSecurityActions.js +144 -0
- package/src/server/accountSecurity/accountSecurityService.js +103 -0
- package/src/server/accountSecurity/bootAccountSecurityRoutes.js +183 -0
- package/src/server/accountSecurity/registerAccountSecurity.js +31 -0
- package/src/server/common/README.md +21 -0
- package/src/server/common/contributors/README.md +11 -0
- package/src/server/common/contributors/workspaceActionContextContributor.js +79 -0
- package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
- package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +79 -0
- package/src/server/common/diTokens.js +21 -0
- package/src/server/common/formatters/README.md +11 -0
- package/src/server/common/formatters/accountAvatarFormatter.js +42 -0
- package/src/server/common/formatters/accountSecurityStatusFormatter.js +71 -0
- package/src/server/common/formatters/accountSettingsResponseFormatter.js +62 -0
- package/src/server/common/formatters/workspaceFormatter.js +46 -0
- package/src/server/common/registerCommonRepositories.js +45 -0
- package/src/server/common/registerSharedApi.js +9 -0
- package/src/server/common/repositories/README.md +24 -0
- package/src/server/common/repositories/repositoryUtils.js +50 -0
- package/src/server/common/repositories/userProfilesRepository.js +251 -0
- package/src/server/common/repositories/userSettingsRepository.js +179 -0
- package/src/server/common/repositories/workspaceInvitesRepository.js +172 -0
- package/src/server/common/repositories/workspaceMembershipsRepository.js +157 -0
- package/src/server/common/repositories/workspacesRepository.js +183 -0
- package/src/server/common/routes/README.md +11 -0
- package/src/server/common/services/README.md +12 -0
- package/src/server/common/services/accountContextService.js +31 -0
- package/src/server/common/services/authProfileSyncService.js +128 -0
- package/src/server/common/services/workspaceContextService.js +270 -0
- package/src/server/common/support/deepFreeze.js +17 -0
- package/src/server/common/support/realtimeServiceEvents.js +94 -0
- package/src/server/common/support/resolveActionUser.js +11 -0
- package/src/server/common/support/workspaceRoutePaths.js +17 -0
- package/src/server/common/validators/README.md +11 -0
- package/src/server/common/validators/authenticatedUserValidator.js +42 -0
- package/src/server/common/validators/routeParamsValidator.js +62 -0
- package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +64 -0
- package/src/server/consoleSettings/consoleService.js +36 -0
- package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
- package/src/server/consoleSettings/consoleSettingsRepository.js +111 -0
- package/src/server/consoleSettings/consoleSettingsService.js +40 -0
- package/src/server/consoleSettings/registerConsoleSettings.js +57 -0
- package/src/server/registerWorkspaceBootstrap.js +36 -0
- package/src/server/registerWorkspaceCore.js +95 -0
- package/src/server/support/resolveWorkspace.js +16 -0
- package/src/server/support/workspaceActionSurfaces.js +135 -0
- package/src/server/support/workspaceInvitationsPolicy.js +45 -0
- package/src/server/support/workspaceRouteInput.js +22 -0
- package/src/server/workspaceBootstrapContributor.js +401 -0
- package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +73 -0
- package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
- package/src/server/workspaceDirectory/workspaceDirectoryActions.js +65 -0
- package/src/server/workspaceMembers/bootWorkspaceMembers.js +238 -0
- package/src/server/workspaceMembers/registerWorkspaceMembers.js +112 -0
- package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
- package/src/server/workspaceMembers/workspaceMembersService.js +210 -0
- package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +63 -0
- package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +128 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
- package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +137 -0
- package/src/server/workspaceSettings/bootWorkspaceSettings.js +77 -0
- package/src/server/workspaceSettings/registerWorkspaceSettings.js +67 -0
- package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
- package/src/server/workspaceSettings/workspaceSettingsRepository.js +135 -0
- package/src/server/workspaceSettings/workspaceSettingsService.js +65 -0
- package/src/shared/events/usersEvents.js +19 -0
- package/src/shared/index.js +91 -0
- package/src/shared/operationMessages.js +16 -0
- package/src/shared/resources/consoleSettingsFields.js +55 -0
- package/src/shared/resources/consoleSettingsResource.js +139 -0
- package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
- package/src/shared/resources/userProfileResource.js +148 -0
- package/src/shared/resources/userSettingsFields.js +71 -0
- package/src/shared/resources/userSettingsResource.js +416 -0
- package/src/shared/resources/workspaceMembersResource.js +352 -0
- package/src/shared/resources/workspacePendingInvitationsResource.js +87 -0
- package/src/shared/resources/workspaceResource.js +149 -0
- package/src/shared/resources/workspaceSettingsFields.js +60 -0
- package/src/shared/resources/workspaceSettingsResource.js +178 -0
- package/src/shared/roles.js +136 -0
- package/src/shared/settings.js +31 -0
- package/src/shared/support/usersApiPaths.js +34 -0
- package/src/shared/support/usersVisibility.js +45 -0
- package/src/shared/support/workspacePathModel.js +145 -0
- package/src/shared/tenancyMode.js +35 -0
- package/src/shared/tenancyProfile.js +73 -0
- package/templates/config/workspaceRoles.js +30 -0
- package/templates/migrations/users_core_console_owner.cjs +39 -0
- package/templates/migrations/users_core_initial.cjs +118 -0
- package/templates/migrations/users_core_profile_username.cjs +98 -0
- package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
- package/templates/packages/main/src/shared/resources/userSettingsFields.js +138 -0
- package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +105 -0
- package/test/authProfileSyncService.test.js +119 -0
- package/test/avatarService.test.js +114 -0
- package/test/avatarStorageService.test.js +61 -0
- package/test/consoleService.test.js +57 -0
- package/test/consoleSettingsService.test.js +86 -0
- package/test/exportsContract.test.js +38 -0
- package/test/registerAvatarMultipartSupport.test.js +64 -0
- package/test/registerServiceRealtimeEvents.test.js +160 -0
- package/test/registerWorkspaceDirectory.test.js +26 -0
- package/test/registerWorkspaceSettings.test.js +44 -0
- package/test/resourcesCanonical.test.js +90 -0
- package/test/roles.test.js +74 -0
- package/test/settingsFieldRegistriesSingleton.test.js +24 -0
- package/test/tenancyProfile.test.js +67 -0
- package/test/userSettingsResource.test.js +31 -0
- package/test/usersApiPaths.test.js +31 -0
- package/test/usersRouteRequestInputValidator.test.js +556 -0
- package/test/usersRouteResources.test.js +113 -0
- package/test/usersRouteValidators.test.js +49 -0
- package/test/usersVisibility.test.js +22 -0
- package/test/workspaceActionContextContributor.test.js +251 -0
- package/test/workspaceActionSurfaces.test.js +105 -0
- package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
- package/test/workspaceBootstrapContributor.test.js +466 -0
- package/test/workspaceInvitationsPolicy.test.js +71 -0
- package/test/workspaceInvitesRepository.test.js +111 -0
- package/test/workspaceMembersService.test.js +400 -0
- package/test/workspacePathModel.test.js +93 -0
- package/test/workspacePendingInvitationsResource.test.js +38 -0
- package/test/workspacePendingInvitationsService.test.js +151 -0
- package/test/workspaceRouteVisibilityResolver.test.js +83 -0
- package/test/workspaceService.test.js +480 -0
- package/test/workspaceSettingsActions.test.js +42 -0
- package/test/workspaceSettingsRepository.test.js +156 -0
- package/test/workspaceSettingsResource.test.js +156 -0
- package/test/workspaceSettingsService.test.js +120 -0
- package/test-support/registerDefaultSettingsFields.js +3 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { buildInviteToken, hashInviteToken } from "@jskit-ai/auth-core/server/inviteTokens";
|
|
2
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
3
|
+
import { OWNER_ROLE_ID, createWorkspaceRoleCatalog, cloneWorkspaceRoleCatalog } from "../../shared/roles.js";
|
|
4
|
+
|
|
5
|
+
function createService({
|
|
6
|
+
workspaceMembershipsRepository,
|
|
7
|
+
workspaceInvitesRepository,
|
|
8
|
+
inviteExpiresInMs,
|
|
9
|
+
roleCatalog = null,
|
|
10
|
+
workspaceInvitationsEnabled = true
|
|
11
|
+
} = {}) {
|
|
12
|
+
if (!workspaceMembershipsRepository || !workspaceInvitesRepository) {
|
|
13
|
+
throw new Error("workspaceMembersService requires membership and invite repositories.");
|
|
14
|
+
}
|
|
15
|
+
const resolvedInviteExpiresInMs = Number(inviteExpiresInMs);
|
|
16
|
+
if (!Number.isInteger(resolvedInviteExpiresInMs) || resolvedInviteExpiresInMs < 1) {
|
|
17
|
+
throw new Error("workspaceMembersService requires inviteExpiresInMs.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const resolvedRoleCatalog = roleCatalog && typeof roleCatalog === "object" ? roleCatalog : createWorkspaceRoleCatalog();
|
|
21
|
+
const assignableRoleIds = Array.isArray(resolvedRoleCatalog.assignableRoleIds)
|
|
22
|
+
? [...resolvedRoleCatalog.assignableRoleIds]
|
|
23
|
+
: [];
|
|
24
|
+
const resolvedWorkspaceInvitationsEnabled = workspaceInvitationsEnabled === true;
|
|
25
|
+
|
|
26
|
+
function ensureWorkspaceInvitationsEnabled() {
|
|
27
|
+
if (resolvedWorkspaceInvitationsEnabled) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
throw new AppError(403, "Workspace invitations are disabled.");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function withRoleCatalog(payload = {}) {
|
|
34
|
+
return {
|
|
35
|
+
...payload,
|
|
36
|
+
roleCatalog: cloneWorkspaceRoleCatalog({
|
|
37
|
+
...resolvedRoleCatalog,
|
|
38
|
+
assignableRoleIds
|
|
39
|
+
})
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function listRoles(options = {}) {
|
|
44
|
+
return cloneWorkspaceRoleCatalog({
|
|
45
|
+
...resolvedRoleCatalog,
|
|
46
|
+
assignableRoleIds
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function listMembersPayload(workspace, options = {}) {
|
|
51
|
+
const members = await workspaceMembershipsRepository.listActiveByWorkspaceId(workspace.id, options);
|
|
52
|
+
|
|
53
|
+
return withRoleCatalog({
|
|
54
|
+
workspace,
|
|
55
|
+
members
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function listMembers(workspace, options = {}) {
|
|
60
|
+
return listMembersPayload(workspace, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function updateMemberRole(workspace, payload = {}, options = {}) {
|
|
64
|
+
const memberUserId = payload.memberUserId;
|
|
65
|
+
const roleId = payload.roleId;
|
|
66
|
+
if (!assignableRoleIds.includes(roleId)) {
|
|
67
|
+
throw new AppError(400, "Validation failed.", {
|
|
68
|
+
details: {
|
|
69
|
+
fieldErrors: {
|
|
70
|
+
roleId: "Role is not assignable."
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const existingMembership = await workspaceMembershipsRepository.findByWorkspaceIdAndUserId(workspace.id, memberUserId, options);
|
|
77
|
+
if (!existingMembership || existingMembership.status !== "active") {
|
|
78
|
+
throw new AppError(404, "Member not found.");
|
|
79
|
+
}
|
|
80
|
+
if (Number(memberUserId) === Number(workspace.ownerUserId) || existingMembership.roleId === OWNER_ROLE_ID) {
|
|
81
|
+
throw new AppError(409, "Cannot change workspace owner role.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await workspaceMembershipsRepository.upsertMembership(
|
|
85
|
+
workspace.id,
|
|
86
|
+
memberUserId,
|
|
87
|
+
{
|
|
88
|
+
roleId,
|
|
89
|
+
status: "active"
|
|
90
|
+
},
|
|
91
|
+
options
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
return listMembersPayload(workspace, options);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function removeMember(workspace, payload = {}, options = {}) {
|
|
98
|
+
const memberUserId = payload.memberUserId;
|
|
99
|
+
|
|
100
|
+
const existingMembership = await workspaceMembershipsRepository.findByWorkspaceIdAndUserId(workspace.id, memberUserId, options);
|
|
101
|
+
if (!existingMembership || existingMembership.status !== "active") {
|
|
102
|
+
throw new AppError(404, "Member not found.");
|
|
103
|
+
}
|
|
104
|
+
if (Number(memberUserId) === Number(workspace.ownerUserId) || existingMembership.roleId === OWNER_ROLE_ID) {
|
|
105
|
+
throw new AppError(409, "Cannot remove workspace owner.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await workspaceMembershipsRepository.upsertMembership(
|
|
109
|
+
workspace.id,
|
|
110
|
+
memberUserId,
|
|
111
|
+
{
|
|
112
|
+
roleId: existingMembership.roleId,
|
|
113
|
+
status: "revoked"
|
|
114
|
+
},
|
|
115
|
+
options
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return listMembersPayload(workspace, options);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function listInvitesPayload(workspace, options = {}) {
|
|
122
|
+
ensureWorkspaceInvitationsEnabled();
|
|
123
|
+
const invites = await workspaceInvitesRepository.listPendingByWorkspaceIdWithWorkspace(workspace.id, options);
|
|
124
|
+
|
|
125
|
+
return withRoleCatalog({
|
|
126
|
+
workspace,
|
|
127
|
+
invites
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function listInvites(workspace, options = {}) {
|
|
132
|
+
return listInvitesPayload(workspace, options);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function createInvite(workspace, user, payload = {}, options = {}) {
|
|
136
|
+
const email = payload.email;
|
|
137
|
+
const roleId = payload.roleId;
|
|
138
|
+
if (!assignableRoleIds.includes(roleId)) {
|
|
139
|
+
throw new AppError(400, "Validation failed.", {
|
|
140
|
+
details: {
|
|
141
|
+
fieldErrors: {
|
|
142
|
+
roleId: "Role is not assignable."
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const token = buildInviteToken();
|
|
149
|
+
const tokenHash = hashInviteToken(token);
|
|
150
|
+
await workspaceInvitesRepository.expirePendingByWorkspaceIdAndEmail(workspace.id, email, options);
|
|
151
|
+
const createdInvite = await workspaceInvitesRepository.insert(
|
|
152
|
+
{
|
|
153
|
+
workspaceId: workspace.id,
|
|
154
|
+
email,
|
|
155
|
+
roleId,
|
|
156
|
+
status: "pending",
|
|
157
|
+
tokenHash,
|
|
158
|
+
invitedByUserId: Number(user?.id || 0) || null,
|
|
159
|
+
expiresAt: new Date(Date.now() + resolvedInviteExpiresInMs).toISOString()
|
|
160
|
+
},
|
|
161
|
+
options
|
|
162
|
+
);
|
|
163
|
+
const createdInviteId = Number(createdInvite?.id);
|
|
164
|
+
if (!Number.isInteger(createdInviteId) || createdInviteId < 1) {
|
|
165
|
+
throw new Error("workspaceMembersService.createInvite expected repository to return created invite id.");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const response = await listInvitesPayload(workspace, options);
|
|
169
|
+
return {
|
|
170
|
+
...response,
|
|
171
|
+
inviteTokenPreview: token,
|
|
172
|
+
createdInviteId
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function revokeInvite(workspace, inviteId, options = {}) {
|
|
177
|
+
const invite = await workspaceInvitesRepository.findPendingByIdForWorkspace(
|
|
178
|
+
inviteId,
|
|
179
|
+
workspace.id,
|
|
180
|
+
options
|
|
181
|
+
);
|
|
182
|
+
if (!invite) {
|
|
183
|
+
throw new AppError(404, "Invite not found.");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
await workspaceInvitesRepository.revokeById(inviteId, options);
|
|
187
|
+
const revokedInviteId = Number(invite?.id);
|
|
188
|
+
if (!Number.isInteger(revokedInviteId) || revokedInviteId < 1) {
|
|
189
|
+
throw new Error("workspaceMembersService.revokeInvite expected repository to return pending invite id.");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const response = await listInvitesPayload(workspace, options);
|
|
193
|
+
return {
|
|
194
|
+
...response,
|
|
195
|
+
revokedInviteId
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return Object.freeze({
|
|
200
|
+
listRoles,
|
|
201
|
+
listMembers,
|
|
202
|
+
updateMemberRole,
|
|
203
|
+
removeMember,
|
|
204
|
+
listInvites,
|
|
205
|
+
createInvite,
|
|
206
|
+
revokeInvite
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export { createService };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
3
|
+
import { workspaceMembersResource } from "../../shared/resources/workspaceMembersResource.js";
|
|
4
|
+
import { workspacePendingInvitationsResource } from "../../shared/resources/workspacePendingInvitationsResource.js";
|
|
5
|
+
|
|
6
|
+
function bootWorkspacePendingInvitations(app) {
|
|
7
|
+
if (!app || typeof app.make !== "function") {
|
|
8
|
+
throw new Error("bootWorkspacePendingInvitations requires application make().");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
12
|
+
|
|
13
|
+
router.register(
|
|
14
|
+
"GET",
|
|
15
|
+
"/api/workspace/invitations/pending",
|
|
16
|
+
{
|
|
17
|
+
auth: "required",
|
|
18
|
+
meta: {
|
|
19
|
+
tags: ["workspace"],
|
|
20
|
+
summary: "List pending workspace invitations for authenticated user"
|
|
21
|
+
},
|
|
22
|
+
responseValidators: withStandardErrorResponses({
|
|
23
|
+
200: workspacePendingInvitationsResource.operations.list.outputValidator
|
|
24
|
+
})
|
|
25
|
+
},
|
|
26
|
+
async function (request, reply) {
|
|
27
|
+
const response = await request.executeAction({
|
|
28
|
+
actionId: "workspace.invitations.pending.list"
|
|
29
|
+
});
|
|
30
|
+
reply.code(200).send(response);
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
router.register(
|
|
35
|
+
"POST",
|
|
36
|
+
"/api/workspace/invitations/redeem",
|
|
37
|
+
{
|
|
38
|
+
auth: "required",
|
|
39
|
+
meta: {
|
|
40
|
+
tags: ["workspace"],
|
|
41
|
+
summary: "Accept or refuse a workspace invitation using an invite token"
|
|
42
|
+
},
|
|
43
|
+
bodyValidator: workspaceMembersResource.operations.redeemInvite.bodyValidator,
|
|
44
|
+
responseValidators: withStandardErrorResponses(
|
|
45
|
+
{
|
|
46
|
+
200: workspaceMembersResource.operations.redeemInvite.outputValidator
|
|
47
|
+
},
|
|
48
|
+
{ includeValidation400: true }
|
|
49
|
+
)
|
|
50
|
+
},
|
|
51
|
+
async function (request, reply) {
|
|
52
|
+
const response = await request.executeAction({
|
|
53
|
+
actionId: "workspace.invite.redeem",
|
|
54
|
+
input: {
|
|
55
|
+
payload: request.input.body
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
reply.code(200).send(response);
|
|
59
|
+
}
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export { bootWorkspacePendingInvitations };
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
|
|
2
|
+
import { createService } from "./workspacePendingInvitationsService.js";
|
|
3
|
+
import { workspacePendingInvitationsActions } from "./workspacePendingInvitationsActions.js";
|
|
4
|
+
import {
|
|
5
|
+
USERS_BOOTSTRAP_CHANGED_EVENT,
|
|
6
|
+
WORKSPACE_INVITES_CHANGED_EVENT,
|
|
7
|
+
WORKSPACE_MEMBERS_CHANGED_EVENT,
|
|
8
|
+
WORKSPACES_CHANGED_EVENT,
|
|
9
|
+
WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT
|
|
10
|
+
} from "../../shared/events/usersEvents.js";
|
|
11
|
+
import { deepFreeze } from "../common/support/deepFreeze.js";
|
|
12
|
+
import {
|
|
13
|
+
USERS_WORKSPACE_PENDING_INVITATIONS_SERVICE_TOKEN
|
|
14
|
+
} from "../common/diTokens.js";
|
|
15
|
+
|
|
16
|
+
function workspaceAudienceFromEntityId({ event } = {}) {
|
|
17
|
+
const workspaceId = Number(event?.entityId);
|
|
18
|
+
if (!Number.isInteger(workspaceId) || workspaceId < 1) {
|
|
19
|
+
return "none";
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
workspaceId
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function actorUserEntityId({ options } = {}) {
|
|
27
|
+
return Number(options?.context?.actor?.id || 0);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createActorUserEvent({ source, entity, realtimeEvent }) {
|
|
31
|
+
return {
|
|
32
|
+
type: "entity.changed",
|
|
33
|
+
source,
|
|
34
|
+
entity,
|
|
35
|
+
operation: "updated",
|
|
36
|
+
entityId: actorUserEntityId,
|
|
37
|
+
realtime: {
|
|
38
|
+
event: realtimeEvent,
|
|
39
|
+
audience: "actor_user"
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function createWorkspaceAudienceEvent({ entity, realtimeEvent }) {
|
|
45
|
+
return {
|
|
46
|
+
type: "entity.changed",
|
|
47
|
+
source: "workspace",
|
|
48
|
+
entity,
|
|
49
|
+
operation: "updated",
|
|
50
|
+
entityId: ({ result }) => result?.workspaceId,
|
|
51
|
+
realtime: {
|
|
52
|
+
event: realtimeEvent,
|
|
53
|
+
audience: workspaceAudienceFromEntityId
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function createInviteDecisionEvents({ includeDirectoryAndMembers = false } = {}) {
|
|
59
|
+
const events = [
|
|
60
|
+
createActorUserEvent({
|
|
61
|
+
source: "workspace",
|
|
62
|
+
entity: "invitation",
|
|
63
|
+
realtimeEvent: WORKSPACE_PENDING_INVITATIONS_CHANGED_EVENT
|
|
64
|
+
}),
|
|
65
|
+
createActorUserEvent({
|
|
66
|
+
source: "users",
|
|
67
|
+
entity: "bootstrap",
|
|
68
|
+
realtimeEvent: USERS_BOOTSTRAP_CHANGED_EVENT
|
|
69
|
+
})
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
if (includeDirectoryAndMembers) {
|
|
73
|
+
events.push(
|
|
74
|
+
createActorUserEvent({
|
|
75
|
+
source: "workspace",
|
|
76
|
+
entity: "directory",
|
|
77
|
+
realtimeEvent: WORKSPACES_CHANGED_EVENT
|
|
78
|
+
}),
|
|
79
|
+
createWorkspaceAudienceEvent({
|
|
80
|
+
entity: "member",
|
|
81
|
+
realtimeEvent: WORKSPACE_MEMBERS_CHANGED_EVENT
|
|
82
|
+
})
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
events.push(
|
|
87
|
+
createWorkspaceAudienceEvent({
|
|
88
|
+
entity: "invite",
|
|
89
|
+
realtimeEvent: WORKSPACE_INVITES_CHANGED_EVENT
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return events;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function registerWorkspacePendingInvitations(app) {
|
|
97
|
+
if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
|
|
98
|
+
throw new Error("registerWorkspacePendingInvitations requires application singleton()/service()/actions().");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
app.service(
|
|
102
|
+
USERS_WORKSPACE_PENDING_INVITATIONS_SERVICE_TOKEN,
|
|
103
|
+
(scope) =>
|
|
104
|
+
createService({
|
|
105
|
+
workspaceInvitesRepository: scope.make("workspaceInvitesRepository"),
|
|
106
|
+
workspaceMembershipsRepository: scope.make("workspaceMembershipsRepository")
|
|
107
|
+
}),
|
|
108
|
+
{
|
|
109
|
+
events: deepFreeze({
|
|
110
|
+
acceptInviteByToken: createInviteDecisionEvents({
|
|
111
|
+
includeDirectoryAndMembers: true
|
|
112
|
+
}),
|
|
113
|
+
refuseInviteByToken: createInviteDecisionEvents()
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
app.actions(
|
|
119
|
+
withActionDefaults(workspacePendingInvitationsActions, {
|
|
120
|
+
domain: "workspace",
|
|
121
|
+
dependencies: {
|
|
122
|
+
workspacePendingInvitationsService: USERS_WORKSPACE_PENDING_INVITATIONS_SERVICE_TOKEN
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export { registerWorkspacePendingInvitations };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_INPUT_VALIDATOR
|
|
3
|
+
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
4
|
+
import { workspaceMembersResource } from "../../shared/resources/workspaceMembersResource.js";
|
|
5
|
+
import { workspacePendingInvitationsResource } from "../../shared/resources/workspacePendingInvitationsResource.js";
|
|
6
|
+
import { resolveActionUser } from "../common/support/resolveActionUser.js";
|
|
7
|
+
|
|
8
|
+
const workspacePendingInvitationsActions = Object.freeze([
|
|
9
|
+
{
|
|
10
|
+
id: "workspace.invitations.pending.list",
|
|
11
|
+
version: 1,
|
|
12
|
+
kind: "query",
|
|
13
|
+
channels: ["api", "automation", "internal"],
|
|
14
|
+
surfacesFrom: "enabled",
|
|
15
|
+
permission: {
|
|
16
|
+
require: "authenticated"
|
|
17
|
+
},
|
|
18
|
+
inputValidator: EMPTY_INPUT_VALIDATOR,
|
|
19
|
+
outputValidator: workspacePendingInvitationsResource.operations.list.outputValidator,
|
|
20
|
+
idempotency: "none",
|
|
21
|
+
audit: {
|
|
22
|
+
actionName: "workspace.invitations.pending.list"
|
|
23
|
+
},
|
|
24
|
+
observability: {},
|
|
25
|
+
async execute(input, context, deps) {
|
|
26
|
+
return {
|
|
27
|
+
pendingInvites: await deps.workspacePendingInvitationsService.listPendingInvitesForUser(resolveActionUser(context, input), {
|
|
28
|
+
context
|
|
29
|
+
})
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "workspace.invite.redeem",
|
|
35
|
+
version: 1,
|
|
36
|
+
kind: "command",
|
|
37
|
+
channels: ["api", "automation", "internal"],
|
|
38
|
+
surfacesFrom: "enabled",
|
|
39
|
+
permission: {
|
|
40
|
+
require: "authenticated"
|
|
41
|
+
},
|
|
42
|
+
inputValidator: {
|
|
43
|
+
payload: workspaceMembersResource.operations.redeemInvite.bodyValidator
|
|
44
|
+
},
|
|
45
|
+
outputValidator: workspaceMembersResource.operations.redeemInvite.outputValidator,
|
|
46
|
+
idempotency: "optional",
|
|
47
|
+
audit: {
|
|
48
|
+
actionName: "workspace.invite.redeem"
|
|
49
|
+
},
|
|
50
|
+
observability: {},
|
|
51
|
+
async execute(input, context, deps) {
|
|
52
|
+
const payload = input.payload || {};
|
|
53
|
+
const user = resolveActionUser(context, input);
|
|
54
|
+
|
|
55
|
+
if (payload.decision === "accept") {
|
|
56
|
+
return deps.workspacePendingInvitationsService.acceptInviteByToken({
|
|
57
|
+
user,
|
|
58
|
+
token: payload.token
|
|
59
|
+
}, {
|
|
60
|
+
context
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return deps.workspacePendingInvitationsService.refuseInviteByToken({
|
|
65
|
+
user,
|
|
66
|
+
token: payload.token
|
|
67
|
+
}, {
|
|
68
|
+
context
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
export { workspacePendingInvitationsActions };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { resolveInviteTokenHash } from "@jskit-ai/auth-core/server/inviteTokens";
|
|
2
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
3
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
4
|
+
import { authenticatedUserValidator } from "../common/validators/authenticatedUserValidator.js";
|
|
5
|
+
|
|
6
|
+
function createService({
|
|
7
|
+
workspaceInvitesRepository,
|
|
8
|
+
workspaceMembershipsRepository
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (!workspaceInvitesRepository || !workspaceMembershipsRepository) {
|
|
11
|
+
throw new Error("workspacePendingInvitationsService requires invite and membership repositories.");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function requireAuthenticatedInviteUser(user) {
|
|
15
|
+
const normalizedUser = authenticatedUserValidator.normalize(user);
|
|
16
|
+
if (!normalizedUser) {
|
|
17
|
+
throw new AppError(401, "Authentication required.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return normalizedUser;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function requireInviteTokenHash(token) {
|
|
24
|
+
const normalizedToken = normalizeText(token);
|
|
25
|
+
if (!normalizedToken) {
|
|
26
|
+
throw new AppError(400, "Invite token is required.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tokenHash = resolveInviteTokenHash(normalizedToken);
|
|
30
|
+
if (!tokenHash) {
|
|
31
|
+
throw new AppError(400, "Invite token is invalid.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return tokenHash;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function requirePendingInviteForUserByToken(user, token, options = {}) {
|
|
38
|
+
const normalizedUser = requireAuthenticatedInviteUser(user);
|
|
39
|
+
const tokenHash = requireInviteTokenHash(token);
|
|
40
|
+
|
|
41
|
+
const invite = await workspaceInvitesRepository.findPendingByTokenHash(tokenHash, options);
|
|
42
|
+
if (!invite) {
|
|
43
|
+
throw new AppError(404, "Invitation not found or already handled.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (normalizeLowerText(invite.email) !== normalizedUser.email) {
|
|
47
|
+
throw new AppError(403, "Invitation email does not match authenticated user.");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
user: normalizedUser,
|
|
52
|
+
invite
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function revokeExpiredInviteAndThrow(invite, options = {}) {
|
|
57
|
+
if (invite.expiresAt && new Date(invite.expiresAt).getTime() < Date.now()) {
|
|
58
|
+
await workspaceInvitesRepository.revokeById(invite.id, options);
|
|
59
|
+
throw new AppError(409, "Invitation has expired.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function listPendingInvitesForUser(user, options = {}) {
|
|
64
|
+
const normalizedUser = requireAuthenticatedInviteUser(user);
|
|
65
|
+
if (!normalizedUser.email) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return workspaceInvitesRepository.listPendingByEmail(normalizedUser.email, options);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function requireWorkspaceIdFromInvite(invite, methodName = "workspacePendingInvitationsService") {
|
|
73
|
+
const workspaceId = Number(invite?.workspaceId);
|
|
74
|
+
if (!Number.isInteger(workspaceId) || workspaceId < 1) {
|
|
75
|
+
throw new Error(`${methodName} expected invite workspace id.`);
|
|
76
|
+
}
|
|
77
|
+
return workspaceId;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolveInviteActionInput(user, token, options = {}, methodName = "workspacePendingInvitationsService") {
|
|
81
|
+
const resolvedInvite = await requirePendingInviteForUserByToken(user, token, options);
|
|
82
|
+
await revokeExpiredInviteAndThrow(resolvedInvite.invite, options);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
resolvedInvite,
|
|
86
|
+
workspaceId: requireWorkspaceIdFromInvite(resolvedInvite.invite, methodName)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function acceptInviteByToken({ user, token } = {}, options = {}) {
|
|
91
|
+
const { resolvedInvite, workspaceId } = await resolveInviteActionInput(
|
|
92
|
+
user,
|
|
93
|
+
token,
|
|
94
|
+
options,
|
|
95
|
+
"workspacePendingInvitationsService.acceptInviteByToken"
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
await workspaceMembershipsRepository.upsertMembership(
|
|
99
|
+
workspaceId,
|
|
100
|
+
resolvedInvite.user.id,
|
|
101
|
+
{
|
|
102
|
+
roleId: resolvedInvite.invite.roleId,
|
|
103
|
+
status: "active"
|
|
104
|
+
},
|
|
105
|
+
options
|
|
106
|
+
);
|
|
107
|
+
await workspaceInvitesRepository.markAcceptedById(resolvedInvite.invite.id, options);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
decision: "accepted",
|
|
111
|
+
workspaceId
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function refuseInviteByToken({ user, token } = {}, options = {}) {
|
|
116
|
+
const { resolvedInvite, workspaceId } = await resolveInviteActionInput(
|
|
117
|
+
user,
|
|
118
|
+
token,
|
|
119
|
+
options,
|
|
120
|
+
"workspacePendingInvitationsService.refuseInviteByToken"
|
|
121
|
+
);
|
|
122
|
+
await workspaceInvitesRepository.revokeById(resolvedInvite.invite.id, options);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
decision: "refused",
|
|
126
|
+
workspaceId
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Object.freeze({
|
|
131
|
+
listPendingInvitesForUser,
|
|
132
|
+
acceptInviteByToken,
|
|
133
|
+
refuseInviteByToken
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export { createService };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
3
|
+
import { workspaceSettingsResource } from "../../shared/resources/workspaceSettingsResource.js";
|
|
4
|
+
import { resolveWorkspaceRoutePath } from "../common/support/workspaceRoutePaths.js";
|
|
5
|
+
import { workspaceSlugParamsValidator } from "../common/validators/routeParamsValidator.js";
|
|
6
|
+
import { resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig } from "../support/workspaceActionSurfaces.js";
|
|
7
|
+
|
|
8
|
+
function bootWorkspaceSettings(app) {
|
|
9
|
+
if (!app || typeof app.make !== "function") {
|
|
10
|
+
throw new Error("bootWorkspaceSettings requires application make().");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
14
|
+
const appConfig = typeof app.has === "function" && app.has("appConfig") ? app.make("appConfig") : {};
|
|
15
|
+
const workspaceRouteSurfaceId = resolveDefaultWorkspaceRouteSurfaceIdFromAppConfig(appConfig);
|
|
16
|
+
|
|
17
|
+
router.register(
|
|
18
|
+
"GET",
|
|
19
|
+
resolveWorkspaceRoutePath("/settings"),
|
|
20
|
+
{
|
|
21
|
+
auth: "required",
|
|
22
|
+
surface: workspaceRouteSurfaceId,
|
|
23
|
+
visibility: "workspace",
|
|
24
|
+
meta: {
|
|
25
|
+
tags: ["workspace"],
|
|
26
|
+
summary: "Get workspace settings and role catalog by workspace slug"
|
|
27
|
+
},
|
|
28
|
+
paramsValidator: workspaceSlugParamsValidator,
|
|
29
|
+
responseValidators: withStandardErrorResponses({
|
|
30
|
+
200: workspaceSettingsResource.operations.view.outputValidator
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
async function (request, reply) {
|
|
34
|
+
const response = await request.executeAction({
|
|
35
|
+
actionId: "workspace.settings.read",
|
|
36
|
+
input: {
|
|
37
|
+
workspaceSlug: request.input.params.workspaceSlug
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
reply.code(200).send(response);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
router.register(
|
|
45
|
+
"PATCH",
|
|
46
|
+
resolveWorkspaceRoutePath("/settings"),
|
|
47
|
+
{
|
|
48
|
+
auth: "required",
|
|
49
|
+
surface: workspaceRouteSurfaceId,
|
|
50
|
+
visibility: "workspace",
|
|
51
|
+
meta: {
|
|
52
|
+
tags: ["workspace"],
|
|
53
|
+
summary: "Update workspace settings by workspace slug"
|
|
54
|
+
},
|
|
55
|
+
paramsValidator: workspaceSlugParamsValidator,
|
|
56
|
+
bodyValidator: workspaceSettingsResource.operations.patch.bodyValidator,
|
|
57
|
+
responseValidators: withStandardErrorResponses(
|
|
58
|
+
{
|
|
59
|
+
200: workspaceSettingsResource.operations.patch.outputValidator
|
|
60
|
+
},
|
|
61
|
+
{ includeValidation400: true }
|
|
62
|
+
)
|
|
63
|
+
},
|
|
64
|
+
async function (request, reply) {
|
|
65
|
+
const response = await request.executeAction({
|
|
66
|
+
actionId: "workspace.settings.update",
|
|
67
|
+
input: {
|
|
68
|
+
workspaceSlug: request.input.params.workspaceSlug,
|
|
69
|
+
patch: request.input.body
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
reply.code(200).send(response);
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { bootWorkspaceSettings };
|