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