@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,41 @@
|
|
|
1
|
+
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
3
|
+
import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
|
|
4
|
+
|
|
5
|
+
function bootAccountNotificationsRoutes(app) {
|
|
6
|
+
if (!app || typeof app.make !== "function") {
|
|
7
|
+
throw new Error("bootAccountNotificationsRoutes requires application make().");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
11
|
+
|
|
12
|
+
router.register(
|
|
13
|
+
"PATCH",
|
|
14
|
+
"/api/settings/notifications",
|
|
15
|
+
{
|
|
16
|
+
auth: "required",
|
|
17
|
+
meta: {
|
|
18
|
+
tags: ["settings"],
|
|
19
|
+
summary: "Update notification settings"
|
|
20
|
+
},
|
|
21
|
+
bodyValidator: userSettingsResource.operations.notificationsUpdate.bodyValidator,
|
|
22
|
+
responseValidators: withStandardErrorResponses(
|
|
23
|
+
{
|
|
24
|
+
200: userSettingsResource.operations.view.outputValidator
|
|
25
|
+
},
|
|
26
|
+
{ includeValidation400: true }
|
|
27
|
+
)
|
|
28
|
+
},
|
|
29
|
+
async function (request, reply) {
|
|
30
|
+
const response = await request.executeAction({
|
|
31
|
+
actionId: "settings.notifications.update",
|
|
32
|
+
input: {
|
|
33
|
+
payload: request.input.body
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
reply.code(200).send(response);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { bootAccountNotificationsRoutes };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
|
|
2
|
+
import { createService as createAccountNotificationsService } from "./accountNotificationsService.js";
|
|
3
|
+
import { accountNotificationsActions } from "./accountNotificationsActions.js";
|
|
4
|
+
import { deepFreeze } from "../common/support/deepFreeze.js";
|
|
5
|
+
import { ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS } from "../common/support/realtimeServiceEvents.js";
|
|
6
|
+
|
|
7
|
+
const USERS_ACCOUNT_NOTIFICATIONS_SERVICE_TOKEN = "users.accountNotifications.service";
|
|
8
|
+
|
|
9
|
+
function registerAccountNotifications(app) {
|
|
10
|
+
if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
|
|
11
|
+
throw new Error("registerAccountNotifications requires application singleton()/service()/actions().");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
app.service(
|
|
15
|
+
USERS_ACCOUNT_NOTIFICATIONS_SERVICE_TOKEN,
|
|
16
|
+
(scope) =>
|
|
17
|
+
createAccountNotificationsService({
|
|
18
|
+
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
19
|
+
userProfilesRepository: scope.make("userProfilesRepository"),
|
|
20
|
+
authService: scope.make("authService")
|
|
21
|
+
}),
|
|
22
|
+
{
|
|
23
|
+
events: deepFreeze({
|
|
24
|
+
updateNotifications: ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
app.actions(
|
|
30
|
+
withActionDefaults(accountNotificationsActions, {
|
|
31
|
+
domain: "settings",
|
|
32
|
+
dependencies: {
|
|
33
|
+
accountNotificationsService: USERS_ACCOUNT_NOTIFICATIONS_SERVICE_TOKEN
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { registerAccountNotifications };
|
|
@@ -0,0 +1,39 @@
|
|
|
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 accountPreferencesActions = Object.freeze([
|
|
8
|
+
{
|
|
9
|
+
id: "settings.preferences.update",
|
|
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.preferencesUpdate.bodyValidator
|
|
19
|
+
},
|
|
20
|
+
outputValidator: userSettingsResource.operations.view.outputValidator,
|
|
21
|
+
idempotency: "optional",
|
|
22
|
+
audit: {
|
|
23
|
+
actionName: "settings.preferences.update"
|
|
24
|
+
},
|
|
25
|
+
observability: {},
|
|
26
|
+
async execute(input, context, deps) {
|
|
27
|
+
return deps.accountPreferencesService.updatePreferences(
|
|
28
|
+
resolveRequest(context),
|
|
29
|
+
resolveActionUser(context, input),
|
|
30
|
+
input.payload,
|
|
31
|
+
{
|
|
32
|
+
context
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
export { accountPreferencesActions };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import {
|
|
3
|
+
resolveUserProfile,
|
|
4
|
+
resolveSecurityStatus
|
|
5
|
+
} from "../common/services/accountContextService.js";
|
|
6
|
+
import {
|
|
7
|
+
accountSettingsResponseFormatter
|
|
8
|
+
} from "../common/formatters/accountSettingsResponseFormatter.js";
|
|
9
|
+
|
|
10
|
+
function createService({
|
|
11
|
+
userSettingsRepository,
|
|
12
|
+
userProfilesRepository,
|
|
13
|
+
authService
|
|
14
|
+
} = {}) {
|
|
15
|
+
if (!userSettingsRepository || !userProfilesRepository) {
|
|
16
|
+
throw new Error("accountPreferencesService requires repositories.");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function updatePreferences(request, user, payload = {}, options = {}) {
|
|
20
|
+
const profile = await resolveUserProfile(userProfilesRepository, user);
|
|
21
|
+
if (!profile) {
|
|
22
|
+
throw new AppError(404, "User profile was not found.");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const settings = await userSettingsRepository.updatePreferences(profile.id, payload);
|
|
26
|
+
const securityStatus = await resolveSecurityStatus(authService, request);
|
|
27
|
+
|
|
28
|
+
return accountSettingsResponseFormatter({
|
|
29
|
+
profile,
|
|
30
|
+
settings,
|
|
31
|
+
securityStatus,
|
|
32
|
+
authService
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return Object.freeze({
|
|
37
|
+
updatePreferences
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { createService };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
|
|
2
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
3
|
+
import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
|
|
4
|
+
|
|
5
|
+
function bootAccountPreferencesRoutes(app) {
|
|
6
|
+
if (!app || typeof app.make !== "function") {
|
|
7
|
+
throw new Error("bootAccountPreferencesRoutes requires application make().");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const router = app.make(KERNEL_TOKENS.HttpRouter);
|
|
11
|
+
|
|
12
|
+
router.register(
|
|
13
|
+
"PATCH",
|
|
14
|
+
"/api/settings/preferences",
|
|
15
|
+
{
|
|
16
|
+
auth: "required",
|
|
17
|
+
meta: {
|
|
18
|
+
tags: ["settings"],
|
|
19
|
+
summary: "Update user preferences"
|
|
20
|
+
},
|
|
21
|
+
bodyValidator: userSettingsResource.operations.preferencesUpdate.bodyValidator,
|
|
22
|
+
responseValidators: withStandardErrorResponses(
|
|
23
|
+
{
|
|
24
|
+
200: userSettingsResource.operations.view.outputValidator
|
|
25
|
+
},
|
|
26
|
+
{ includeValidation400: true }
|
|
27
|
+
)
|
|
28
|
+
},
|
|
29
|
+
async function (request, reply) {
|
|
30
|
+
const response = await request.executeAction({
|
|
31
|
+
actionId: "settings.preferences.update",
|
|
32
|
+
input: {
|
|
33
|
+
payload: request.input.body
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
reply.code(200).send(response);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export { bootAccountPreferencesRoutes };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
|
|
2
|
+
import { createService as createAccountPreferencesService } from "./accountPreferencesService.js";
|
|
3
|
+
import { accountPreferencesActions } from "./accountPreferencesActions.js";
|
|
4
|
+
import { deepFreeze } from "../common/support/deepFreeze.js";
|
|
5
|
+
import { ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS } from "../common/support/realtimeServiceEvents.js";
|
|
6
|
+
|
|
7
|
+
const USERS_ACCOUNT_PREFERENCES_SERVICE_TOKEN = "users.accountPreferences.service";
|
|
8
|
+
|
|
9
|
+
function registerAccountPreferences(app) {
|
|
10
|
+
if (!app || typeof app.singleton !== "function" || typeof app.service !== "function" || typeof app.actions !== "function") {
|
|
11
|
+
throw new Error("registerAccountPreferences requires application singleton()/service()/actions().");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
app.service(
|
|
15
|
+
USERS_ACCOUNT_PREFERENCES_SERVICE_TOKEN,
|
|
16
|
+
(scope) =>
|
|
17
|
+
createAccountPreferencesService({
|
|
18
|
+
userSettingsRepository: scope.make("userSettingsRepository"),
|
|
19
|
+
userProfilesRepository: scope.make("userProfilesRepository"),
|
|
20
|
+
authService: scope.make("authService")
|
|
21
|
+
}),
|
|
22
|
+
{
|
|
23
|
+
events: deepFreeze({
|
|
24
|
+
updatePreferences: ACCOUNT_SETTINGS_AND_BOOTSTRAP_EVENTS
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
app.actions(
|
|
30
|
+
withActionDefaults(accountPreferencesActions, {
|
|
31
|
+
domain: "settings",
|
|
32
|
+
dependencies: {
|
|
33
|
+
accountPreferencesService: USERS_ACCOUNT_PREFERENCES_SERVICE_TOKEN
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { registerAccountPreferences };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_INPUT_VALIDATOR,
|
|
3
|
+
resolveRequest
|
|
4
|
+
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import { normalizeObjectInput } from "@jskit-ai/kernel/shared/validators/inputNormalization";
|
|
7
|
+
import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
|
|
8
|
+
import { userProfileResource } from "../../shared/resources/userProfileResource.js";
|
|
9
|
+
import { resolveActionUser } from "../common/support/resolveActionUser.js";
|
|
10
|
+
|
|
11
|
+
const settingsProfileUpdateOutputValidator = Object.freeze({
|
|
12
|
+
schema: Type.Object(
|
|
13
|
+
{
|
|
14
|
+
settings: userSettingsResource.operations.view.outputValidator.schema,
|
|
15
|
+
session: Type.Union([Type.Object({}, { additionalProperties: true }), Type.Null()])
|
|
16
|
+
},
|
|
17
|
+
{ additionalProperties: false }
|
|
18
|
+
),
|
|
19
|
+
normalize(payload = {}) {
|
|
20
|
+
const source = normalizeObjectInput(payload);
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
settings: userSettingsResource.operations.view.outputValidator.normalize(source.settings),
|
|
24
|
+
session: source.session && typeof source.session === "object" ? source.session : null
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const accountProfileActions = Object.freeze([
|
|
30
|
+
{
|
|
31
|
+
id: "settings.read",
|
|
32
|
+
version: 1,
|
|
33
|
+
kind: "query",
|
|
34
|
+
channels: ["api", "automation", "internal"],
|
|
35
|
+
surfacesFrom: "enabled",
|
|
36
|
+
permission: {
|
|
37
|
+
require: "authenticated"
|
|
38
|
+
},
|
|
39
|
+
inputValidator: EMPTY_INPUT_VALIDATOR,
|
|
40
|
+
outputValidator: userSettingsResource.operations.view.outputValidator,
|
|
41
|
+
idempotency: "none",
|
|
42
|
+
audit: {
|
|
43
|
+
actionName: "settings.read"
|
|
44
|
+
},
|
|
45
|
+
observability: {},
|
|
46
|
+
async execute(input, context, deps) {
|
|
47
|
+
return deps.accountProfileService.getForUser(resolveRequest(context), resolveActionUser(context, input), {
|
|
48
|
+
context
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "settings.profile.update",
|
|
54
|
+
version: 1,
|
|
55
|
+
kind: "command",
|
|
56
|
+
channels: ["api", "automation", "internal"],
|
|
57
|
+
surfacesFrom: "enabled",
|
|
58
|
+
permission: {
|
|
59
|
+
require: "authenticated"
|
|
60
|
+
},
|
|
61
|
+
inputValidator: {
|
|
62
|
+
payload: userProfileResource.operations.patch.bodyValidator
|
|
63
|
+
},
|
|
64
|
+
outputValidator: settingsProfileUpdateOutputValidator,
|
|
65
|
+
idempotency: "optional",
|
|
66
|
+
audit: {
|
|
67
|
+
actionName: "settings.profile.update"
|
|
68
|
+
},
|
|
69
|
+
observability: {},
|
|
70
|
+
async execute(input, context, deps) {
|
|
71
|
+
return deps.accountProfileService.updateProfile(
|
|
72
|
+
resolveRequest(context),
|
|
73
|
+
resolveActionUser(context, input),
|
|
74
|
+
input.payload,
|
|
75
|
+
{
|
|
76
|
+
context
|
|
77
|
+
}
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
id: "settings.profile.avatar.upload",
|
|
83
|
+
version: 1,
|
|
84
|
+
kind: "command",
|
|
85
|
+
channels: ["api", "automation", "internal"],
|
|
86
|
+
surfacesFrom: "enabled",
|
|
87
|
+
permission: {
|
|
88
|
+
require: "authenticated"
|
|
89
|
+
},
|
|
90
|
+
inputValidator: userProfileResource.operations.avatarUpload.bodyValidator,
|
|
91
|
+
outputValidator: userProfileResource.operations.avatarUpload.outputValidator,
|
|
92
|
+
idempotency: "none",
|
|
93
|
+
audit: {
|
|
94
|
+
actionName: "settings.profile.avatar.upload"
|
|
95
|
+
},
|
|
96
|
+
observability: {},
|
|
97
|
+
async execute(input, context, deps) {
|
|
98
|
+
return deps.accountProfileService.uploadAvatar(
|
|
99
|
+
resolveRequest(context),
|
|
100
|
+
resolveActionUser(context, input),
|
|
101
|
+
input,
|
|
102
|
+
{
|
|
103
|
+
context
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: "settings.profile.avatar.delete",
|
|
110
|
+
version: 1,
|
|
111
|
+
kind: "command",
|
|
112
|
+
channels: ["api", "automation", "internal"],
|
|
113
|
+
surfacesFrom: "enabled",
|
|
114
|
+
permission: {
|
|
115
|
+
require: "authenticated"
|
|
116
|
+
},
|
|
117
|
+
inputValidator: userProfileResource.operations.avatarDelete.bodyValidator,
|
|
118
|
+
outputValidator: userProfileResource.operations.avatarDelete.outputValidator,
|
|
119
|
+
idempotency: "none",
|
|
120
|
+
audit: {
|
|
121
|
+
actionName: "settings.profile.avatar.delete"
|
|
122
|
+
},
|
|
123
|
+
observability: {},
|
|
124
|
+
async execute(input, context, deps) {
|
|
125
|
+
return deps.accountProfileService.deleteAvatar(
|
|
126
|
+
resolveRequest(context),
|
|
127
|
+
resolveActionUser(context, input),
|
|
128
|
+
input,
|
|
129
|
+
{
|
|
130
|
+
context
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
export { accountProfileActions };
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import {
|
|
3
|
+
resolveUserProfile,
|
|
4
|
+
resolveSecurityStatus
|
|
5
|
+
} from "../common/services/accountContextService.js";
|
|
6
|
+
import {
|
|
7
|
+
accountSettingsResponseFormatter
|
|
8
|
+
} from "../common/formatters/accountSettingsResponseFormatter.js";
|
|
9
|
+
|
|
10
|
+
function createService({
|
|
11
|
+
userSettingsRepository,
|
|
12
|
+
userProfilesRepository,
|
|
13
|
+
authService,
|
|
14
|
+
avatarService
|
|
15
|
+
} = {}) {
|
|
16
|
+
if (!userSettingsRepository || !userProfilesRepository || !avatarService) {
|
|
17
|
+
throw new Error("accountProfileService requires repositories and avatarService.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function getForUser(request, user, options = {}) {
|
|
21
|
+
const profile = await resolveUserProfile(userProfilesRepository, user);
|
|
22
|
+
if (!profile) {
|
|
23
|
+
throw new AppError(404, "User profile was not found.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const settings = await userSettingsRepository.ensureForUserId(profile.id);
|
|
27
|
+
const securityStatus = await resolveSecurityStatus(authService, request);
|
|
28
|
+
|
|
29
|
+
return accountSettingsResponseFormatter({
|
|
30
|
+
profile,
|
|
31
|
+
settings,
|
|
32
|
+
securityStatus,
|
|
33
|
+
authService
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function updateProfile(request, user, payload = {}, options = {}) {
|
|
38
|
+
const profile = await resolveUserProfile(userProfilesRepository, user);
|
|
39
|
+
if (!profile) {
|
|
40
|
+
throw new AppError(404, "User profile was not found.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let session = null;
|
|
44
|
+
let updatedProfile = null;
|
|
45
|
+
if (authService && typeof authService.updateDisplayName === "function") {
|
|
46
|
+
const result = await authService.updateDisplayName(request, payload.displayName);
|
|
47
|
+
session = result?.session || null;
|
|
48
|
+
updatedProfile = result?.profile || null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!updatedProfile) {
|
|
52
|
+
updatedProfile = await userProfilesRepository.updateDisplayNameById(profile.id, payload.displayName);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const settings = await userSettingsRepository.ensureForUserId(updatedProfile.id);
|
|
56
|
+
const securityStatus = await resolveSecurityStatus(authService, request);
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
session,
|
|
60
|
+
settings: accountSettingsResponseFormatter({
|
|
61
|
+
profile: updatedProfile,
|
|
62
|
+
settings,
|
|
63
|
+
securityStatus,
|
|
64
|
+
authService
|
|
65
|
+
})
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function uploadAvatar(request, user, payload = {}, options = {}) {
|
|
70
|
+
void options;
|
|
71
|
+
|
|
72
|
+
const avatarUpload = await avatarService.uploadForUser(user, payload);
|
|
73
|
+
const profile = avatarUpload?.profile || null;
|
|
74
|
+
if (!profile) {
|
|
75
|
+
throw new AppError(500, "Avatar upload completed without a profile result.");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const settings = await userSettingsRepository.ensureForUserId(profile.id);
|
|
79
|
+
const securityStatus = await resolveSecurityStatus(authService, request);
|
|
80
|
+
|
|
81
|
+
return accountSettingsResponseFormatter({
|
|
82
|
+
profile,
|
|
83
|
+
settings,
|
|
84
|
+
securityStatus,
|
|
85
|
+
authService
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function deleteAvatar(request, user, _payload = {}, options = {}) {
|
|
90
|
+
void options;
|
|
91
|
+
|
|
92
|
+
const profile = await avatarService.clearForUser(user);
|
|
93
|
+
const settings = await userSettingsRepository.ensureForUserId(profile.id);
|
|
94
|
+
const securityStatus = await resolveSecurityStatus(authService, request);
|
|
95
|
+
|
|
96
|
+
return accountSettingsResponseFormatter({
|
|
97
|
+
profile,
|
|
98
|
+
settings,
|
|
99
|
+
securityStatus,
|
|
100
|
+
authService
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function readAvatar(_request, user, _payload = {}, options = {}) {
|
|
105
|
+
void options;
|
|
106
|
+
|
|
107
|
+
const avatar = await avatarService.readForUser(user);
|
|
108
|
+
if (!avatar) {
|
|
109
|
+
throw new AppError(404, "Avatar not found.");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return avatar;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return Object.freeze({
|
|
116
|
+
getForUser,
|
|
117
|
+
updateProfile,
|
|
118
|
+
uploadAvatar,
|
|
119
|
+
deleteAvatar,
|
|
120
|
+
readAvatar
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export { createService };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { AppError, createValidationError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
import { resolveUserProfile } from "../common/services/accountContextService.js";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_AVATAR_POLICY = Object.freeze({
|
|
5
|
+
allowedMimeTypes: Object.freeze(["image/jpeg", "image/png", "image/webp"]),
|
|
6
|
+
maxUploadBytes: 5 * 1024 * 1024
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
function resolveAvatarPolicy(policy = {}) {
|
|
10
|
+
const source = policy && typeof policy === "object" ? policy : {};
|
|
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
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function readAvatarBuffer(stream, { maxBytes = DEFAULT_AVATAR_POLICY.maxUploadBytes } = {}) {
|
|
30
|
+
if (!stream || typeof stream.on !== "function") {
|
|
31
|
+
throw new TypeError("Avatar upload stream is required.");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const chunks = [];
|
|
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();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function createService({ userProfilesRepository, avatarStorageService, avatarPolicy } = {}) {
|
|
64
|
+
if (!userProfilesRepository) {
|
|
65
|
+
throw new TypeError("avatarService requires userProfilesRepository.");
|
|
66
|
+
}
|
|
67
|
+
if (!avatarStorageService) {
|
|
68
|
+
throw new TypeError("avatarService requires avatarStorageService.");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const resolvedAvatarPolicy = resolveAvatarPolicy(avatarPolicy);
|
|
72
|
+
|
|
73
|
+
async function resolveProfile(user) {
|
|
74
|
+
const profile = await resolveUserProfile(userProfilesRepository, user);
|
|
75
|
+
if (!profile) {
|
|
76
|
+
throw new AppError(404, "User profile was not found.");
|
|
77
|
+
}
|
|
78
|
+
return profile;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function uploadForUser(user, payload = {}) {
|
|
82
|
+
const profile = await resolveProfile(user);
|
|
83
|
+
const mimeType = normalizeMimeType(payload?.mimeType);
|
|
84
|
+
if (!resolvedAvatarPolicy.allowedMimeTypes.includes(mimeType)) {
|
|
85
|
+
throw createValidationError({
|
|
86
|
+
avatar: `Avatar must be one of: ${resolvedAvatarPolicy.allowedMimeTypes.join(", ")}.`
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const buffer = await readAvatarBuffer(payload.stream, {
|
|
91
|
+
maxBytes: resolvedAvatarPolicy.maxUploadBytes
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const avatarVersionMs = Date.now();
|
|
95
|
+
const avatarVersion = String(avatarVersionMs);
|
|
96
|
+
const savedAvatar = await avatarStorageService.saveAvatar({
|
|
97
|
+
userId: profile.id,
|
|
98
|
+
buffer
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const updatedProfile = await userProfilesRepository.updateAvatarById(profile.id, {
|
|
102
|
+
avatarStorageKey: savedAvatar.storageKey,
|
|
103
|
+
avatarVersion,
|
|
104
|
+
avatarUpdatedAt: new Date(avatarVersionMs)
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return Object.freeze({
|
|
108
|
+
profile: updatedProfile
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function clearForUser(user) {
|
|
113
|
+
const profile = await resolveProfile(user);
|
|
114
|
+
if (profile.avatarStorageKey) {
|
|
115
|
+
await avatarStorageService.deleteAvatar(profile.avatarStorageKey);
|
|
116
|
+
}
|
|
117
|
+
return userProfilesRepository.clearAvatarById(profile.id);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function readForUser(user) {
|
|
121
|
+
const profile = await resolveProfile(user);
|
|
122
|
+
if (!profile.avatarStorageKey) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return avatarStorageService.readAvatar(profile.avatarStorageKey);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
uploadForUser,
|
|
131
|
+
clearForUser,
|
|
132
|
+
readForUser
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const __testables = Object.freeze({
|
|
137
|
+
resolveAvatarPolicy,
|
|
138
|
+
readAvatarBuffer
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export { createService, __testables };
|