@jskit-ai/users-core 0.1.65 → 0.1.67

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 (56) hide show
  1. package/package.descriptor.mjs +14 -65
  2. package/package.json +10 -10
  3. package/src/server/UsersCoreServiceProvider.js +18 -2
  4. package/src/server/accountNotifications/accountNotificationsActions.js +3 -5
  5. package/src/server/accountNotifications/accountNotificationsService.js +3 -2
  6. package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +12 -11
  7. package/src/server/accountPreferences/accountPreferencesActions.js +3 -5
  8. package/src/server/accountPreferences/accountPreferencesService.js +3 -2
  9. package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +12 -11
  10. package/src/server/accountProfile/accountProfileActions.js +15 -32
  11. package/src/server/accountProfile/accountProfileService.js +9 -8
  12. package/src/server/accountProfile/bootAccountProfileRoutes.js +25 -19
  13. package/src/server/accountSecurity/accountSecurityActions.js +21 -16
  14. package/src/server/accountSecurity/accountSecurityService.js +16 -6
  15. package/src/server/accountSecurity/bootAccountSecurityRoutes.js +52 -40
  16. package/src/server/common/formatters/accountSettingsResponseFormatter.js +8 -18
  17. package/src/server/common/registerCommonRepositories.js +5 -2
  18. package/src/server/common/repositories/userProfilesRepository.js +227 -88
  19. package/src/server/common/repositories/userSettingsRepository.js +108 -100
  20. package/src/server/common/support/accountSettingsJsonApiTransport.js +10 -0
  21. package/src/server/usersBootstrapContributor.js +13 -32
  22. package/src/shared/resources/accountSettingsSchemas.js +83 -0
  23. package/src/shared/resources/userProfileResource.js +146 -126
  24. package/src/shared/resources/userSettingsResource.js +376 -353
  25. package/templates/packages/users/package.descriptor.mjs +4 -5
  26. package/templates/packages/users/package.json +0 -1
  27. package/templates/packages/users/src/server/UsersProvider.js +23 -24
  28. package/templates/packages/users/src/server/actions.js +26 -28
  29. package/templates/packages/users/src/server/registerRoutes.js +29 -15
  30. package/templates/packages/users/src/server/repository.js +35 -28
  31. package/templates/packages/users/src/server/service.js +20 -15
  32. package/templates/packages/users/src/shared/userResource.js +55 -68
  33. package/templates/packages/users-workspace/package.descriptor.mjs +4 -5
  34. package/templates/packages/users-workspace/src/server/UsersProvider.js +23 -24
  35. package/templates/packages/users-workspace/src/server/actions.js +28 -28
  36. package/templates/packages/users-workspace/src/server/registerRoutes.js +34 -16
  37. package/test/accountSecurityService.test.js +32 -0
  38. package/test/providerLifecycle.test.js +63 -0
  39. package/test/registerCommonRepositories.test.js +28 -8
  40. package/test/repositoryContracts.test.js +177 -28
  41. package/test/resourcesCanonical.test.js +18 -11
  42. package/test/userSettingsInternalResource.test.js +8 -0
  43. package/test/userSettingsResource.test.js +24 -7
  44. package/test/usersBootstrapContributor.test.js +40 -1
  45. package/test/usersPackageScaffoldContract.test.js +82 -4
  46. package/test/usersRouteRequestInputValidator.test.js +92 -23
  47. package/test/usersRouteResources.test.js +28 -18
  48. package/src/server/common/resources/userProfilesResource.js +0 -203
  49. package/src/server/common/validators/authenticatedUserValidator.js +0 -43
  50. package/src/shared/resources/resolveGlobalArrayRegistry.js +0 -6
  51. package/src/shared/resources/userSettingsFields.js +0 -76
  52. package/templates/packages/main/src/shared/resources/userSettingsFields.js +0 -138
  53. package/templates/packages/users/src/server/actionIds.js +0 -6
  54. package/templates/packages/users/src/server/listConfig.js +0 -16
  55. package/test/settingsFieldRegistriesSingleton.test.js +0 -14
  56. package/test-support/registerDefaultSettingsFields.js +0 -2
@@ -1,9 +1,18 @@
1
1
  import {
2
2
  resolveRequest
3
3
  } from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
4
+ import { composeSchemaDefinitions } from "@jskit-ai/kernel/shared/validators";
4
5
  import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
5
6
  import { resolveActionUser } from "../common/support/resolveActionUser.js";
6
7
 
8
+ const oauthLinkStartInputValidator = composeSchemaDefinitions([
9
+ userSettingsResource.operations.oauthLinkStart.params,
10
+ userSettingsResource.operations.oauthLinkStart.query
11
+ ], {
12
+ mode: "patch",
13
+ context: "accountSecurityActions.oauthLinkStartInputValidator"
14
+ });
15
+
7
16
  const accountSecurityActions = Object.freeze([
8
17
  {
9
18
  id: "settings.security.password.change",
@@ -14,10 +23,8 @@ const accountSecurityActions = Object.freeze([
14
23
  permission: {
15
24
  require: "authenticated"
16
25
  },
17
- inputValidator: {
18
- payload: userSettingsResource.operations.passwordChange.bodyValidator
19
- },
20
- outputValidator: userSettingsResource.operations.passwordChange.outputValidator,
26
+ input: userSettingsResource.operations.passwordChange.body,
27
+ output: null,
21
28
  idempotency: "none",
22
29
  audit: {
23
30
  actionName: "settings.security.password.change"
@@ -27,7 +34,7 @@ const accountSecurityActions = Object.freeze([
27
34
  return deps.accountSecurityService.changePassword(
28
35
  resolveRequest(context),
29
36
  resolveActionUser(context, input),
30
- input.payload,
37
+ input,
31
38
  {
32
39
  context
33
40
  }
@@ -43,10 +50,8 @@ const accountSecurityActions = Object.freeze([
43
50
  permission: {
44
51
  require: "authenticated"
45
52
  },
46
- inputValidator: {
47
- payload: userSettingsResource.operations.passwordMethodToggle.bodyValidator
48
- },
49
- outputValidator: userSettingsResource.operations.passwordMethodToggle.outputValidator,
53
+ input: userSettingsResource.operations.passwordMethodToggle.body,
54
+ output: null,
50
55
  idempotency: "none",
51
56
  audit: {
52
57
  actionName: "settings.security.password_method.toggle"
@@ -56,7 +61,7 @@ const accountSecurityActions = Object.freeze([
56
61
  return deps.accountSecurityService.setPasswordMethodEnabled(
57
62
  resolveRequest(context),
58
63
  resolveActionUser(context, input),
59
- input.payload,
64
+ input,
60
65
  {
61
66
  context
62
67
  }
@@ -72,8 +77,8 @@ const accountSecurityActions = Object.freeze([
72
77
  permission: {
73
78
  require: "authenticated"
74
79
  },
75
- inputValidator: [userSettingsResource.operations.oauthLinkStart.paramsValidator, userSettingsResource.operations.oauthLinkStart.queryValidator],
76
- outputValidator: userSettingsResource.operations.oauthLinkStart.outputValidator,
80
+ input: oauthLinkStartInputValidator,
81
+ output: null,
77
82
  idempotency: "none",
78
83
  audit: {
79
84
  actionName: "settings.security.oauth.link.start"
@@ -99,8 +104,8 @@ const accountSecurityActions = Object.freeze([
99
104
  permission: {
100
105
  require: "authenticated"
101
106
  },
102
- inputValidator: userSettingsResource.operations.oauthUnlink.paramsValidator,
103
- outputValidator: userSettingsResource.operations.oauthUnlink.outputValidator,
107
+ input: userSettingsResource.operations.oauthUnlink.params,
108
+ output: null,
104
109
  idempotency: "none",
105
110
  audit: {
106
111
  actionName: "settings.security.oauth.unlink"
@@ -126,8 +131,8 @@ const accountSecurityActions = Object.freeze([
126
131
  permission: {
127
132
  require: "authenticated"
128
133
  },
129
- inputValidator: userSettingsResource.operations.logoutOtherSessions.bodyValidator,
130
- outputValidator: userSettingsResource.operations.logoutOtherSessions.outputValidator,
134
+ input: userSettingsResource.operations.logoutOtherSessions.body,
135
+ output: null,
131
136
  idempotency: "none",
132
137
  audit: {
133
138
  actionName: "settings.security.sessions.logout_others"
@@ -1,5 +1,6 @@
1
1
  import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
2
  import { createValidationError } from "@jskit-ai/kernel/server/runtime";
3
+ import { returnJsonApiData, returnJsonApiMeta } from "@jskit-ai/http-runtime/shared";
3
4
  import {
4
5
  resolveUserProfile,
5
6
  resolveSecurityStatus
@@ -28,11 +29,18 @@ function createService({
28
29
  });
29
30
  }
30
31
 
31
- return authService.changePassword(request, {
32
+ const result = await authService.changePassword(request, {
32
33
  currentPassword: payload.currentPassword,
33
34
  newPassword: payload.newPassword,
34
35
  confirmPassword: payload.confirmPassword
35
36
  });
37
+
38
+ return {
39
+ session: result?.session || null,
40
+ response: returnJsonApiMeta({
41
+ message: "Password updated."
42
+ })
43
+ };
36
44
  }
37
45
 
38
46
  async function setPasswordMethodEnabled(request, user, payload = {}, options = {}) {
@@ -51,7 +59,7 @@ function createService({
51
59
  const settings = await userSettingsRepository.ensureForUserId(profile.id);
52
60
  const securityStatus = await resolveSecurityStatus(authService, request);
53
61
 
54
- return {
62
+ return returnJsonApiData({
55
63
  ...(response && typeof response === "object" ? response : {}),
56
64
  settings: accountSettingsResponseFormatter({
57
65
  profile,
@@ -59,7 +67,7 @@ function createService({
59
67
  securityStatus,
60
68
  authService
61
69
  })
62
- };
70
+ });
63
71
  }
64
72
 
65
73
  async function startOAuthProviderLink(request, user, payload = {}, options = {}) {
@@ -78,9 +86,9 @@ function createService({
78
86
  throw new AppError(501, "OAuth unlink is not available.");
79
87
  }
80
88
 
81
- return authService.unlinkProvider(request, {
89
+ return returnJsonApiData(await authService.unlinkProvider(request, {
82
90
  provider: payload.provider
83
- });
91
+ }));
84
92
  }
85
93
 
86
94
  async function logoutOtherSessions(request, _user, options = {}) {
@@ -88,7 +96,9 @@ function createService({
88
96
  throw new AppError(501, "Logout other sessions is not available.");
89
97
  }
90
98
 
91
- return authService.signOutOtherSessions(request);
99
+ await authService.signOutOtherSessions(request);
100
+
101
+ return null;
92
102
  }
93
103
 
94
104
  return Object.freeze({
@@ -1,6 +1,20 @@
1
- import { Type } from "@fastify/type-provider-typebox";
1
+ import { createSchema } from "json-rest-schema";
2
+ import { createJsonApiResourceRouteContract } from "@jskit-ai/http-runtime/shared/validators/jsonApiRouteTransport";
2
3
  import { withStandardErrorResponses } from "@jskit-ai/http-runtime/shared/validators/errorResponses";
4
+ import { deepFreeze } from "@jskit-ai/kernel/shared/support/deepFreeze";
3
5
  import { userSettingsResource } from "../../shared/resources/userSettingsResource.js";
6
+ import { resolveAccountSettingsResourceId } from "../common/support/accountSettingsJsonApiTransport.js";
7
+
8
+ const passwordChangeMetaOutputValidator = deepFreeze({
9
+ schema: createSchema({
10
+ message: {
11
+ type: "string",
12
+ required: true,
13
+ minLength: 1
14
+ }
15
+ }),
16
+ mode: "replace"
17
+ });
4
18
 
5
19
  function bootAccountSecurityRoutes(app) {
6
20
  if (!app || typeof app.make !== "function") {
@@ -8,7 +22,6 @@ function bootAccountSecurityRoutes(app) {
8
22
  }
9
23
 
10
24
  const router = app.make("jskit.http.router");
11
- const authService = app.make("authService");
12
25
 
13
26
  router.register(
14
27
  "POST",
@@ -19,13 +32,13 @@ function bootAccountSecurityRoutes(app) {
19
32
  tags: ["settings"],
20
33
  summary: "Set or change authenticated user's password"
21
34
  },
22
- bodyValidator: userSettingsResource.operations.passwordChange.bodyValidator,
23
- responseValidators: withStandardErrorResponses(
24
- {
25
- 200: userSettingsResource.operations.passwordChange.outputValidator
26
- },
27
- { includeValidation400: true }
28
- ),
35
+ ...createJsonApiResourceRouteContract({
36
+ requestType: "user-password-changes",
37
+ body: userSettingsResource.operations.passwordChange.body,
38
+ output: passwordChangeMetaOutputValidator,
39
+ outputKind: "meta",
40
+ includeValidation400: true
41
+ }),
29
42
  rateLimit: {
30
43
  max: 10,
31
44
  timeWindow: "1 minute"
@@ -34,19 +47,15 @@ function bootAccountSecurityRoutes(app) {
34
47
  async function (request, reply) {
35
48
  const result = await request.executeAction({
36
49
  actionId: "settings.security.password.change",
37
- input: {
38
- payload: request.input.body
39
- }
50
+ input: request.input.body
40
51
  });
41
52
 
53
+ const authService = app.make("authService");
42
54
  if (result?.session && typeof authService.writeSessionCookies === "function") {
43
55
  authService.writeSessionCookies(reply, result.session);
44
56
  }
45
57
 
46
- reply.code(200).send({
47
- ok: true,
48
- message: result?.message || "Password updated."
49
- });
58
+ reply.code(200).send(result?.response || result);
50
59
  }
51
60
  );
52
61
 
@@ -59,13 +68,15 @@ function bootAccountSecurityRoutes(app) {
59
68
  tags: ["settings"],
60
69
  summary: "Enable or disable password sign-in method"
61
70
  },
62
- bodyValidator: userSettingsResource.operations.passwordMethodToggle.bodyValidator,
63
- responseValidators: withStandardErrorResponses(
64
- {
65
- 200: userSettingsResource.operations.passwordMethodToggle.outputValidator
66
- },
67
- { includeValidation400: true }
68
- ),
71
+ ...createJsonApiResourceRouteContract({
72
+ requestType: "user-password-method-settings",
73
+ responseType: "user-security-settings",
74
+ body: userSettingsResource.operations.passwordMethodToggle.body,
75
+ output: userSettingsResource.operations.passwordMethodToggle.output,
76
+ outputKind: "record",
77
+ getRecordId: resolveAccountSettingsResourceId,
78
+ includeValidation400: true
79
+ }),
69
80
  rateLimit: {
70
81
  max: 20,
71
82
  timeWindow: "1 minute"
@@ -74,9 +85,7 @@ function bootAccountSecurityRoutes(app) {
74
85
  async function (request, reply) {
75
86
  const response = await request.executeAction({
76
87
  actionId: "settings.security.password_method.toggle",
77
- input: {
78
- payload: request.input.body
79
- }
88
+ input: request.input.body
80
89
  });
81
90
 
82
91
  reply.code(200).send(response);
@@ -93,11 +102,11 @@ function bootAccountSecurityRoutes(app) {
93
102
  tags: ["settings"],
94
103
  summary: "Start linking an OAuth provider for authenticated user"
95
104
  },
96
- paramsValidator: userSettingsResource.operations.oauthLinkStart.paramsValidator,
97
- queryValidator: userSettingsResource.operations.oauthLinkStart.queryValidator,
98
- responseValidators: withStandardErrorResponses(
105
+ params: userSettingsResource.operations.oauthLinkStart.params,
106
+ query: userSettingsResource.operations.oauthLinkStart.query,
107
+ responses: withStandardErrorResponses(
99
108
  {
100
- 302: { schema: Type.Unknown() }
109
+ 302: userSettingsResource.operations.oauthLinkStart.output
101
110
  },
102
111
  { includeValidation400: true }
103
112
  ),
@@ -128,13 +137,14 @@ function bootAccountSecurityRoutes(app) {
128
137
  tags: ["settings"],
129
138
  summary: "Unlink an OAuth provider from authenticated account"
130
139
  },
131
- paramsValidator: userSettingsResource.operations.oauthUnlink.paramsValidator,
132
- responseValidators: withStandardErrorResponses(
133
- {
134
- 200: userSettingsResource.operations.oauthUnlink.outputValidator
135
- },
136
- { includeValidation400: true }
137
- ),
140
+ params: userSettingsResource.operations.oauthUnlink.params,
141
+ ...createJsonApiResourceRouteContract({
142
+ responseType: "user-security-settings",
143
+ output: userSettingsResource.operations.oauthUnlink.output,
144
+ outputKind: "record",
145
+ getRecordId: resolveAccountSettingsResourceId,
146
+ includeValidation400: true
147
+ }),
138
148
  rateLimit: {
139
149
  max: 20,
140
150
  timeWindow: "1 minute"
@@ -161,8 +171,10 @@ function bootAccountSecurityRoutes(app) {
161
171
  tags: ["settings"],
162
172
  summary: "Sign out from other active sessions"
163
173
  },
164
- responseValidators: withStandardErrorResponses({
165
- 200: userSettingsResource.operations.logoutOtherSessions.outputValidator
174
+ ...createJsonApiResourceRouteContract({
175
+ responseType: "user-security-session-operations",
176
+ outputKind: "no-content",
177
+ successStatus: 204
166
178
  }),
167
179
  rateLimit: {
168
180
  max: 20,
@@ -174,7 +186,7 @@ function bootAccountSecurityRoutes(app) {
174
186
  actionId: "settings.security.sessions.logout_others",
175
187
  input: {}
176
188
  });
177
- reply.code(200).send(response);
189
+ reply.code(204).send(response);
178
190
  }
179
191
  );
180
192
  }
@@ -1,8 +1,8 @@
1
1
  import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
2
2
  import {
3
- USER_SETTINGS_SECTIONS,
4
- userSettingsFields
5
- } from "../../../shared/resources/userSettingsFields.js";
3
+ USER_SETTINGS_NOTIFICATION_KEYS,
4
+ USER_SETTINGS_PREFERENCE_KEYS
5
+ } from "../../../shared/resources/userSettingsResource.js";
6
6
  import { accountAvatarFormatter } from "./accountAvatarFormatter.js";
7
7
  import { accountSecurityStatusFormatter } from "./accountSecurityStatusFormatter.js";
8
8
 
@@ -21,22 +21,12 @@ function resolveAuthProfileSettings(authService) {
21
21
  };
22
22
  }
23
23
 
24
- function formatUserSettingsSection(section, settings = {}) {
24
+ function formatUserSettingsSection(fieldKeys, settings = {}) {
25
25
  const source = settings && typeof settings === "object" ? settings : {};
26
26
  const formatted = {};
27
27
 
28
- for (const field of userSettingsFields) {
29
- if (field.section !== section) {
30
- continue;
31
- }
32
- const rawValue = Object.hasOwn(source, field.key)
33
- ? source[field.key]
34
- : field.resolveDefault({
35
- settings: source
36
- });
37
- formatted[field.key] = field.normalizeOutput(rawValue, {
38
- settings: source
39
- });
28
+ for (const fieldKey of fieldKeys) {
29
+ formatted[fieldKey] = source[fieldKey];
40
30
  }
41
31
 
42
32
  return formatted;
@@ -54,8 +44,8 @@ function accountSettingsResponseFormatter({ profile, settings, securityStatus, a
54
44
  avatar: accountAvatarFormatter(profile, settings)
55
45
  },
56
46
  security: accountSecurityStatusFormatter(securityStatus),
57
- preferences: formatUserSettingsSection(USER_SETTINGS_SECTIONS.PREFERENCES, settings),
58
- notifications: formatUserSettingsSection(USER_SETTINGS_SECTIONS.NOTIFICATIONS, settings)
47
+ preferences: formatUserSettingsSection(USER_SETTINGS_PREFERENCE_KEYS, settings),
48
+ notifications: formatUserSettingsSection(USER_SETTINGS_NOTIFICATION_KEYS, settings)
59
49
  };
60
50
  }
61
51
 
@@ -1,3 +1,4 @@
1
+ import { INTERNAL_JSON_REST_API } from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
1
2
  import { createRepository as createUserProfilesRepository } from "./repositories/userProfilesRepository.js";
2
3
  import { createRepository as createUserSettingsRepository } from "./repositories/userSettingsRepository.js";
3
4
 
@@ -7,13 +8,15 @@ function registerCommonRepositories(app) {
7
8
  }
8
9
 
9
10
  app.singleton("internal.repository.user-settings", (scope) => {
11
+ const api = scope.make(INTERNAL_JSON_REST_API);
10
12
  const knex = scope.make("jskit.database.knex");
11
- return createUserSettingsRepository(knex);
13
+ return createUserSettingsRepository({ api, knex });
12
14
  });
13
15
 
14
16
  app.singleton("internal.repository.user-profiles", (scope) => {
17
+ const api = scope.make(INTERNAL_JSON_REST_API);
15
18
  const knex = scope.make("jskit.database.knex");
16
- return createUserProfilesRepository(knex);
19
+ return createUserProfilesRepository({ api, knex });
17
20
  });
18
21
  }
19
22