@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
- ```typescript
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
- // Create a user
28
- const userId = await usersModule.createUser({
29
- email: 'user@example.com',
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
- // Update profile
34
- await usersModule.updateProfile(userId, {
35
- displayName: 'John Doe',
22
+ await users.updateProfile('user-123', {
23
+ profile: { displayName: 'Jane Doe' },
36
24
  });
37
25
  ```
38
26
 
39
- ## API Overview
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
- - Sensitive data (password hashes, tokens) stripped from event emissions via `removeConfidentialServiceHashes()`
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
- See [SECURITY.md](../../SECURITY.md) for complete security documentation.
31
+ For security reporting and the implementation's security properties, see [SECURITY.md](../../SECURITY.md).
153
32
 
154
33
  ## License
155
34
 
@@ -45,6 +45,8 @@ export type User = {
45
45
  pushSubscriptions: PushSubscriptionObject[];
46
46
  username?: string;
47
47
  meta?: any;
48
+ tokenVersion?: number;
49
+ oidcLogoutAt?: Date;
48
50
  } & TimestampFields;
49
51
  export interface UserQuery {
50
52
  includeGuests?: boolean;
@@ -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, escapeRegexString, } from '@unchainedshop/mongodb';
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 db.collection('sessions').deleteMany({
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: async (_id, roles) => {
765
- if (!roles.includes(ADMIN_ROLE))
766
- await assertAnotherActiveAdminExists(_id);
767
- const user = await Users.findOneAndUpdate(generateDbFilterById(_id), {
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
- await emit('USER_UPDATE_ROLE', {
776
- user: removeConfidentialServiceHashes(user),
777
- });
778
- return user;
800
+ return { tokenVersion: user.tokenVersion ?? 0 };
779
801
  },
780
- updateTags: async (_id, tags) => {
781
- const user = await Users.findOneAndUpdate(generateDbFilterById(_id), {
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
- returnDocument: 'after',
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.8.29",
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": "^4.8.27",
40
- "@unchainedshop/logger": "^4.8.27",
41
- "@unchainedshop/mongodb": "^4.8.27",
42
- "@unchainedshop/utils": "^4.8.27",
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": "^5.8.3"
64
+ "typescript": "^6.0.3"
62
65
  }
63
66
  }