@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,132 @@
|
|
|
1
|
+
import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
|
|
2
|
+
|
|
3
|
+
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
|
+
|
|
9
|
+
function buildAvatarStorageKey(userId) {
|
|
10
|
+
const normalizedUserId = parsePositiveInteger(userId);
|
|
11
|
+
if (!normalizedUserId) {
|
|
12
|
+
throw new TypeError("Avatar storage requires a positive integer user id.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return `${AVATAR_STORAGE_PREFIX}/${normalizedUserId}/avatar`;
|
|
16
|
+
}
|
|
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
|
+
function createService({ storage } = {}) {
|
|
75
|
+
if (!storage || typeof storage.getItemRaw !== "function" || typeof storage.setItemRaw !== "function") {
|
|
76
|
+
throw new TypeError("avatarStorageService requires a storage binding with getItemRaw()/setItemRaw().");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function saveAvatar({ userId, buffer }) {
|
|
80
|
+
if (!Buffer.isBuffer(buffer)) {
|
|
81
|
+
throw new TypeError("Avatar buffer must be a Buffer instance.");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const storageKey = buildAvatarStorageKey(userId);
|
|
85
|
+
await storage.setItemRaw(storageKey, buffer);
|
|
86
|
+
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
storageKey
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readAvatar(storageKey) {
|
|
93
|
+
const normalizedStorageKey = normalizeStorageKey(storageKey);
|
|
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
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function deleteAvatar(storageKey) {
|
|
112
|
+
const normalizedStorageKey = normalizeStorageKey(storageKey);
|
|
113
|
+
if (!normalizedStorageKey || typeof storage.removeItem !== "function") {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await storage.removeItem(normalizedStorageKey);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
saveAvatar,
|
|
122
|
+
readAvatar,
|
|
123
|
+
deleteAvatar
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const __testables = Object.freeze({
|
|
128
|
+
buildAvatarStorageKey,
|
|
129
|
+
detectAvatarMimeTypeFromBuffer
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export { createService, __testables };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
3
|
+
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
4
|
+
import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
|
|
5
|
+
import { userProfileResource } from "../../shared/resources/userProfileResource.js";
|
|
6
|
+
import { USERS_ACCOUNT_PROFILE_SERVICE_TOKEN } from "./registerAccountProfile.js";
|
|
7
|
+
|
|
8
|
+
function bootAccountProfileRoutes(app) {
|
|
9
|
+
if (!app || typeof app.make !== "function") {
|
|
10
|
+
throw new Error("bootAccountProfileRoutes requires application make().");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
14
|
+
const authService = app.make("authService");
|
|
15
|
+
const accountProfileService = app.make(USERS_ACCOUNT_PROFILE_SERVICE_TOKEN);
|
|
16
|
+
|
|
17
|
+
router.register(
|
|
18
|
+
"GET",
|
|
19
|
+
"/api/settings",
|
|
20
|
+
{
|
|
21
|
+
auth: "required",
|
|
22
|
+
meta: {
|
|
23
|
+
tags: ["settings"],
|
|
24
|
+
summary: "Get authenticated user's settings"
|
|
25
|
+
},
|
|
26
|
+
responseValidators: withStandardErrorResponses({
|
|
27
|
+
200: userSettingsResource.operations.view.outputValidator
|
|
28
|
+
})
|
|
29
|
+
},
|
|
30
|
+
async function (request, reply) {
|
|
31
|
+
const response = await request.executeAction({
|
|
32
|
+
actionId: "settings.read"
|
|
33
|
+
});
|
|
34
|
+
reply.code(200).send(response);
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
router.register(
|
|
39
|
+
"PATCH",
|
|
40
|
+
"/api/settings/profile",
|
|
41
|
+
{
|
|
42
|
+
auth: "required",
|
|
43
|
+
meta: {
|
|
44
|
+
tags: ["settings"],
|
|
45
|
+
summary: "Update profile settings"
|
|
46
|
+
},
|
|
47
|
+
bodyValidator: userProfileResource.operations.patch.bodyValidator,
|
|
48
|
+
responseValidators: withStandardErrorResponses(
|
|
49
|
+
{
|
|
50
|
+
200: userSettingsResource.operations.view.outputValidator
|
|
51
|
+
},
|
|
52
|
+
{ includeValidation400: true }
|
|
53
|
+
)
|
|
54
|
+
},
|
|
55
|
+
async function (request, reply) {
|
|
56
|
+
const result = await request.executeAction({
|
|
57
|
+
actionId: "settings.profile.update",
|
|
58
|
+
input: {
|
|
59
|
+
payload: request.input.body
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (result?.session && typeof authService.writeSessionCookies === "function") {
|
|
64
|
+
authService.writeSessionCookies(reply, result.session);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
reply.code(200).send(result?.settings || result);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
router.register(
|
|
72
|
+
"GET",
|
|
73
|
+
"/api/settings/profile/avatar",
|
|
74
|
+
{
|
|
75
|
+
auth: "required",
|
|
76
|
+
meta: {
|
|
77
|
+
tags: ["settings"],
|
|
78
|
+
summary: "Read authenticated user's uploaded avatar."
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
async function (request, reply) {
|
|
82
|
+
const avatar = await accountProfileService.readAvatar(request, request.user, {}, {
|
|
83
|
+
context: {
|
|
84
|
+
actor: request.user
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
reply
|
|
89
|
+
.header("Cache-Control", "private, max-age=31536000, immutable")
|
|
90
|
+
.header("Content-Type", avatar.mimeType)
|
|
91
|
+
.send(avatar.buffer);
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
router.register(
|
|
96
|
+
"POST",
|
|
97
|
+
"/api/settings/profile/avatar",
|
|
98
|
+
{
|
|
99
|
+
auth: "required",
|
|
100
|
+
meta: {
|
|
101
|
+
tags: ["settings"],
|
|
102
|
+
summary: "Upload profile avatar",
|
|
103
|
+
description: "Multipart upload (avatar file required, optional uploadDimension field)."
|
|
104
|
+
},
|
|
105
|
+
advanced: {
|
|
106
|
+
fastifySchema: {
|
|
107
|
+
consumes: ["multipart/form-data"]
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
responseValidators: withStandardErrorResponses(
|
|
111
|
+
{
|
|
112
|
+
200: userProfileResource.operations.avatarUpload.outputValidator
|
|
113
|
+
},
|
|
114
|
+
{ includeValidation400: true }
|
|
115
|
+
)
|
|
116
|
+
},
|
|
117
|
+
async function (request, reply) {
|
|
118
|
+
const filePart = await request.file();
|
|
119
|
+
if (!filePart) {
|
|
120
|
+
throw new AppError(400, "Validation failed.", {
|
|
121
|
+
details: {
|
|
122
|
+
fieldErrors: {
|
|
123
|
+
avatar: "Avatar file is required."
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const uploadDimension = filePart.fields?.uploadDimension?.value;
|
|
130
|
+
const response = await request.executeAction({
|
|
131
|
+
actionId: "settings.profile.avatar.upload",
|
|
132
|
+
input: {
|
|
133
|
+
stream: filePart.file,
|
|
134
|
+
mimeType: filePart.mimetype,
|
|
135
|
+
fileName: filePart.filename,
|
|
136
|
+
uploadDimension
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
reply.code(200).send(response);
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
router.register(
|
|
145
|
+
"DELETE",
|
|
146
|
+
"/api/settings/profile/avatar",
|
|
147
|
+
{
|
|
148
|
+
auth: "required",
|
|
149
|
+
meta: {
|
|
150
|
+
tags: ["settings"],
|
|
151
|
+
summary: "Delete profile avatar and fallback to gravatar"
|
|
152
|
+
},
|
|
153
|
+
responseValidators: withStandardErrorResponses({
|
|
154
|
+
200: userProfileResource.operations.avatarDelete.outputValidator
|
|
155
|
+
})
|
|
156
|
+
},
|
|
157
|
+
async function (request, reply) {
|
|
158
|
+
const response = await request.executeAction({
|
|
159
|
+
actionId: "settings.profile.avatar.delete"
|
|
160
|
+
});
|
|
161
|
+
reply.code(200).send(response);
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export { bootAccountProfileRoutes };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
2
|
+
import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
|
|
3
|
+
import { createService as createAccountProfileService } from "./accountProfileService.js";
|
|
4
|
+
import { createService as createAvatarStorageService } from "./avatarStorageService.js";
|
|
5
|
+
import { createService as createAvatarService } from "./avatarService.js";
|
|
6
|
+
import { accountProfileActions } from "./accountProfileActions.js";
|
|
7
|
+
import { deepFreeze } from "../common/support/deepFreeze.js";
|
|
8
|
+
import {
|
|
9
|
+
USERS_AVATAR_STORAGE_SERVICE_TOKEN,
|
|
10
|
+
USERS_AVATAR_SERVICE_TOKEN
|
|
11
|
+
} from "../common/diTokens.js";
|
|
12
|
+
import { ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS } from "../common/support/realtimeServiceEvents.js";
|
|
13
|
+
|
|
14
|
+
const USERS_ACCOUNT_PROFILE_SERVICE_TOKEN = "users.accountProfile.service";
|
|
15
|
+
|
|
16
|
+
function registerAccountProfile(app) {
|
|
17
|
+
if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
|
|
18
|
+
throw new Error("registerAccountProfile requires application singleton()/service()/actions().");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
app.singleton(USERS_AVATAR_STORAGE_SERVICE_TOKEN, (scope) =>
|
|
22
|
+
createAvatarStorageService({
|
|
23
|
+
storage: scope.make(KERNEL_TOKENS.Storage)
|
|
24
|
+
})
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
app.singleton(USERS_AVATAR_SERVICE_TOKEN, (scope) =>
|
|
28
|
+
createAvatarService({
|
|
29
|
+
userProfilesRepository: scope.make("userProfilesRepository"),
|
|
30
|
+
avatarStorageService: scope.make(USERS_AVATAR_STORAGE_SERVICE_TOKEN)
|
|
31
|
+
})
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
app.service(
|
|
35
|
+
USERS_ACCOUNT_PROFILE_SERVICE_TOKEN,
|
|
36
|
+
(scope) =>
|
|
37
|
+
createAccountProfileService({
|
|
38
|
+
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
39
|
+
userProfilesRepository: scope.make("userProfilesRepository"),
|
|
40
|
+
authService: scope.make("authService"),
|
|
41
|
+
avatarService: scope.make(USERS_AVATAR_SERVICE_TOKEN)
|
|
42
|
+
}),
|
|
43
|
+
{
|
|
44
|
+
events: deepFreeze({
|
|
45
|
+
updateProfile: ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS,
|
|
46
|
+
uploadAvatar: ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS,
|
|
47
|
+
deleteAvatar: ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
app.actions(
|
|
53
|
+
withActionDefaults(accountProfileActions, {
|
|
54
|
+
domain: "settings",
|
|
55
|
+
dependencies: {
|
|
56
|
+
accountProfileService: USERS_ACCOUNT_PROFILE_SERVICE_TOKEN
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { USERS_ACCOUNT_PROFILE_SERVICE_TOKEN, registerAccountProfile };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fastifyMultipart from "@fastify/multipart";
|
|
2
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
3
|
+
|
|
4
|
+
const AVATAR_MULTIPART_SUPPORT_FLAG = Symbol.for("jskit.users-core.avatar.multipart.support");
|
|
5
|
+
|
|
6
|
+
async function registerAvatarMultipartSupport(app) {
|
|
7
|
+
if (!app || typeof app.has !== "function" || typeof app.make !== "function") {
|
|
8
|
+
throw new Error("registerAvatarMultipartSupport requires application has()/make().");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!app.has(KERNEL_TOKENS.Fastify)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const fastify = app.make(KERNEL_TOKENS.Fastify);
|
|
16
|
+
if (!fastify || typeof fastify.register !== "function") {
|
|
17
|
+
throw new Error("registerAvatarMultipartSupport requires Fastify register().");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (fastify[AVATAR_MULTIPART_SUPPORT_FLAG] === true) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof fastify.hasContentTypeParser === "function" && fastify.hasContentTypeParser("multipart")) {
|
|
25
|
+
Object.defineProperty(fastify, AVATAR_MULTIPART_SUPPORT_FLAG, {
|
|
26
|
+
value: true,
|
|
27
|
+
configurable: false,
|
|
28
|
+
enumerable: false,
|
|
29
|
+
writable: false
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
await fastify.register(fastifyMultipart);
|
|
35
|
+
Object.defineProperty(fastify, AVATAR_MULTIPART_SUPPORT_FLAG, {
|
|
36
|
+
value: true,
|
|
37
|
+
configurable: false,
|
|
38
|
+
enumerable: false,
|
|
39
|
+
writable: false
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export { registerAvatarMultipartSupport };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveRequest
|
|
3
|
+
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
4
|
+
import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
|
|
5
|
+
import { resolveActionUser } from "../common/support/resolveActionUser.js";
|
|
6
|
+
|
|
7
|
+
const accountSecurityActions = Object.freeze([
|
|
8
|
+
{
|
|
9
|
+
id: "settings.security.password.change",
|
|
10
|
+
version: 1,
|
|
11
|
+
kind: "command",
|
|
12
|
+
channels: ["api", "automation", "internal"],
|
|
13
|
+
surfacesFrom: "enabled",
|
|
14
|
+
permission: {
|
|
15
|
+
require: "authenticated"
|
|
16
|
+
},
|
|
17
|
+
inputValidator: {
|
|
18
|
+
payload: userSettingsResource.operations.passwordChange.bodyValidator
|
|
19
|
+
},
|
|
20
|
+
outputValidator: userSettingsResource.operations.passwordChange.outputValidator,
|
|
21
|
+
idempotency: "none",
|
|
22
|
+
audit: {
|
|
23
|
+
actionName: "settings.security.password.change"
|
|
24
|
+
},
|
|
25
|
+
observability: {},
|
|
26
|
+
async execute(input, context, deps) {
|
|
27
|
+
return deps.accountSecurityService.changePassword(
|
|
28
|
+
resolveRequest(context),
|
|
29
|
+
resolveActionUser(context, input),
|
|
30
|
+
input.payload,
|
|
31
|
+
{
|
|
32
|
+
context
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "settings.security.password_method.toggle",
|
|
39
|
+
version: 1,
|
|
40
|
+
kind: "command",
|
|
41
|
+
channels: ["api", "automation", "internal"],
|
|
42
|
+
surfacesFrom: "enabled",
|
|
43
|
+
permission: {
|
|
44
|
+
require: "authenticated"
|
|
45
|
+
},
|
|
46
|
+
inputValidator: {
|
|
47
|
+
payload: userSettingsResource.operations.passwordMethodToggle.bodyValidator
|
|
48
|
+
},
|
|
49
|
+
outputValidator: userSettingsResource.operations.passwordMethodToggle.outputValidator,
|
|
50
|
+
idempotency: "none",
|
|
51
|
+
audit: {
|
|
52
|
+
actionName: "settings.security.password_method.toggle"
|
|
53
|
+
},
|
|
54
|
+
observability: {},
|
|
55
|
+
async execute(input, context, deps) {
|
|
56
|
+
return deps.accountSecurityService.setPasswordMethodEnabled(
|
|
57
|
+
resolveRequest(context),
|
|
58
|
+
resolveActionUser(context, input),
|
|
59
|
+
input.payload,
|
|
60
|
+
{
|
|
61
|
+
context
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "settings.security.oauth.link.start",
|
|
68
|
+
version: 1,
|
|
69
|
+
kind: "query",
|
|
70
|
+
channels: ["api", "automation", "internal"],
|
|
71
|
+
surfacesFrom: "enabled",
|
|
72
|
+
permission: {
|
|
73
|
+
require: "authenticated"
|
|
74
|
+
},
|
|
75
|
+
inputValidator: [userSettingsResource.operations.oauthLinkStart.paramsValidator, userSettingsResource.operations.oauthLinkStart.queryValidator],
|
|
76
|
+
outputValidator: userSettingsResource.operations.oauthLinkStart.outputValidator,
|
|
77
|
+
idempotency: "none",
|
|
78
|
+
audit: {
|
|
79
|
+
actionName: "settings.security.oauth.link.start"
|
|
80
|
+
},
|
|
81
|
+
observability: {},
|
|
82
|
+
async execute(input, context, deps) {
|
|
83
|
+
return deps.accountSecurityService.startOAuthProviderLink(
|
|
84
|
+
resolveRequest(context),
|
|
85
|
+
resolveActionUser(context, input),
|
|
86
|
+
input,
|
|
87
|
+
{
|
|
88
|
+
context
|
|
89
|
+
}
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "settings.security.oauth.unlink",
|
|
95
|
+
version: 1,
|
|
96
|
+
kind: "command",
|
|
97
|
+
channels: ["api", "automation", "internal"],
|
|
98
|
+
surfacesFrom: "enabled",
|
|
99
|
+
permission: {
|
|
100
|
+
require: "authenticated"
|
|
101
|
+
},
|
|
102
|
+
inputValidator: userSettingsResource.operations.oauthUnlink.paramsValidator,
|
|
103
|
+
outputValidator: userSettingsResource.operations.oauthUnlink.outputValidator,
|
|
104
|
+
idempotency: "none",
|
|
105
|
+
audit: {
|
|
106
|
+
actionName: "settings.security.oauth.unlink"
|
|
107
|
+
},
|
|
108
|
+
observability: {},
|
|
109
|
+
async execute(input, context, deps) {
|
|
110
|
+
return deps.accountSecurityService.unlinkOAuthProvider(
|
|
111
|
+
resolveRequest(context),
|
|
112
|
+
resolveActionUser(context, input),
|
|
113
|
+
input,
|
|
114
|
+
{
|
|
115
|
+
context
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "settings.security.sessions.logout_others",
|
|
122
|
+
version: 1,
|
|
123
|
+
kind: "command",
|
|
124
|
+
channels: ["api", "automation", "internal"],
|
|
125
|
+
surfacesFrom: "enabled",
|
|
126
|
+
permission: {
|
|
127
|
+
require: "authenticated"
|
|
128
|
+
},
|
|
129
|
+
inputValidator: userSettingsResource.operations.logoutOtherSessions.bodyValidator,
|
|
130
|
+
outputValidator: userSettingsResource.operations.logoutOtherSessions.outputValidator,
|
|
131
|
+
idempotency: "none",
|
|
132
|
+
audit: {
|
|
133
|
+
actionName: "settings.security.sessions.logout_others"
|
|
134
|
+
},
|
|
135
|
+
observability: {},
|
|
136
|
+
async execute(input, context, deps) {
|
|
137
|
+
return deps.accountSecurityService.logoutOtherSessions(resolveRequest(context), resolveActionUser(context, input), {
|
|
138
|
+
context
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
export { accountSecurityActions };
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { createValidationError } from "@jskit-ai/kernel/server/runtime";
|
|
3
|
+
import {
|
|
4
|
+
resolveUserProfile,
|
|
5
|
+
resolveSecurityStatus
|
|
6
|
+
} from "../common/services/accountContextService.js";
|
|
7
|
+
import {
|
|
8
|
+
accountSettingsResponseFormatter
|
|
9
|
+
} from "../common/formatters/accountSettingsResponseFormatter.js";
|
|
10
|
+
|
|
11
|
+
function createService({
|
|
12
|
+
userSettingsRepository,
|
|
13
|
+
userProfilesRepository,
|
|
14
|
+
authService
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (!userSettingsRepository || !userProfilesRepository) {
|
|
17
|
+
throw new Error("accountSecurityService requires repositories.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function changePassword(request, user, payload = {}, options = {}) {
|
|
21
|
+
if (!authService || typeof authService.changePassword !== "function") {
|
|
22
|
+
throw new AppError(501, "Password change is not available.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (payload.confirmPassword !== payload.newPassword) {
|
|
26
|
+
throw createValidationError({
|
|
27
|
+
confirmPassword: "Password confirmation does not match."
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return authService.changePassword(request, {
|
|
32
|
+
currentPassword: payload.currentPassword,
|
|
33
|
+
newPassword: payload.newPassword,
|
|
34
|
+
confirmPassword: payload.confirmPassword
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function setPasswordMethodEnabled(request, user, payload = {}, options = {}) {
|
|
39
|
+
if (!authService || typeof authService.setPasswordSignInEnabled !== "function") {
|
|
40
|
+
throw new AppError(501, "Password method toggle is not available.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const profile = await resolveUserProfile(userProfilesRepository, user);
|
|
44
|
+
if (!profile) {
|
|
45
|
+
throw new AppError(404, "User profile was not found.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const enabled = payload.enabled === true;
|
|
49
|
+
const response = await authService.setPasswordSignInEnabled(request, { enabled });
|
|
50
|
+
await userSettingsRepository.updatePasswordSignInEnabled(profile.id, enabled);
|
|
51
|
+
const settings = await userSettingsRepository.ensureForUserId(profile.id);
|
|
52
|
+
const securityStatus = await resolveSecurityStatus(authService, request);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
...(response && typeof response === "object" ? response : {}),
|
|
56
|
+
settings: accountSettingsResponseFormatter({
|
|
57
|
+
profile,
|
|
58
|
+
settings,
|
|
59
|
+
securityStatus,
|
|
60
|
+
authService
|
|
61
|
+
})
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function startOAuthProviderLink(request, user, payload = {}, options = {}) {
|
|
66
|
+
if (!authService || typeof authService.startProviderLink !== "function") {
|
|
67
|
+
throw new AppError(501, "OAuth linking is not available.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return authService.startProviderLink(request, {
|
|
71
|
+
provider: payload.provider,
|
|
72
|
+
returnTo: payload.returnTo
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function unlinkOAuthProvider(request, user, payload = {}, options = {}) {
|
|
77
|
+
if (!authService || typeof authService.unlinkProvider !== "function") {
|
|
78
|
+
throw new AppError(501, "OAuth unlink is not available.");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return authService.unlinkProvider(request, {
|
|
82
|
+
provider: payload.provider
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function logoutOtherSessions(request, _user, options = {}) {
|
|
87
|
+
if (!authService || typeof authService.signOutOtherSessions !== "function") {
|
|
88
|
+
throw new AppError(501, "Logout other sessions is not available.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return authService.signOutOtherSessions(request);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
changePassword,
|
|
96
|
+
setPasswordMethodEnabled,
|
|
97
|
+
startOAuthProviderLink,
|
|
98
|
+
unlinkOAuthProvider,
|
|
99
|
+
logoutOtherSessions
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { createService };
|