@unchainedshop/core-users 5.0.0-alpha.4 → 5.0.0-alpha.5
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.
|
@@ -142,5 +142,16 @@ export declare const configureUsersModule: (moduleInput: ModuleInput<UserSetting
|
|
|
142
142
|
}) => Promise<void>;
|
|
143
143
|
removePushSubscription: (userId: string, p256dh: string) => Promise<void>;
|
|
144
144
|
existingTags: () => Promise<string[]>;
|
|
145
|
+
bulkUpdateTags: (userIds: string[], { add, remove }: {
|
|
146
|
+
add?: string[];
|
|
147
|
+
remove?: string[];
|
|
148
|
+
}) => Promise<{
|
|
149
|
+
successIds: string[];
|
|
150
|
+
failedIds: string[];
|
|
151
|
+
}>;
|
|
152
|
+
bulkUpdateRoles: (userIds: string[], roles: string[]) => Promise<{
|
|
153
|
+
successIds: string[];
|
|
154
|
+
failedIds: string[];
|
|
155
|
+
}>;
|
|
145
156
|
}>;
|
|
146
157
|
export type UsersModule = Awaited<ReturnType<typeof configureUsersModule>>;
|
|
@@ -2,7 +2,7 @@ import * as bcrypt from 'bcryptjs';
|
|
|
2
2
|
import { generateDbFilterById, buildSortOptions, generateDbObjectId, insensitiveTrimmedRegexOperator, } from '@unchainedshop/mongodb';
|
|
3
3
|
import { UsersCollection, } from "../db/UsersCollection.js";
|
|
4
4
|
import { emit, registerEvents } from '@unchainedshop/events';
|
|
5
|
-
import { systemLocale, SortDirection, sha256, normalizePhoneNumber, } from '@unchainedshop/utils';
|
|
5
|
+
import { executeBulkOperation, systemLocale, SortDirection, sha256, normalizePhoneNumber, } from '@unchainedshop/utils';
|
|
6
6
|
import { UserAccountAction, userSettings, } from "../users-settings.js";
|
|
7
7
|
import { configureUsersWebAuthnModule } from "./configureUsersWebAuthnModule.js";
|
|
8
8
|
import * as pbkdf2 from "./pbkdf2.js";
|
|
@@ -27,6 +27,7 @@ const USER_EVENTS = [
|
|
|
27
27
|
'USER_UPDATE_WEB3_ADDRESS',
|
|
28
28
|
'USER_REMOVE',
|
|
29
29
|
];
|
|
30
|
+
const ADMIN_ROLE = 'admin';
|
|
30
31
|
export const removeConfidentialServiceHashes = (rawUser) => {
|
|
31
32
|
const user = { ...rawUser };
|
|
32
33
|
delete user?.services;
|
|
@@ -88,6 +89,44 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
88
89
|
registerEvents(USER_EVENTS);
|
|
89
90
|
const Users = await UsersCollection(db);
|
|
90
91
|
const webAuthn = await configureUsersWebAuthnModule(moduleInput);
|
|
92
|
+
const assertAnotherActiveAdminExists = async (userId) => {
|
|
93
|
+
const user = await Users.findOne({ _id: userId, deleted: null });
|
|
94
|
+
if (!user?.roles?.includes(ADMIN_ROLE))
|
|
95
|
+
return;
|
|
96
|
+
const activeAdminCount = await Users.countDocuments({ deleted: null, roles: ADMIN_ROLE }, { limit: 2 });
|
|
97
|
+
if (activeAdminCount <= 1)
|
|
98
|
+
throw new Error('At least one active administrator is required. Assign another administrator before removing this account or revoking its admin role.', { cause: 'LAST_ADMIN' });
|
|
99
|
+
};
|
|
100
|
+
const updateUserRoles = async (_id, roles) => {
|
|
101
|
+
if (!roles.includes(ADMIN_ROLE))
|
|
102
|
+
await assertAnotherActiveAdminExists(_id);
|
|
103
|
+
const user = await Users.findOneAndUpdate(generateDbFilterById(_id), {
|
|
104
|
+
$set: {
|
|
105
|
+
updated: new Date(),
|
|
106
|
+
roles,
|
|
107
|
+
},
|
|
108
|
+
}, { returnDocument: 'after' });
|
|
109
|
+
if (!user)
|
|
110
|
+
return null;
|
|
111
|
+
await emit('USER_UPDATE_ROLE', {
|
|
112
|
+
user: removeConfidentialServiceHashes(user),
|
|
113
|
+
});
|
|
114
|
+
return user;
|
|
115
|
+
};
|
|
116
|
+
const updateUserTags = async (_id, tags) => {
|
|
117
|
+
const user = await Users.findOneAndUpdate(generateDbFilterById(_id), {
|
|
118
|
+
$set: {
|
|
119
|
+
updated: new Date(),
|
|
120
|
+
tags,
|
|
121
|
+
},
|
|
122
|
+
}, { returnDocument: 'after' });
|
|
123
|
+
if (!user)
|
|
124
|
+
return null;
|
|
125
|
+
await emit('USER_UPDATE_TAGS', {
|
|
126
|
+
user: removeConfidentialServiceHashes(user),
|
|
127
|
+
});
|
|
128
|
+
return user;
|
|
129
|
+
};
|
|
91
130
|
return {
|
|
92
131
|
webAuthn,
|
|
93
132
|
async count(query) {
|
|
@@ -606,6 +645,7 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
606
645
|
return user;
|
|
607
646
|
},
|
|
608
647
|
markDeleted: async (userId) => {
|
|
648
|
+
await assertAnotherActiveAdminExists(userId);
|
|
609
649
|
await Users.updateOne({ _id: userId }, { $inc: { tokenVersion: 1 } });
|
|
610
650
|
const user = await Users.findOneAndUpdate({ _id: userId }, {
|
|
611
651
|
$set: {
|
|
@@ -633,6 +673,7 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
633
673
|
return user;
|
|
634
674
|
},
|
|
635
675
|
deletePermanently: async ({ userId }) => {
|
|
676
|
+
await assertAnotherActiveAdminExists(userId);
|
|
636
677
|
return Users.findOneAndDelete({ _id: userId });
|
|
637
678
|
},
|
|
638
679
|
updateProfile: async (userId, updatedData) => {
|
|
@@ -748,23 +789,7 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
748
789
|
});
|
|
749
790
|
return updatedUser;
|
|
750
791
|
},
|
|
751
|
-
updateRoles:
|
|
752
|
-
const modifier = {
|
|
753
|
-
$set: {
|
|
754
|
-
updated: new Date(),
|
|
755
|
-
roles,
|
|
756
|
-
},
|
|
757
|
-
};
|
|
758
|
-
const user = await Users.findOneAndUpdate(generateDbFilterById(_id), modifier, {
|
|
759
|
-
returnDocument: 'after',
|
|
760
|
-
});
|
|
761
|
-
if (!user)
|
|
762
|
-
return null;
|
|
763
|
-
await emit('USER_UPDATE_ROLE', {
|
|
764
|
-
user: removeConfidentialServiceHashes(user),
|
|
765
|
-
});
|
|
766
|
-
return user;
|
|
767
|
-
},
|
|
792
|
+
updateRoles: updateUserRoles,
|
|
768
793
|
incrementTokenVersion: async (userId) => {
|
|
769
794
|
const user = await Users.findOneAndUpdate(generateDbFilterById(userId), {
|
|
770
795
|
$inc: { tokenVersion: 1 },
|
|
@@ -784,22 +809,7 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
784
809
|
}, { returnDocument: 'after' });
|
|
785
810
|
return user;
|
|
786
811
|
},
|
|
787
|
-
updateTags:
|
|
788
|
-
const user = await Users.findOneAndUpdate(generateDbFilterById(_id), {
|
|
789
|
-
$set: {
|
|
790
|
-
updated: new Date(),
|
|
791
|
-
tags,
|
|
792
|
-
},
|
|
793
|
-
}, {
|
|
794
|
-
returnDocument: 'after',
|
|
795
|
-
});
|
|
796
|
-
if (!user)
|
|
797
|
-
return null;
|
|
798
|
-
await emit('USER_UPDATE_TAGS', {
|
|
799
|
-
user: removeConfidentialServiceHashes(user),
|
|
800
|
-
});
|
|
801
|
-
return user;
|
|
802
|
-
},
|
|
812
|
+
updateTags: updateUserTags,
|
|
803
813
|
updateUser: async (selector, modifier, updateOptions) => {
|
|
804
814
|
const user = await Users.findOneAndUpdate(selector, modifier, {
|
|
805
815
|
...updateOptions,
|
|
@@ -843,5 +853,25 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
843
853
|
}));
|
|
844
854
|
return tags.filter(Boolean).toSorted();
|
|
845
855
|
},
|
|
856
|
+
bulkUpdateTags: async (userIds, { add = [], remove = [] }) => {
|
|
857
|
+
const tagsToRemove = new Set(remove);
|
|
858
|
+
return executeBulkOperation(userIds, async (userId) => {
|
|
859
|
+
const user = await Users.findOne({ _id: userId, deleted: null }, {});
|
|
860
|
+
if (!user)
|
|
861
|
+
throw new Error('not-found');
|
|
862
|
+
const tags = (user.tags || []).filter((tag) => !tagsToRemove.has(tag));
|
|
863
|
+
for (const tag of add) {
|
|
864
|
+
if (!tags.includes(tag))
|
|
865
|
+
tags.push(tag);
|
|
866
|
+
}
|
|
867
|
+
await updateUserTags(userId, tags);
|
|
868
|
+
});
|
|
869
|
+
},
|
|
870
|
+
bulkUpdateRoles: async (userIds, roles) => executeBulkOperation(userIds, async (userId) => {
|
|
871
|
+
const user = await Users.findOne({ _id: userId, deleted: null }, { projection: { _id: 1 } });
|
|
872
|
+
if (!user)
|
|
873
|
+
throw new Error('not-found');
|
|
874
|
+
await updateUserRoles(userId, roles);
|
|
875
|
+
}),
|
|
846
876
|
};
|
|
847
877
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unchainedshop/core-users",
|
|
3
3
|
"description": "User management module for the Unchained Engine with authentication support",
|
|
4
|
-
"version": "5.0.0-alpha.
|
|
4
|
+
"version": "5.0.0-alpha.5",
|
|
5
5
|
"main": "lib/users-index.js",
|
|
6
6
|
"types": "lib/users-index.d.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"homepage": "https://github.com/unchainedshop/unchained#readme",
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@passwordless-id/webauthn": "^2.3.1",
|
|
39
|
-
"@unchainedshop/events": "^5.0.0-alpha.
|
|
40
|
-
"@unchainedshop/logger": "^5.0.0-alpha.
|
|
41
|
-
"@unchainedshop/mongodb": "^5.0.0-alpha.
|
|
42
|
-
"@unchainedshop/utils": "^5.0.0-alpha.
|
|
39
|
+
"@unchainedshop/events": "^5.0.0-alpha.5",
|
|
40
|
+
"@unchainedshop/logger": "^5.0.0-alpha.5",
|
|
41
|
+
"@unchainedshop/mongodb": "^5.0.0-alpha.5",
|
|
42
|
+
"@unchainedshop/utils": "^5.0.0-alpha.5",
|
|
43
43
|
"bcryptjs": "^3.0.2"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|