@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,29 +1,97 @@
|
|
|
1
|
-
import { createCrudResourceRuntime } from "@jskit-ai/crud-core/server/resourceRuntime";
|
|
2
1
|
import {
|
|
2
|
+
createWithTransaction,
|
|
3
3
|
isDuplicateEntryError,
|
|
4
4
|
normalizeDbRecordId,
|
|
5
5
|
normalizeLowerText,
|
|
6
6
|
normalizeRecordId,
|
|
7
|
-
|
|
7
|
+
normalizeText,
|
|
8
|
+
nowDb,
|
|
9
|
+
toIsoString
|
|
8
10
|
} from "./repositoryUtils.js";
|
|
11
|
+
import {
|
|
12
|
+
createJsonApiInputRecord,
|
|
13
|
+
createJsonRestContext,
|
|
14
|
+
simplifyJsonApiDocument
|
|
15
|
+
} from "@jskit-ai/json-rest-api-core/server/jsonRestApiHost";
|
|
9
16
|
import { normalizeIdentity } from "../support/identity.js";
|
|
10
|
-
import { resource } from "../resources/userProfilesResource.js";
|
|
11
17
|
|
|
18
|
+
const RESOURCE_TYPE = "userProfiles";
|
|
12
19
|
const USERNAME_MAX_LENGTH = 120;
|
|
13
|
-
const REPOSITORY_CONFIG = Object.freeze({
|
|
14
|
-
context: "internal.repository.user-profiles"
|
|
15
|
-
});
|
|
16
20
|
|
|
17
|
-
function
|
|
18
|
-
|
|
21
|
+
function normalizeUsername(value) {
|
|
22
|
+
const normalized = normalizeLowerText(value)
|
|
23
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
24
|
+
.replace(/^-+|-+$/g, "")
|
|
25
|
+
.slice(0, USERNAME_MAX_LENGTH);
|
|
26
|
+
|
|
27
|
+
return normalized || "";
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
function
|
|
22
|
-
|
|
30
|
+
function normalizeNullableString(value) {
|
|
31
|
+
if (value === null || value === undefined) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return normalizeText(value);
|
|
23
36
|
}
|
|
24
37
|
|
|
25
|
-
function
|
|
26
|
-
|
|
38
|
+
function normalizeNullableVersion(value) {
|
|
39
|
+
if (value === null || value === undefined || value === "") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return String(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeProfileRecord(payload = null) {
|
|
47
|
+
if (!payload || typeof payload !== "object") {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
id: normalizeDbRecordId(payload.id, { fallback: null }),
|
|
53
|
+
authProvider: normalizeLowerText(payload.authProvider),
|
|
54
|
+
authProviderUserSid: normalizeText(payload.authProviderUserSid),
|
|
55
|
+
email: normalizeLowerText(payload.email),
|
|
56
|
+
username: normalizeUsername(payload.username),
|
|
57
|
+
displayName: normalizeText(payload.displayName),
|
|
58
|
+
avatarStorageKey: normalizeNullableString(payload.avatarStorageKey),
|
|
59
|
+
avatarVersion: normalizeNullableVersion(payload.avatarVersion),
|
|
60
|
+
avatarUpdatedAt: payload.avatarUpdatedAt ? toIsoString(payload.avatarUpdatedAt) : null,
|
|
61
|
+
createdAt: payload.createdAt ? toIsoString(payload.createdAt) : null
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeCreatePayload(payload = {}) {
|
|
66
|
+
const source = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
67
|
+
const normalized = {};
|
|
68
|
+
|
|
69
|
+
if (Object.hasOwn(source, "authProvider") || Object.hasOwn(source, "provider")) {
|
|
70
|
+
normalized.authProvider = normalizeLowerText(source.authProvider ?? source.provider);
|
|
71
|
+
}
|
|
72
|
+
if (Object.hasOwn(source, "authProviderUserSid") || Object.hasOwn(source, "providerUserId")) {
|
|
73
|
+
normalized.authProviderUserSid = normalizeText(source.authProviderUserSid ?? source.providerUserId);
|
|
74
|
+
}
|
|
75
|
+
if (Object.hasOwn(source, "email")) {
|
|
76
|
+
normalized.email = normalizeLowerText(source.email);
|
|
77
|
+
}
|
|
78
|
+
if (Object.hasOwn(source, "username")) {
|
|
79
|
+
normalized.username = normalizeUsername(source.username);
|
|
80
|
+
}
|
|
81
|
+
if (Object.hasOwn(source, "displayName")) {
|
|
82
|
+
normalized.displayName = normalizeText(source.displayName);
|
|
83
|
+
}
|
|
84
|
+
if (Object.hasOwn(source, "avatarStorageKey")) {
|
|
85
|
+
normalized.avatarStorageKey = normalizeNullableString(source.avatarStorageKey);
|
|
86
|
+
}
|
|
87
|
+
if (Object.hasOwn(source, "avatarVersion")) {
|
|
88
|
+
normalized.avatarVersion = normalizeNullableVersion(source.avatarVersion);
|
|
89
|
+
}
|
|
90
|
+
if (Object.hasOwn(source, "avatarUpdatedAt")) {
|
|
91
|
+
normalized.avatarUpdatedAt = source.avatarUpdatedAt == null ? null : new Date(source.avatarUpdatedAt);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return normalized;
|
|
27
95
|
}
|
|
28
96
|
|
|
29
97
|
function usernameBaseFromEmail(email) {
|
|
@@ -69,11 +137,23 @@ function createDuplicateEmailConflictError() {
|
|
|
69
137
|
return error;
|
|
70
138
|
}
|
|
71
139
|
|
|
72
|
-
async function resolveUniqueUsername(
|
|
140
|
+
async function resolveUniqueUsername(api, baseUsername, { excludeUserId = null, transaction = null } = {}) {
|
|
73
141
|
const normalizedExcludeUserId = normalizeDbRecordId(excludeUserId, { fallback: null });
|
|
142
|
+
|
|
74
143
|
for (let suffix = 0; suffix < 1000; suffix += 1) {
|
|
75
144
|
const candidate = buildUsernameCandidate(baseUsername, suffix);
|
|
76
|
-
const
|
|
145
|
+
const result = await api.resources.userProfiles.query({
|
|
146
|
+
queryParams: {
|
|
147
|
+
filters: {
|
|
148
|
+
username: candidate
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
transaction,
|
|
152
|
+
simplified: false
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const existingRows = simplifyJsonApiDocument(result);
|
|
156
|
+
const existing = Array.isArray(existingRows) ? existingRows[0] || null : null;
|
|
77
157
|
const existingId = normalizeDbRecordId(existing?.id, { fallback: null });
|
|
78
158
|
if (!existing || existingId === normalizedExcludeUserId) {
|
|
79
159
|
return candidate;
|
|
@@ -83,13 +163,31 @@ async function resolveUniqueUsername(client, baseUsername, { excludeUserId = nul
|
|
|
83
163
|
throw new Error("Unable to generate unique username.");
|
|
84
164
|
}
|
|
85
165
|
|
|
86
|
-
function createRepository(knex) {
|
|
166
|
+
function createRepository({ api, knex } = {}) {
|
|
167
|
+
if (!api?.resources?.userProfiles) {
|
|
168
|
+
throw new TypeError("internal.repository.user-profiles requires json-rest-api userProfiles resource.");
|
|
169
|
+
}
|
|
87
170
|
if (typeof knex !== "function") {
|
|
88
171
|
throw new TypeError("internal.repository.user-profiles requires knex.");
|
|
89
172
|
}
|
|
90
173
|
|
|
91
|
-
const
|
|
92
|
-
|
|
174
|
+
const withTransaction = createWithTransaction(knex);
|
|
175
|
+
|
|
176
|
+
async function queryFirst(filters = {}, options = {}) {
|
|
177
|
+
const result = await api.resources.userProfiles.query(
|
|
178
|
+
{
|
|
179
|
+
queryParams: {
|
|
180
|
+
filters
|
|
181
|
+
},
|
|
182
|
+
transaction: options?.trx || null,
|
|
183
|
+
simplified: false
|
|
184
|
+
},
|
|
185
|
+
createJsonRestContext(options?.context || null)
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const rows = simplifyJsonApiDocument(result);
|
|
189
|
+
return Array.isArray(rows) ? rows[0] || null : null;
|
|
190
|
+
}
|
|
93
191
|
|
|
94
192
|
async function findById(userId, options = {}) {
|
|
95
193
|
const normalizedUserId = normalizeRecordId(userId, { fallback: null });
|
|
@@ -97,10 +195,7 @@ function createRepository(knex) {
|
|
|
97
195
|
return null;
|
|
98
196
|
}
|
|
99
197
|
|
|
100
|
-
return
|
|
101
|
-
...options,
|
|
102
|
-
include: "none"
|
|
103
|
-
});
|
|
198
|
+
return normalizeProfileRecord(await queryFirst({ id: normalizedUserId }, options));
|
|
104
199
|
}
|
|
105
200
|
|
|
106
201
|
async function findByEmail(email, options = {}) {
|
|
@@ -109,9 +204,7 @@ function createRepository(knex) {
|
|
|
109
204
|
return null;
|
|
110
205
|
}
|
|
111
206
|
|
|
112
|
-
|
|
113
|
-
const row = await client("users").where({ email: normalizedEmail }).first();
|
|
114
|
-
return row ? normalizeProfileRecord(row) : null;
|
|
207
|
+
return normalizeProfileRecord(await queryFirst({ email: normalizedEmail }, options));
|
|
115
208
|
}
|
|
116
209
|
|
|
117
210
|
async function findByIdentity(identityLike, options = {}) {
|
|
@@ -120,15 +213,15 @@ function createRepository(knex) {
|
|
|
120
213
|
return null;
|
|
121
214
|
}
|
|
122
215
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
216
|
+
return normalizeProfileRecord(
|
|
217
|
+
await queryFirst(
|
|
218
|
+
{
|
|
219
|
+
authProvider: identity.provider,
|
|
220
|
+
authProviderUserSid: identity.providerUserId
|
|
221
|
+
},
|
|
222
|
+
options
|
|
223
|
+
)
|
|
224
|
+
);
|
|
132
225
|
}
|
|
133
226
|
|
|
134
227
|
async function updateDisplayNameById(userId, displayName, options = {}) {
|
|
@@ -137,14 +230,25 @@ function createRepository(knex) {
|
|
|
137
230
|
return null;
|
|
138
231
|
}
|
|
139
232
|
|
|
140
|
-
|
|
141
|
-
normalizedUserId,
|
|
142
|
-
{ displayName },
|
|
233
|
+
const updated = await api.resources.userProfiles.patch(
|
|
143
234
|
{
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
235
|
+
inputRecord: createJsonApiInputRecord(
|
|
236
|
+
RESOURCE_TYPE,
|
|
237
|
+
{
|
|
238
|
+
displayName,
|
|
239
|
+
updatedAt: new Date()
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: normalizedUserId
|
|
243
|
+
}
|
|
244
|
+
),
|
|
245
|
+
transaction: options?.trx || null,
|
|
246
|
+
simplified: false
|
|
247
|
+
},
|
|
248
|
+
createJsonRestContext(options?.context || null)
|
|
147
249
|
);
|
|
250
|
+
|
|
251
|
+
return normalizeProfileRecord(simplifyJsonApiDocument(updated));
|
|
148
252
|
}
|
|
149
253
|
|
|
150
254
|
async function updateAvatarById(userId, avatar = {}, options = {}) {
|
|
@@ -153,18 +257,27 @@ function createRepository(knex) {
|
|
|
153
257
|
return null;
|
|
154
258
|
}
|
|
155
259
|
|
|
156
|
-
|
|
157
|
-
normalizedUserId,
|
|
260
|
+
const updated = await api.resources.userProfiles.patch(
|
|
158
261
|
{
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
262
|
+
inputRecord: createJsonApiInputRecord(
|
|
263
|
+
RESOURCE_TYPE,
|
|
264
|
+
{
|
|
265
|
+
avatarStorageKey: avatar.avatarStorageKey ?? null,
|
|
266
|
+
avatarVersion: avatar.avatarVersion ?? null,
|
|
267
|
+
avatarUpdatedAt: avatar.avatarUpdatedAt ?? nowDb(),
|
|
268
|
+
updatedAt: new Date()
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: normalizedUserId
|
|
272
|
+
}
|
|
273
|
+
),
|
|
274
|
+
transaction: options?.trx || null,
|
|
275
|
+
simplified: false
|
|
162
276
|
},
|
|
163
|
-
|
|
164
|
-
...options,
|
|
165
|
-
include: "none"
|
|
166
|
-
}
|
|
277
|
+
createJsonRestContext(options?.context || null)
|
|
167
278
|
);
|
|
279
|
+
|
|
280
|
+
return normalizeProfileRecord(simplifyJsonApiDocument(updated));
|
|
168
281
|
}
|
|
169
282
|
|
|
170
283
|
async function clearAvatarById(userId, options = {}) {
|
|
@@ -173,18 +286,27 @@ function createRepository(knex) {
|
|
|
173
286
|
return null;
|
|
174
287
|
}
|
|
175
288
|
|
|
176
|
-
|
|
177
|
-
normalizedUserId,
|
|
289
|
+
const updated = await api.resources.userProfiles.patch(
|
|
178
290
|
{
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
291
|
+
inputRecord: createJsonApiInputRecord(
|
|
292
|
+
RESOURCE_TYPE,
|
|
293
|
+
{
|
|
294
|
+
avatarStorageKey: null,
|
|
295
|
+
avatarVersion: null,
|
|
296
|
+
avatarUpdatedAt: null,
|
|
297
|
+
updatedAt: new Date()
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
id: normalizedUserId
|
|
301
|
+
}
|
|
302
|
+
),
|
|
303
|
+
transaction: options?.trx || null,
|
|
304
|
+
simplified: false
|
|
182
305
|
},
|
|
183
|
-
|
|
184
|
-
...options,
|
|
185
|
-
include: "none"
|
|
186
|
-
}
|
|
306
|
+
createJsonRestContext(options?.context || null)
|
|
187
307
|
);
|
|
308
|
+
|
|
309
|
+
return normalizeProfileRecord(simplifyJsonApiDocument(updated));
|
|
188
310
|
}
|
|
189
311
|
|
|
190
312
|
async function upsert(profileLike = {}, options = {}) {
|
|
@@ -205,53 +327,68 @@ function createRepository(knex) {
|
|
|
205
327
|
}
|
|
206
328
|
|
|
207
329
|
const executeUpsert = async (trx) => {
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
};
|
|
212
|
-
const existing = await trx("users").where(where).first();
|
|
330
|
+
const existing = await findByIdentity(identity, {
|
|
331
|
+
trx,
|
|
332
|
+
context: options?.context || null
|
|
333
|
+
});
|
|
213
334
|
|
|
214
335
|
try {
|
|
215
336
|
if (existing) {
|
|
216
337
|
const existingUsername = normalizeUsername(existing.username);
|
|
217
338
|
const username = existingUsername || (
|
|
218
339
|
await resolveUniqueUsername(
|
|
219
|
-
|
|
340
|
+
api,
|
|
220
341
|
requestedUsername || usernameBaseFromEmail(email),
|
|
221
|
-
{ excludeUserId: existing.id }
|
|
342
|
+
{ excludeUserId: existing.id, transaction: trx }
|
|
222
343
|
)
|
|
223
344
|
);
|
|
224
|
-
|
|
225
|
-
|
|
345
|
+
|
|
346
|
+
const updated = await api.resources.userProfiles.patch(
|
|
226
347
|
{
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
348
|
+
inputRecord: createJsonApiInputRecord(
|
|
349
|
+
RESOURCE_TYPE,
|
|
350
|
+
{
|
|
351
|
+
email,
|
|
352
|
+
displayName,
|
|
353
|
+
username,
|
|
354
|
+
updatedAt: new Date()
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
id: normalizeDbRecordId(existing.id, { fallback: null })
|
|
358
|
+
}
|
|
359
|
+
),
|
|
360
|
+
transaction: trx,
|
|
361
|
+
simplified: false
|
|
230
362
|
},
|
|
231
|
-
|
|
232
|
-
trx,
|
|
233
|
-
include: "none"
|
|
234
|
-
}
|
|
363
|
+
createJsonRestContext(options?.context || null)
|
|
235
364
|
);
|
|
365
|
+
|
|
366
|
+
return normalizeProfileRecord(simplifyJsonApiDocument(updated));
|
|
236
367
|
}
|
|
237
368
|
|
|
238
369
|
const username = await resolveUniqueUsername(
|
|
239
|
-
|
|
240
|
-
requestedUsername || usernameBaseFromEmail(email)
|
|
370
|
+
api,
|
|
371
|
+
requestedUsername || usernameBaseFromEmail(email),
|
|
372
|
+
{ transaction: trx }
|
|
241
373
|
);
|
|
242
|
-
|
|
374
|
+
|
|
375
|
+
const created = await api.resources.userProfiles.post(
|
|
243
376
|
{
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
377
|
+
inputRecord: createJsonApiInputRecord(RESOURCE_TYPE, {
|
|
378
|
+
authProvider: identity.provider,
|
|
379
|
+
authProviderUserSid: identity.providerUserId,
|
|
380
|
+
email,
|
|
381
|
+
displayName,
|
|
382
|
+
username,
|
|
383
|
+
createdAt: new Date()
|
|
384
|
+
}),
|
|
385
|
+
transaction: trx,
|
|
386
|
+
simplified: false
|
|
249
387
|
},
|
|
250
|
-
|
|
251
|
-
trx,
|
|
252
|
-
include: "none"
|
|
253
|
-
}
|
|
388
|
+
createJsonRestContext(options?.context || null)
|
|
254
389
|
);
|
|
390
|
+
|
|
391
|
+
return normalizeProfileRecord(simplifyJsonApiDocument(created));
|
|
255
392
|
} catch (error) {
|
|
256
393
|
if (duplicateTargetsEmail(error)) {
|
|
257
394
|
throw createDuplicateEmailConflictError();
|
|
@@ -264,15 +401,17 @@ function createRepository(knex) {
|
|
|
264
401
|
}
|
|
265
402
|
}
|
|
266
403
|
|
|
267
|
-
|
|
268
|
-
|
|
404
|
+
return findByIdentity(identity, {
|
|
405
|
+
trx,
|
|
406
|
+
context: options?.context || null
|
|
407
|
+
});
|
|
269
408
|
};
|
|
270
409
|
|
|
271
410
|
if (options?.trx) {
|
|
272
411
|
return executeUpsert(options.trx);
|
|
273
412
|
}
|
|
274
413
|
|
|
275
|
-
return
|
|
414
|
+
return withTransaction(executeUpsert);
|
|
276
415
|
}
|
|
277
416
|
|
|
278
417
|
return Object.freeze({
|