@unchainedshop/core-users 4.8.29 → 5.0.0-alpha.10
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/README.md
CHANGED
|
@@ -13,143 +13,22 @@ npm install @unchainedshop/core-users
|
|
|
13
13
|
|
|
14
14
|
## Usage
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
import { configureUsersModule } from '@unchainedshop/core-users';
|
|
18
|
-
|
|
19
|
-
const usersModule = await configureUsersModule({ db });
|
|
20
|
-
|
|
21
|
-
// Find users
|
|
22
|
-
const users = await usersModule.findUsers({
|
|
23
|
-
includeGuests: false,
|
|
24
|
-
limit: 50,
|
|
25
|
-
});
|
|
16
|
+
The platform initializes this module as `platform.unchainedAPI.modules.users`.
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
password: 'securePassword',
|
|
31
|
-
});
|
|
18
|
+
```typescript
|
|
19
|
+
const { users } = platform.unchainedAPI.modules;
|
|
20
|
+
const customers = await users.findUsers({ includeGuests: false, limit: 50 });
|
|
32
21
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
displayName: 'John Doe',
|
|
22
|
+
await users.updateProfile('user-123', {
|
|
23
|
+
profile: { displayName: 'Jane Doe' },
|
|
36
24
|
});
|
|
37
25
|
```
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
### Module Configuration
|
|
42
|
-
|
|
43
|
-
| Export | Description |
|
|
44
|
-
|--------|-------------|
|
|
45
|
-
| `configureUsersModule` | Configure and return the users module |
|
|
46
|
-
| `configureUsersWebAuthnModule` | Configure WebAuthn submodule |
|
|
47
|
-
|
|
48
|
-
### Queries
|
|
49
|
-
|
|
50
|
-
| Method | Description |
|
|
51
|
-
|--------|-------------|
|
|
52
|
-
| `findUser` | Find user by ID, email, or username |
|
|
53
|
-
| `findUsers` | Find users with filtering, sorting, and pagination |
|
|
54
|
-
| `count` | Count users matching query |
|
|
55
|
-
| `userExists` | Check if a user exists |
|
|
56
|
-
|
|
57
|
-
### Mutations
|
|
58
|
-
|
|
59
|
-
| Method | Description |
|
|
60
|
-
|--------|-------------|
|
|
61
|
-
| `createUser` | Create a new user account |
|
|
62
|
-
| `updateUser` | Update user data |
|
|
63
|
-
| `updateProfile` | Update user profile |
|
|
64
|
-
| `updateRoles` | Update user roles |
|
|
65
|
-
| `updateTags` | Update user tags |
|
|
66
|
-
| `updateAvatar` | Set user avatar |
|
|
67
|
-
| `updateBillingAddress` | Update billing address |
|
|
68
|
-
| `updatePassword` | Change user password |
|
|
69
|
-
| `updateUsername` | Change username |
|
|
70
|
-
| `delete` | Soft delete a user |
|
|
71
|
-
| `replaceUserId` | Migrate data between users |
|
|
72
|
-
|
|
73
|
-
### Authentication
|
|
74
|
-
|
|
75
|
-
| Method | Description |
|
|
76
|
-
|--------|-------------|
|
|
77
|
-
| `verifyPassword` | Verify password against stored hash |
|
|
78
|
-
| `hashPassword` | Hash a password for storage |
|
|
79
|
-
| `addEmail` | Add email address to user |
|
|
80
|
-
| `removeEmail` | Remove email address |
|
|
81
|
-
| `verifyEmail` | Mark email as verified |
|
|
82
|
-
| `updateHeartbeat` | Update last activity timestamp |
|
|
83
|
-
| `updateLastLogin` | Record login event |
|
|
84
|
-
|
|
85
|
-
### WebAuthn Submodule
|
|
86
|
-
|
|
87
|
-
| Method | Description |
|
|
88
|
-
|--------|-------------|
|
|
89
|
-
| `webAuthn.findCredentials` | Find WebAuthn credentials for user |
|
|
90
|
-
| `webAuthn.createCredential` | Register new WebAuthn credential |
|
|
91
|
-
| `webAuthn.removeCredential` | Remove WebAuthn credential |
|
|
92
|
-
|
|
93
|
-
### Settings
|
|
94
|
-
|
|
95
|
-
| Export | Description |
|
|
96
|
-
|--------|-------------|
|
|
97
|
-
| `userSettings` | Access user module settings |
|
|
98
|
-
| `UserAccountAction` | Account action types enum |
|
|
99
|
-
|
|
100
|
-
### Types
|
|
101
|
-
|
|
102
|
-
| Export | Description |
|
|
103
|
-
|--------|-------------|
|
|
104
|
-
| `User` | User document type |
|
|
105
|
-
| `UserQuery` | Query parameters type |
|
|
106
|
-
| `UserProfile` | User profile type |
|
|
107
|
-
| `Email` | Email address type |
|
|
108
|
-
| `UsersModule` | Module interface type |
|
|
109
|
-
| `UserSettingsOptions` | Configuration options type |
|
|
110
|
-
|
|
111
|
-
## Events
|
|
112
|
-
|
|
113
|
-
| Event | Description |
|
|
114
|
-
|-------|-------------|
|
|
115
|
-
| `USER_CREATE` | User created |
|
|
116
|
-
| `USER_UPDATE` | User updated |
|
|
117
|
-
| `USER_REMOVE` | User deleted |
|
|
118
|
-
| `USER_UPDATE_PROFILE` | Profile updated |
|
|
119
|
-
| `USER_UPDATE_PASSWORD` | Password changed |
|
|
120
|
-
| `USER_UPDATE_ROLES` | Roles changed |
|
|
121
|
-
| `USER_ACCOUNT_ACTION` | Account action triggered |
|
|
122
|
-
|
|
123
|
-
## Security
|
|
124
|
-
|
|
125
|
-
This module implements security best practices for user authentication and data protection.
|
|
126
|
-
|
|
127
|
-
### Password Security
|
|
128
|
-
|
|
129
|
-
- **Algorithm**: PBKDF2 with SHA-512
|
|
130
|
-
- **Iterations**: 300,000 (exceeds OWASP recommendation)
|
|
131
|
-
- **Salt**: 16 bytes, cryptographically random per password
|
|
132
|
-
- **Key Length**: 256 bytes
|
|
133
|
-
- **FIPS 140-3**: Compatible when running on FIPS-enabled Node.js
|
|
134
|
-
|
|
135
|
-
### Token Security
|
|
136
|
-
|
|
137
|
-
- **Generation**: `crypto.randomUUID()` (CSPRNG-based)
|
|
138
|
-
- **Storage**: SHA-256 hashed before database storage
|
|
139
|
-
- **Expiration**: Time-limited (configurable, default 1 hour)
|
|
140
|
-
- **Single-use**: Tokens invalidated after verification
|
|
141
|
-
|
|
142
|
-
### WebAuthn/FIDO2
|
|
143
|
-
|
|
144
|
-
Full support for passwordless authentication via hardware security keys and platform authenticators, providing phishing-resistant authentication.
|
|
145
|
-
|
|
146
|
-
### Data Protection
|
|
27
|
+
`updateProfile` accepts an object containing `profile` and optional `meta`. Account creation uses `createUser`; account deletion uses `markDeleted` or `deletePermanently`. Authentication flows and permissions are coordinated by the API layer.
|
|
147
28
|
|
|
148
|
-
|
|
149
|
-
- Soft delete preserves audit trail while removing PII
|
|
150
|
-
- Email addresses and profile data access-controlled via RBAC
|
|
29
|
+
See the [module settings](https://docs.unchained.shop/platform-configuration/modules/users), [authentication guide](https://docs.unchained.shop/concepts/authentication), [public exports](src/users-index.ts), and [module implementation](src/module/configureUsersModule.ts).
|
|
151
30
|
|
|
152
|
-
|
|
31
|
+
For security reporting and the implementation's security properties, see [SECURITY.md](../../SECURITY.md).
|
|
153
32
|
|
|
154
33
|
## License
|
|
155
34
|
|
|
@@ -130,6 +130,10 @@ export declare const configureUsersModule: (moduleInput: ModuleInput<UserSetting
|
|
|
130
130
|
updateLastBillingAddress: (_id: string, lastBillingAddress: Address) => Promise<mongodb.WithId<User> | null>;
|
|
131
131
|
updateLastContact: (_id: string, lastContact: Contact) => Promise<mongodb.WithId<User> | null>;
|
|
132
132
|
updateRoles: (_id: string, roles: string[]) => Promise<mongodb.WithId<User> | null>;
|
|
133
|
+
incrementTokenVersion: (userId: string) => Promise<{
|
|
134
|
+
tokenVersion: number;
|
|
135
|
+
} | null>;
|
|
136
|
+
updateOidcLogoutAt: (userId: string, date: Date) => Promise<User | null>;
|
|
133
137
|
updateTags: (_id: string, tags: string[]) => Promise<mongodb.WithId<User> | null>;
|
|
134
138
|
updateUser: (selector: mongodb.Filter<User>, modifier: mongodb.UpdateFilter<User>, updateOptions?: mongodb.FindOneAndUpdateOptions) => Promise<mongodb.WithId<User> | null>;
|
|
135
139
|
addPushSubscription: (userId: string, subscription: any, subscriptionOptions?: {
|
|
@@ -138,5 +142,16 @@ export declare const configureUsersModule: (moduleInput: ModuleInput<UserSetting
|
|
|
138
142
|
}) => Promise<void>;
|
|
139
143
|
removePushSubscription: (userId: string, p256dh: string) => Promise<void>;
|
|
140
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
|
+
}>;
|
|
141
156
|
}>;
|
|
142
157
|
export type UsersModule = Awaited<ReturnType<typeof configureUsersModule>>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import * as bcrypt from 'bcryptjs';
|
|
2
|
-
import { generateDbFilterById, buildSortOptions, generateDbObjectId, insensitiveTrimmedRegexOperator,
|
|
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";
|
|
@@ -97,6 +97,36 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
97
97
|
if (activeAdminCount <= 1)
|
|
98
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
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
|
+
};
|
|
100
130
|
return {
|
|
101
131
|
webAuthn,
|
|
102
132
|
async count(query) {
|
|
@@ -616,9 +646,7 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
616
646
|
},
|
|
617
647
|
markDeleted: async (userId) => {
|
|
618
648
|
await assertAnotherActiveAdminExists(userId);
|
|
619
|
-
await
|
|
620
|
-
session: { $regex: `"user(Id)?":"${escapeRegexString(userId)}"` },
|
|
621
|
-
});
|
|
649
|
+
await Users.updateOne({ _id: userId }, { $inc: { tokenVersion: 1 } });
|
|
622
650
|
const user = await Users.findOneAndUpdate({ _id: userId }, {
|
|
623
651
|
$set: {
|
|
624
652
|
username: `deleted-${Date.now()}`,
|
|
@@ -761,38 +789,27 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
761
789
|
});
|
|
762
790
|
return updatedUser;
|
|
763
791
|
},
|
|
764
|
-
updateRoles:
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
$set: {
|
|
769
|
-
updated: new Date(),
|
|
770
|
-
roles,
|
|
771
|
-
},
|
|
792
|
+
updateRoles: updateUserRoles,
|
|
793
|
+
incrementTokenVersion: async (userId) => {
|
|
794
|
+
const user = await Users.findOneAndUpdate(generateDbFilterById(userId), {
|
|
795
|
+
$inc: { tokenVersion: 1 },
|
|
796
|
+
$set: { updated: new Date() },
|
|
772
797
|
}, { returnDocument: 'after' });
|
|
773
798
|
if (!user)
|
|
774
799
|
return null;
|
|
775
|
-
|
|
776
|
-
user: removeConfidentialServiceHashes(user),
|
|
777
|
-
});
|
|
778
|
-
return user;
|
|
800
|
+
return { tokenVersion: user.tokenVersion ?? 0 };
|
|
779
801
|
},
|
|
780
|
-
|
|
781
|
-
const user = await Users.findOneAndUpdate(generateDbFilterById(
|
|
802
|
+
updateOidcLogoutAt: async (userId, date) => {
|
|
803
|
+
const user = await Users.findOneAndUpdate(generateDbFilterById(userId), {
|
|
782
804
|
$set: {
|
|
805
|
+
oidcLogoutAt: date,
|
|
783
806
|
updated: new Date(),
|
|
784
|
-
tags,
|
|
785
807
|
},
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
});
|
|
789
|
-
if (!user)
|
|
790
|
-
return null;
|
|
791
|
-
await emit('USER_UPDATE_TAGS', {
|
|
792
|
-
user: removeConfidentialServiceHashes(user),
|
|
793
|
-
});
|
|
808
|
+
$inc: { tokenVersion: 1 },
|
|
809
|
+
}, { returnDocument: 'after' });
|
|
794
810
|
return user;
|
|
795
811
|
},
|
|
812
|
+
updateTags: updateUserTags,
|
|
796
813
|
updateUser: async (selector, modifier, updateOptions) => {
|
|
797
814
|
const user = await Users.findOneAndUpdate(selector, modifier, {
|
|
798
815
|
...updateOptions,
|
|
@@ -836,5 +853,25 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
836
853
|
}));
|
|
837
854
|
return tags.filter(Boolean).toSorted();
|
|
838
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
|
+
}),
|
|
839
876
|
};
|
|
840
877
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unchainedshop/core-users",
|
|
3
3
|
"description": "User management module for the Unchained Engine with authentication support",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "5.0.0-alpha.10",
|
|
5
|
+
"files": [
|
|
6
|
+
"lib"
|
|
7
|
+
],
|
|
5
8
|
"main": "lib/users-index.js",
|
|
6
9
|
"types": "lib/users-index.d.ts",
|
|
7
10
|
"type": "module",
|
|
@@ -36,10 +39,10 @@
|
|
|
36
39
|
"homepage": "https://github.com/unchainedshop/unchained#readme",
|
|
37
40
|
"dependencies": {
|
|
38
41
|
"@passwordless-id/webauthn": "^2.3.1",
|
|
39
|
-
"@unchainedshop/events": "^
|
|
40
|
-
"@unchainedshop/logger": "^
|
|
41
|
-
"@unchainedshop/mongodb": "^
|
|
42
|
-
"@unchainedshop/utils": "^
|
|
42
|
+
"@unchainedshop/events": "^5.0.0-alpha.10",
|
|
43
|
+
"@unchainedshop/logger": "^5.0.0-alpha.10",
|
|
44
|
+
"@unchainedshop/mongodb": "^5.0.0-alpha.10",
|
|
45
|
+
"@unchainedshop/utils": "^5.0.0-alpha.10",
|
|
43
46
|
"bcryptjs": "^3.0.2"
|
|
44
47
|
},
|
|
45
48
|
"peerDependencies": {
|
|
@@ -58,6 +61,6 @@
|
|
|
58
61
|
"@noble/curves": "^2.0.0",
|
|
59
62
|
"@noble/hashes": "^2.0.0",
|
|
60
63
|
"@types/node": "^26.2.0",
|
|
61
|
-
"typescript": "^
|
|
64
|
+
"typescript": "^6.0.3"
|
|
62
65
|
}
|
|
63
66
|
}
|