@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.
Files changed (148) hide show
  1. package/package.descriptor.mjs +464 -0
  2. package/package.json +35 -0
  3. package/src/server/UsersCoreServiceProvider.js +74 -0
  4. package/src/server/accountNotifications/accountNotificationsActions.js +39 -0
  5. package/src/server/accountNotifications/accountNotificationsService.js +41 -0
  6. package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +41 -0
  7. package/src/server/accountNotifications/registerAccountNotifications.js +39 -0
  8. package/src/server/accountPreferences/accountPreferencesActions.js +39 -0
  9. package/src/server/accountPreferences/accountPreferencesService.js +41 -0
  10. package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +41 -0
  11. package/src/server/accountPreferences/registerAccountPreferences.js +39 -0
  12. package/src/server/accountProfile/accountProfileActions.js +137 -0
  13. package/src/server/accountProfile/accountProfileService.js +124 -0
  14. package/src/server/accountProfile/avatarService.js +141 -0
  15. package/src/server/accountProfile/avatarStorageService.js +132 -0
  16. package/src/server/accountProfile/bootAccountProfileRoutes.js +166 -0
  17. package/src/server/accountProfile/registerAccountProfile.js +62 -0
  18. package/src/server/accountProfile/registerAvatarMultipartSupport.js +43 -0
  19. package/src/server/accountSecurity/accountSecurityActions.js +144 -0
  20. package/src/server/accountSecurity/accountSecurityService.js +103 -0
  21. package/src/server/accountSecurity/bootAccountSecurityRoutes.js +183 -0
  22. package/src/server/accountSecurity/registerAccountSecurity.js +31 -0
  23. package/src/server/common/README.md +21 -0
  24. package/src/server/common/contributors/README.md +11 -0
  25. package/src/server/common/contributors/workspaceActionContextContributor.js +79 -0
  26. package/src/server/common/contributors/workspaceAuthPolicyContextResolver.js +34 -0
  27. package/src/server/common/contributors/workspaceRouteVisibilityResolver.js +79 -0
  28. package/src/server/common/diTokens.js +21 -0
  29. package/src/server/common/formatters/README.md +11 -0
  30. package/src/server/common/formatters/accountAvatarFormatter.js +42 -0
  31. package/src/server/common/formatters/accountSecurityStatusFormatter.js +71 -0
  32. package/src/server/common/formatters/accountSettingsResponseFormatter.js +62 -0
  33. package/src/server/common/formatters/workspaceFormatter.js +46 -0
  34. package/src/server/common/registerCommonRepositories.js +45 -0
  35. package/src/server/common/registerSharedApi.js +9 -0
  36. package/src/server/common/repositories/README.md +24 -0
  37. package/src/server/common/repositories/repositoryUtils.js +50 -0
  38. package/src/server/common/repositories/userProfilesRepository.js +251 -0
  39. package/src/server/common/repositories/userSettingsRepository.js +179 -0
  40. package/src/server/common/repositories/workspaceInvitesRepository.js +172 -0
  41. package/src/server/common/repositories/workspaceMembershipsRepository.js +157 -0
  42. package/src/server/common/repositories/workspacesRepository.js +183 -0
  43. package/src/server/common/routes/README.md +11 -0
  44. package/src/server/common/services/README.md +12 -0
  45. package/src/server/common/services/accountContextService.js +31 -0
  46. package/src/server/common/services/authProfileSyncService.js +128 -0
  47. package/src/server/common/services/workspaceContextService.js +270 -0
  48. package/src/server/common/support/deepFreeze.js +17 -0
  49. package/src/server/common/support/realtimeServiceEvents.js +94 -0
  50. package/src/server/common/support/resolveActionUser.js +11 -0
  51. package/src/server/common/support/workspaceRoutePaths.js +17 -0
  52. package/src/server/common/validators/README.md +11 -0
  53. package/src/server/common/validators/authenticatedUserValidator.js +42 -0
  54. package/src/server/common/validators/routeParamsValidator.js +62 -0
  55. package/src/server/consoleSettings/bootConsoleSettingsRoutes.js +64 -0
  56. package/src/server/consoleSettings/consoleService.js +36 -0
  57. package/src/server/consoleSettings/consoleSettingsActions.js +55 -0
  58. package/src/server/consoleSettings/consoleSettingsRepository.js +111 -0
  59. package/src/server/consoleSettings/consoleSettingsService.js +40 -0
  60. package/src/server/consoleSettings/registerConsoleSettings.js +57 -0
  61. package/src/server/registerWorkspaceBootstrap.js +36 -0
  62. package/src/server/registerWorkspaceCore.js +95 -0
  63. package/src/server/support/resolveWorkspace.js +16 -0
  64. package/src/server/support/workspaceActionSurfaces.js +135 -0
  65. package/src/server/support/workspaceInvitationsPolicy.js +45 -0
  66. package/src/server/support/workspaceRouteInput.js +22 -0
  67. package/src/server/workspaceBootstrapContributor.js +401 -0
  68. package/src/server/workspaceDirectory/bootWorkspaceDirectoryRoutes.js +73 -0
  69. package/src/server/workspaceDirectory/registerWorkspaceDirectory.js +19 -0
  70. package/src/server/workspaceDirectory/workspaceDirectoryActions.js +65 -0
  71. package/src/server/workspaceMembers/bootWorkspaceMembers.js +238 -0
  72. package/src/server/workspaceMembers/registerWorkspaceMembers.js +112 -0
  73. package/src/server/workspaceMembers/workspaceMembersActions.js +186 -0
  74. package/src/server/workspaceMembers/workspaceMembersService.js +210 -0
  75. package/src/server/workspacePendingInvitations/bootWorkspacePendingInvitations.js +63 -0
  76. package/src/server/workspacePendingInvitations/registerWorkspacePendingInvitations.js +128 -0
  77. package/src/server/workspacePendingInvitations/workspacePendingInvitationsActions.js +74 -0
  78. package/src/server/workspacePendingInvitations/workspacePendingInvitationsService.js +137 -0
  79. package/src/server/workspaceSettings/bootWorkspaceSettings.js +77 -0
  80. package/src/server/workspaceSettings/registerWorkspaceSettings.js +67 -0
  81. package/src/server/workspaceSettings/workspaceSettingsActions.js +72 -0
  82. package/src/server/workspaceSettings/workspaceSettingsRepository.js +135 -0
  83. package/src/server/workspaceSettings/workspaceSettingsService.js +65 -0
  84. package/src/shared/events/usersEvents.js +19 -0
  85. package/src/shared/index.js +91 -0
  86. package/src/shared/operationMessages.js +16 -0
  87. package/src/shared/resources/consoleSettingsFields.js +55 -0
  88. package/src/shared/resources/consoleSettingsResource.js +139 -0
  89. package/src/shared/resources/resolveGlobalArrayRegistry.js +6 -0
  90. package/src/shared/resources/userProfileResource.js +148 -0
  91. package/src/shared/resources/userSettingsFields.js +71 -0
  92. package/src/shared/resources/userSettingsResource.js +416 -0
  93. package/src/shared/resources/workspaceMembersResource.js +352 -0
  94. package/src/shared/resources/workspacePendingInvitationsResource.js +87 -0
  95. package/src/shared/resources/workspaceResource.js +149 -0
  96. package/src/shared/resources/workspaceSettingsFields.js +60 -0
  97. package/src/shared/resources/workspaceSettingsResource.js +178 -0
  98. package/src/shared/roles.js +136 -0
  99. package/src/shared/settings.js +31 -0
  100. package/src/shared/support/usersApiPaths.js +34 -0
  101. package/src/shared/support/usersVisibility.js +45 -0
  102. package/src/shared/support/workspacePathModel.js +145 -0
  103. package/src/shared/tenancyMode.js +35 -0
  104. package/src/shared/tenancyProfile.js +73 -0
  105. package/templates/config/workspaceRoles.js +30 -0
  106. package/templates/migrations/users_core_console_owner.cjs +39 -0
  107. package/templates/migrations/users_core_initial.cjs +118 -0
  108. package/templates/migrations/users_core_profile_username.cjs +98 -0
  109. package/templates/packages/main/src/shared/resources/consoleSettingsFields.js +11 -0
  110. package/templates/packages/main/src/shared/resources/userSettingsFields.js +138 -0
  111. package/templates/packages/main/src/shared/resources/workspaceSettingsFields.js +105 -0
  112. package/test/authProfileSyncService.test.js +119 -0
  113. package/test/avatarService.test.js +114 -0
  114. package/test/avatarStorageService.test.js +61 -0
  115. package/test/consoleService.test.js +57 -0
  116. package/test/consoleSettingsService.test.js +86 -0
  117. package/test/exportsContract.test.js +38 -0
  118. package/test/registerAvatarMultipartSupport.test.js +64 -0
  119. package/test/registerServiceRealtimeEvents.test.js +160 -0
  120. package/test/registerWorkspaceDirectory.test.js +26 -0
  121. package/test/registerWorkspaceSettings.test.js +44 -0
  122. package/test/resourcesCanonical.test.js +90 -0
  123. package/test/roles.test.js +74 -0
  124. package/test/settingsFieldRegistriesSingleton.test.js +24 -0
  125. package/test/tenancyProfile.test.js +67 -0
  126. package/test/userSettingsResource.test.js +31 -0
  127. package/test/usersApiPaths.test.js +31 -0
  128. package/test/usersRouteRequestInputValidator.test.js +556 -0
  129. package/test/usersRouteResources.test.js +113 -0
  130. package/test/usersRouteValidators.test.js +49 -0
  131. package/test/usersVisibility.test.js +22 -0
  132. package/test/workspaceActionContextContributor.test.js +251 -0
  133. package/test/workspaceActionSurfaces.test.js +105 -0
  134. package/test/workspaceAuthPolicyContextResolver.test.js +119 -0
  135. package/test/workspaceBootstrapContributor.test.js +466 -0
  136. package/test/workspaceInvitationsPolicy.test.js +71 -0
  137. package/test/workspaceInvitesRepository.test.js +111 -0
  138. package/test/workspaceMembersService.test.js +400 -0
  139. package/test/workspacePathModel.test.js +93 -0
  140. package/test/workspacePendingInvitationsResource.test.js +38 -0
  141. package/test/workspacePendingInvitationsService.test.js +151 -0
  142. package/test/workspaceRouteVisibilityResolver.test.js +83 -0
  143. package/test/workspaceService.test.js +480 -0
  144. package/test/workspaceSettingsActions.test.js +42 -0
  145. package/test/workspaceSettingsRepository.test.js +156 -0
  146. package/test/workspaceSettingsResource.test.js +156 -0
  147. package/test/workspaceSettingsService.test.js +120 -0
  148. 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 };