@unchainedshop/core-users 4.3.5 → 4.5.0
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 +124 -2
- package/lib/db/UsersCollection.d.ts +8 -5
- package/lib/db/UsersCollection.js +0 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts +1 -2
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js +0 -1
- package/lib/migrations/20241218092300-convert-locale.d.ts +2 -0
- package/lib/migrations/20241218092300-convert-locale.js +36 -0
- package/lib/module/configureUsersModule.d.ts +53 -23
- package/lib/module/configureUsersModule.js +127 -18
- package/lib/module/configureUsersWebAuthnModule.d.ts +78 -20
- package/lib/module/configureUsersWebAuthnModule.js +126 -112
- package/lib/module/pbkdf2.d.ts +0 -1
- package/lib/module/pbkdf2.js +3 -7
- package/lib/users-index.d.ts +5 -6
- package/lib/users-index.js +5 -6
- package/lib/users-settings.d.ts +11 -11
- package/lib/users-settings.js +7 -10
- package/lib/utils/web3-verification.d.ts +1 -0
- package/lib/utils/web3-verification.js +81 -0
- package/package.json +26 -16
- package/lib/db/UsersCollection.d.ts.map +0 -1
- package/lib/db/UsersCollection.js.map +0 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.d.ts.map +0 -1
- package/lib/db/WebAuthnCredentialsCreationRequestsCollection.js.map +0 -1
- package/lib/module/configureUsersModule.d.ts.map +0 -1
- package/lib/module/configureUsersModule.js.map +0 -1
- package/lib/module/configureUsersWebAuthnModule.d.ts.map +0 -1
- package/lib/module/configureUsersWebAuthnModule.js.map +0 -1
- package/lib/module/pbkdf2.d.ts.map +0 -1
- package/lib/module/pbkdf2.js.map +0 -1
- package/lib/users-index.d.ts.map +0 -1
- package/lib/users-index.js.map +0 -1
- package/lib/users-settings.d.ts.map +0 -1
- package/lib/users-settings.js.map +0 -1
- package/src/db/UsersCollection.ts +0 -174
- package/src/db/WebAuthnCredentialsCreationRequestsCollection.ts +0 -26
- package/src/module/buildFindSelector.test.ts +0 -34
- package/src/module/configureUsersModule.ts +0 -857
- package/src/module/configureUsersWebAuthnModule.ts +0 -234
- package/src/module/pbkdf2.ts +0 -46
- package/src/module/removeConfidentialServiceHashes.test.ts +0 -12
- package/src/users-index.ts +0 -5
- package/src/users-settings.ts +0 -99
- package/tests/mock/user-mock.ts +0 -72
- package/tsconfig.json +0 -10
package/README.md
CHANGED
|
@@ -1,3 +1,125 @@
|
|
|
1
|
-
|
|
1
|
+
[](https://npmjs.com/package/@unchainedshop/core-users)
|
|
2
|
+
[](https://opensource.org/licenses/EUPL-1.2)
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
# @unchainedshop/core-users
|
|
5
|
+
|
|
6
|
+
User management module for the Unchained Engine. Handles user accounts, authentication, profiles, and WebAuthn support.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm install @unchainedshop/core-users
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
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
|
+
});
|
|
26
|
+
|
|
27
|
+
// Create a user
|
|
28
|
+
const userId = await usersModule.createUser({
|
|
29
|
+
email: 'user@example.com',
|
|
30
|
+
password: 'securePassword',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// Update profile
|
|
34
|
+
await usersModule.updateProfile(userId, {
|
|
35
|
+
displayName: 'John Doe',
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
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
|
+
## License
|
|
124
|
+
|
|
125
|
+
EUPL-1.2
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { TimestampFields, Address, Contact, mongodb } from '@unchainedshop/mongodb';
|
|
2
|
-
import { DateFilterInput } from '@unchainedshop/utils';
|
|
1
|
+
import { type TimestampFields, type Address, type Contact, type mongodb } from '@unchainedshop/mongodb';
|
|
2
|
+
import type { DateFilterInput } from '@unchainedshop/utils';
|
|
3
3
|
export interface UserProfile {
|
|
4
4
|
displayName?: string;
|
|
5
5
|
birthday?: Date;
|
|
@@ -46,12 +46,15 @@ export type User = {
|
|
|
46
46
|
username?: string;
|
|
47
47
|
meta?: any;
|
|
48
48
|
} & TimestampFields;
|
|
49
|
-
export
|
|
49
|
+
export interface UserQuery {
|
|
50
50
|
includeGuests?: boolean;
|
|
51
51
|
includeDeleted?: boolean;
|
|
52
52
|
queryString?: string;
|
|
53
53
|
emailVerified?: boolean;
|
|
54
54
|
lastLogin?: DateFilterInput;
|
|
55
|
-
|
|
55
|
+
tags?: string[];
|
|
56
|
+
userIds?: string[];
|
|
57
|
+
username?: string;
|
|
58
|
+
web3Verified?: boolean;
|
|
59
|
+
}
|
|
56
60
|
export declare const UsersCollection: (db: mongodb.Db) => Promise<mongodb.Collection<User>>;
|
|
57
|
-
//# sourceMappingURL=UsersCollection.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mongodb } from '@unchainedshop/mongodb';
|
|
1
|
+
import { type mongodb } from '@unchainedshop/mongodb';
|
|
2
2
|
export interface WebAuthnCredentialsCreationRequest {
|
|
3
3
|
challenge: string;
|
|
4
4
|
username: string;
|
|
@@ -10,4 +10,3 @@ type Collection = WebAuthnCredentialsCreationRequest & {
|
|
|
10
10
|
};
|
|
11
11
|
export declare const WebAuthnCredentialsCreationRequestsCollection: (db: mongodb.Db) => Promise<mongodb.Collection<Collection>>;
|
|
12
12
|
export {};
|
|
13
|
-
//# sourceMappingURL=WebAuthnCredentialsCreationRequestsCollection.d.ts.map
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { UsersCollection } from "../db/UsersCollection.js";
|
|
2
|
+
import { systemLocale } from '@unchainedshop/utils';
|
|
3
|
+
export default function convertUserLocale(repository) {
|
|
4
|
+
repository?.register({
|
|
5
|
+
id: 20241218092300,
|
|
6
|
+
name: 'Convert user.lastLogin.locale',
|
|
7
|
+
up: async () => {
|
|
8
|
+
const Users = await UsersCollection(repository.db);
|
|
9
|
+
const users = await Users.find({
|
|
10
|
+
'lastLogin.locale': { $exists: true },
|
|
11
|
+
}, { projection: { _id: true, lastLogin: true } }).toArray();
|
|
12
|
+
for (const user of users) {
|
|
13
|
+
let newLocale;
|
|
14
|
+
const currentLocale = user.lastLogin?.locale;
|
|
15
|
+
if (!currentLocale)
|
|
16
|
+
continue;
|
|
17
|
+
try {
|
|
18
|
+
newLocale = new Intl.Locale(currentLocale).baseName;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
try {
|
|
22
|
+
newLocale = new Intl.Locale(currentLocale.split('_').join('-')).baseName;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
newLocale = systemLocale.baseName;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
await Users.updateOne({
|
|
29
|
+
_id: user._id,
|
|
30
|
+
}, {
|
|
31
|
+
$set: { 'lastLogin.locale': newLocale },
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -1,29 +1,42 @@
|
|
|
1
|
-
import { ModuleInput, Address, Contact, mongodb } from '@unchainedshop/mongodb';
|
|
2
|
-
import { User, UserQuery, Email, UserLastLogin, UserProfile } from '../db/UsersCollection.
|
|
3
|
-
import { SortOption } from '@unchainedshop/utils';
|
|
4
|
-
import { UserRegistrationData, UserSettingsOptions } from '../users-settings.
|
|
1
|
+
import { type ModuleInput, type Address, type Contact, type mongodb } from '@unchainedshop/mongodb';
|
|
2
|
+
import { type User, type UserQuery, type Email, type UserLastLogin, type UserProfile } from '../db/UsersCollection.ts';
|
|
3
|
+
import { type SortOption } from '@unchainedshop/utils';
|
|
4
|
+
import { type UserRegistrationData, type UserSettingsOptions } from '../users-settings.ts';
|
|
5
5
|
export declare const removeConfidentialServiceHashes: (rawUser: User) => User;
|
|
6
|
-
export declare const buildFindSelector: ({ includeGuests, includeDeleted, queryString, emailVerified, lastLogin, tags,
|
|
6
|
+
export declare const buildFindSelector: ({ includeGuests, includeDeleted, queryString, emailVerified, lastLogin, tags, userIds, username, web3Verified, }: UserQuery) => mongodb.Filter<User>;
|
|
7
7
|
export declare const configureUsersModule: (moduleInput: ModuleInput<UserSettingsOptions>) => Promise<{
|
|
8
8
|
webAuthn: {
|
|
9
|
-
findMDSMetadataForAAGUID: (aaguid: string) => Promise<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
verifyCredentialCreation: (username: string, credentials: any) => Promise<{
|
|
19
|
-
publicKey: any;
|
|
20
|
-
counter: any;
|
|
21
|
-
id: any;
|
|
22
|
-
aaguid: string;
|
|
23
|
-
created: Date;
|
|
9
|
+
findMDSMetadataForAAGUID: (aaguid: string) => Promise<{
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
description?: string;
|
|
12
|
+
icon?: string;
|
|
13
|
+
authenticatorGetInfo?: {
|
|
14
|
+
versions?: string[];
|
|
15
|
+
extensions?: string[];
|
|
16
|
+
options?: Record<string, boolean>;
|
|
17
|
+
};
|
|
24
18
|
} | null>;
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
createCredentialCreationOptions: (origin: string, username: string, extensionOptions?: {
|
|
20
|
+
timeout?: number;
|
|
21
|
+
authenticatorSelection?: import("./configureUsersWebAuthnModule.ts").WebAuthnCredentialCreationOptions["authenticatorSelection"];
|
|
22
|
+
}) => Promise<import("./configureUsersWebAuthnModule.ts").WebAuthnCredentialCreationOptions>;
|
|
23
|
+
createCredentialRequestOptions: (origin: string, username: string, extensionOptions?: {
|
|
24
|
+
timeout?: number;
|
|
25
|
+
userVerification?: "required" | "preferred" | "discouraged";
|
|
26
|
+
allowCredentials?: import("./configureUsersWebAuthnModule.ts").WebAuthnCredentialRequestOptions["allowCredentials"];
|
|
27
|
+
}) => Promise<import("./configureUsersWebAuthnModule.ts").WebAuthnCredentialRequestOptions>;
|
|
28
|
+
verifyCredentialCreation: (username: string, credentials: import("@passwordless-id/webauthn/dist/esm/types.js").RegistrationJSON) => Promise<import("./configureUsersWebAuthnModule.ts").WebAuthnCredential | null>;
|
|
29
|
+
verifyCredentialRequest: (userPublicKeys: {
|
|
30
|
+
id: string;
|
|
31
|
+
publicKey: string;
|
|
32
|
+
algorithm?: import("@passwordless-id/webauthn/dist/esm/types.js").NamedAlgo;
|
|
33
|
+
counter?: number;
|
|
34
|
+
transports?: import("@passwordless-id/webauthn/dist/esm/types.js").ExtendedAuthenticatorTransport[];
|
|
35
|
+
}[], username: string, credentials: import("@passwordless-id/webauthn/dist/esm/types.js").AuthenticationJSON & {
|
|
36
|
+
requestId: number;
|
|
37
|
+
}) => Promise<{
|
|
38
|
+
userHandle: string;
|
|
39
|
+
counter: number;
|
|
27
40
|
} | null>;
|
|
28
41
|
deleteUserWebAuthnCredentials: (username: string) => Promise<number>;
|
|
29
42
|
};
|
|
@@ -69,6 +82,24 @@ export declare const configureUsersModule: (moduleInput: ModuleInput<UserSetting
|
|
|
69
82
|
}, plainPassword: string): Promise<boolean>;
|
|
70
83
|
addEmail(userId: string, address: string): Promise<void>;
|
|
71
84
|
removeEmail(userId: string, address: string): Promise<void>;
|
|
85
|
+
addWeb3Address(userId: string, address: string): Promise<User | null>;
|
|
86
|
+
removeWeb3Address(userId: string, address: string): Promise<User | null>;
|
|
87
|
+
findWeb3Address(user: User, address: string): {
|
|
88
|
+
address: string;
|
|
89
|
+
nonce?: string;
|
|
90
|
+
verified?: boolean;
|
|
91
|
+
} | null;
|
|
92
|
+
addWebAuthnCredential(userId: string, webAuthnService: {
|
|
93
|
+
id: string;
|
|
94
|
+
publicKey: string;
|
|
95
|
+
created: Date;
|
|
96
|
+
}): Promise<User | null>;
|
|
97
|
+
removeWebAuthnCredential(userId: string, credentialsId: string): Promise<User | null>;
|
|
98
|
+
setAccessToken(username: string, plainSecret: string): Promise<User | null>;
|
|
99
|
+
verifyWeb3SignatureAndUpdate(user: User, credentials: {
|
|
100
|
+
address: string;
|
|
101
|
+
nonce: string;
|
|
102
|
+
}, signature: `0x${string}`): Promise<User | null>;
|
|
72
103
|
sendResetPasswordEmail(userId: string, email: string, isEnrollment?: boolean): Promise<void>;
|
|
73
104
|
sendVerificationEmail(userId: string, email: string): Promise<void>;
|
|
74
105
|
addRoles: (userId: string, roles: string[]) => Promise<mongodb.WithId<User> | null>;
|
|
@@ -99,4 +130,3 @@ export declare const configureUsersModule: (moduleInput: ModuleInput<UserSetting
|
|
|
99
130
|
existingTags: () => Promise<string[]>;
|
|
100
131
|
}>;
|
|
101
132
|
export type UsersModule = Awaited<ReturnType<typeof configureUsersModule>>;
|
|
102
|
-
//# sourceMappingURL=configureUsersModule.d.ts.map
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import * as bcrypt from 'bcryptjs';
|
|
2
2
|
import { generateDbFilterById, buildSortOptions, generateDbObjectId, insensitiveTrimmedRegexOperator, assertDocumentDBCompatMode, } from '@unchainedshop/mongodb';
|
|
3
|
-
import { UsersCollection, } from
|
|
3
|
+
import { UsersCollection, } from "../db/UsersCollection.js";
|
|
4
4
|
import { emit, registerEvents } from '@unchainedshop/events';
|
|
5
5
|
import { systemLocale, SortDirection, sha256 } from '@unchainedshop/utils';
|
|
6
|
-
import { UserAccountAction, userSettings, } from
|
|
7
|
-
import { configureUsersWebAuthnModule } from
|
|
8
|
-
import * as pbkdf2 from
|
|
6
|
+
import { UserAccountAction, userSettings, } from "../users-settings.js";
|
|
7
|
+
import { configureUsersWebAuthnModule } from "./configureUsersWebAuthnModule.js";
|
|
8
|
+
import * as pbkdf2 from "./pbkdf2.js";
|
|
9
|
+
import { verifyWeb3Signature } from "../utils/web3-verification.js";
|
|
10
|
+
import convertUserLocale from "../migrations/20241218092300-convert-locale.js";
|
|
9
11
|
const USER_EVENTS = [
|
|
10
12
|
'USER_ACCOUNT_ACTION',
|
|
11
13
|
'USER_CREATE',
|
|
@@ -21,6 +23,7 @@ const USER_EVENTS = [
|
|
|
21
23
|
'USER_UPDATE_HEARTBEAT',
|
|
22
24
|
'USER_UPDATE_BILLING_ADDRESS',
|
|
23
25
|
'USER_UPDATE_LAST_CONTACT',
|
|
26
|
+
'USER_UPDATE_WEB3_ADDRESS',
|
|
24
27
|
'USER_REMOVE',
|
|
25
28
|
];
|
|
26
29
|
export const removeConfidentialServiceHashes = (rawUser) => {
|
|
@@ -28,12 +31,18 @@ export const removeConfidentialServiceHashes = (rawUser) => {
|
|
|
28
31
|
delete user?.services;
|
|
29
32
|
return user;
|
|
30
33
|
};
|
|
31
|
-
export const buildFindSelector = ({ includeGuests, includeDeleted, queryString, emailVerified, lastLogin, tags,
|
|
32
|
-
const selector = {
|
|
34
|
+
export const buildFindSelector = ({ includeGuests, includeDeleted, queryString, emailVerified, lastLogin, tags, userIds, username, web3Verified, }) => {
|
|
35
|
+
const selector = {};
|
|
33
36
|
if (!includeDeleted)
|
|
34
37
|
selector.deleted = null;
|
|
35
38
|
if (!includeGuests)
|
|
36
39
|
selector.guest = { $ne: true };
|
|
40
|
+
if (userIds) {
|
|
41
|
+
selector._id = { $in: userIds };
|
|
42
|
+
}
|
|
43
|
+
if (username) {
|
|
44
|
+
selector.username = insensitiveTrimmedRegexOperator(username);
|
|
45
|
+
}
|
|
37
46
|
if (emailVerified === true) {
|
|
38
47
|
selector['emails.verified'] = true;
|
|
39
48
|
}
|
|
@@ -41,10 +50,11 @@ export const buildFindSelector = ({ includeGuests, includeDeleted, queryString,
|
|
|
41
50
|
selector.tags = { $in: tags };
|
|
42
51
|
}
|
|
43
52
|
if (emailVerified === false) {
|
|
44
|
-
// We need to use $ne here else we'd also find users with many emails where one is
|
|
45
|
-
// unverified
|
|
46
53
|
selector['emails.verified'] = { $ne: true };
|
|
47
54
|
}
|
|
55
|
+
if (web3Verified === true) {
|
|
56
|
+
selector['services.web3.verified'] = true;
|
|
57
|
+
}
|
|
48
58
|
if (lastLogin?.start) {
|
|
49
59
|
selector['lastLogin.timestamp'] = { $exists: true };
|
|
50
60
|
}
|
|
@@ -61,13 +71,13 @@ export const buildFindSelector = ({ includeGuests, includeDeleted, queryString,
|
|
|
61
71
|
return selector;
|
|
62
72
|
};
|
|
63
73
|
export const configureUsersModule = async (moduleInput) => {
|
|
64
|
-
const { db, options } = moduleInput;
|
|
74
|
+
const { db, options, migrationRepository } = moduleInput;
|
|
75
|
+
convertUserLocale(migrationRepository);
|
|
65
76
|
userSettings.configureSettings(options || {}, db);
|
|
66
77
|
registerEvents(USER_EVENTS);
|
|
67
78
|
const Users = await UsersCollection(db);
|
|
68
79
|
const webAuthn = await configureUsersWebAuthnModule(moduleInput);
|
|
69
80
|
return {
|
|
70
|
-
// Queries
|
|
71
81
|
webAuthn,
|
|
72
82
|
async count(query) {
|
|
73
83
|
const userCount = await Users.countDocuments(buildFindSelector(query));
|
|
@@ -182,7 +192,6 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
182
192
|
const userCount = await Users.countDocuments({ _id: userId, deleted: { $exists: false } }, { limit: 1 });
|
|
183
193
|
return userCount === 1;
|
|
184
194
|
},
|
|
185
|
-
// Transformations
|
|
186
195
|
primaryEmail(user) {
|
|
187
196
|
return (user.emails || []).toSorted((left, right) => Number(right.verified) - Number(left.verified))?.[0];
|
|
188
197
|
},
|
|
@@ -196,11 +205,13 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
196
205
|
return systemLocale;
|
|
197
206
|
}
|
|
198
207
|
},
|
|
199
|
-
// Mutations
|
|
200
208
|
async createUser(rawUserData, { skipMessaging, skipPasswordEnrollment, } = {}) {
|
|
201
209
|
const { password, email, username, initialPassword, roles, webAuthnPublicKeyCredentials, ...userData } = await userSettings.validateNewUser(rawUserData);
|
|
202
210
|
const webAuthnService = webAuthnPublicKeyCredentials &&
|
|
203
211
|
(await this.webAuthn.verifyCredentialCreation(username, webAuthnPublicKeyCredentials));
|
|
212
|
+
if (webAuthnPublicKeyCredentials && !webAuthnService) {
|
|
213
|
+
throw new Error('WebAuthn credential verification failed', { cause: 'WEBAUTHN_INVALID' });
|
|
214
|
+
}
|
|
204
215
|
const services = {};
|
|
205
216
|
if (email) {
|
|
206
217
|
if (!(await userSettings.validateEmail(email))) {
|
|
@@ -250,7 +261,6 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
250
261
|
}
|
|
251
262
|
}
|
|
252
263
|
catch {
|
|
253
|
-
/* */
|
|
254
264
|
}
|
|
255
265
|
const user = (await Users.findOne({ _id: userId }, {}));
|
|
256
266
|
await emit('USER_CREATE', {
|
|
@@ -294,6 +304,110 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
294
304
|
},
|
|
295
305
|
});
|
|
296
306
|
},
|
|
307
|
+
async addWeb3Address(userId, address) {
|
|
308
|
+
const user = await Users.findOne(generateDbFilterById(userId), {});
|
|
309
|
+
if (!user)
|
|
310
|
+
return null;
|
|
311
|
+
const existingEntry = user.services?.web3?.find((service) => service.address.toLowerCase() === address.toLowerCase());
|
|
312
|
+
if (existingEntry)
|
|
313
|
+
return user;
|
|
314
|
+
const nonce = Math.floor(Math.random() * 1000000).toString();
|
|
315
|
+
const updatedUser = await Users.findOneAndUpdate(generateDbFilterById(userId), {
|
|
316
|
+
$push: {
|
|
317
|
+
'services.web3': {
|
|
318
|
+
address,
|
|
319
|
+
nonce,
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
}, { returnDocument: 'after' });
|
|
323
|
+
if (!updatedUser)
|
|
324
|
+
return null;
|
|
325
|
+
await emit('USER_UPDATE_WEB3_ADDRESS', {
|
|
326
|
+
action: 'add',
|
|
327
|
+
address,
|
|
328
|
+
user: removeConfidentialServiceHashes(updatedUser),
|
|
329
|
+
});
|
|
330
|
+
return updatedUser;
|
|
331
|
+
},
|
|
332
|
+
async removeWeb3Address(userId, address) {
|
|
333
|
+
const user = await Users.findOne(generateDbFilterById(userId), {});
|
|
334
|
+
if (!user)
|
|
335
|
+
return null;
|
|
336
|
+
const existingEntry = user.services?.web3?.find((service) => service.address.toLowerCase() === address.toLowerCase());
|
|
337
|
+
if (!existingEntry)
|
|
338
|
+
return null;
|
|
339
|
+
const updatedUser = await Users.findOneAndUpdate(generateDbFilterById(userId), {
|
|
340
|
+
$pull: {
|
|
341
|
+
'services.web3': { address: existingEntry.address },
|
|
342
|
+
},
|
|
343
|
+
}, { returnDocument: 'after' });
|
|
344
|
+
if (!updatedUser)
|
|
345
|
+
return null;
|
|
346
|
+
await emit('USER_UPDATE_WEB3_ADDRESS', {
|
|
347
|
+
action: 'remove',
|
|
348
|
+
address: existingEntry.address,
|
|
349
|
+
user: removeConfidentialServiceHashes(updatedUser),
|
|
350
|
+
});
|
|
351
|
+
return updatedUser;
|
|
352
|
+
},
|
|
353
|
+
findWeb3Address(user, address) {
|
|
354
|
+
return (user.services?.web3?.find((service) => service.address.toLowerCase() === address.toLowerCase()) || null);
|
|
355
|
+
},
|
|
356
|
+
async addWebAuthnCredential(userId, webAuthnService) {
|
|
357
|
+
const updatedUser = await Users.findOneAndUpdate(generateDbFilterById(userId), {
|
|
358
|
+
$push: {
|
|
359
|
+
'services.webAuthn': webAuthnService,
|
|
360
|
+
},
|
|
361
|
+
}, { returnDocument: 'after' });
|
|
362
|
+
return updatedUser;
|
|
363
|
+
},
|
|
364
|
+
async removeWebAuthnCredential(userId, credentialsId) {
|
|
365
|
+
const updatedUser = await Users.findOneAndUpdate(generateDbFilterById(userId), {
|
|
366
|
+
$pull: {
|
|
367
|
+
'services.webAuthn': { id: credentialsId },
|
|
368
|
+
},
|
|
369
|
+
}, { returnDocument: 'after' });
|
|
370
|
+
return updatedUser;
|
|
371
|
+
},
|
|
372
|
+
async setAccessToken(username, plainSecret) {
|
|
373
|
+
const secret = await sha256(`${username}:${plainSecret}`);
|
|
374
|
+
const updatedUser = await Users.findOneAndUpdate({ username: insensitiveTrimmedRegexOperator(username) }, {
|
|
375
|
+
$set: {
|
|
376
|
+
'services.token': { secret },
|
|
377
|
+
},
|
|
378
|
+
}, { returnDocument: 'after' });
|
|
379
|
+
return updatedUser;
|
|
380
|
+
},
|
|
381
|
+
async verifyWeb3SignatureAndUpdate(user, credentials, signature) {
|
|
382
|
+
const isValid = await verifyWeb3Signature(credentials.nonce, signature, credentials.address);
|
|
383
|
+
if (!isValid)
|
|
384
|
+
return null;
|
|
385
|
+
const web3Services = user.services?.web3?.map((service) => {
|
|
386
|
+
if (service.address.toLowerCase() === credentials.address.toLowerCase()) {
|
|
387
|
+
return {
|
|
388
|
+
...service,
|
|
389
|
+
nonce: undefined,
|
|
390
|
+
verified: true,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
return service;
|
|
394
|
+
});
|
|
395
|
+
if (!web3Services)
|
|
396
|
+
return null;
|
|
397
|
+
const updatedUser = await Users.findOneAndUpdate(generateDbFilterById(user._id), {
|
|
398
|
+
$set: {
|
|
399
|
+
'services.web3': web3Services,
|
|
400
|
+
},
|
|
401
|
+
}, { returnDocument: 'after' });
|
|
402
|
+
if (!updatedUser)
|
|
403
|
+
return null;
|
|
404
|
+
await emit('USER_UPDATE_WEB3_ADDRESS', {
|
|
405
|
+
action: 'verify',
|
|
406
|
+
address: credentials.address,
|
|
407
|
+
user: removeConfidentialServiceHashes(updatedUser),
|
|
408
|
+
});
|
|
409
|
+
return updatedUser;
|
|
410
|
+
},
|
|
297
411
|
async sendResetPasswordEmail(userId, email, isEnrollment) {
|
|
298
412
|
const plainToken = crypto.randomUUID();
|
|
299
413
|
const resetToken = {
|
|
@@ -384,7 +498,6 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
384
498
|
return null;
|
|
385
499
|
const updatedUser = await this.setPassword(resetToken.userId, newPassword);
|
|
386
500
|
if (updatedUser) {
|
|
387
|
-
// Now invalidate the reset token
|
|
388
501
|
await Users.updateOne({
|
|
389
502
|
_id: resetToken.userId,
|
|
390
503
|
}, {
|
|
@@ -500,8 +613,6 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
500
613
|
}, {});
|
|
501
614
|
}
|
|
502
615
|
if (meta) {
|
|
503
|
-
// eslint-disable-next-line
|
|
504
|
-
// @ts-ignore
|
|
505
616
|
modifier.$set.meta = meta;
|
|
506
617
|
}
|
|
507
618
|
const user = await Users.findOneAndUpdate(userFilter, modifier, {
|
|
@@ -561,7 +672,6 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
561
672
|
},
|
|
562
673
|
};
|
|
563
674
|
if ((!profile.phoneMobile || isGuest) && lastContact.telNumber) {
|
|
564
|
-
// Backport the contact phone number to the user profile
|
|
565
675
|
modifier.$set['profile.phoneMobile'] = lastContact.telNumber;
|
|
566
676
|
}
|
|
567
677
|
const updatedUser = await Users.findOneAndUpdate(userFilter, modifier, {
|
|
@@ -652,4 +762,3 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
652
762
|
},
|
|
653
763
|
};
|
|
654
764
|
};
|
|
655
|
-
//# sourceMappingURL=configureUsersModule.js.map
|
|
@@ -1,26 +1,84 @@
|
|
|
1
|
-
import { ModuleInput } from '@unchainedshop/mongodb';
|
|
2
|
-
import type {
|
|
3
|
-
type
|
|
1
|
+
import type { ModuleInput } from '@unchainedshop/mongodb';
|
|
2
|
+
import type { RegistrationJSON, AuthenticationJSON, CredentialInfo, NamedAlgo, ExtendedAuthenticatorTransport } from '@passwordless-id/webauthn/dist/esm/types.js';
|
|
3
|
+
export type { RegistrationJSON, AuthenticationJSON, CredentialInfo, NamedAlgo, ExtendedAuthenticatorTransport, };
|
|
4
|
+
export declare function toArrayBuffer(buffer: Buffer): ArrayBuffer;
|
|
5
|
+
export declare function buf2hex(buffer: ArrayBuffer): string;
|
|
6
|
+
export interface WebAuthnCredentialCreationOptions {
|
|
4
7
|
challenge: string;
|
|
5
8
|
requestId: number;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
rp: {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
};
|
|
13
|
+
user: {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
displayName: string;
|
|
17
|
+
};
|
|
18
|
+
pubKeyCredParams: {
|
|
19
|
+
type: 'public-key';
|
|
20
|
+
alg: number;
|
|
21
|
+
}[];
|
|
22
|
+
timeout: number;
|
|
23
|
+
attestation: 'none' | 'indirect' | 'direct';
|
|
24
|
+
authenticatorSelection?: {
|
|
25
|
+
authenticatorAttachment?: 'platform' | 'cross-platform';
|
|
26
|
+
requireResidentKey?: boolean;
|
|
27
|
+
userVerification?: 'required' | 'preferred' | 'discouraged';
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export interface WebAuthnCredentialRequestOptions {
|
|
31
|
+
challenge: string;
|
|
32
|
+
requestId: number;
|
|
33
|
+
rpId: string;
|
|
34
|
+
timeout: number;
|
|
35
|
+
userVerification?: 'required' | 'preferred' | 'discouraged';
|
|
36
|
+
allowCredentials?: {
|
|
37
|
+
id: string;
|
|
38
|
+
type: 'public-key';
|
|
39
|
+
transports?: ('usb' | 'nfc' | 'ble' | 'internal')[];
|
|
40
|
+
}[];
|
|
41
|
+
}
|
|
42
|
+
export interface WebAuthnCredential {
|
|
43
|
+
id: string;
|
|
44
|
+
publicKey: string;
|
|
45
|
+
algorithm: NamedAlgo;
|
|
46
|
+
aaguid: string;
|
|
47
|
+
counter: number;
|
|
48
|
+
created: Date;
|
|
49
|
+
}
|
|
50
|
+
export declare const configureUsersWebAuthnModule: ({ db }: ModuleInput<Record<string, any>>) => Promise<{
|
|
51
|
+
findMDSMetadataForAAGUID: (aaguid: string) => Promise<{
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
description?: string;
|
|
54
|
+
icon?: string;
|
|
55
|
+
authenticatorGetInfo?: {
|
|
56
|
+
versions?: string[];
|
|
57
|
+
extensions?: string[];
|
|
58
|
+
options?: Record<string, boolean>;
|
|
59
|
+
};
|
|
19
60
|
} | null>;
|
|
20
|
-
|
|
21
|
-
|
|
61
|
+
createCredentialCreationOptions: (origin: string, username: string, extensionOptions?: {
|
|
62
|
+
timeout?: number;
|
|
63
|
+
authenticatorSelection?: WebAuthnCredentialCreationOptions["authenticatorSelection"];
|
|
64
|
+
}) => Promise<WebAuthnCredentialCreationOptions>;
|
|
65
|
+
createCredentialRequestOptions: (origin: string, username: string, extensionOptions?: {
|
|
66
|
+
timeout?: number;
|
|
67
|
+
userVerification?: "required" | "preferred" | "discouraged";
|
|
68
|
+
allowCredentials?: WebAuthnCredentialRequestOptions["allowCredentials"];
|
|
69
|
+
}) => Promise<WebAuthnCredentialRequestOptions>;
|
|
70
|
+
verifyCredentialCreation: (username: string, credentials: RegistrationJSON) => Promise<WebAuthnCredential | null>;
|
|
71
|
+
verifyCredentialRequest: (userPublicKeys: {
|
|
72
|
+
id: string;
|
|
73
|
+
publicKey: string;
|
|
74
|
+
algorithm?: NamedAlgo;
|
|
75
|
+
counter?: number;
|
|
76
|
+
transports?: ExtendedAuthenticatorTransport[];
|
|
77
|
+
}[], username: string, credentials: AuthenticationJSON & {
|
|
78
|
+
requestId: number;
|
|
79
|
+
}) => Promise<{
|
|
80
|
+
userHandle: string;
|
|
81
|
+
counter: number;
|
|
22
82
|
} | null>;
|
|
23
83
|
deleteUserWebAuthnCredentials: (username: string) => Promise<number>;
|
|
24
84
|
}>;
|
|
25
|
-
export {};
|
|
26
|
-
//# sourceMappingURL=configureUsersWebAuthnModule.d.ts.map
|