@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.
- package/package.descriptor.mjs +14 -65
- package/package.json +10 -10
- package/src/server/UsersCoreServiceProvider.js +18 -2
- package/src/server/accountNotifications/accountNotificationsActions.js +3 -5
- package/src/server/accountNotifications/accountNotificationsService.js +3 -2
- package/src/server/accountNotifications/bootAccountNotificationsRoutes.js +12 -11
- package/src/server/accountPreferences/accountPreferencesActions.js +3 -5
- package/src/server/accountPreferences/accountPreferencesService.js +3 -2
- package/src/server/accountPreferences/bootAccountPreferencesRoutes.js +12 -11
- package/src/server/accountProfile/accountProfileActions.js +15 -32
- package/src/server/accountProfile/accountProfileService.js +9 -8
- package/src/server/accountProfile/bootAccountProfileRoutes.js +25 -19
- package/src/server/accountSecurity/accountSecurityActions.js +21 -16
- package/src/server/accountSecurity/accountSecurityService.js +16 -6
- package/src/server/accountSecurity/bootAccountSecurityRoutes.js +52 -40
- package/src/server/common/formatters/accountSettingsResponseFormatter.js +8 -18
- package/src/server/common/registerCommonRepositories.js +5 -2
- package/src/server/common/repositories/userProfilesRepository.js +227 -88
- package/src/server/common/repositories/userSettingsRepository.js +108 -100
- package/src/server/common/support/accountSettingsJsonApiTransport.js +10 -0
- package/src/server/usersBootstrapContributor.js +13 -32
- package/src/shared/resources/accountSettingsSchemas.js +83 -0
- package/src/shared/resources/userProfileResource.js +146 -126
- package/src/shared/resources/userSettingsResource.js +376 -353
- package/templates/packages/users/package.descriptor.mjs +4 -5
- package/templates/packages/users/package.json +0 -1
- package/templates/packages/users/src/server/UsersProvider.js +23 -24
- package/templates/packages/users/src/server/actions.js +26 -28
- package/templates/packages/users/src/server/registerRoutes.js +29 -15
- package/templates/packages/users/src/server/repository.js +35 -28
- package/templates/packages/users/src/server/service.js +20 -15
- package/templates/packages/users/src/shared/userResource.js +55 -68
- package/templates/packages/users-workspace/package.descriptor.mjs +4 -5
- package/templates/packages/users-workspace/src/server/UsersProvider.js +23 -24
- package/templates/packages/users-workspace/src/server/actions.js +28 -28
- package/templates/packages/users-workspace/src/server/registerRoutes.js +34 -16
- package/test/accountSecurityService.test.js +32 -0
- package/test/providerLifecycle.test.js +63 -0
- package/test/registerCommonRepositories.test.js +28 -8
- package/test/repositoryContracts.test.js +177 -28
- package/test/resourcesCanonical.test.js +18 -11
- package/test/userSettingsInternalResource.test.js +8 -0
- package/test/userSettingsResource.test.js +24 -7
- package/test/usersBootstrapContributor.test.js +40 -1
- package/test/usersPackageScaffoldContract.test.js +82 -4
- package/test/usersRouteRequestInputValidator.test.js +92 -23
- package/test/usersRouteResources.test.js +28 -18
- package/src/server/common/resources/userProfilesResource.js +0 -203
- package/src/server/common/validators/authenticatedUserValidator.js +0 -43
- package/src/shared/resources/resolveGlobalArrayRegistry.js +0 -6
- package/src/shared/resources/userSettingsFields.js +0 -76
- package/templates/packages/main/src/shared/resources/userSettingsFields.js +0 -138
- package/templates/packages/users/src/server/actionIds.js +0 -6
- package/templates/packages/users/src/server/listConfig.js +0 -16
- package/test/settingsFieldRegistriesSingleton.test.js +0 -14
- 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
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
+
await authService.signOutOtherSessions(request);
|
|
100
|
+
|
|
101
|
+
return null;
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
return Object.freeze({
|
|
@@ -1,6 +1,20 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
params: userSettingsResource.operations.oauthLinkStart.params,
|
|
106
|
+
query: userSettingsResource.operations.oauthLinkStart.query,
|
|
107
|
+
responses: withStandardErrorResponses(
|
|
99
108
|
{
|
|
100
|
-
302:
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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(
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
} from "../../../shared/resources/
|
|
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(
|
|
24
|
+
function formatUserSettingsSection(fieldKeys, settings = {}) {
|
|
25
25
|
const source = settings && typeof settings === "object" ? settings : {};
|
|
26
26
|
const formatted = {};
|
|
27
27
|
|
|
28
|
-
for (const
|
|
29
|
-
|
|
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(
|
|
58
|
-
notifications: formatUserSettingsSection(
|
|
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
|
|