@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
package/package.descriptor.mjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/users-core",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.33",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Users/workspace domain runtime plus HTTP routes for workspace, account, and console features.",
|
|
7
7
|
dependsOn: [
|
|
8
8
|
"@jskit-ai/auth-core",
|
|
9
9
|
"@jskit-ai/database-runtime",
|
|
10
10
|
"@jskit-ai/http-runtime",
|
|
11
|
+
"@jskit-ai/uploads-runtime",
|
|
11
12
|
"@jskit-ai/storage-runtime"
|
|
12
13
|
],
|
|
13
14
|
capabilities: {
|
|
@@ -19,6 +20,7 @@ export default Object.freeze({
|
|
|
19
20
|
"runtime.actions",
|
|
20
21
|
"runtime.database",
|
|
21
22
|
"runtime.storage",
|
|
23
|
+
"runtime.uploads",
|
|
22
24
|
"auth.provider",
|
|
23
25
|
"auth.policy"
|
|
24
26
|
]
|
|
@@ -93,7 +95,7 @@ export default Object.freeze({
|
|
|
93
95
|
{
|
|
94
96
|
method: "GET",
|
|
95
97
|
path: "/api/w/:workspaceSlug/roles",
|
|
96
|
-
summary: "Get
|
|
98
|
+
summary: "Get role catalog by workspace slug."
|
|
97
99
|
},
|
|
98
100
|
{
|
|
99
101
|
method: "GET",
|
|
@@ -196,11 +198,11 @@ export default Object.freeze({
|
|
|
196
198
|
mutations: {
|
|
197
199
|
dependencies: {
|
|
198
200
|
runtime: {
|
|
199
|
-
"@jskit-ai/auth-core": "0.1.
|
|
200
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
201
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
202
|
-
"@jskit-ai/kernel": "0.1.
|
|
203
|
-
"@
|
|
201
|
+
"@jskit-ai/auth-core": "0.1.23",
|
|
202
|
+
"@jskit-ai/database-runtime": "0.1.24",
|
|
203
|
+
"@jskit-ai/http-runtime": "0.1.23",
|
|
204
|
+
"@jskit-ai/kernel": "0.1.24",
|
|
205
|
+
"@jskit-ai/uploads-runtime": "0.1.2",
|
|
204
206
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
205
207
|
"typebox": "^1.0.81"
|
|
206
208
|
},
|
|
@@ -288,12 +290,12 @@ export default Object.freeze({
|
|
|
288
290
|
id: "users-core-app-owned-user-settings-fields"
|
|
289
291
|
},
|
|
290
292
|
{
|
|
291
|
-
from: "templates/config/
|
|
292
|
-
to: "config/
|
|
293
|
+
from: "templates/config/roles.js",
|
|
294
|
+
to: "config/roles.js",
|
|
293
295
|
preserveOnRemove: true,
|
|
294
|
-
reason: "Install app-owned
|
|
296
|
+
reason: "Install app-owned role catalog in a dedicated config file.",
|
|
295
297
|
category: "users-core",
|
|
296
|
-
id: "users-core-app-owned-
|
|
298
|
+
id: "users-core-app-owned-role-catalog-config"
|
|
297
299
|
}
|
|
298
300
|
],
|
|
299
301
|
text: [
|
|
@@ -360,11 +362,11 @@ export default Object.freeze({
|
|
|
360
362
|
op: "append-text",
|
|
361
363
|
file: "config/public.js",
|
|
362
364
|
position: "top",
|
|
363
|
-
skipIfContains: "import {
|
|
364
|
-
value: "import {
|
|
365
|
-
reason: "Load app-owned
|
|
365
|
+
skipIfContains: "import { roleCatalog } from \"./roles.js\";",
|
|
366
|
+
value: "import { roleCatalog } from \"./roles.js\";\n",
|
|
367
|
+
reason: "Load app-owned role catalog from dedicated config file.",
|
|
366
368
|
category: "users-core",
|
|
367
|
-
id: "users-core-
|
|
369
|
+
id: "users-core-role-catalog-public-import"
|
|
368
370
|
},
|
|
369
371
|
{
|
|
370
372
|
op: "append-text",
|
|
@@ -426,11 +428,11 @@ export default Object.freeze({
|
|
|
426
428
|
op: "append-text",
|
|
427
429
|
file: "config/public.js",
|
|
428
430
|
position: "bottom",
|
|
429
|
-
skipIfContains: "config.
|
|
430
|
-
value: "\nconfig.
|
|
431
|
-
reason: "Bind app-owned
|
|
431
|
+
skipIfContains: "config.roleCatalog = roleCatalog;",
|
|
432
|
+
value: "\nconfig.roleCatalog = roleCatalog;\n",
|
|
433
|
+
reason: "Bind app-owned role catalog onto public config.",
|
|
432
434
|
category: "users-core",
|
|
433
|
-
id: "users-core-
|
|
435
|
+
id: "users-core-role-catalog-public-config"
|
|
434
436
|
},
|
|
435
437
|
{
|
|
436
438
|
op: "append-text",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/users-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"./shared/resources/consoleSettingsFields": "./src/shared/resources/consoleSettingsFields.js"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@jskit-ai/auth-core": "0.1.
|
|
28
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
29
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
30
|
-
"@jskit-ai/kernel": "0.1.
|
|
31
|
-
"@
|
|
27
|
+
"@jskit-ai/auth-core": "0.1.23",
|
|
28
|
+
"@jskit-ai/database-runtime": "0.1.24",
|
|
29
|
+
"@jskit-ai/http-runtime": "0.1.23",
|
|
30
|
+
"@jskit-ai/kernel": "0.1.24",
|
|
31
|
+
"@jskit-ai/uploads-runtime": "0.1.2",
|
|
32
32
|
"@fastify/type-provider-typebox": "^6.1.0",
|
|
33
33
|
"typebox": "^1.0.81"
|
|
34
34
|
}
|
|
@@ -23,13 +23,12 @@ import { registerAccountNotifications } from "./accountNotifications/registerAcc
|
|
|
23
23
|
import { registerAccountProfile } from "./accountProfile/registerAccountProfile.js";
|
|
24
24
|
import { registerAccountSecurity } from "./accountSecurity/registerAccountSecurity.js";
|
|
25
25
|
import { registerConsoleSettings } from "./consoleSettings/registerConsoleSettings.js";
|
|
26
|
-
import { registerAvatarMultipartSupport } from "./accountProfile/registerAvatarMultipartSupport.js";
|
|
27
26
|
import { registerUsersCoreActionSurfaceSources } from "./support/workspaceActionSurfaces.js";
|
|
28
27
|
|
|
29
28
|
class UsersCoreServiceProvider {
|
|
30
29
|
static id = "users.core";
|
|
31
30
|
|
|
32
|
-
static dependsOn = ["runtime.server", "runtime.actions", "runtime.database", "runtime.storage", "auth.provider"];
|
|
31
|
+
static dependsOn = ["runtime.server", "runtime.actions", "runtime.database", "runtime.storage", "auth.provider", "runtime.uploads"];
|
|
33
32
|
|
|
34
33
|
register(app) {
|
|
35
34
|
registerUsersCoreActionSurfaceSources(app);
|
|
@@ -58,7 +57,6 @@ class UsersCoreServiceProvider {
|
|
|
58
57
|
bootWorkspaceSettings(app);
|
|
59
58
|
bootWorkspaceMembers(app);
|
|
60
59
|
}
|
|
61
|
-
await registerAvatarMultipartSupport(app);
|
|
62
60
|
bootAccountProfileRoutes(app);
|
|
63
61
|
bootAccountPreferencesRoutes(app);
|
|
64
62
|
bootAccountNotificationsRoutes(app);
|
|
@@ -9,15 +9,15 @@ import {
|
|
|
9
9
|
|
|
10
10
|
function createService({
|
|
11
11
|
userSettingsRepository,
|
|
12
|
-
|
|
12
|
+
usersRepository,
|
|
13
13
|
authService
|
|
14
14
|
} = {}) {
|
|
15
|
-
if (!userSettingsRepository || !
|
|
15
|
+
if (!userSettingsRepository || !usersRepository) {
|
|
16
16
|
throw new Error("accountNotificationsService requires repositories.");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
async function updateNotifications(request, user, payload = {}, options = {}) {
|
|
20
|
-
const profile = await resolveUserProfile(
|
|
20
|
+
const profile = await resolveUserProfile(usersRepository, user);
|
|
21
21
|
if (!profile) {
|
|
22
22
|
throw new AppError(404, "User profile was not found.");
|
|
23
23
|
}
|
|
@@ -14,7 +14,7 @@ function registerAccountNotifications(app) {
|
|
|
14
14
|
(scope) =>
|
|
15
15
|
createAccountNotificationsService({
|
|
16
16
|
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
17
|
-
|
|
17
|
+
usersRepository: scope.make("usersRepository"),
|
|
18
18
|
authService: scope.make("authService")
|
|
19
19
|
}),
|
|
20
20
|
{
|
|
@@ -9,15 +9,15 @@ import {
|
|
|
9
9
|
|
|
10
10
|
function createService({
|
|
11
11
|
userSettingsRepository,
|
|
12
|
-
|
|
12
|
+
usersRepository,
|
|
13
13
|
authService
|
|
14
14
|
} = {}) {
|
|
15
|
-
if (!userSettingsRepository || !
|
|
15
|
+
if (!userSettingsRepository || !usersRepository) {
|
|
16
16
|
throw new Error("accountPreferencesService requires repositories.");
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
async function updatePreferences(request, user, payload = {}, options = {}) {
|
|
20
|
-
const profile = await resolveUserProfile(
|
|
20
|
+
const profile = await resolveUserProfile(usersRepository, user);
|
|
21
21
|
if (!profile) {
|
|
22
22
|
throw new AppError(404, "User profile was not found.");
|
|
23
23
|
}
|
|
@@ -14,7 +14,7 @@ function registerAccountPreferences(app) {
|
|
|
14
14
|
(scope) =>
|
|
15
15
|
createAccountPreferencesService({
|
|
16
16
|
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
17
|
-
|
|
17
|
+
usersRepository: scope.make("usersRepository"),
|
|
18
18
|
authService: scope.make("authService")
|
|
19
19
|
}),
|
|
20
20
|
{
|
|
@@ -95,10 +95,17 @@ const accountProfileActions = Object.freeze([
|
|
|
95
95
|
},
|
|
96
96
|
observability: {},
|
|
97
97
|
async execute(input, context, deps) {
|
|
98
|
+
const avatarUpload = {
|
|
99
|
+
stream: input.stream,
|
|
100
|
+
mimeType: input.mimeType,
|
|
101
|
+
fileName: input.fileName,
|
|
102
|
+
uploadDimension: input.uploadDimension
|
|
103
|
+
};
|
|
104
|
+
|
|
98
105
|
return deps.accountProfileService.uploadAvatar(
|
|
99
106
|
resolveRequest(context),
|
|
100
107
|
resolveActionUser(context, input),
|
|
101
|
-
|
|
108
|
+
avatarUpload,
|
|
102
109
|
{
|
|
103
110
|
context
|
|
104
111
|
}
|
|
@@ -125,7 +132,6 @@ const accountProfileActions = Object.freeze([
|
|
|
125
132
|
return deps.accountProfileService.deleteAvatar(
|
|
126
133
|
resolveRequest(context),
|
|
127
134
|
resolveActionUser(context, input),
|
|
128
|
-
input,
|
|
129
135
|
{
|
|
130
136
|
context
|
|
131
137
|
}
|
|
@@ -9,16 +9,16 @@ import {
|
|
|
9
9
|
|
|
10
10
|
function createService({
|
|
11
11
|
userSettingsRepository,
|
|
12
|
-
|
|
12
|
+
usersRepository,
|
|
13
13
|
authService,
|
|
14
14
|
avatarService
|
|
15
15
|
} = {}) {
|
|
16
|
-
if (!userSettingsRepository || !
|
|
16
|
+
if (!userSettingsRepository || !usersRepository || !avatarService) {
|
|
17
17
|
throw new Error("accountProfileService requires repositories and avatarService.");
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
async function getForUser(request, user, options = {}) {
|
|
21
|
-
const profile = await resolveUserProfile(
|
|
21
|
+
const profile = await resolveUserProfile(usersRepository, user);
|
|
22
22
|
if (!profile) {
|
|
23
23
|
throw new AppError(404, "User profile was not found.");
|
|
24
24
|
}
|
|
@@ -35,7 +35,7 @@ function createService({
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
async function updateProfile(request, user, payload = {}, options = {}) {
|
|
38
|
-
const profile = await resolveUserProfile(
|
|
38
|
+
const profile = await resolveUserProfile(usersRepository, user);
|
|
39
39
|
if (!profile) {
|
|
40
40
|
throw new AppError(404, "User profile was not found.");
|
|
41
41
|
}
|
|
@@ -49,7 +49,7 @@ function createService({
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
if (!updatedProfile) {
|
|
52
|
-
updatedProfile = await
|
|
52
|
+
updatedProfile = await usersRepository.updateDisplayNameById(profile.id, payload.displayName);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
const settings = await userSettingsRepository.ensureForUserId(updatedProfile.id);
|
|
@@ -66,11 +66,11 @@ function createService({
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
async function uploadAvatar(request, user,
|
|
69
|
+
async function uploadAvatar(request, user, avatarUpload = {}, options = {}) {
|
|
70
70
|
void options;
|
|
71
71
|
|
|
72
|
-
const
|
|
73
|
-
const profile =
|
|
72
|
+
const result = await avatarService.uploadForUser(user, avatarUpload);
|
|
73
|
+
const profile = result?.profile || null;
|
|
74
74
|
if (!profile) {
|
|
75
75
|
throw new AppError(500, "Avatar upload completed without a profile result.");
|
|
76
76
|
}
|
|
@@ -86,7 +86,7 @@ function createService({
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
async function deleteAvatar(request, user,
|
|
89
|
+
async function deleteAvatar(request, user, options = {}) {
|
|
90
90
|
void options;
|
|
91
91
|
|
|
92
92
|
const profile = await avatarService.clearForUser(user);
|
|
@@ -101,7 +101,7 @@ function createService({
|
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
async function readAvatar(_request, user,
|
|
104
|
+
async function readAvatar(_request, user, options = {}) {
|
|
105
105
|
void options;
|
|
106
106
|
|
|
107
107
|
const avatar = await avatarService.readForUser(user);
|
|
@@ -1,68 +1,29 @@
|
|
|
1
|
-
import { AppError
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { DEFAULT_IMAGE_UPLOAD_POLICY } from "@jskit-ai/uploads-runtime/shared";
|
|
3
|
+
import {
|
|
4
|
+
normalizeUploadPolicy,
|
|
5
|
+
readUploadBuffer,
|
|
6
|
+
validateUploadMimeType
|
|
7
|
+
} from "@jskit-ai/uploads-runtime/server/policy/uploadPolicy";
|
|
2
8
|
import { resolveUserProfile } from "../common/services/accountContextService.js";
|
|
3
9
|
|
|
4
|
-
const DEFAULT_AVATAR_POLICY =
|
|
5
|
-
allowedMimeTypes: Object.freeze(["image/jpeg", "image/png", "image/webp"]),
|
|
6
|
-
maxUploadBytes: 5 * 1024 * 1024
|
|
7
|
-
});
|
|
10
|
+
const DEFAULT_AVATAR_POLICY = DEFAULT_IMAGE_UPLOAD_POLICY;
|
|
8
11
|
|
|
9
12
|
function resolveAvatarPolicy(policy = {}) {
|
|
10
|
-
|
|
11
|
-
const allowedMimeTypes =
|
|
12
|
-
Array.isArray(source.allowedMimeTypes) && source.allowedMimeTypes.length > 0
|
|
13
|
-
? source.allowedMimeTypes
|
|
14
|
-
.map((value) => String(value || "").trim().toLowerCase())
|
|
15
|
-
.filter((value) => value.length > 0)
|
|
16
|
-
: [...DEFAULT_AVATAR_POLICY.allowedMimeTypes];
|
|
17
|
-
const normalizedMaxUploadBytes = Number(source.maxUploadBytes);
|
|
18
|
-
const maxUploadBytes =
|
|
19
|
-
Number.isInteger(normalizedMaxUploadBytes) && normalizedMaxUploadBytes > 0
|
|
20
|
-
? normalizedMaxUploadBytes
|
|
21
|
-
: DEFAULT_AVATAR_POLICY.maxUploadBytes;
|
|
22
|
-
|
|
23
|
-
return Object.freeze({
|
|
24
|
-
allowedMimeTypes: Object.freeze(allowedMimeTypes),
|
|
25
|
-
maxUploadBytes
|
|
26
|
-
});
|
|
13
|
+
return normalizeUploadPolicy(policy, DEFAULT_AVATAR_POLICY);
|
|
27
14
|
}
|
|
28
15
|
|
|
29
16
|
async function readAvatarBuffer(stream, { maxBytes = DEFAULT_AVATAR_POLICY.maxUploadBytes } = {}) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let total = 0;
|
|
36
|
-
|
|
37
|
-
for await (const chunk of stream) {
|
|
38
|
-
const bufferChunk = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
39
|
-
total += bufferChunk.length;
|
|
40
|
-
|
|
41
|
-
if (total > maxBytes) {
|
|
42
|
-
throw createValidationError({
|
|
43
|
-
avatar: `Avatar file is too large. Maximum allowed size is ${Math.floor(maxBytes / (1024 * 1024))}MB.`
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
chunks.push(bufferChunk);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (chunks.length === 0) {
|
|
51
|
-
throw createValidationError({
|
|
52
|
-
avatar: "Avatar file is empty."
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return Buffer.concat(chunks);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function normalizeMimeType(value) {
|
|
60
|
-
return String(value || "").trim().toLowerCase();
|
|
17
|
+
return readUploadBuffer(stream, {
|
|
18
|
+
maxBytes,
|
|
19
|
+
fieldName: "avatar",
|
|
20
|
+
label: "Avatar"
|
|
21
|
+
});
|
|
61
22
|
}
|
|
62
23
|
|
|
63
|
-
function createService({
|
|
64
|
-
if (!
|
|
65
|
-
throw new TypeError("avatarService requires
|
|
24
|
+
function createService({ usersRepository, avatarStorageService, avatarPolicy } = {}) {
|
|
25
|
+
if (!usersRepository) {
|
|
26
|
+
throw new TypeError("avatarService requires usersRepository.");
|
|
66
27
|
}
|
|
67
28
|
if (!avatarStorageService) {
|
|
68
29
|
throw new TypeError("avatarService requires avatarStorageService.");
|
|
@@ -71,23 +32,21 @@ function createService({ userProfilesRepository, avatarStorageService, avatarPol
|
|
|
71
32
|
const resolvedAvatarPolicy = resolveAvatarPolicy(avatarPolicy);
|
|
72
33
|
|
|
73
34
|
async function resolveProfile(user) {
|
|
74
|
-
const profile = await resolveUserProfile(
|
|
35
|
+
const profile = await resolveUserProfile(usersRepository, user);
|
|
75
36
|
if (!profile) {
|
|
76
37
|
throw new AppError(404, "User profile was not found.");
|
|
77
38
|
}
|
|
78
39
|
return profile;
|
|
79
40
|
}
|
|
80
41
|
|
|
81
|
-
async function uploadForUser(user,
|
|
42
|
+
async function uploadForUser(user, avatarUpload = {}) {
|
|
82
43
|
const profile = await resolveProfile(user);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
88
|
-
}
|
|
44
|
+
validateUploadMimeType(avatarUpload?.mimeType, resolvedAvatarPolicy, {
|
|
45
|
+
fieldName: "avatar",
|
|
46
|
+
label: "Avatar"
|
|
47
|
+
});
|
|
89
48
|
|
|
90
|
-
const buffer = await readAvatarBuffer(
|
|
49
|
+
const buffer = await readAvatarBuffer(avatarUpload?.stream, {
|
|
91
50
|
maxBytes: resolvedAvatarPolicy.maxUploadBytes
|
|
92
51
|
});
|
|
93
52
|
|
|
@@ -98,7 +57,7 @@ function createService({ userProfilesRepository, avatarStorageService, avatarPol
|
|
|
98
57
|
buffer
|
|
99
58
|
});
|
|
100
59
|
|
|
101
|
-
const updatedProfile = await
|
|
60
|
+
const updatedProfile = await usersRepository.updateAvatarById(profile.id, {
|
|
102
61
|
avatarStorageKey: savedAvatar.storageKey,
|
|
103
62
|
avatarVersion,
|
|
104
63
|
avatarUpdatedAt: new Date(avatarVersionMs)
|
|
@@ -114,7 +73,7 @@ function createService({ userProfilesRepository, avatarStorageService, avatarPol
|
|
|
114
73
|
if (profile.avatarStorageKey) {
|
|
115
74
|
await avatarStorageService.deleteAvatar(profile.avatarStorageKey);
|
|
116
75
|
}
|
|
117
|
-
return
|
|
76
|
+
return usersRepository.clearAvatarById(profile.id);
|
|
118
77
|
}
|
|
119
78
|
|
|
120
79
|
async function readForUser(user) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { parsePositiveInteger } from "@jskit-ai/kernel/server/runtime";
|
|
2
|
+
import {
|
|
3
|
+
createUploadStorageService,
|
|
4
|
+
detectCommonMimeTypeFromBuffer
|
|
5
|
+
} from "@jskit-ai/uploads-runtime/server/storage/createUploadStorageService";
|
|
2
6
|
|
|
3
7
|
const AVATAR_STORAGE_PREFIX = "users/avatars";
|
|
4
|
-
const AVATAR_MIME_TYPE_JPEG = "image/jpeg";
|
|
5
|
-
const AVATAR_MIME_TYPE_PNG = "image/png";
|
|
6
|
-
const AVATAR_MIME_TYPE_WEBP = "image/webp";
|
|
7
|
-
const AVATAR_MIME_TYPE_FALLBACK = "application/octet-stream";
|
|
8
8
|
|
|
9
9
|
function buildAvatarStorageKey(userId) {
|
|
10
10
|
const normalizedUserId = parsePositiveInteger(userId);
|
|
@@ -15,106 +15,25 @@ function buildAvatarStorageKey(userId) {
|
|
|
15
15
|
return `${AVATAR_STORAGE_PREFIX}/${normalizedUserId}/avatar`;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function normalizeStorageKey(value) {
|
|
19
|
-
const normalized = String(value || "").trim();
|
|
20
|
-
if (!normalized) {
|
|
21
|
-
return "";
|
|
22
|
-
}
|
|
23
|
-
if (normalized.startsWith("/") || normalized.includes("..")) {
|
|
24
|
-
return "";
|
|
25
|
-
}
|
|
26
|
-
return normalized;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function detectAvatarMimeTypeFromBuffer(buffer) {
|
|
30
|
-
if (!Buffer.isBuffer(buffer) || buffer.length < 4) {
|
|
31
|
-
return AVATAR_MIME_TYPE_FALLBACK;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (
|
|
35
|
-
buffer.length >= 3 &&
|
|
36
|
-
buffer[0] === 0xff &&
|
|
37
|
-
buffer[1] === 0xd8 &&
|
|
38
|
-
buffer[2] === 0xff
|
|
39
|
-
) {
|
|
40
|
-
return AVATAR_MIME_TYPE_JPEG;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
buffer.length >= 8 &&
|
|
45
|
-
buffer[0] === 0x89 &&
|
|
46
|
-
buffer[1] === 0x50 &&
|
|
47
|
-
buffer[2] === 0x4e &&
|
|
48
|
-
buffer[3] === 0x47 &&
|
|
49
|
-
buffer[4] === 0x0d &&
|
|
50
|
-
buffer[5] === 0x0a &&
|
|
51
|
-
buffer[6] === 0x1a &&
|
|
52
|
-
buffer[7] === 0x0a
|
|
53
|
-
) {
|
|
54
|
-
return AVATAR_MIME_TYPE_PNG;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
buffer.length >= 12 &&
|
|
59
|
-
buffer[0] === 0x52 &&
|
|
60
|
-
buffer[1] === 0x49 &&
|
|
61
|
-
buffer[2] === 0x46 &&
|
|
62
|
-
buffer[3] === 0x46 &&
|
|
63
|
-
buffer[8] === 0x57 &&
|
|
64
|
-
buffer[9] === 0x45 &&
|
|
65
|
-
buffer[10] === 0x42 &&
|
|
66
|
-
buffer[11] === 0x50
|
|
67
|
-
) {
|
|
68
|
-
return AVATAR_MIME_TYPE_WEBP;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
return AVATAR_MIME_TYPE_FALLBACK;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
18
|
function createService({ storage } = {}) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
19
|
+
const uploadStorageService = createUploadStorageService({
|
|
20
|
+
storage,
|
|
21
|
+
mimeTypeDetector: detectCommonMimeTypeFromBuffer
|
|
22
|
+
});
|
|
78
23
|
|
|
79
24
|
async function saveAvatar({ userId, buffer }) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const storageKey = buildAvatarStorageKey(userId);
|
|
85
|
-
await storage.setItemRaw(storageKey, buffer);
|
|
86
|
-
|
|
87
|
-
return Object.freeze({
|
|
88
|
-
storageKey
|
|
25
|
+
return uploadStorageService.saveFile({
|
|
26
|
+
storageKey: buildAvatarStorageKey(userId),
|
|
27
|
+
buffer
|
|
89
28
|
});
|
|
90
29
|
}
|
|
91
30
|
|
|
92
31
|
async function readAvatar(storageKey) {
|
|
93
|
-
|
|
94
|
-
if (!normalizedStorageKey) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const value = await storage.getItemRaw(normalizedStorageKey);
|
|
99
|
-
if (value == null) {
|
|
100
|
-
return null;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const buffer = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
104
|
-
return Object.freeze({
|
|
105
|
-
storageKey: normalizedStorageKey,
|
|
106
|
-
buffer,
|
|
107
|
-
mimeType: detectAvatarMimeTypeFromBuffer(buffer)
|
|
108
|
-
});
|
|
32
|
+
return uploadStorageService.readFile(storageKey);
|
|
109
33
|
}
|
|
110
34
|
|
|
111
35
|
async function deleteAvatar(storageKey) {
|
|
112
|
-
|
|
113
|
-
if (!normalizedStorageKey || typeof storage.removeItem !== "function") {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
await storage.removeItem(normalizedStorageKey);
|
|
36
|
+
await uploadStorageService.deleteFile(storageKey);
|
|
118
37
|
}
|
|
119
38
|
|
|
120
39
|
return Object.freeze({
|
|
@@ -126,7 +45,7 @@ function createService({ storage } = {}) {
|
|
|
126
45
|
|
|
127
46
|
const __testables = Object.freeze({
|
|
128
47
|
buildAvatarStorageKey,
|
|
129
|
-
detectAvatarMimeTypeFromBuffer
|
|
48
|
+
detectAvatarMimeTypeFromBuffer: detectCommonMimeTypeFromBuffer
|
|
130
49
|
});
|
|
131
50
|
|
|
132
51
|
export { createService, __testables };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
1
|
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
+
import { DEFAULT_IMAGE_UPLOAD_MAX_BYTES } from "@jskit-ai/uploads-runtime/shared";
|
|
3
|
+
import { readSingleMultipartFile } from "@jskit-ai/uploads-runtime/server/multipart/readSingleMultipartFile";
|
|
3
4
|
import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
|
|
4
5
|
import { userProfileResource } from "../../shared/resources/userProfileResource.js";
|
|
5
6
|
|
|
@@ -77,7 +78,7 @@ function bootAccountProfileRoutes(app) {
|
|
|
77
78
|
}
|
|
78
79
|
},
|
|
79
80
|
async function (request, reply) {
|
|
80
|
-
const avatar = await accountProfileService.readAvatar(request, request.user, {
|
|
81
|
+
const avatar = await accountProfileService.readAvatar(request, request.user, {
|
|
81
82
|
context: {
|
|
82
83
|
actor: request.user
|
|
83
84
|
}
|
|
@@ -113,24 +114,21 @@ function bootAccountProfileRoutes(app) {
|
|
|
113
114
|
)
|
|
114
115
|
},
|
|
115
116
|
async function (request, reply) {
|
|
116
|
-
const filePart = await request
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
117
|
+
const filePart = await readSingleMultipartFile(request, {
|
|
118
|
+
fieldName: "avatar",
|
|
119
|
+
required: true,
|
|
120
|
+
fieldErrorKey: "avatar",
|
|
121
|
+
label: "Avatar",
|
|
122
|
+
maxBytes: DEFAULT_IMAGE_UPLOAD_MAX_BYTES
|
|
123
|
+
});
|
|
126
124
|
|
|
127
125
|
const uploadDimension = filePart.fields?.uploadDimension?.value;
|
|
128
126
|
const response = await request.executeAction({
|
|
129
127
|
actionId: "settings.profile.avatar.upload",
|
|
130
128
|
input: {
|
|
131
|
-
stream: filePart.
|
|
132
|
-
mimeType: filePart.
|
|
133
|
-
fileName: filePart.
|
|
129
|
+
stream: filePart.stream,
|
|
130
|
+
mimeType: filePart.mimeType,
|
|
131
|
+
fileName: filePart.fileName,
|
|
134
132
|
uploadDimension
|
|
135
133
|
}
|
|
136
134
|
});
|
|
@@ -20,7 +20,7 @@ function registerAccountProfile(app) {
|
|
|
20
20
|
|
|
21
21
|
app.singleton("users.avatar.service", (scope) =>
|
|
22
22
|
createAvatarService({
|
|
23
|
-
|
|
23
|
+
usersRepository: scope.make("usersRepository"),
|
|
24
24
|
avatarStorageService: scope.make("users.avatar.storage.service")
|
|
25
25
|
})
|
|
26
26
|
);
|
|
@@ -30,7 +30,7 @@ function registerAccountProfile(app) {
|
|
|
30
30
|
(scope) =>
|
|
31
31
|
createAccountProfileService({
|
|
32
32
|
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
33
|
-
|
|
33
|
+
usersRepository: scope.make("usersRepository"),
|
|
34
34
|
authService: scope.make("authService"),
|
|
35
35
|
avatarService: scope.make("users.avatar.service")
|
|
36
36
|
}),
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
|
|
11
11
|
function createService({
|
|
12
12
|
userSettingsRepository,
|
|
13
|
-
|
|
13
|
+
usersRepository,
|
|
14
14
|
authService
|
|
15
15
|
} = {}) {
|
|
16
|
-
if (!userSettingsRepository || !
|
|
16
|
+
if (!userSettingsRepository || !usersRepository) {
|
|
17
17
|
throw new Error("accountSecurityService requires repositories.");
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -40,7 +40,7 @@ function createService({
|
|
|
40
40
|
throw new AppError(501, "Password method toggle is not available.");
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const profile = await resolveUserProfile(
|
|
43
|
+
const profile = await resolveUserProfile(usersRepository, user);
|
|
44
44
|
if (!profile) {
|
|
45
45
|
throw new AppError(404, "User profile was not found.");
|
|
46
46
|
}
|