@unchainedshop/core-users 1.1.3

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 (45) hide show
  1. package/.npm/package/README +7 -0
  2. package/.npm/package/npm-shrinkwrap.json +210 -0
  3. package/.versions +53 -0
  4. package/README.md +3 -0
  5. package/lib/db/UsersCollection.d.ts +3 -0
  6. package/lib/db/UsersCollection.js +76 -0
  7. package/lib/db/UsersCollection.js.map +1 -0
  8. package/lib/module/addMigrations.d.ts +3 -0
  9. package/lib/module/addMigrations.js +19 -0
  10. package/lib/module/addMigrations.js.map +1 -0
  11. package/lib/module/configureUsersModule.d.ts +3 -0
  12. package/lib/module/configureUsersModule.js +275 -0
  13. package/lib/module/configureUsersModule.js.map +1 -0
  14. package/lib/services/getUserCountryService.d.ts +2 -0
  15. package/lib/services/getUserCountryService.js +7 -0
  16. package/lib/services/getUserCountryService.js.map +1 -0
  17. package/lib/services/getUserLanguageService.d.ts +2 -0
  18. package/lib/services/getUserLanguageService.js +7 -0
  19. package/lib/services/getUserLanguageService.js.map +1 -0
  20. package/lib/services/getUserRoleActionsService.d.ts +2 -0
  21. package/lib/services/getUserRoleActionsService.js +30 -0
  22. package/lib/services/getUserRoleActionsService.js.map +1 -0
  23. package/lib/services/updateUserAvatarAfterUploadService.d.ts +2 -0
  24. package/lib/services/updateUserAvatarAfterUploadService.js +12 -0
  25. package/lib/services/updateUserAvatarAfterUploadService.js.map +1 -0
  26. package/lib/services/userServices.d.ts +2 -0
  27. package/lib/services/userServices.js +11 -0
  28. package/lib/services/userServices.js.map +1 -0
  29. package/lib/users-index.d.ts +2 -0
  30. package/lib/users-index.js +3 -0
  31. package/lib/users-index.js.map +1 -0
  32. package/package.js +37 -0
  33. package/package.json +45 -0
  34. package/src/db/UsersCollection.ts +84 -0
  35. package/src/module/addMigrations.ts +31 -0
  36. package/src/module/configureUsersModule.ts +342 -0
  37. package/src/services/getUserCountryService.ts +9 -0
  38. package/src/services/getUserLanguageService.ts +9 -0
  39. package/src/services/getUserRoleActionsService.ts +29 -0
  40. package/src/services/updateUserAvatarAfterUploadService.ts +21 -0
  41. package/src/services/userServices.ts +12 -0
  42. package/src/users-index.ts +3 -0
  43. package/tests/users-index.test.ts +47 -0
  44. package/tsconfig.build.json +26 -0
  45. package/tsconfig.json +8 -0
@@ -0,0 +1,342 @@
1
+ import { Locale } from 'locale';
2
+ import { ModuleInput, ModuleMutations, Query } from '@unchainedshop/types/common';
3
+ import { User, UserQuery, UsersModule } from '@unchainedshop/types/user';
4
+ import { log } from '@unchainedshop/logger';
5
+ import { emit, registerEvents } from '@unchainedshop/events';
6
+ import {
7
+ generateDbFilterById,
8
+ generateDbMutations,
9
+ Schemas,
10
+ systemLocale,
11
+ } from '@unchainedshop/utils';
12
+ import { FileDirector } from '@unchainedshop/file-upload';
13
+ import { Context } from '@unchainedshop/types/api';
14
+ import { UsersCollection } from '../db/UsersCollection';
15
+ import addMigrations from './addMigrations';
16
+
17
+ const USER_EVENTS = [
18
+ 'USER_UPDATE',
19
+ 'USER_UPDATE_PROFILE',
20
+ 'USER_ADD_ROLES',
21
+ 'USER_UPDATE_ROLE',
22
+ 'USER_UPDATE_TAGS',
23
+ 'USER_UPDATE_AVATAR',
24
+ 'USER_UPDATE_HEARTBEAT',
25
+ 'USER_UPDATE_BILLING_ADDRESS',
26
+ 'USER_UPDATE_LAST_CONTACT',
27
+ ];
28
+ const removeConfidentialServiceHashes = (rawUser: User): User => {
29
+ const user = rawUser;
30
+ delete user?.services;
31
+ return user;
32
+ };
33
+
34
+ const buildFindSelector = ({ includeGuests, queryString, ...rest }: UserQuery) => {
35
+ const selector: Query = { ...rest };
36
+ if (!includeGuests) selector.guest = { $ne: true };
37
+ if (queryString) {
38
+ selector.$text = { $search: queryString };
39
+ }
40
+ return selector;
41
+ };
42
+
43
+ const getUserLocale = (user: User, params: { localeContext?: Locale } = {}) => {
44
+ const locale =
45
+ params.localeContext ||
46
+ (user.lastLogin?.locale && new Locale(user.lastLogin.locale)) ||
47
+ systemLocale;
48
+ return locale;
49
+ };
50
+
51
+ FileDirector.registerFileUploadCallback('user-avatars', async (file, context: Context) => {
52
+ const { services } = context;
53
+
54
+ return services.users.updateUserAvatarAfterUpload({ file }, context);
55
+ });
56
+
57
+ export const configureUsersModule = async ({
58
+ db,
59
+ migrationRepository,
60
+ }: ModuleInput<Record<string, never>>): Promise<UsersModule> => {
61
+ registerEvents(USER_EVENTS);
62
+ const Users = await UsersCollection(db);
63
+
64
+ // Migration
65
+ addMigrations(migrationRepository);
66
+
67
+ const mutations = generateDbMutations<User>(Users, Schemas.User) as ModuleMutations<User>;
68
+
69
+ return {
70
+ // Queries
71
+ count: async (query) => {
72
+ const userCount = await Users.countDocuments(buildFindSelector(query));
73
+ return userCount;
74
+ },
75
+
76
+ async findUserById(userId) {
77
+ if (!userId) return null;
78
+ return Users.findOne(generateDbFilterById(userId), {});
79
+ },
80
+
81
+ async findUserByToken({ resetToken, hashedToken }) {
82
+ if (hashedToken) {
83
+ return Users.findOne({
84
+ 'services.resume.loginTokens.hashedToken': hashedToken,
85
+ });
86
+ }
87
+
88
+ if (resetToken) {
89
+ return Users.findOne({
90
+ 'services.password.reset.token': resetToken,
91
+ });
92
+ }
93
+
94
+ return null;
95
+ },
96
+
97
+ findUser: async (query, options) => {
98
+ const selector = buildFindSelector(query);
99
+
100
+ return Users.findOne(selector, options);
101
+ },
102
+
103
+ findUsers: async ({ limit, offset, ...query }) => {
104
+ const selector = buildFindSelector(query);
105
+
106
+ if (query.queryString) {
107
+ return Users.find(selector, {
108
+ skip: offset,
109
+ limit,
110
+ projection: { score: { $meta: 'textScore' } },
111
+ sort: { score: { $meta: 'textScore' } },
112
+ }).toArray();
113
+ }
114
+
115
+ return Users.find(selector, { skip: offset, limit }).toArray();
116
+ },
117
+
118
+ userExists: async ({ userId }) => {
119
+ const selector = generateDbFilterById(userId);
120
+ const userCount = await Users.countDocuments(selector, { limit: 1 });
121
+ return !!userCount;
122
+ },
123
+
124
+ // Transformations
125
+ primaryEmail: (user) => {
126
+ return (user.emails || []).sort(
127
+ (left, right) => Number(right.verified) - Number(left.verified),
128
+ )?.[0];
129
+ },
130
+
131
+ userLocale: (user, params = {}) => {
132
+ return getUserLocale(user, params);
133
+ },
134
+
135
+ // Mutations
136
+ addRoles: async (userId, roles) => {
137
+ const selector = generateDbFilterById(userId);
138
+ const updateResult = await Users.updateOne(selector, {
139
+ $addToSet: { roles: { $each: roles } },
140
+ });
141
+
142
+ const user = await Users.findOne(selector, {});
143
+ emit('USER_ADD_ROLES', {
144
+ user: removeConfidentialServiceHashes(user),
145
+ });
146
+
147
+ return updateResult.modifiedCount;
148
+ },
149
+
150
+ updateAvatar: async (_id, fileId, userId) => {
151
+ const userFilter = generateDbFilterById(_id);
152
+ log('Update Avatar', { userId: _id });
153
+
154
+ const modifier = {
155
+ $set: {
156
+ avatarId: fileId,
157
+ updated: new Date(),
158
+ updatedBy: userId,
159
+ },
160
+ };
161
+
162
+ await mutations.update(_id, modifier, userId);
163
+ const user = await Users.findOne(userFilter, {});
164
+ emit('USER_UPDATE_AVATAR', {
165
+ user: removeConfidentialServiceHashes(user),
166
+ });
167
+ return user;
168
+ },
169
+
170
+ updateGuest: async (user, guest) => {
171
+ log('Update guest', { userId: user._id });
172
+
173
+ const modifier = { $set: { guest } };
174
+ await Users.updateOne(generateDbFilterById(user._id), modifier);
175
+ },
176
+
177
+ updateHeartbeat: async (userId, lastLogin) => {
178
+ const userFilter = generateDbFilterById(userId);
179
+
180
+ const modifier = {
181
+ $set: {
182
+ lastLogin: {
183
+ timestamp: new Date(),
184
+ ...lastLogin,
185
+ },
186
+ },
187
+ };
188
+
189
+ await mutations.update(userId, modifier, userId);
190
+ const user = await Users.findOne(userFilter, {});
191
+
192
+ emit('USER_UPDATE_HEARTBEAT', {
193
+ user: removeConfidentialServiceHashes(user),
194
+ });
195
+ return user;
196
+ },
197
+
198
+ updateInitialPassword: async (user, initialPassword) => {
199
+ log(`Update initial password flag to ${initialPassword}`, { userId: user._id, level: 'verbose' });
200
+
201
+ const modifier = { $set: { initialPassword } };
202
+ await Users.updateOne(generateDbFilterById(user._id), modifier);
203
+ },
204
+
205
+ updateProfile: async (_id, updatedData, userId) => {
206
+ const userFilter = generateDbFilterById(_id);
207
+ const { meta, profile } = updatedData;
208
+
209
+ if (!meta && !profile) {
210
+ return Users.findOne(userFilter, {});
211
+ }
212
+
213
+ const modifier = { $set: {} };
214
+
215
+ if (profile) {
216
+ modifier.$set = Object.keys(profile).reduce((acc, profileKey) => {
217
+ return {
218
+ ...acc,
219
+ [`profile.${profileKey}`]: profile[profileKey],
220
+ };
221
+ }, {});
222
+ }
223
+
224
+ if (meta) {
225
+ modifier.$set.meta = meta;
226
+ }
227
+
228
+ await mutations.update(_id, modifier, userId);
229
+ const user = await Users.findOne(userFilter, {});
230
+ emit('USER_UPDATE_PROFILE', {
231
+ user: removeConfidentialServiceHashes(user),
232
+ });
233
+ return user;
234
+ },
235
+
236
+ updateLastBillingAddress: async (_id, lastBillingAddress, userId) => {
237
+ const userFilter = generateDbFilterById(_id);
238
+ const user = await Users.findOne(userFilter, {});
239
+
240
+ log('Store Last Billing Address', { userId });
241
+
242
+ const modifier = {
243
+ $set: {
244
+ lastBillingAddress,
245
+ updated: new Date(),
246
+ updatedBy: userId,
247
+ },
248
+ };
249
+ const profile = user.profile || {};
250
+ const isGuest = !!user.guest;
251
+
252
+ if (!profile.displayName || isGuest) {
253
+ modifier.$set['profile.displayName'] = [
254
+ lastBillingAddress.firstName,
255
+ lastBillingAddress.lastName,
256
+ ]
257
+ .filter(Boolean)
258
+ .join(' ');
259
+ }
260
+
261
+ await mutations.update(_id, modifier, userId);
262
+ const updatedUser = await Users.findOne(userFilter, {});
263
+ emit('USER_UPDATE_BILLING_ADDRESS', {
264
+ user: removeConfidentialServiceHashes(user),
265
+ });
266
+ return updatedUser;
267
+ },
268
+
269
+ updateLastContact: async (_id, lastContact, userId) => {
270
+ const userFilter = generateDbFilterById(_id);
271
+ const user = await Users.findOne(userFilter, {});
272
+
273
+ log('Store Last Contact', { userId });
274
+
275
+ const profile = user.profile || {};
276
+ const isGuest = !!user.guest;
277
+
278
+ const modifier = {
279
+ $set: {
280
+ updated: new Date(),
281
+ updatedBy: userId,
282
+ lastContact,
283
+ },
284
+ };
285
+
286
+ if ((!profile.phoneMobile || isGuest) && lastContact.telNumber) {
287
+ // Backport the contact phone number to the user profile
288
+ modifier.$set['profile.phoneMobile'] = lastContact.telNumber;
289
+ }
290
+
291
+ await mutations.update(_id, modifier, userId);
292
+ const updatedUser = await Users.findOne(userFilter, {});
293
+ emit('USER_UPDATE_LAST_CONTACT', {
294
+ user: removeConfidentialServiceHashes(user),
295
+ });
296
+ return updatedUser;
297
+ },
298
+
299
+ updateRoles: async (_id, roles, userId) => {
300
+ const userFilter = generateDbFilterById(_id);
301
+
302
+ const modifier = {
303
+ $set: {
304
+ updated: new Date(),
305
+ updateBy: userId,
306
+ roles,
307
+ },
308
+ };
309
+ await mutations.update(_id, modifier, userId);
310
+ const user = await Users.findOne(userFilter, {});
311
+ emit('USER_UPDATE_ROLE', {
312
+ user: removeConfidentialServiceHashes(user),
313
+ });
314
+ return user;
315
+ },
316
+ updateTags: async (_id, tags, userId) => {
317
+ const userFilter = generateDbFilterById(_id);
318
+
319
+ const modifier = {
320
+ $set: {
321
+ updated: new Date(),
322
+ updateBy: userId,
323
+ tags,
324
+ },
325
+ };
326
+
327
+ await mutations.update(_id, modifier, userId);
328
+ const user = await Users.findOne(userFilter, {});
329
+ emit('USER_UPDATE_TAGS', {
330
+ user: removeConfidentialServiceHashes(user),
331
+ });
332
+ return user;
333
+ },
334
+ updateUser: async (query, modifier, options) => {
335
+ await Users.updateOne(query, modifier, options);
336
+ const user = await Users.findOne(query);
337
+ emit('USER_UPDATE', {
338
+ user: removeConfidentialServiceHashes(user),
339
+ });
340
+ },
341
+ };
342
+ };
@@ -0,0 +1,9 @@
1
+ import { GetUserCountryService } from '@unchainedshop/types/user';
2
+
3
+ export const getUserCountryService: GetUserCountryService = async (user, params, { modules }) => {
4
+ const userLocale = modules.users.userLocale(user, params);
5
+
6
+ return modules.countries.findCountry({
7
+ isoCode: userLocale.country.toUpperCase(),
8
+ });
9
+ };
@@ -0,0 +1,9 @@
1
+ import { GetUserLanguageService } from '@unchainedshop/types/user';
2
+
3
+ export const getUserLanguageService: GetUserLanguageService = async (user, params, { modules }) => {
4
+ const userLocale = modules.users.userLocale(user, params);
5
+
6
+ return modules.languages.findLanguage({
7
+ isoCode: userLocale.language,
8
+ });
9
+ };
@@ -0,0 +1,29 @@
1
+ import { GetUserRoleActionsService } from '@unchainedshop/types/user';
2
+ import { Roles } from '@unchainedshop/roles';
3
+
4
+ const permissions = async (userRoles: any, allRoles: any) => {
5
+ const actions = userRoles?.flatMap((role) => {
6
+ const foundRole: any = Object.values(allRoles).find((r: any) => r.name === role);
7
+ if (!foundRole) return [];
8
+ return Object.entries(foundRole.allowRules || {}).flatMap(([roleName, funcs]: any) => {
9
+ return funcs.map(async (f) => {
10
+ try {
11
+ // return permissions that are by default activated for that role,
12
+ // accidentally don't check context
13
+ const r = await f(null, null, null);
14
+ if (r) return roleName;
15
+ return null;
16
+ } catch (e) {
17
+ return null;
18
+ }
19
+ });
20
+ });
21
+ });
22
+ const resolvedActions = await Promise.all(actions);
23
+ return [...new Set(resolvedActions)].filter(Boolean).sort((a: any, b: any) => a.localeCompare(b));
24
+ };
25
+
26
+ export const getUserRoleActionsService: GetUserRoleActionsService = async (user, context) => {
27
+ const userRoles = Roles.getUserRoles(user?._id, user.roles, true);
28
+ return permissions(userRoles, context.roles) as Promise<string[]>;
29
+ };
@@ -0,0 +1,21 @@
1
+ import { UpdateUserAvatarAfterUploadService } from '@unchainedshop/types/user';
2
+
3
+ export const updateUserAvatarAfterUploadService: UpdateUserAvatarAfterUploadService = async (
4
+ { file },
5
+ context,
6
+ ) => {
7
+ const { modules, services, userId: currentUserId } = context;
8
+ const { userId } = file.meta as { userId: string };
9
+ const user = await modules.users.findUserById(userId);
10
+
11
+ if (user?.avatarId) {
12
+ await services.files.removeFiles(
13
+ {
14
+ fileIds: [user.avatarId as string],
15
+ },
16
+ context,
17
+ );
18
+ }
19
+
20
+ await modules.users.updateAvatar(userId, file._id, currentUserId || file.updatedBy || file.createdBy);
21
+ };
@@ -0,0 +1,12 @@
1
+ import { UserServices } from '@unchainedshop/types/user';
2
+ import { getUserCountryService } from './getUserCountryService';
3
+ import { getUserLanguageService } from './getUserLanguageService';
4
+ import { getUserRoleActionsService } from './getUserRoleActionsService';
5
+ import { updateUserAvatarAfterUploadService } from './updateUserAvatarAfterUploadService';
6
+
7
+ export const userServices: UserServices = {
8
+ getUserCountry: getUserCountryService,
9
+ getUserLanguage: getUserLanguageService,
10
+ getUserRoleActions: getUserRoleActionsService,
11
+ updateUserAvatarAfterUpload: updateUserAvatarAfterUploadService,
12
+ };
@@ -0,0 +1,3 @@
1
+ export { configureUsersModule } from './module/configureUsersModule';
2
+
3
+ export { userServices } from './services/userServices';
@@ -0,0 +1,47 @@
1
+ import { assert } from 'chai';
2
+ import { initDb } from 'meteor/unchained:mongodb';
3
+ import { configureUsersModule } from 'meteor/unchained:core-users';
4
+ import { UsersModule } from '@unchainedshop/types/user';
5
+ import { Db } from '@unchainedshop/types/common';
6
+
7
+ describe('Test exports', () => {
8
+ let module: UsersModule;
9
+ let db: Db;
10
+ let userId: string;
11
+
12
+ before(async () => {
13
+ db = await initDb();
14
+ module = await configureUsersModule({ db });
15
+ assert.ok(module);
16
+ });
17
+ before(async () => {
18
+ const Users = db.collection('users');
19
+ await Users.deleteMany({});
20
+ const insertResult = await Users.insertOne({
21
+ emails: [],
22
+ username: 'test.user',
23
+ lastLogin: {},
24
+ profile: {
25
+ displayName: 'Test User',
26
+ birthday: new Date('1995-01-12'),
27
+ phoneMobile: '+41701231212',
28
+ gender: 'Male',
29
+ },
30
+ guest: false,
31
+ tags: [],
32
+ services: {},
33
+ roles: ['admin'],
34
+ });
35
+
36
+ userId = insertResult.insertedId.toHexString();
37
+ });
38
+
39
+ it('Check queries', async () => {
40
+ assert.isTrue(await module.userExists({ userId }));
41
+ assert.isFalse(await module.userExists({ userId: '123123133123ABCD' }));
42
+ assert.equal(await module.count({ queryString: 'Test' }), 1);
43
+ assert.equal(await module.count({ queryString: 'Urrghh' }), 0);
44
+ assert.ok(await module.findUserById(userId));
45
+ assert.lengthOf(await module.findUsers({ limit: 10 }), 1);
46
+ });
47
+ });
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "allowSyntheticDefaultImports": true,
5
+ "declaration": true,
6
+ "esModuleInterop": true,
7
+ "experimentalDecorators": true,
8
+ "forceConsistentCasingInFileNames": true,
9
+ "lib": ["esnext"],
10
+ "module": "esnext",
11
+ "moduleResolution": "node",
12
+ "noImplicitReturns": true,
13
+ "noUnusedLocals": false,
14
+ "outDir": "lib",
15
+ "preserveWatchOutput": true,
16
+ "skipLibCheck": true,
17
+ "sourceMap": true,
18
+ "target": "esnext",
19
+ "types": ["node"],
20
+ "baseUrl": ".", // This must be specified if "paths" is.
21
+ "paths": {
22
+ "meteor/unchained:*": ["node_modules/@unchainedshop/types/index.d.ts"]
23
+ }
24
+ },
25
+ "include": ["src"]
26
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.build.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "types": ["node", "mocha"]
6
+ },
7
+ "include": ["src", "tests"]
8
+ }