@unchainedshop/core-users 4.8.25 → 4.8.26
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/lib/module/configureUsersModule.js +15 -5
- package/package.json +1 -1
- package/lib/domain/model/Email.d.ts +0 -17
- package/lib/domain/model/Email.js +0 -21
- package/lib/domain/model/PasswordHash.d.ts +0 -16
- package/lib/domain/model/PasswordHash.js +0 -31
- package/lib/domain/model/PushSubscription.d.ts +0 -23
- package/lib/domain/model/PushSubscription.js +0 -25
- package/lib/domain/model/User.d.ts +0 -116
- package/lib/domain/model/User.js +0 -386
- package/lib/domain/model/UserProfile.d.ts +0 -14
- package/lib/domain/model/UserProfile.js +0 -29
- package/lib/domain/model/Web3Address.d.ts +0 -16
- package/lib/domain/model/Web3Address.js +0 -32
- package/lib/domain/model/WebAuthnCredential.d.ts +0 -22
- package/lib/domain/model/WebAuthnCredential.js +0 -28
- package/lib/domain/model/index.d.ts +0 -14
- package/lib/domain/model/index.js +0 -7
- package/lib/domain/repository/UserRepository.d.ts +0 -48
- package/lib/domain/repository/UserRepository.js +0 -1
- package/lib/domain/services/PasswordService.d.ts +0 -5
- package/lib/domain/services/PasswordService.js +0 -1
- package/lib/domain/services/TokenService.d.ts +0 -8
- package/lib/domain/services/TokenService.js +0 -1
- package/lib/domain/types.d.ts +0 -28
- package/lib/domain/types.js +0 -1
- package/lib/infrastructure/persistence/MongoUserRepository.d.ts +0 -4
- package/lib/infrastructure/persistence/MongoUserRepository.js +0 -159
- package/lib/infrastructure/persistence/buildUserSelector.d.ts +0 -4
- package/lib/infrastructure/persistence/buildUserSelector.js +0 -54
- package/lib/infrastructure/services/CryptoTokenService.d.ts +0 -2
- package/lib/infrastructure/services/CryptoTokenService.js +0 -16
- package/lib/infrastructure/services/Pbkdf2PasswordService.d.ts +0 -2
- package/lib/infrastructure/services/Pbkdf2PasswordService.js +0 -23
|
@@ -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,14 @@ 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
|
+
};
|
|
91
100
|
return {
|
|
92
101
|
webAuthn,
|
|
93
102
|
async count(query) {
|
|
@@ -606,6 +615,7 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
606
615
|
return user;
|
|
607
616
|
},
|
|
608
617
|
markDeleted: async (userId) => {
|
|
618
|
+
await assertAnotherActiveAdminExists(userId);
|
|
609
619
|
await db.collection('sessions').deleteMany({
|
|
610
620
|
session: { $regex: `"user(Id)?":"${escapeRegexString(userId)}"` },
|
|
611
621
|
});
|
|
@@ -635,6 +645,7 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
635
645
|
return user;
|
|
636
646
|
},
|
|
637
647
|
deletePermanently: async ({ userId }) => {
|
|
648
|
+
await assertAnotherActiveAdminExists(userId);
|
|
638
649
|
return Users.findOneAndDelete({ _id: userId });
|
|
639
650
|
},
|
|
640
651
|
updateProfile: async (userId, updatedData) => {
|
|
@@ -751,15 +762,14 @@ export const configureUsersModule = async (moduleInput) => {
|
|
|
751
762
|
return updatedUser;
|
|
752
763
|
},
|
|
753
764
|
updateRoles: async (_id, roles) => {
|
|
754
|
-
|
|
765
|
+
if (!roles.includes(ADMIN_ROLE))
|
|
766
|
+
await assertAnotherActiveAdminExists(_id);
|
|
767
|
+
const user = await Users.findOneAndUpdate(generateDbFilterById(_id), {
|
|
755
768
|
$set: {
|
|
756
769
|
updated: new Date(),
|
|
757
770
|
roles,
|
|
758
771
|
},
|
|
759
|
-
};
|
|
760
|
-
const user = await Users.findOneAndUpdate(generateDbFilterById(_id), modifier, {
|
|
761
|
-
returnDocument: 'after',
|
|
762
|
-
});
|
|
772
|
+
}, { returnDocument: 'after' });
|
|
763
773
|
if (!user)
|
|
764
774
|
return null;
|
|
765
775
|
await emit('USER_UPDATE_ROLE', {
|
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": "4.8.
|
|
4
|
+
"version": "4.8.26",
|
|
5
5
|
"main": "lib/users-index.js",
|
|
6
6
|
"types": "lib/users-index.d.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export interface Email {
|
|
2
|
-
readonly address: string;
|
|
3
|
-
verified: boolean;
|
|
4
|
-
}
|
|
5
|
-
export declare const Email: {
|
|
6
|
-
create(address: string, verified?: boolean): Email;
|
|
7
|
-
fromData(data: {
|
|
8
|
-
address: string;
|
|
9
|
-
verified: boolean;
|
|
10
|
-
}): Email;
|
|
11
|
-
isValid(address: string): boolean;
|
|
12
|
-
equals(a: Email, b: Email): boolean;
|
|
13
|
-
toData(email: Email): {
|
|
14
|
-
address: string;
|
|
15
|
-
verified: boolean;
|
|
16
|
-
};
|
|
17
|
-
};
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export const Email = {
|
|
2
|
-
create(address, verified = false) {
|
|
3
|
-
const normalized = address.trim().toLowerCase();
|
|
4
|
-
if (!Email.isValid(normalized)) {
|
|
5
|
-
throw new Error(`Invalid email address: ${address}`);
|
|
6
|
-
}
|
|
7
|
-
return { address: normalized, verified };
|
|
8
|
-
},
|
|
9
|
-
fromData(data) {
|
|
10
|
-
return { address: data.address, verified: data.verified };
|
|
11
|
-
},
|
|
12
|
-
isValid(address) {
|
|
13
|
-
return typeof address === 'string' && address.includes('@') && address.length >= 3;
|
|
14
|
-
},
|
|
15
|
-
equals(a, b) {
|
|
16
|
-
return a.address.toLowerCase() === b.address.toLowerCase();
|
|
17
|
-
},
|
|
18
|
-
toData(email) {
|
|
19
|
-
return { address: email.address, verified: email.verified };
|
|
20
|
-
},
|
|
21
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface PasswordHash {
|
|
2
|
-
pbkdf2?: string;
|
|
3
|
-
bcrypt?: string;
|
|
4
|
-
}
|
|
5
|
-
export declare const PasswordHash: {
|
|
6
|
-
create(pbkdf2Hash: string): PasswordHash;
|
|
7
|
-
fromLegacyBcrypt(bcryptHash: string): PasswordHash;
|
|
8
|
-
fromData(data: {
|
|
9
|
-
pbkdf2?: string;
|
|
10
|
-
bcrypt?: string;
|
|
11
|
-
}): PasswordHash;
|
|
12
|
-
isEmpty(hash: PasswordHash | undefined): boolean;
|
|
13
|
-
isLegacyBcrypt(hash: PasswordHash): boolean;
|
|
14
|
-
getSalt(hash: PasswordHash): string | undefined;
|
|
15
|
-
getHash(hash: PasswordHash): string | undefined;
|
|
16
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export const PasswordHash = {
|
|
2
|
-
create(pbkdf2Hash) {
|
|
3
|
-
return { pbkdf2: pbkdf2Hash };
|
|
4
|
-
},
|
|
5
|
-
fromLegacyBcrypt(bcryptHash) {
|
|
6
|
-
return { bcrypt: bcryptHash };
|
|
7
|
-
},
|
|
8
|
-
fromData(data) {
|
|
9
|
-
return { ...data };
|
|
10
|
-
},
|
|
11
|
-
isEmpty(hash) {
|
|
12
|
-
if (!hash)
|
|
13
|
-
return true;
|
|
14
|
-
return !hash.pbkdf2 && !hash.bcrypt;
|
|
15
|
-
},
|
|
16
|
-
isLegacyBcrypt(hash) {
|
|
17
|
-
return !!hash.bcrypt && !hash.pbkdf2;
|
|
18
|
-
},
|
|
19
|
-
getSalt(hash) {
|
|
20
|
-
if (!hash.pbkdf2)
|
|
21
|
-
return undefined;
|
|
22
|
-
const [salt] = hash.pbkdf2.split(':');
|
|
23
|
-
return salt;
|
|
24
|
-
},
|
|
25
|
-
getHash(hash) {
|
|
26
|
-
if (!hash.pbkdf2)
|
|
27
|
-
return undefined;
|
|
28
|
-
const [, hashValue] = hash.pbkdf2.split(':');
|
|
29
|
-
return hashValue;
|
|
30
|
-
},
|
|
31
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
export interface PushSubscription {
|
|
2
|
-
userAgent: string;
|
|
3
|
-
endpoint: string;
|
|
4
|
-
expirationTime: number;
|
|
5
|
-
keys: {
|
|
6
|
-
auth: string;
|
|
7
|
-
p256dh: string;
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
|
-
export declare const PushSubscription: {
|
|
11
|
-
create(data: {
|
|
12
|
-
userAgent: string;
|
|
13
|
-
endpoint: string;
|
|
14
|
-
expirationTime?: number;
|
|
15
|
-
keys: {
|
|
16
|
-
auth: string;
|
|
17
|
-
p256dh: string;
|
|
18
|
-
};
|
|
19
|
-
}): PushSubscription;
|
|
20
|
-
fromData(data: PushSubscription): PushSubscription;
|
|
21
|
-
equals(a: PushSubscription, b: PushSubscription): boolean;
|
|
22
|
-
matchesKey(subscription: PushSubscription, p256dh: string): boolean;
|
|
23
|
-
};
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export const PushSubscription = {
|
|
2
|
-
create(data) {
|
|
3
|
-
if (!data.endpoint || !data.keys?.p256dh || !data.keys?.auth) {
|
|
4
|
-
throw new Error('Push subscription requires endpoint and keys');
|
|
5
|
-
}
|
|
6
|
-
return {
|
|
7
|
-
userAgent: data.userAgent,
|
|
8
|
-
endpoint: data.endpoint,
|
|
9
|
-
expirationTime: data.expirationTime ?? 0,
|
|
10
|
-
keys: {
|
|
11
|
-
auth: data.keys.auth,
|
|
12
|
-
p256dh: data.keys.p256dh,
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
},
|
|
16
|
-
fromData(data) {
|
|
17
|
-
return { ...data };
|
|
18
|
-
},
|
|
19
|
-
equals(a, b) {
|
|
20
|
-
return a.keys.p256dh === b.keys.p256dh;
|
|
21
|
-
},
|
|
22
|
-
matchesKey(subscription, p256dh) {
|
|
23
|
-
return subscription.keys.p256dh === p256dh;
|
|
24
|
-
},
|
|
25
|
-
};
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import { type Email } from './Email.ts';
|
|
2
|
-
import { type UserProfile } from './UserProfile.ts';
|
|
3
|
-
import { type Web3Address } from './Web3Address.ts';
|
|
4
|
-
import { type WebAuthnCredential } from './WebAuthnCredential.ts';
|
|
5
|
-
import { type PasswordHash } from './PasswordHash.ts';
|
|
6
|
-
import { type PushSubscription } from './PushSubscription.ts';
|
|
7
|
-
import type { Address, Contact, UserLastLogin, TokenRecord } from '../types.ts';
|
|
8
|
-
export interface UserData {
|
|
9
|
-
_id: string;
|
|
10
|
-
emails: Email[];
|
|
11
|
-
username?: string;
|
|
12
|
-
profile?: UserProfile;
|
|
13
|
-
roles: string[];
|
|
14
|
-
tags: string[];
|
|
15
|
-
guest: boolean;
|
|
16
|
-
initialPassword: boolean;
|
|
17
|
-
avatarId?: string;
|
|
18
|
-
lastBillingAddress?: Address;
|
|
19
|
-
lastContact?: Contact;
|
|
20
|
-
lastLogin?: UserLastLogin;
|
|
21
|
-
pushSubscriptions: PushSubscription[];
|
|
22
|
-
meta?: Record<string, unknown>;
|
|
23
|
-
created: Date;
|
|
24
|
-
updated?: Date;
|
|
25
|
-
deleted?: Date | null;
|
|
26
|
-
services: {
|
|
27
|
-
password?: PasswordHash;
|
|
28
|
-
web3?: Web3Address[];
|
|
29
|
-
webAuthn?: WebAuthnCredential[];
|
|
30
|
-
token?: {
|
|
31
|
-
secret: string;
|
|
32
|
-
};
|
|
33
|
-
email?: {
|
|
34
|
-
verificationTokens: TokenRecord[];
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
export interface CreateUserParams {
|
|
39
|
-
_id?: string;
|
|
40
|
-
email?: string;
|
|
41
|
-
username?: string;
|
|
42
|
-
password?: PasswordHash;
|
|
43
|
-
webAuthnCredential?: WebAuthnCredential;
|
|
44
|
-
roles?: string[];
|
|
45
|
-
tags?: string[];
|
|
46
|
-
guest?: boolean;
|
|
47
|
-
initialPassword?: boolean;
|
|
48
|
-
profile?: Partial<UserProfile>;
|
|
49
|
-
meta?: Record<string, unknown>;
|
|
50
|
-
pushSubscriptions?: PushSubscription[];
|
|
51
|
-
}
|
|
52
|
-
export interface User {
|
|
53
|
-
readonly id: string;
|
|
54
|
-
readonly _id: string;
|
|
55
|
-
readonly emails: Email[];
|
|
56
|
-
readonly username: string | undefined;
|
|
57
|
-
readonly profile: UserProfile | undefined;
|
|
58
|
-
readonly roles: string[];
|
|
59
|
-
readonly tags: string[];
|
|
60
|
-
readonly guest: boolean;
|
|
61
|
-
readonly initialPassword: boolean;
|
|
62
|
-
readonly avatarId: string | undefined;
|
|
63
|
-
readonly lastBillingAddress: Address | undefined;
|
|
64
|
-
readonly lastContact: Contact | undefined;
|
|
65
|
-
readonly lastLogin: UserLastLogin | undefined;
|
|
66
|
-
readonly pushSubscriptions: PushSubscription[];
|
|
67
|
-
readonly meta: Record<string, unknown> | undefined;
|
|
68
|
-
readonly created: Date;
|
|
69
|
-
readonly updated: Date | undefined;
|
|
70
|
-
readonly deleted: Date | null | undefined;
|
|
71
|
-
readonly services: UserData['services'];
|
|
72
|
-
readonly isDeleted: boolean;
|
|
73
|
-
primaryEmail(): Email | undefined;
|
|
74
|
-
hasVerifiedEmail(): boolean;
|
|
75
|
-
hasPassword(): boolean;
|
|
76
|
-
getPasswordHash(): PasswordHash | undefined;
|
|
77
|
-
findWeb3Address(address: string): Web3Address | undefined;
|
|
78
|
-
getWebAuthnCredentials(): WebAuthnCredential[];
|
|
79
|
-
findEmailByAddress(address: string): Email | undefined;
|
|
80
|
-
addEmail(email: Email): void;
|
|
81
|
-
removeEmail(address: string): void;
|
|
82
|
-
markEmailVerified(address: string): boolean;
|
|
83
|
-
setUsername(username: string): void;
|
|
84
|
-
setProfile(profile: Partial<UserProfile>): void;
|
|
85
|
-
setMeta(meta: Record<string, unknown>): void;
|
|
86
|
-
setAvatar(fileId: string): void;
|
|
87
|
-
setPassword(passwordHash: PasswordHash): void;
|
|
88
|
-
setRoles(roles: string[]): void;
|
|
89
|
-
addRoles(roles: string[]): void;
|
|
90
|
-
setTags(tags: string[]): void;
|
|
91
|
-
setGuest(isGuest: boolean): void;
|
|
92
|
-
updateLastLogin(loginInfo: Omit<UserLastLogin, 'timestamp'>): void;
|
|
93
|
-
updateLastBillingAddress(address: Address, updateDisplayName?: boolean): void;
|
|
94
|
-
updateLastContact(contact: Contact, updatePhoneMobile?: boolean): void;
|
|
95
|
-
addWeb3Address(address: string): string;
|
|
96
|
-
removeWeb3Address(address: string): boolean;
|
|
97
|
-
verifyWeb3Address(address: string): boolean;
|
|
98
|
-
addWebAuthnCredential(credential: WebAuthnCredential): void;
|
|
99
|
-
removeWebAuthnCredential(credentialId: string): boolean;
|
|
100
|
-
updateWebAuthnCounter(credentialId: string, counter: number): boolean;
|
|
101
|
-
addPushSubscription(subscription: PushSubscription): boolean;
|
|
102
|
-
removePushSubscription(p256dh: string): boolean;
|
|
103
|
-
setAccessToken(hashedToken: string): void;
|
|
104
|
-
addVerificationToken(token: TokenRecord): void;
|
|
105
|
-
addResetToken(token: TokenRecord): void;
|
|
106
|
-
removeResetToken(hashedToken: string): void;
|
|
107
|
-
clearVerificationTokens(address: string): void;
|
|
108
|
-
markAsDeleted(): void;
|
|
109
|
-
readonly isDirty: boolean;
|
|
110
|
-
readonly changes: readonly string[];
|
|
111
|
-
clearChanges(): void;
|
|
112
|
-
toData(): UserData;
|
|
113
|
-
toPublicData(): Omit<UserData, 'services'>;
|
|
114
|
-
}
|
|
115
|
-
export declare const createUser: (params: CreateUserParams) => User;
|
|
116
|
-
export declare const reconstituteUser: (data: UserData) => User;
|
package/lib/domain/model/User.js
DELETED
|
@@ -1,386 +0,0 @@
|
|
|
1
|
-
import { Email as EmailVO } from "./Email.js";
|
|
2
|
-
import { UserProfile as UserProfileVO } from "./UserProfile.js";
|
|
3
|
-
import { Web3Address as Web3AddressVO } from "./Web3Address.js";
|
|
4
|
-
import { WebAuthnCredential as WebAuthnCredentialVO, } from "./WebAuthnCredential.js";
|
|
5
|
-
import { PasswordHash as PasswordHashVO } from "./PasswordHash.js";
|
|
6
|
-
import { PushSubscription as PushSubscriptionVO } from "./PushSubscription.js";
|
|
7
|
-
export const createUser = (params) => {
|
|
8
|
-
const now = new Date();
|
|
9
|
-
const data = {
|
|
10
|
-
_id: params._id || crypto.randomUUID(),
|
|
11
|
-
emails: params.email ? [EmailVO.create(params.email)] : [],
|
|
12
|
-
username: params.username?.trim().toLowerCase(),
|
|
13
|
-
profile: params.profile ? UserProfileVO.create(params.profile) : undefined,
|
|
14
|
-
roles: params.roles || [],
|
|
15
|
-
tags: params.tags || [],
|
|
16
|
-
guest: params.guest ?? false,
|
|
17
|
-
initialPassword: params.initialPassword ?? false,
|
|
18
|
-
pushSubscriptions: params.pushSubscriptions || [],
|
|
19
|
-
meta: params.meta,
|
|
20
|
-
created: now,
|
|
21
|
-
updated: undefined,
|
|
22
|
-
deleted: null,
|
|
23
|
-
services: {},
|
|
24
|
-
};
|
|
25
|
-
if (params.password) {
|
|
26
|
-
data.services.password = params.password;
|
|
27
|
-
}
|
|
28
|
-
if (params.webAuthnCredential) {
|
|
29
|
-
data.services.webAuthn = [params.webAuthnCredential];
|
|
30
|
-
}
|
|
31
|
-
const isDirty = true;
|
|
32
|
-
const changes = ['created'];
|
|
33
|
-
return createUserFromData(data, isDirty, changes);
|
|
34
|
-
};
|
|
35
|
-
export const reconstituteUser = (data) => {
|
|
36
|
-
const reconstituted = {
|
|
37
|
-
...data,
|
|
38
|
-
emails: (data.emails || []).map((e) => EmailVO.fromData(e)),
|
|
39
|
-
profile: data.profile ? UserProfileVO.fromData(data.profile) : undefined,
|
|
40
|
-
roles: [...(data.roles || [])],
|
|
41
|
-
tags: [...(data.tags || [])],
|
|
42
|
-
pushSubscriptions: (data.pushSubscriptions || []).map((p) => PushSubscriptionVO.fromData(p)),
|
|
43
|
-
services: {
|
|
44
|
-
...data.services,
|
|
45
|
-
password: data.services?.password ? PasswordHashVO.fromData(data.services.password) : undefined,
|
|
46
|
-
web3: data.services?.web3?.map((w) => Web3AddressVO.fromData(w)),
|
|
47
|
-
webAuthn: data.services?.webAuthn?.map((w) => WebAuthnCredentialVO.fromData(w)),
|
|
48
|
-
},
|
|
49
|
-
};
|
|
50
|
-
return createUserFromData(reconstituted, false, []);
|
|
51
|
-
};
|
|
52
|
-
const createUserFromData = (data, initialDirty, initialChanges) => {
|
|
53
|
-
let isDirty = initialDirty;
|
|
54
|
-
const changes = [...initialChanges];
|
|
55
|
-
const markDirty = (field) => {
|
|
56
|
-
isDirty = true;
|
|
57
|
-
data.updated = new Date();
|
|
58
|
-
if (!changes.includes(field)) {
|
|
59
|
-
changes.push(field);
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
const user = {
|
|
63
|
-
get id() {
|
|
64
|
-
return data._id;
|
|
65
|
-
},
|
|
66
|
-
get _id() {
|
|
67
|
-
return data._id;
|
|
68
|
-
},
|
|
69
|
-
get emails() {
|
|
70
|
-
return [...data.emails];
|
|
71
|
-
},
|
|
72
|
-
get username() {
|
|
73
|
-
return data.username;
|
|
74
|
-
},
|
|
75
|
-
get profile() {
|
|
76
|
-
return data.profile ? { ...data.profile } : undefined;
|
|
77
|
-
},
|
|
78
|
-
get roles() {
|
|
79
|
-
return [...data.roles];
|
|
80
|
-
},
|
|
81
|
-
get tags() {
|
|
82
|
-
return [...data.tags];
|
|
83
|
-
},
|
|
84
|
-
get guest() {
|
|
85
|
-
return data.guest;
|
|
86
|
-
},
|
|
87
|
-
get initialPassword() {
|
|
88
|
-
return data.initialPassword;
|
|
89
|
-
},
|
|
90
|
-
get avatarId() {
|
|
91
|
-
return data.avatarId;
|
|
92
|
-
},
|
|
93
|
-
get lastBillingAddress() {
|
|
94
|
-
return data.lastBillingAddress ? { ...data.lastBillingAddress } : undefined;
|
|
95
|
-
},
|
|
96
|
-
get lastContact() {
|
|
97
|
-
return data.lastContact ? { ...data.lastContact } : undefined;
|
|
98
|
-
},
|
|
99
|
-
get lastLogin() {
|
|
100
|
-
return data.lastLogin ? { ...data.lastLogin } : undefined;
|
|
101
|
-
},
|
|
102
|
-
get pushSubscriptions() {
|
|
103
|
-
return [...data.pushSubscriptions];
|
|
104
|
-
},
|
|
105
|
-
get meta() {
|
|
106
|
-
return data.meta ? { ...data.meta } : undefined;
|
|
107
|
-
},
|
|
108
|
-
get created() {
|
|
109
|
-
return data.created;
|
|
110
|
-
},
|
|
111
|
-
get updated() {
|
|
112
|
-
return data.updated;
|
|
113
|
-
},
|
|
114
|
-
get deleted() {
|
|
115
|
-
return data.deleted;
|
|
116
|
-
},
|
|
117
|
-
get services() {
|
|
118
|
-
return data.services;
|
|
119
|
-
},
|
|
120
|
-
get isDeleted() {
|
|
121
|
-
return data.deleted != null;
|
|
122
|
-
},
|
|
123
|
-
primaryEmail() {
|
|
124
|
-
return [...data.emails].sort((a, b) => Number(b.verified) - Number(a.verified))[0];
|
|
125
|
-
},
|
|
126
|
-
hasVerifiedEmail() {
|
|
127
|
-
return data.emails.some((e) => e.verified);
|
|
128
|
-
},
|
|
129
|
-
hasPassword() {
|
|
130
|
-
return !PasswordHashVO.isEmpty(data.services.password);
|
|
131
|
-
},
|
|
132
|
-
getPasswordHash() {
|
|
133
|
-
return data.services.password;
|
|
134
|
-
},
|
|
135
|
-
findWeb3Address(address) {
|
|
136
|
-
return data.services.web3?.find((w) => w.address.toLowerCase() === address.toLowerCase());
|
|
137
|
-
},
|
|
138
|
-
getWebAuthnCredentials() {
|
|
139
|
-
return data.services.webAuthn || [];
|
|
140
|
-
},
|
|
141
|
-
findEmailByAddress(address) {
|
|
142
|
-
return data.emails.find((e) => e.address.toLowerCase() === address.toLowerCase());
|
|
143
|
-
},
|
|
144
|
-
addEmail(email) {
|
|
145
|
-
const exists = data.emails.some((e) => EmailVO.equals(e, email));
|
|
146
|
-
if (!exists) {
|
|
147
|
-
data.emails.push(email);
|
|
148
|
-
markDirty('emails');
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
removeEmail(address) {
|
|
152
|
-
const index = data.emails.findIndex((e) => e.address.toLowerCase() === address.toLowerCase());
|
|
153
|
-
if (index !== -1) {
|
|
154
|
-
data.emails.splice(index, 1);
|
|
155
|
-
markDirty('emails');
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
markEmailVerified(address) {
|
|
159
|
-
const email = data.emails.find((e) => e.address.toLowerCase() === address.toLowerCase());
|
|
160
|
-
if (email && !email.verified) {
|
|
161
|
-
email.verified = true;
|
|
162
|
-
if (data.services.email?.verificationTokens) {
|
|
163
|
-
data.services.email.verificationTokens = data.services.email.verificationTokens.filter((t) => t.address.toLowerCase() !== address.toLowerCase());
|
|
164
|
-
}
|
|
165
|
-
markDirty('emails');
|
|
166
|
-
return true;
|
|
167
|
-
}
|
|
168
|
-
return false;
|
|
169
|
-
},
|
|
170
|
-
setUsername(username) {
|
|
171
|
-
data.username = username.trim().toLowerCase();
|
|
172
|
-
markDirty('username');
|
|
173
|
-
},
|
|
174
|
-
setProfile(profile) {
|
|
175
|
-
data.profile = UserProfileVO.merge(data.profile, profile);
|
|
176
|
-
markDirty('profile');
|
|
177
|
-
},
|
|
178
|
-
setMeta(meta) {
|
|
179
|
-
data.meta = meta;
|
|
180
|
-
markDirty('meta');
|
|
181
|
-
},
|
|
182
|
-
setAvatar(fileId) {
|
|
183
|
-
data.avatarId = fileId;
|
|
184
|
-
markDirty('avatar');
|
|
185
|
-
},
|
|
186
|
-
setPassword(passwordHash) {
|
|
187
|
-
data.services.password = passwordHash;
|
|
188
|
-
data.initialPassword = false;
|
|
189
|
-
markDirty('password');
|
|
190
|
-
},
|
|
191
|
-
setRoles(roles) {
|
|
192
|
-
data.roles = [...roles];
|
|
193
|
-
markDirty('roles');
|
|
194
|
-
},
|
|
195
|
-
addRoles(roles) {
|
|
196
|
-
const newRoles = roles.filter((r) => !data.roles.includes(r));
|
|
197
|
-
if (newRoles.length > 0) {
|
|
198
|
-
data.roles.push(...newRoles);
|
|
199
|
-
markDirty('roles');
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
setTags(tags) {
|
|
203
|
-
data.tags = [...tags];
|
|
204
|
-
markDirty('tags');
|
|
205
|
-
},
|
|
206
|
-
setGuest(isGuest) {
|
|
207
|
-
data.guest = isGuest;
|
|
208
|
-
markDirty('guest');
|
|
209
|
-
},
|
|
210
|
-
updateLastLogin(loginInfo) {
|
|
211
|
-
data.lastLogin = {
|
|
212
|
-
timestamp: new Date(),
|
|
213
|
-
...loginInfo,
|
|
214
|
-
};
|
|
215
|
-
markDirty('lastLogin');
|
|
216
|
-
},
|
|
217
|
-
updateLastBillingAddress(address, updateDisplayName = false) {
|
|
218
|
-
data.lastBillingAddress = address;
|
|
219
|
-
if (updateDisplayName || !data.profile?.displayName || data.guest) {
|
|
220
|
-
const displayName = [address.firstName, address.lastName].filter(Boolean).join(' ');
|
|
221
|
-
if (displayName) {
|
|
222
|
-
data.profile = UserProfileVO.merge(data.profile, { displayName });
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
markDirty('lastBillingAddress');
|
|
226
|
-
},
|
|
227
|
-
updateLastContact(contact, updatePhoneMobile = false) {
|
|
228
|
-
data.lastContact = contact;
|
|
229
|
-
if ((updatePhoneMobile || !data.profile?.phoneMobile || data.guest) && contact.telNumber) {
|
|
230
|
-
data.profile = UserProfileVO.merge(data.profile, { phoneMobile: contact.telNumber });
|
|
231
|
-
}
|
|
232
|
-
markDirty('lastContact');
|
|
233
|
-
},
|
|
234
|
-
addWeb3Address(address) {
|
|
235
|
-
if (!data.services.web3) {
|
|
236
|
-
data.services.web3 = [];
|
|
237
|
-
}
|
|
238
|
-
const existing = data.services.web3.find((w) => w.address.toLowerCase() === address.toLowerCase());
|
|
239
|
-
if (existing) {
|
|
240
|
-
return existing.nonce || '';
|
|
241
|
-
}
|
|
242
|
-
const nonce = crypto.randomUUID();
|
|
243
|
-
data.services.web3.push(Web3AddressVO.create(address, nonce));
|
|
244
|
-
markDirty('web3');
|
|
245
|
-
return nonce;
|
|
246
|
-
},
|
|
247
|
-
removeWeb3Address(address) {
|
|
248
|
-
if (!data.services.web3)
|
|
249
|
-
return false;
|
|
250
|
-
const index = data.services.web3.findIndex((w) => w.address.toLowerCase() === address.toLowerCase());
|
|
251
|
-
if (index !== -1) {
|
|
252
|
-
data.services.web3.splice(index, 1);
|
|
253
|
-
markDirty('web3');
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
return false;
|
|
257
|
-
},
|
|
258
|
-
verifyWeb3Address(address) {
|
|
259
|
-
if (!data.services.web3)
|
|
260
|
-
return false;
|
|
261
|
-
const web3Address = data.services.web3.find((w) => w.address.toLowerCase() === address.toLowerCase());
|
|
262
|
-
if (web3Address && !web3Address.verified) {
|
|
263
|
-
web3Address.verified = true;
|
|
264
|
-
web3Address.nonce = undefined;
|
|
265
|
-
markDirty('web3');
|
|
266
|
-
return true;
|
|
267
|
-
}
|
|
268
|
-
return false;
|
|
269
|
-
},
|
|
270
|
-
addWebAuthnCredential(credential) {
|
|
271
|
-
if (!data.services.webAuthn) {
|
|
272
|
-
data.services.webAuthn = [];
|
|
273
|
-
}
|
|
274
|
-
data.services.webAuthn.push(credential);
|
|
275
|
-
markDirty('webAuthn');
|
|
276
|
-
},
|
|
277
|
-
removeWebAuthnCredential(credentialId) {
|
|
278
|
-
if (!data.services.webAuthn)
|
|
279
|
-
return false;
|
|
280
|
-
const index = data.services.webAuthn.findIndex((c) => c.id === credentialId);
|
|
281
|
-
if (index !== -1) {
|
|
282
|
-
data.services.webAuthn.splice(index, 1);
|
|
283
|
-
markDirty('webAuthn');
|
|
284
|
-
return true;
|
|
285
|
-
}
|
|
286
|
-
return false;
|
|
287
|
-
},
|
|
288
|
-
updateWebAuthnCounter(credentialId, counter) {
|
|
289
|
-
if (!data.services.webAuthn)
|
|
290
|
-
return false;
|
|
291
|
-
const credential = data.services.webAuthn.find((c) => c.id === credentialId);
|
|
292
|
-
if (credential) {
|
|
293
|
-
credential.counter = counter;
|
|
294
|
-
markDirty('webAuthn');
|
|
295
|
-
return true;
|
|
296
|
-
}
|
|
297
|
-
return false;
|
|
298
|
-
},
|
|
299
|
-
addPushSubscription(subscription) {
|
|
300
|
-
const exists = data.pushSubscriptions.some((p) => PushSubscriptionVO.equals(p, subscription));
|
|
301
|
-
if (!exists) {
|
|
302
|
-
data.pushSubscriptions.push(subscription);
|
|
303
|
-
markDirty('pushSubscriptions');
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
return false;
|
|
307
|
-
},
|
|
308
|
-
removePushSubscription(p256dh) {
|
|
309
|
-
const index = data.pushSubscriptions.findIndex((p) => PushSubscriptionVO.matchesKey(p, p256dh));
|
|
310
|
-
if (index !== -1) {
|
|
311
|
-
data.pushSubscriptions.splice(index, 1);
|
|
312
|
-
markDirty('pushSubscriptions');
|
|
313
|
-
return true;
|
|
314
|
-
}
|
|
315
|
-
return false;
|
|
316
|
-
},
|
|
317
|
-
setAccessToken(hashedToken) {
|
|
318
|
-
data.services.token = { secret: hashedToken };
|
|
319
|
-
markDirty('token');
|
|
320
|
-
},
|
|
321
|
-
addVerificationToken(token) {
|
|
322
|
-
if (!data.services.email) {
|
|
323
|
-
data.services.email = { verificationTokens: [] };
|
|
324
|
-
}
|
|
325
|
-
data.services.email.verificationTokens.push(token);
|
|
326
|
-
markDirty('verificationTokens');
|
|
327
|
-
},
|
|
328
|
-
addResetToken(token) {
|
|
329
|
-
if (!data.services.password) {
|
|
330
|
-
data.services.password = {};
|
|
331
|
-
}
|
|
332
|
-
const passwordWithReset = data.services.password;
|
|
333
|
-
if (!passwordWithReset.reset) {
|
|
334
|
-
passwordWithReset.reset = [];
|
|
335
|
-
}
|
|
336
|
-
passwordWithReset.reset.push(token);
|
|
337
|
-
markDirty('resetTokens');
|
|
338
|
-
},
|
|
339
|
-
removeResetToken(hashedToken) {
|
|
340
|
-
const passwordWithReset = data.services.password;
|
|
341
|
-
if (passwordWithReset?.reset) {
|
|
342
|
-
passwordWithReset.reset = passwordWithReset.reset.filter((t) => t.token !== hashedToken);
|
|
343
|
-
markDirty('resetTokens');
|
|
344
|
-
}
|
|
345
|
-
},
|
|
346
|
-
clearVerificationTokens(address) {
|
|
347
|
-
if (data.services.email?.verificationTokens) {
|
|
348
|
-
data.services.email.verificationTokens = data.services.email.verificationTokens.filter((t) => t.address.toLowerCase() !== address.toLowerCase());
|
|
349
|
-
markDirty('verificationTokens');
|
|
350
|
-
}
|
|
351
|
-
},
|
|
352
|
-
markAsDeleted() {
|
|
353
|
-
data.deleted = new Date();
|
|
354
|
-
data.username = `deleted-${Date.now()}`;
|
|
355
|
-
data.emails = [];
|
|
356
|
-
data.roles = [];
|
|
357
|
-
data.services = {};
|
|
358
|
-
data.pushSubscriptions = [];
|
|
359
|
-
data.initialPassword = false;
|
|
360
|
-
data.profile = undefined;
|
|
361
|
-
data.lastBillingAddress = undefined;
|
|
362
|
-
data.lastContact = undefined;
|
|
363
|
-
data.lastLogin = undefined;
|
|
364
|
-
data.avatarId = undefined;
|
|
365
|
-
markDirty('deleted');
|
|
366
|
-
},
|
|
367
|
-
get isDirty() {
|
|
368
|
-
return isDirty;
|
|
369
|
-
},
|
|
370
|
-
get changes() {
|
|
371
|
-
return [...changes];
|
|
372
|
-
},
|
|
373
|
-
clearChanges() {
|
|
374
|
-
isDirty = false;
|
|
375
|
-
changes.length = 0;
|
|
376
|
-
},
|
|
377
|
-
toData() {
|
|
378
|
-
return JSON.parse(JSON.stringify(data));
|
|
379
|
-
},
|
|
380
|
-
toPublicData() {
|
|
381
|
-
const { services, ...publicData } = data;
|
|
382
|
-
return JSON.parse(JSON.stringify(publicData));
|
|
383
|
-
},
|
|
384
|
-
};
|
|
385
|
-
return user;
|
|
386
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import type { Address } from '../types.ts';
|
|
2
|
-
export interface UserProfile {
|
|
3
|
-
displayName?: string;
|
|
4
|
-
birthday?: Date;
|
|
5
|
-
phoneMobile?: string;
|
|
6
|
-
gender?: string;
|
|
7
|
-
address?: Address;
|
|
8
|
-
}
|
|
9
|
-
export declare const UserProfile: {
|
|
10
|
-
create(data?: Partial<UserProfile>): UserProfile;
|
|
11
|
-
fromData(data: UserProfile): UserProfile;
|
|
12
|
-
merge(existing: UserProfile | undefined, updates: Partial<UserProfile>): UserProfile;
|
|
13
|
-
isEmpty(profile: UserProfile | undefined): boolean;
|
|
14
|
-
};
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
export const UserProfile = {
|
|
2
|
-
create(data = {}) {
|
|
3
|
-
return {
|
|
4
|
-
displayName: data.displayName?.trim(),
|
|
5
|
-
birthday: data.birthday,
|
|
6
|
-
phoneMobile: data.phoneMobile?.trim(),
|
|
7
|
-
gender: data.gender?.trim(),
|
|
8
|
-
address: data.address,
|
|
9
|
-
};
|
|
10
|
-
},
|
|
11
|
-
fromData(data) {
|
|
12
|
-
return { ...data };
|
|
13
|
-
},
|
|
14
|
-
merge(existing, updates) {
|
|
15
|
-
return {
|
|
16
|
-
...existing,
|
|
17
|
-
...updates,
|
|
18
|
-
};
|
|
19
|
-
},
|
|
20
|
-
isEmpty(profile) {
|
|
21
|
-
if (!profile)
|
|
22
|
-
return true;
|
|
23
|
-
return (!profile.displayName &&
|
|
24
|
-
!profile.birthday &&
|
|
25
|
-
!profile.phoneMobile &&
|
|
26
|
-
!profile.gender &&
|
|
27
|
-
!profile.address);
|
|
28
|
-
},
|
|
29
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export interface Web3Address {
|
|
2
|
-
readonly address: string;
|
|
3
|
-
nonce?: string;
|
|
4
|
-
verified: boolean;
|
|
5
|
-
}
|
|
6
|
-
export declare const Web3Address: {
|
|
7
|
-
create(address: string, nonce?: string): Web3Address;
|
|
8
|
-
fromData(data: {
|
|
9
|
-
address: string;
|
|
10
|
-
nonce?: string;
|
|
11
|
-
verified?: boolean;
|
|
12
|
-
}): Web3Address;
|
|
13
|
-
isValidAddress(address: string): boolean;
|
|
14
|
-
equals(a: Web3Address, b: Web3Address): boolean;
|
|
15
|
-
markVerified(web3Address: Web3Address): Web3Address;
|
|
16
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export const Web3Address = {
|
|
2
|
-
create(address, nonce) {
|
|
3
|
-
if (!Web3Address.isValidAddress(address)) {
|
|
4
|
-
throw new Error(`Invalid Web3 address: ${address}`);
|
|
5
|
-
}
|
|
6
|
-
return {
|
|
7
|
-
address,
|
|
8
|
-
nonce,
|
|
9
|
-
verified: false,
|
|
10
|
-
};
|
|
11
|
-
},
|
|
12
|
-
fromData(data) {
|
|
13
|
-
return {
|
|
14
|
-
address: data.address,
|
|
15
|
-
nonce: data.nonce,
|
|
16
|
-
verified: data.verified ?? false,
|
|
17
|
-
};
|
|
18
|
-
},
|
|
19
|
-
isValidAddress(address) {
|
|
20
|
-
return /^0x[a-fA-F0-9]{40}$/.test(address);
|
|
21
|
-
},
|
|
22
|
-
equals(a, b) {
|
|
23
|
-
return a.address.toLowerCase() === b.address.toLowerCase();
|
|
24
|
-
},
|
|
25
|
-
markVerified(web3Address) {
|
|
26
|
-
return {
|
|
27
|
-
...web3Address,
|
|
28
|
-
nonce: undefined,
|
|
29
|
-
verified: true,
|
|
30
|
-
};
|
|
31
|
-
},
|
|
32
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export type NamedAlgo = 'RS256' | 'ES256';
|
|
2
|
-
export interface WebAuthnCredential {
|
|
3
|
-
readonly id: string;
|
|
4
|
-
readonly publicKey: string;
|
|
5
|
-
readonly algorithm: NamedAlgo;
|
|
6
|
-
readonly aaguid: string;
|
|
7
|
-
readonly type: 'public-key';
|
|
8
|
-
counter: number;
|
|
9
|
-
readonly created: Date;
|
|
10
|
-
}
|
|
11
|
-
export declare const WebAuthnCredential: {
|
|
12
|
-
create(data: {
|
|
13
|
-
id: string;
|
|
14
|
-
publicKey: string;
|
|
15
|
-
algorithm?: NamedAlgo;
|
|
16
|
-
aaguid?: string;
|
|
17
|
-
counter?: number;
|
|
18
|
-
}): WebAuthnCredential;
|
|
19
|
-
fromData(data: WebAuthnCredential): WebAuthnCredential;
|
|
20
|
-
equals(a: WebAuthnCredential, b: WebAuthnCredential): boolean;
|
|
21
|
-
updateCounter(credential: WebAuthnCredential, newCounter: number): WebAuthnCredential;
|
|
22
|
-
};
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export const WebAuthnCredential = {
|
|
2
|
-
create(data) {
|
|
3
|
-
if (!data.id || !data.publicKey) {
|
|
4
|
-
throw new Error('WebAuthn credential requires id and publicKey');
|
|
5
|
-
}
|
|
6
|
-
return {
|
|
7
|
-
id: data.id,
|
|
8
|
-
publicKey: data.publicKey,
|
|
9
|
-
algorithm: data.algorithm ?? 'ES256',
|
|
10
|
-
aaguid: data.aaguid ?? '',
|
|
11
|
-
type: 'public-key',
|
|
12
|
-
counter: data.counter ?? 0,
|
|
13
|
-
created: new Date(),
|
|
14
|
-
};
|
|
15
|
-
},
|
|
16
|
-
fromData(data) {
|
|
17
|
-
return { ...data, type: 'public-key' };
|
|
18
|
-
},
|
|
19
|
-
equals(a, b) {
|
|
20
|
-
return a.id === b.id;
|
|
21
|
-
},
|
|
22
|
-
updateCounter(credential, newCounter) {
|
|
23
|
-
return {
|
|
24
|
-
...credential,
|
|
25
|
-
counter: newCounter,
|
|
26
|
-
};
|
|
27
|
-
},
|
|
28
|
-
};
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export type { Email } from './Email.ts';
|
|
2
|
-
export { Email as EmailFactory } from './Email.ts';
|
|
3
|
-
export type { UserProfile } from './UserProfile.ts';
|
|
4
|
-
export { UserProfile as UserProfileFactory } from './UserProfile.ts';
|
|
5
|
-
export type { Web3Address } from './Web3Address.ts';
|
|
6
|
-
export { Web3Address as Web3AddressFactory } from './Web3Address.ts';
|
|
7
|
-
export type { WebAuthnCredential, NamedAlgo } from './WebAuthnCredential.ts';
|
|
8
|
-
export { WebAuthnCredential as WebAuthnCredentialFactory } from './WebAuthnCredential.ts';
|
|
9
|
-
export type { PasswordHash } from './PasswordHash.ts';
|
|
10
|
-
export { PasswordHash as PasswordHashFactory } from './PasswordHash.ts';
|
|
11
|
-
export type { PushSubscription } from './PushSubscription.ts';
|
|
12
|
-
export { PushSubscription as PushSubscriptionFactory } from './PushSubscription.ts';
|
|
13
|
-
export type { User, UserData, CreateUserParams } from './User.ts';
|
|
14
|
-
export { createUser, reconstituteUser } from './User.ts';
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export { Email as EmailFactory } from "./Email.js";
|
|
2
|
-
export { UserProfile as UserProfileFactory } from "./UserProfile.js";
|
|
3
|
-
export { Web3Address as Web3AddressFactory } from "./Web3Address.js";
|
|
4
|
-
export { WebAuthnCredential as WebAuthnCredentialFactory } from "./WebAuthnCredential.js";
|
|
5
|
-
export { PasswordHash as PasswordHashFactory } from "./PasswordHash.js";
|
|
6
|
-
export { PushSubscription as PushSubscriptionFactory } from "./PushSubscription.js";
|
|
7
|
-
export { createUser, reconstituteUser } from "./User.js";
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import type { User } from '../model/User.ts';
|
|
2
|
-
export interface UserQuery {
|
|
3
|
-
includeGuests?: boolean;
|
|
4
|
-
includeDeleted?: boolean;
|
|
5
|
-
queryString?: string;
|
|
6
|
-
emailVerified?: boolean;
|
|
7
|
-
lastLogin?: {
|
|
8
|
-
start?: string;
|
|
9
|
-
end?: string;
|
|
10
|
-
};
|
|
11
|
-
tags?: string[];
|
|
12
|
-
userIds?: string[];
|
|
13
|
-
username?: string;
|
|
14
|
-
usernames?: string[];
|
|
15
|
-
emails?: string[];
|
|
16
|
-
web3Verified?: boolean;
|
|
17
|
-
}
|
|
18
|
-
export interface FindOptions {
|
|
19
|
-
sort?: {
|
|
20
|
-
key: string;
|
|
21
|
-
value: 'ASC' | 'DESC';
|
|
22
|
-
}[];
|
|
23
|
-
limit?: number;
|
|
24
|
-
offset?: number;
|
|
25
|
-
}
|
|
26
|
-
export interface TokenResult {
|
|
27
|
-
userId: string;
|
|
28
|
-
address: string;
|
|
29
|
-
when: Date;
|
|
30
|
-
token: string;
|
|
31
|
-
}
|
|
32
|
-
export interface UserRepository {
|
|
33
|
-
findById(id: string): Promise<User | null>;
|
|
34
|
-
findByUsername(username: string): Promise<User | null>;
|
|
35
|
-
findByEmail(email: string): Promise<User | null>;
|
|
36
|
-
findByToken(hashedToken: string): Promise<User | null>;
|
|
37
|
-
findOne(query: UserQuery, options?: FindOptions): Promise<User | null>;
|
|
38
|
-
findMany(query: UserQuery, options?: FindOptions): Promise<User[]>;
|
|
39
|
-
count(query: UserQuery): Promise<number>;
|
|
40
|
-
exists(userId: string): Promise<boolean>;
|
|
41
|
-
findByVerificationToken(hashedToken: string, validAfter: Date): Promise<TokenResult | null>;
|
|
42
|
-
findByResetToken(hashedToken: string, validAfter: Date): Promise<TokenResult | null>;
|
|
43
|
-
save(user: User): Promise<void>;
|
|
44
|
-
insert(user: User): Promise<string>;
|
|
45
|
-
delete(userId: string): Promise<boolean>;
|
|
46
|
-
deleteUserSessions(userId: string): Promise<void>;
|
|
47
|
-
distinctTags(): Promise<string[]>;
|
|
48
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/lib/domain/types.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export interface Address {
|
|
2
|
-
firstName?: string;
|
|
3
|
-
lastName?: string;
|
|
4
|
-
company?: string;
|
|
5
|
-
addressLine?: string;
|
|
6
|
-
addressLine2?: string;
|
|
7
|
-
postalCode?: string;
|
|
8
|
-
regionCode?: string;
|
|
9
|
-
city?: string;
|
|
10
|
-
countryCode?: string;
|
|
11
|
-
}
|
|
12
|
-
export interface Contact {
|
|
13
|
-
telNumber?: string;
|
|
14
|
-
emailAddress?: string;
|
|
15
|
-
}
|
|
16
|
-
export interface UserLastLogin {
|
|
17
|
-
timestamp?: Date;
|
|
18
|
-
locale?: string;
|
|
19
|
-
countryCode?: string;
|
|
20
|
-
remoteAddress?: string;
|
|
21
|
-
remotePort?: number;
|
|
22
|
-
userAgent?: string;
|
|
23
|
-
}
|
|
24
|
-
export interface TokenRecord {
|
|
25
|
-
token: string;
|
|
26
|
-
address: string;
|
|
27
|
-
when: Date;
|
|
28
|
-
}
|
package/lib/domain/types.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { type mongodb } from '@unchainedshop/mongodb';
|
|
2
|
-
import type { UserRepository } from '../../domain/repository/UserRepository.ts';
|
|
3
|
-
import { type UserData } from '../../domain/model/User.ts';
|
|
4
|
-
export declare const createMongoUserRepository: (collection: mongodb.Collection<UserData>, db: mongodb.Db) => UserRepository;
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import { generateDbFilterById, buildSortOptions, insensitiveTrimmedRegexOperator, } from '@unchainedshop/mongodb';
|
|
2
|
-
import { SortDirection } from '@unchainedshop/utils';
|
|
3
|
-
import { reconstituteUser } from "../../domain/model/User.js";
|
|
4
|
-
import { buildUserSelector } from "./buildUserSelector.js";
|
|
5
|
-
export const createMongoUserRepository = (collection, db) => {
|
|
6
|
-
const buildFindOptions = (options) => {
|
|
7
|
-
const findOptions = {};
|
|
8
|
-
if (options?.offset) {
|
|
9
|
-
findOptions.skip = options.offset;
|
|
10
|
-
}
|
|
11
|
-
if (options?.limit) {
|
|
12
|
-
findOptions.limit = options.limit;
|
|
13
|
-
}
|
|
14
|
-
if (options?.sort) {
|
|
15
|
-
findOptions.sort = buildSortOptions(options.sort);
|
|
16
|
-
}
|
|
17
|
-
return findOptions;
|
|
18
|
-
};
|
|
19
|
-
return {
|
|
20
|
-
async findById(id) {
|
|
21
|
-
if (!id)
|
|
22
|
-
return null;
|
|
23
|
-
const data = await collection.findOne(generateDbFilterById(id));
|
|
24
|
-
return data ? reconstituteUser(data) : null;
|
|
25
|
-
},
|
|
26
|
-
async findByUsername(username) {
|
|
27
|
-
if (!username)
|
|
28
|
-
return null;
|
|
29
|
-
const data = await collection.findOne({
|
|
30
|
-
username: insensitiveTrimmedRegexOperator(username),
|
|
31
|
-
});
|
|
32
|
-
return data ? reconstituteUser(data) : null;
|
|
33
|
-
},
|
|
34
|
-
async findByEmail(email) {
|
|
35
|
-
if (!email)
|
|
36
|
-
return null;
|
|
37
|
-
const data = await collection.findOne({
|
|
38
|
-
'emails.address': insensitiveTrimmedRegexOperator(email),
|
|
39
|
-
});
|
|
40
|
-
return data ? reconstituteUser(data) : null;
|
|
41
|
-
},
|
|
42
|
-
async findByToken(hashedToken) {
|
|
43
|
-
if (!hashedToken)
|
|
44
|
-
return null;
|
|
45
|
-
const data = await collection.findOne({
|
|
46
|
-
'services.token.secret': hashedToken,
|
|
47
|
-
});
|
|
48
|
-
return data ? reconstituteUser(data) : null;
|
|
49
|
-
},
|
|
50
|
-
async findOne(query, options) {
|
|
51
|
-
const selector = buildUserSelector(query);
|
|
52
|
-
const findOptions = buildFindOptions(options);
|
|
53
|
-
const data = await collection.findOne(selector, findOptions);
|
|
54
|
-
return data ? reconstituteUser(data) : null;
|
|
55
|
-
},
|
|
56
|
-
async findMany(query, options) {
|
|
57
|
-
const selector = buildUserSelector(query);
|
|
58
|
-
const defaultSort = [{ key: 'created', value: SortDirection.ASC }];
|
|
59
|
-
const findOptions = buildFindOptions(options);
|
|
60
|
-
if (query.queryString) {
|
|
61
|
-
const docs = await collection
|
|
62
|
-
.find(selector, {
|
|
63
|
-
...findOptions,
|
|
64
|
-
projection: { score: { $meta: 'textScore' } },
|
|
65
|
-
sort: { score: { $meta: 'textScore' } },
|
|
66
|
-
})
|
|
67
|
-
.toArray();
|
|
68
|
-
return docs.map(reconstituteUser);
|
|
69
|
-
}
|
|
70
|
-
if (!findOptions.sort) {
|
|
71
|
-
findOptions.sort = buildSortOptions(defaultSort);
|
|
72
|
-
}
|
|
73
|
-
const docs = await collection.find(selector, findOptions).toArray();
|
|
74
|
-
return docs.map(reconstituteUser);
|
|
75
|
-
},
|
|
76
|
-
async count(query) {
|
|
77
|
-
const selector = buildUserSelector(query);
|
|
78
|
-
return collection.countDocuments(selector);
|
|
79
|
-
},
|
|
80
|
-
async exists(userId) {
|
|
81
|
-
const count = await collection.countDocuments({ _id: userId, deleted: null }, { limit: 1 });
|
|
82
|
-
return count === 1;
|
|
83
|
-
},
|
|
84
|
-
async findByVerificationToken(hashedToken, validAfter) {
|
|
85
|
-
if (!hashedToken)
|
|
86
|
-
return null;
|
|
87
|
-
const data = await collection.findOne({
|
|
88
|
-
'services.email.verificationTokens': {
|
|
89
|
-
$elemMatch: {
|
|
90
|
-
token: hashedToken,
|
|
91
|
-
when: { $gt: validAfter },
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
if (!data)
|
|
96
|
-
return null;
|
|
97
|
-
const verificationToken = data.services?.email?.verificationTokens?.find((v) => v.token === hashedToken);
|
|
98
|
-
if (!verificationToken)
|
|
99
|
-
return null;
|
|
100
|
-
return {
|
|
101
|
-
userId: data._id,
|
|
102
|
-
address: verificationToken.address,
|
|
103
|
-
when: verificationToken.when,
|
|
104
|
-
token: verificationToken.token,
|
|
105
|
-
};
|
|
106
|
-
},
|
|
107
|
-
async findByResetToken(hashedToken, validAfter) {
|
|
108
|
-
if (!hashedToken)
|
|
109
|
-
return null;
|
|
110
|
-
const data = await collection.findOne({
|
|
111
|
-
'services.password.reset': {
|
|
112
|
-
$elemMatch: {
|
|
113
|
-
token: hashedToken,
|
|
114
|
-
when: { $gt: validAfter },
|
|
115
|
-
},
|
|
116
|
-
},
|
|
117
|
-
});
|
|
118
|
-
if (!data)
|
|
119
|
-
return null;
|
|
120
|
-
const passwordService = data.services?.password;
|
|
121
|
-
const resetToken = passwordService?.reset?.find((v) => v.token === hashedToken);
|
|
122
|
-
if (!resetToken)
|
|
123
|
-
return null;
|
|
124
|
-
return {
|
|
125
|
-
userId: data._id,
|
|
126
|
-
address: resetToken.address,
|
|
127
|
-
when: resetToken.when,
|
|
128
|
-
token: resetToken.token,
|
|
129
|
-
};
|
|
130
|
-
},
|
|
131
|
-
async save(user) {
|
|
132
|
-
const data = user.toData();
|
|
133
|
-
await collection.replaceOne({ _id: data._id }, data, { upsert: true });
|
|
134
|
-
user.clearChanges();
|
|
135
|
-
},
|
|
136
|
-
async insert(user) {
|
|
137
|
-
const data = user.toData();
|
|
138
|
-
await collection.insertOne(data);
|
|
139
|
-
user.clearChanges();
|
|
140
|
-
return data._id;
|
|
141
|
-
},
|
|
142
|
-
async delete(userId) {
|
|
143
|
-
const result = await collection.deleteOne({ _id: userId });
|
|
144
|
-
return result.deletedCount > 0;
|
|
145
|
-
},
|
|
146
|
-
async deleteUserSessions(userId) {
|
|
147
|
-
await db.collection('sessions').deleteMany({
|
|
148
|
-
session: insensitiveTrimmedRegexOperator(`"user":"${userId}"`),
|
|
149
|
-
});
|
|
150
|
-
},
|
|
151
|
-
async distinctTags() {
|
|
152
|
-
const tags = (await collection.distinct('tags', {
|
|
153
|
-
tags: { $exists: true },
|
|
154
|
-
deleted: null,
|
|
155
|
-
}));
|
|
156
|
-
return tags.filter(Boolean).sort();
|
|
157
|
-
},
|
|
158
|
-
};
|
|
159
|
-
};
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
import { type mongodb } from '@unchainedshop/mongodb';
|
|
2
|
-
import type { UserQuery } from '../../domain/repository/UserRepository.ts';
|
|
3
|
-
import type { UserData } from '../../domain/model/User.ts';
|
|
4
|
-
export declare const buildUserSelector: ({ includeGuests, includeDeleted, queryString, emailVerified, lastLogin, tags, userIds, username, usernames, emails, web3Verified, }: UserQuery) => mongodb.Filter<UserData>;
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { insensitiveTrimmedRegexOperator, assertDocumentDBCompatMode, } from '@unchainedshop/mongodb';
|
|
2
|
-
export const buildUserSelector = ({ includeGuests, includeDeleted, queryString, emailVerified, lastLogin, tags, userIds, username, usernames, emails, web3Verified, }) => {
|
|
3
|
-
const selector = {};
|
|
4
|
-
if (!includeDeleted) {
|
|
5
|
-
selector.deleted = null;
|
|
6
|
-
}
|
|
7
|
-
if (!includeGuests) {
|
|
8
|
-
selector.guest = { $ne: true };
|
|
9
|
-
}
|
|
10
|
-
if (userIds) {
|
|
11
|
-
selector._id = { $in: userIds };
|
|
12
|
-
}
|
|
13
|
-
if (username) {
|
|
14
|
-
selector.username = insensitiveTrimmedRegexOperator(username);
|
|
15
|
-
}
|
|
16
|
-
if (usernames?.length) {
|
|
17
|
-
selector.username = {
|
|
18
|
-
$in: usernames.map((u) => insensitiveTrimmedRegexOperator(u)),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
if (emails?.length) {
|
|
22
|
-
selector['emails.address'] = {
|
|
23
|
-
$in: emails.map((e) => insensitiveTrimmedRegexOperator(e)),
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
if (emailVerified === true) {
|
|
27
|
-
selector['emails.verified'] = true;
|
|
28
|
-
}
|
|
29
|
-
if (emailVerified === false) {
|
|
30
|
-
selector['emails.verified'] = { $ne: true };
|
|
31
|
-
}
|
|
32
|
-
if (Array.isArray(tags) && tags.length) {
|
|
33
|
-
selector.tags = { $in: tags };
|
|
34
|
-
}
|
|
35
|
-
if (web3Verified === true) {
|
|
36
|
-
selector['services.web3.verified'] = true;
|
|
37
|
-
}
|
|
38
|
-
if (lastLogin?.start || lastLogin?.end) {
|
|
39
|
-
const timestampFilter = { $exists: true };
|
|
40
|
-
if (lastLogin.end) {
|
|
41
|
-
timestampFilter.$lte = typeof lastLogin.end === 'string' ? new Date(lastLogin.end) : lastLogin.end;
|
|
42
|
-
}
|
|
43
|
-
if (lastLogin.start) {
|
|
44
|
-
timestampFilter.$gte =
|
|
45
|
-
typeof lastLogin.start === 'string' ? new Date(lastLogin.start) : lastLogin.start;
|
|
46
|
-
}
|
|
47
|
-
selector['lastLogin.timestamp'] = timestampFilter;
|
|
48
|
-
}
|
|
49
|
-
if (queryString) {
|
|
50
|
-
assertDocumentDBCompatMode();
|
|
51
|
-
selector.$text = { $search: queryString };
|
|
52
|
-
}
|
|
53
|
-
return selector;
|
|
54
|
-
};
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { sha256 } from '@unchainedshop/utils';
|
|
2
|
-
export const createCryptoTokenService = () => {
|
|
3
|
-
return {
|
|
4
|
-
generateToken() {
|
|
5
|
-
return crypto.randomUUID();
|
|
6
|
-
},
|
|
7
|
-
async hashToken(plainToken) {
|
|
8
|
-
return sha256(plainToken);
|
|
9
|
-
},
|
|
10
|
-
async generateHashedToken() {
|
|
11
|
-
const plain = crypto.randomUUID();
|
|
12
|
-
const hashed = await sha256(plain);
|
|
13
|
-
return { plain, hashed };
|
|
14
|
-
},
|
|
15
|
-
};
|
|
16
|
-
};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import * as bcrypt from 'bcryptjs';
|
|
2
|
-
import { sha256 } from '@unchainedshop/utils';
|
|
3
|
-
import * as pbkdf2 from "../../module/pbkdf2.js";
|
|
4
|
-
export const createPbkdf2PasswordService = () => {
|
|
5
|
-
return {
|
|
6
|
-
async hash(plainPassword) {
|
|
7
|
-
const salt = pbkdf2.generateSalt();
|
|
8
|
-
const hashedPassword = await pbkdf2.getDerivedKey(salt, plainPassword);
|
|
9
|
-
return { pbkdf2: `${salt}:${hashedPassword}` };
|
|
10
|
-
},
|
|
11
|
-
async verify(hash, plainPassword) {
|
|
12
|
-
if (hash.pbkdf2) {
|
|
13
|
-
const [pbkdf2Salt, pbkdf2Hash] = hash.pbkdf2.split(':');
|
|
14
|
-
return pbkdf2.compare(plainPassword, pbkdf2Hash, pbkdf2Salt);
|
|
15
|
-
}
|
|
16
|
-
if (hash.bcrypt) {
|
|
17
|
-
const password = await sha256(plainPassword);
|
|
18
|
-
return bcrypt.compare(password, hash.bcrypt);
|
|
19
|
-
}
|
|
20
|
-
return false;
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
};
|