@drax/identity-back 3.14.0 → 3.15.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/dist/config/PasswordPolicyConfig.js +14 -0
- package/dist/controllers/UserController.js +10 -0
- package/dist/factory/PasswordPolicyResolverFactory.js +9 -0
- package/dist/factory/PasswordPolicyServiceFactory.js +27 -0
- package/dist/factory/UserPasswordHistoryServiceFactory.js +25 -0
- package/dist/factory/UserServiceFactory.js +3 -1
- package/dist/index.js +20 -8
- package/dist/interfaces/IUserPasswordHistory.js +1 -0
- package/dist/interfaces/IUserPasswordHistoryRepository.js +1 -0
- package/dist/models/UserPasswordHistoryModel.js +30 -0
- package/dist/policies/defaultPasswordPolicy.js +12 -0
- package/dist/repository/mongo/UserPasswordHistoryMongoRepository.js +20 -0
- package/dist/repository/sqlite/UserPasswordHistorySqliteRepository.js +38 -0
- package/dist/resolver/PasswordPolicyResolver.js +27 -0
- package/dist/routes/UserRoutes.js +10 -0
- package/dist/schemas/PasswordPolicySchema.js +18 -0
- package/dist/schemas/RegisterSchema.js +1 -3
- package/dist/schemas/UserSchema.js +1 -3
- package/dist/security/constants/defaultPasswordPolicy.js +12 -0
- package/dist/security/interfaces/IPasswordPolicy.js +1 -0
- package/dist/security/interfaces/IPasswordPolicyProjectContext.js +1 -0
- package/dist/security/schemas/PasswordPolicySchema.js +18 -0
- package/dist/security/services/PasswordPolicyResolver.js +21 -0
- package/dist/security/services/PasswordPolicyService.js +147 -0
- package/dist/security/utils/PasswordPolicySchemaFactory.js +36 -0
- package/dist/security/utils/getPasswordEnvPolicy.js +19 -0
- package/dist/services/PasswordPolicyService.js +147 -0
- package/dist/services/UserPasswordHistoryService.js +18 -0
- package/dist/services/UserService.js +34 -9
- package/dist/setup/LoadIdentityConfigFromEnv.js +10 -0
- package/dist/setup/SetProjectPasswordPolicy.js +7 -0
- package/dist/utils/PasswordPolicySchemaFactory.js +36 -0
- package/dist/utils/getPasswordEnvPolicy.js +19 -0
- package/docs/password-policy.md +33 -0
- package/package.json +4 -4
- package/src/config/PasswordPolicyConfig.ts +14 -0
- package/src/controllers/UserController.ts +10 -1
- package/src/factory/PasswordPolicyResolverFactory.ts +14 -0
- package/src/factory/PasswordPolicyServiceFactory.ts +38 -0
- package/src/factory/UserPasswordHistoryServiceFactory.ts +31 -0
- package/src/factory/UserServiceFactory.ts +7 -1
- package/src/index.ts +28 -3
- package/src/interfaces/IUserPasswordHistory.ts +21 -0
- package/src/interfaces/IUserPasswordHistoryRepository.ts +8 -0
- package/src/models/UserPasswordHistoryModel.ts +42 -0
- package/src/policies/defaultPasswordPolicy.ts +17 -0
- package/src/repository/mongo/UserPasswordHistoryMongoRepository.ts +25 -0
- package/src/repository/sqlite/UserPasswordHistorySqliteRepository.ts +47 -0
- package/src/resolver/PasswordPolicyResolver.ts +33 -0
- package/src/routes/UserRoutes.ts +11 -0
- package/src/schemas/PasswordPolicySchema.ts +29 -0
- package/src/schemas/RegisterSchema.ts +1 -3
- package/src/schemas/UserSchema.ts +1 -3
- package/src/services/PasswordPolicyService.ts +184 -0
- package/src/services/UserPasswordHistoryService.ts +23 -0
- package/src/services/UserService.ts +38 -9
- package/src/setup/LoadIdentityConfigFromEnv.ts +11 -0
- package/src/setup/SetProjectPasswordPolicy.ts +12 -0
- package/src/utils/PasswordPolicySchemaFactory.ts +47 -0
- package/src/utils/getPasswordEnvPolicy.ts +25 -0
- package/test/data-obj/users/root-mongo-user.ts +1 -1
- package/test/data-obj/users/root-sqlite-user.ts +1 -1
- package/test/endpoints/data/users-data.ts +3 -3
- package/test/endpoints/password-policy-route.test.ts +33 -0
- package/test/endpoints/user-route.test.ts +17 -4
- package/test/security/password-policy-resolver.test.ts +55 -0
- package/test/security/password-policy-schema-factory.test.ts +40 -0
- package/test/services/user-service.test.ts +218 -31
- package/test/setup/TestSetup.ts +22 -4
- package/test/setup/data/basic-user.ts +1 -1
- package/test/setup/data/root-user.ts +1 -1
- package/tsconfig.tsbuildinfo +1 -1
- package/types/config/PasswordPolicyConfig.d.ts +14 -0
- package/types/config/PasswordPolicyConfig.d.ts.map +1 -0
- package/types/controllers/UserController.d.ts +1 -0
- package/types/controllers/UserController.d.ts.map +1 -1
- package/types/factory/PasswordPolicyResolverFactory.d.ts +4 -0
- package/types/factory/PasswordPolicyResolverFactory.d.ts.map +1 -0
- package/types/factory/PasswordPolicyServiceFactory.d.ts +4 -0
- package/types/factory/PasswordPolicyServiceFactory.d.ts.map +1 -0
- package/types/factory/UserPasswordHistoryServiceFactory.d.ts +4 -0
- package/types/factory/UserPasswordHistoryServiceFactory.d.ts.map +1 -0
- package/types/factory/UserServiceFactory.d.ts.map +1 -1
- package/types/index.d.ts +15 -2
- package/types/index.d.ts.map +1 -1
- package/types/interfaces/IUserPasswordHistory.d.ts +17 -0
- package/types/interfaces/IUserPasswordHistory.d.ts.map +1 -0
- package/types/interfaces/IUserPasswordHistoryRepository.d.ts +7 -0
- package/types/interfaces/IUserPasswordHistoryRepository.d.ts.map +1 -0
- package/types/models/UserPasswordHistoryModel.d.ts +15 -0
- package/types/models/UserPasswordHistoryModel.d.ts.map +1 -0
- package/types/policies/defaultPasswordPolicy.d.ts +4 -0
- package/types/policies/defaultPasswordPolicy.d.ts.map +1 -0
- package/types/repository/mongo/UserPasswordHistoryMongoRepository.d.ts +10 -0
- package/types/repository/mongo/UserPasswordHistoryMongoRepository.d.ts.map +1 -0
- package/types/repository/sqlite/UserPasswordHistorySqliteRepository.d.ts +25 -0
- package/types/repository/sqlite/UserPasswordHistorySqliteRepository.d.ts.map +1 -0
- package/types/resolver/PasswordPolicyResolver.d.ts +10 -0
- package/types/resolver/PasswordPolicyResolver.d.ts.map +1 -0
- package/types/routes/UserRoutes.d.ts.map +1 -1
- package/types/schemas/PasswordPolicySchema.d.ts +25 -0
- package/types/schemas/PasswordPolicySchema.d.ts.map +1 -0
- package/types/schemas/RegisterSchema.d.ts.map +1 -1
- package/types/schemas/UserSchema.d.ts.map +1 -1
- package/types/security/constants/defaultPasswordPolicy.d.ts +4 -0
- package/types/security/constants/defaultPasswordPolicy.d.ts.map +1 -0
- package/types/security/interfaces/IPasswordPolicy.d.ts +13 -0
- package/types/security/interfaces/IPasswordPolicy.d.ts.map +1 -0
- package/types/security/interfaces/IPasswordPolicyProjectContext.d.ts +6 -0
- package/types/security/interfaces/IPasswordPolicyProjectContext.d.ts.map +1 -0
- package/types/security/schemas/PasswordPolicySchema.d.ts +25 -0
- package/types/security/schemas/PasswordPolicySchema.d.ts.map +1 -0
- package/types/security/services/PasswordPolicyResolver.d.ts +9 -0
- package/types/security/services/PasswordPolicyResolver.d.ts.map +1 -0
- package/types/security/services/PasswordPolicyService.d.ts +35 -0
- package/types/security/services/PasswordPolicyService.d.ts.map +1 -0
- package/types/security/utils/PasswordPolicySchemaFactory.d.ts +9 -0
- package/types/security/utils/PasswordPolicySchemaFactory.d.ts.map +1 -0
- package/types/security/utils/getPasswordEnvPolicy.d.ts +5 -0
- package/types/security/utils/getPasswordEnvPolicy.d.ts.map +1 -0
- package/types/services/PasswordPolicyService.d.ts +34 -0
- package/types/services/PasswordPolicyService.d.ts.map +1 -0
- package/types/services/UserPasswordHistoryService.d.ts +10 -0
- package/types/services/UserPasswordHistoryService.d.ts.map +1 -0
- package/types/services/UserService.d.ts +5 -1
- package/types/services/UserService.d.ts.map +1 -1
- package/types/setup/LoadIdentityConfigFromEnv.d.ts.map +1 -1
- package/types/setup/SetProjectPasswordPolicy.d.ts +5 -0
- package/types/setup/SetProjectPasswordPolicy.d.ts.map +1 -0
- package/types/utils/PasswordPolicySchemaFactory.d.ts +9 -0
- package/types/utils/PasswordPolicySchemaFactory.d.ts.map +1 -0
- package/types/utils/getPasswordEnvPolicy.d.ts +5 -0
- package/types/utils/getPasswordEnvPolicy.d.ts.map +1 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
class PasswordPolicySchemaFactory {
|
|
3
|
+
static create(policy) {
|
|
4
|
+
const cacheKey = JSON.stringify(policy);
|
|
5
|
+
const cached = this.cache.get(cacheKey);
|
|
6
|
+
if (cached) {
|
|
7
|
+
return cached;
|
|
8
|
+
}
|
|
9
|
+
let schema = z.string({ error: "validation.required" })
|
|
10
|
+
.min(1, "validation.required")
|
|
11
|
+
.min(policy.minLength, "validation.password.minLength")
|
|
12
|
+
.max(policy.maxLength, "validation.password.maxLength");
|
|
13
|
+
if (policy.requireUppercase) {
|
|
14
|
+
schema = schema.regex(/[A-Z]/, "validation.password.requireUppercase");
|
|
15
|
+
}
|
|
16
|
+
if (policy.requireLowercase) {
|
|
17
|
+
schema = schema.regex(/[a-z]/, "validation.password.requireLowercase");
|
|
18
|
+
}
|
|
19
|
+
if (policy.requireNumber) {
|
|
20
|
+
schema = schema.regex(/[0-9]/, "validation.password.requireNumber");
|
|
21
|
+
}
|
|
22
|
+
if (policy.requireSpecialChar) {
|
|
23
|
+
schema = schema.regex(/[^A-Za-z0-9]/, "validation.password.requireSpecialChar");
|
|
24
|
+
}
|
|
25
|
+
if (policy.disallowSpaces) {
|
|
26
|
+
schema = schema.refine((value) => !/\s/.test(value), {
|
|
27
|
+
message: "validation.password.disallowSpaces"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
this.cache.set(cacheKey, schema);
|
|
31
|
+
return schema;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
PasswordPolicySchemaFactory.cache = new Map();
|
|
35
|
+
export default PasswordPolicySchemaFactory;
|
|
36
|
+
export { PasswordPolicySchemaFactory };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DraxConfig } from "@drax/common-back";
|
|
2
|
+
import PasswordPolicyConfig from "../../config/PasswordPolicyConfig.js";
|
|
3
|
+
import { PartialPasswordPolicySchema } from "../schemas/PasswordPolicySchema.js";
|
|
4
|
+
function getPasswordEnvPolicy() {
|
|
5
|
+
const envPolicy = {
|
|
6
|
+
minLength: DraxConfig.getOrLoad(PasswordPolicyConfig.MinLength, "number"),
|
|
7
|
+
maxLength: DraxConfig.getOrLoad(PasswordPolicyConfig.MaxLength, "number"),
|
|
8
|
+
requireUppercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireUppercase, "boolean"),
|
|
9
|
+
requireLowercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireLowercase, "boolean"),
|
|
10
|
+
requireNumber: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireNumber, "boolean"),
|
|
11
|
+
requireSpecialChar: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireSpecialChar, "boolean"),
|
|
12
|
+
disallowSpaces: DraxConfig.getOrLoad(PasswordPolicyConfig.DisallowSpaces, "boolean"),
|
|
13
|
+
preventReuse: DraxConfig.getOrLoad(PasswordPolicyConfig.PreventReuse, "number"),
|
|
14
|
+
expirationDays: DraxConfig.getOrLoad(PasswordPolicyConfig.ExpirationDays, "number")
|
|
15
|
+
};
|
|
16
|
+
return PartialPasswordPolicySchema.parse(Object.fromEntries(Object.entries(envPolicy).filter(([, value]) => value !== undefined)));
|
|
17
|
+
}
|
|
18
|
+
export default getPasswordEnvPolicy;
|
|
19
|
+
export { getPasswordEnvPolicy };
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { randomInt } from "crypto";
|
|
2
|
+
import { ZodError } from "zod";
|
|
3
|
+
import { ValidationError, ZodErrorToValidationError } from "@drax/common-back";
|
|
4
|
+
import AuthUtils from "../utils/AuthUtils.js";
|
|
5
|
+
import PasswordPolicySchemaFactory from "../utils/PasswordPolicySchemaFactory.js";
|
|
6
|
+
class PasswordPolicyService {
|
|
7
|
+
constructor(resolver, userRepository, userPasswordHistoryService) {
|
|
8
|
+
this.resolver = resolver;
|
|
9
|
+
this.userRepository = userRepository;
|
|
10
|
+
this.userPasswordHistoryService = userPasswordHistoryService;
|
|
11
|
+
}
|
|
12
|
+
async getFinalPolicy() {
|
|
13
|
+
return this.resolver.resolve();
|
|
14
|
+
}
|
|
15
|
+
async getPasswordSchema() {
|
|
16
|
+
const policy = await this.getFinalPolicy();
|
|
17
|
+
return PasswordPolicySchemaFactory.create(policy);
|
|
18
|
+
}
|
|
19
|
+
async validatePassword(password, options) {
|
|
20
|
+
const field = options?.field || "password";
|
|
21
|
+
try {
|
|
22
|
+
const schema = await this.getPasswordSchema();
|
|
23
|
+
await schema.parseAsync(password);
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
if (e instanceof ZodError) {
|
|
27
|
+
const validationError = ZodErrorToValidationError(e, { [field]: password });
|
|
28
|
+
validationError.errors = validationError.errors.map((error) => ({ ...error, field }));
|
|
29
|
+
throw validationError;
|
|
30
|
+
}
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
if (options?.userId) {
|
|
34
|
+
await this.validateReuse(password, options.userId, {
|
|
35
|
+
field,
|
|
36
|
+
currentPasswordHash: options.currentPasswordHash
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async generateCompatiblePassword() {
|
|
41
|
+
const policy = await this.getFinalPolicy();
|
|
42
|
+
const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
43
|
+
const lowercaseChars = "abcdefghijkmnopqrstuvwxyz";
|
|
44
|
+
const numericChars = "0123456789";
|
|
45
|
+
const specialChars = "!@#$%&*_-+=";
|
|
46
|
+
const fallbackSpecial = policy.disallowSpaces ? "!" : " ";
|
|
47
|
+
const combinedChars = [
|
|
48
|
+
uppercaseChars,
|
|
49
|
+
lowercaseChars,
|
|
50
|
+
numericChars,
|
|
51
|
+
policy.requireSpecialChar ? specialChars : "",
|
|
52
|
+
policy.disallowSpaces ? "" : " "
|
|
53
|
+
].join("") || `${uppercaseChars}${lowercaseChars}${numericChars}${fallbackSpecial}`;
|
|
54
|
+
const chars = [];
|
|
55
|
+
if (policy.requireUppercase) {
|
|
56
|
+
chars.push(this.randomChar(uppercaseChars));
|
|
57
|
+
}
|
|
58
|
+
if (policy.requireLowercase) {
|
|
59
|
+
chars.push(this.randomChar(lowercaseChars));
|
|
60
|
+
}
|
|
61
|
+
if (policy.requireNumber) {
|
|
62
|
+
chars.push(this.randomChar(numericChars));
|
|
63
|
+
}
|
|
64
|
+
if (policy.requireSpecialChar) {
|
|
65
|
+
chars.push(this.randomChar(specialChars));
|
|
66
|
+
}
|
|
67
|
+
if (!chars.length) {
|
|
68
|
+
chars.push(this.randomChar(`${uppercaseChars}${lowercaseChars}${numericChars}`));
|
|
69
|
+
}
|
|
70
|
+
while (chars.length < policy.minLength) {
|
|
71
|
+
chars.push(this.randomChar(combinedChars));
|
|
72
|
+
}
|
|
73
|
+
const password = this.shuffle(chars).join("").slice(0, policy.maxLength);
|
|
74
|
+
await this.validatePassword(password);
|
|
75
|
+
return password;
|
|
76
|
+
}
|
|
77
|
+
async getPasswordExpiration(user) {
|
|
78
|
+
const policy = await this.getFinalPolicy();
|
|
79
|
+
if (!policy.expirationDays) {
|
|
80
|
+
return { expired: false, expiresAt: null };
|
|
81
|
+
}
|
|
82
|
+
const lastPasswordChange = await this.getLastPasswordChangeDate(user);
|
|
83
|
+
if (!lastPasswordChange) {
|
|
84
|
+
return { expired: false, expiresAt: null };
|
|
85
|
+
}
|
|
86
|
+
const expiresAt = new Date(lastPasswordChange);
|
|
87
|
+
expiresAt.setDate(expiresAt.getDate() + policy.expirationDays);
|
|
88
|
+
return {
|
|
89
|
+
expired: expiresAt.getTime() <= Date.now(),
|
|
90
|
+
expiresAt
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async recordPassword(userId, passwordHash) {
|
|
94
|
+
await this.userPasswordHistoryService?.create(userId, passwordHash);
|
|
95
|
+
}
|
|
96
|
+
async validateReuse(password, userId, options) {
|
|
97
|
+
const policy = await this.getFinalPolicy();
|
|
98
|
+
if (!policy.preventReuse) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const field = options?.field || "password";
|
|
102
|
+
const recentHashes = await this.getRecentPasswordHashes(userId, policy.preventReuse, options?.currentPasswordHash);
|
|
103
|
+
const reused = recentHashes.some((item) => AuthUtils.checkPassword(password, item.passwordHash));
|
|
104
|
+
if (reused) {
|
|
105
|
+
throw new ValidationError([{ field, reason: "validation.password.preventReuse" }]);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async getRecentPasswordHashes(userId, limit, currentPasswordHash) {
|
|
109
|
+
const recent = await this.userPasswordHistoryService?.findLatestByUserId(userId, limit) || [];
|
|
110
|
+
const hashes = [...recent];
|
|
111
|
+
if (currentPasswordHash && !hashes.some((item) => item.passwordHash === currentPasswordHash)) {
|
|
112
|
+
hashes.unshift({ user: userId, passwordHash: currentPasswordHash });
|
|
113
|
+
}
|
|
114
|
+
else if (!currentPasswordHash && this.userRepository) {
|
|
115
|
+
const user = await this.userRepository.findByIdWithPassword(userId);
|
|
116
|
+
if (user?.password && !hashes.some((item) => item.passwordHash === user.password)) {
|
|
117
|
+
hashes.unshift({ user: userId, passwordHash: user.password });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return hashes.slice(0, limit);
|
|
121
|
+
}
|
|
122
|
+
async getLastPasswordChangeDate(user) {
|
|
123
|
+
if (!user?._id || !this.userPasswordHistoryService) {
|
|
124
|
+
return user?.updatedAt ? new Date(user.updatedAt) : user?.createdAt ? new Date(user.createdAt) : null;
|
|
125
|
+
}
|
|
126
|
+
const latest = await this.userPasswordHistoryService.findLatestByUserId(user._id.toString(), 1);
|
|
127
|
+
if (latest[0]?.createdAt) {
|
|
128
|
+
return new Date(latest[0].createdAt);
|
|
129
|
+
}
|
|
130
|
+
return user?.updatedAt ? new Date(user.updatedAt) : user?.createdAt ? new Date(user.createdAt) : null;
|
|
131
|
+
}
|
|
132
|
+
randomChar(source) {
|
|
133
|
+
return source[randomInt(0, source.length)];
|
|
134
|
+
}
|
|
135
|
+
shuffle(chars) {
|
|
136
|
+
const items = [...chars];
|
|
137
|
+
for (let i = items.length - 1; i > 0; i -= 1) {
|
|
138
|
+
const j = randomInt(0, i + 1);
|
|
139
|
+
const temp = items[i];
|
|
140
|
+
items[i] = items[j];
|
|
141
|
+
items[j] = temp;
|
|
142
|
+
}
|
|
143
|
+
return items;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export default PasswordPolicyService;
|
|
147
|
+
export { PasswordPolicyService };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class UserPasswordHistoryService {
|
|
2
|
+
constructor(repository) {
|
|
3
|
+
this.repository = repository;
|
|
4
|
+
}
|
|
5
|
+
async create(userId, passwordHash) {
|
|
6
|
+
return this.repository.create({
|
|
7
|
+
user: userId,
|
|
8
|
+
passwordHash
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
async findLatestByUserId(userId, limit) {
|
|
12
|
+
if (limit <= 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return this.repository.findLatestByUserId(userId, limit);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export default UserPasswordHistoryService;
|
|
@@ -7,9 +7,13 @@ import { AbstractService } from "@drax/crud-back";
|
|
|
7
7
|
import { randomUUID } from "crypto";
|
|
8
8
|
import UserLoginFailServiceFactory from "../factory/UserLoginFailServiceFactory.js";
|
|
9
9
|
import UserSessionServiceFactory from "../factory/UserSessionServiceFactory.js";
|
|
10
|
+
import PasswordPolicyServiceFactory from "../factory/PasswordPolicyServiceFactory.js";
|
|
11
|
+
import UserPasswordHistoryServiceFactory from "../factory/UserPasswordHistoryServiceFactory.js";
|
|
10
12
|
class UserService extends AbstractService {
|
|
11
|
-
constructor(userRepository) {
|
|
13
|
+
constructor(userRepository, passwordPolicyService = PasswordPolicyServiceFactory(), userPasswordHistoryService = UserPasswordHistoryServiceFactory()) {
|
|
12
14
|
super(userRepository, UserBaseSchema);
|
|
15
|
+
this.passwordPolicyService = passwordPolicyService;
|
|
16
|
+
this.userPasswordHistoryService = userPasswordHistoryService;
|
|
13
17
|
this._repository = userRepository;
|
|
14
18
|
}
|
|
15
19
|
async auth(username, password, { userAgent, ip }) {
|
|
@@ -60,7 +64,7 @@ class UserService extends AbstractService {
|
|
|
60
64
|
console.log("auth email", email);
|
|
61
65
|
user = await this.findByEmail(email);
|
|
62
66
|
if (!user && createIfNotFound) {
|
|
63
|
-
userData.password = userData.password ? userData.password :
|
|
67
|
+
userData.password = userData.password ? userData.password : await this.passwordPolicyService.generateCompatiblePassword();
|
|
64
68
|
userData.active = userData.active === undefined ? true : userData.active;
|
|
65
69
|
user = await this.create(userData);
|
|
66
70
|
}
|
|
@@ -85,8 +89,14 @@ class UserService extends AbstractService {
|
|
|
85
89
|
async changeUserPassword(userId, newPassword) {
|
|
86
90
|
const user = await this._repository.findByIdWithPassword(userId);
|
|
87
91
|
if (user) {
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
await this.passwordPolicyService.validatePassword(newPassword, {
|
|
93
|
+
field: 'newPassword',
|
|
94
|
+
userId,
|
|
95
|
+
currentPasswordHash: user.password
|
|
96
|
+
});
|
|
97
|
+
const newPasswordHash = AuthUtils.hashPassword(newPassword);
|
|
98
|
+
await this._repository.changePassword(userId, newPasswordHash);
|
|
99
|
+
await this.userPasswordHistoryService.create(userId, newPasswordHash);
|
|
90
100
|
delete user.password;
|
|
91
101
|
return user;
|
|
92
102
|
}
|
|
@@ -101,8 +111,14 @@ class UserService extends AbstractService {
|
|
|
101
111
|
throw new ValidationError([{ field: 'newPassword', reason: 'validation.password.currentDifferent' }]);
|
|
102
112
|
}
|
|
103
113
|
if (AuthUtils.checkPassword(currentPassword, user.password)) {
|
|
104
|
-
|
|
105
|
-
|
|
114
|
+
await this.passwordPolicyService.validatePassword(newPassword, {
|
|
115
|
+
field: 'newPassword',
|
|
116
|
+
userId,
|
|
117
|
+
currentPasswordHash: user.password
|
|
118
|
+
});
|
|
119
|
+
const newPasswordHash = AuthUtils.hashPassword(newPassword);
|
|
120
|
+
await this._repository.changePassword(userId, newPasswordHash);
|
|
121
|
+
await this.userPasswordHistoryService.create(userId, newPasswordHash);
|
|
106
122
|
delete user.password;
|
|
107
123
|
return user;
|
|
108
124
|
}
|
|
@@ -145,8 +161,14 @@ class UserService extends AbstractService {
|
|
|
145
161
|
try {
|
|
146
162
|
const user = await this._repository.findByRecoveryCode(recoveryCode);
|
|
147
163
|
if (user && user.active) {
|
|
148
|
-
|
|
149
|
-
|
|
164
|
+
await this.passwordPolicyService.validatePassword(newPassword, {
|
|
165
|
+
field: 'newPassword',
|
|
166
|
+
userId: user._id.toString(),
|
|
167
|
+
currentPasswordHash: user.password
|
|
168
|
+
});
|
|
169
|
+
const newPasswordHash = AuthUtils.hashPassword(newPassword);
|
|
170
|
+
await this._repository.changePassword(user._id, newPasswordHash);
|
|
171
|
+
await this.userPasswordHistoryService.create(user._id.toString(), newPasswordHash);
|
|
150
172
|
await this._repository.updatePartial(user._id, { recoveryCode: null });
|
|
151
173
|
return user;
|
|
152
174
|
}
|
|
@@ -210,8 +232,11 @@ class UserService extends AbstractService {
|
|
|
210
232
|
userData.password = userData?.password.trim();
|
|
211
233
|
userData.tenant = userData.tenant === "" ? null : userData.tenant;
|
|
212
234
|
await UserCreateSchema.parseAsync(userData);
|
|
213
|
-
|
|
235
|
+
await this.passwordPolicyService.validatePassword(userData.password);
|
|
236
|
+
const passwordHash = AuthUtils.hashPassword(userData.password.trim());
|
|
237
|
+
userData.password = passwordHash;
|
|
214
238
|
const user = await this._repository.create(userData);
|
|
239
|
+
await this.userPasswordHistoryService.create(user._id.toString(), passwordHash);
|
|
215
240
|
return user;
|
|
216
241
|
}
|
|
217
242
|
catch (e) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { DraxConfig } from "@drax/common-back";
|
|
2
2
|
import IdentityConfig from "../config/IdentityConfig.js";
|
|
3
|
+
import PasswordPolicyConfig from "../config/PasswordPolicyConfig.js";
|
|
3
4
|
function LoadIdentityConfigFromEnv() {
|
|
4
5
|
DraxConfig.set(IdentityConfig.JwtSecret, process.env[IdentityConfig.JwtSecret]);
|
|
5
6
|
DraxConfig.set(IdentityConfig.JwtExpiration, process.env[IdentityConfig.JwtExpiration]);
|
|
@@ -7,6 +8,15 @@ function LoadIdentityConfigFromEnv() {
|
|
|
7
8
|
DraxConfig.set(IdentityConfig.ApiKeySecret, process.env[IdentityConfig.ApiKeySecret]);
|
|
8
9
|
DraxConfig.set(IdentityConfig.RbacCacheTTL, process.env[IdentityConfig.RbacCacheTTL]);
|
|
9
10
|
DraxConfig.set(IdentityConfig.AvatarDir, process.env[IdentityConfig.AvatarDir]);
|
|
11
|
+
DraxConfig.set(PasswordPolicyConfig.MinLength, process.env[PasswordPolicyConfig.MinLength]);
|
|
12
|
+
DraxConfig.set(PasswordPolicyConfig.MaxLength, process.env[PasswordPolicyConfig.MaxLength]);
|
|
13
|
+
DraxConfig.set(PasswordPolicyConfig.RequireUppercase, process.env[PasswordPolicyConfig.RequireUppercase]);
|
|
14
|
+
DraxConfig.set(PasswordPolicyConfig.RequireLowercase, process.env[PasswordPolicyConfig.RequireLowercase]);
|
|
15
|
+
DraxConfig.set(PasswordPolicyConfig.RequireNumber, process.env[PasswordPolicyConfig.RequireNumber]);
|
|
16
|
+
DraxConfig.set(PasswordPolicyConfig.RequireSpecialChar, process.env[PasswordPolicyConfig.RequireSpecialChar]);
|
|
17
|
+
DraxConfig.set(PasswordPolicyConfig.DisallowSpaces, process.env[PasswordPolicyConfig.DisallowSpaces]);
|
|
18
|
+
DraxConfig.set(PasswordPolicyConfig.PreventReuse, process.env[PasswordPolicyConfig.PreventReuse]);
|
|
19
|
+
DraxConfig.set(PasswordPolicyConfig.ExpirationDays, process.env[PasswordPolicyConfig.ExpirationDays]);
|
|
10
20
|
}
|
|
11
21
|
export default LoadIdentityConfigFromEnv;
|
|
12
22
|
export { LoadIdentityConfigFromEnv };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import PasswordPolicyResolverFactory from "../factory/PasswordPolicyResolverFactory.js";
|
|
2
|
+
function SetProjectPasswordPolicy(projectPasswordPolicy) {
|
|
3
|
+
const passwordPolicyResolver = PasswordPolicyResolverFactory();
|
|
4
|
+
passwordPolicyResolver.setProjectPolicy(projectPasswordPolicy);
|
|
5
|
+
}
|
|
6
|
+
export default SetProjectPasswordPolicy;
|
|
7
|
+
export { SetProjectPasswordPolicy };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
class PasswordPolicySchemaFactory {
|
|
3
|
+
static create(policy) {
|
|
4
|
+
const cacheKey = JSON.stringify(policy);
|
|
5
|
+
const cached = this.cache.get(cacheKey);
|
|
6
|
+
if (cached) {
|
|
7
|
+
return cached;
|
|
8
|
+
}
|
|
9
|
+
let schema = z.string({ error: "validation.required" })
|
|
10
|
+
.min(1, "validation.required")
|
|
11
|
+
.min(policy.minLength, "validation.password.minLength")
|
|
12
|
+
.max(policy.maxLength, "validation.password.maxLength");
|
|
13
|
+
if (policy.requireUppercase) {
|
|
14
|
+
schema = schema.regex(/[A-Z]/, "validation.password.requireUppercase");
|
|
15
|
+
}
|
|
16
|
+
if (policy.requireLowercase) {
|
|
17
|
+
schema = schema.regex(/[a-z]/, "validation.password.requireLowercase");
|
|
18
|
+
}
|
|
19
|
+
if (policy.requireNumber) {
|
|
20
|
+
schema = schema.regex(/[0-9]/, "validation.password.requireNumber");
|
|
21
|
+
}
|
|
22
|
+
if (policy.requireSpecialChar) {
|
|
23
|
+
schema = schema.regex(/[^A-Za-z0-9]/, "validation.password.requireSpecialChar");
|
|
24
|
+
}
|
|
25
|
+
if (policy.disallowSpaces) {
|
|
26
|
+
schema = schema.refine((value) => !/\s/.test(value), {
|
|
27
|
+
message: "validation.password.disallowSpaces"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
this.cache.set(cacheKey, schema);
|
|
31
|
+
return schema;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
PasswordPolicySchemaFactory.cache = new Map();
|
|
35
|
+
export default PasswordPolicySchemaFactory;
|
|
36
|
+
export { PasswordPolicySchemaFactory };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DraxConfig } from "@drax/common-back";
|
|
2
|
+
import PasswordPolicyConfig from "../config/PasswordPolicyConfig.js";
|
|
3
|
+
import { PartialPasswordPolicySchema } from "../schemas/PasswordPolicySchema.js";
|
|
4
|
+
function getPasswordEnvPolicy() {
|
|
5
|
+
const envPolicy = {
|
|
6
|
+
minLength: DraxConfig.getOrLoad(PasswordPolicyConfig.MinLength, "number"),
|
|
7
|
+
maxLength: DraxConfig.getOrLoad(PasswordPolicyConfig.MaxLength, "number"),
|
|
8
|
+
requireUppercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireUppercase, "boolean"),
|
|
9
|
+
requireLowercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireLowercase, "boolean"),
|
|
10
|
+
requireNumber: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireNumber, "boolean"),
|
|
11
|
+
requireSpecialChar: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireSpecialChar, "boolean"),
|
|
12
|
+
disallowSpaces: DraxConfig.getOrLoad(PasswordPolicyConfig.DisallowSpaces, "boolean"),
|
|
13
|
+
preventReuse: DraxConfig.getOrLoad(PasswordPolicyConfig.PreventReuse, "number"),
|
|
14
|
+
expirationDays: DraxConfig.getOrLoad(PasswordPolicyConfig.ExpirationDays, "number")
|
|
15
|
+
};
|
|
16
|
+
return PartialPasswordPolicySchema.parse(Object.fromEntries(Object.entries(envPolicy).filter(([, value]) => value !== undefined)));
|
|
17
|
+
}
|
|
18
|
+
export default getPasswordEnvPolicy;
|
|
19
|
+
export { getPasswordEnvPolicy };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Password Policy
|
|
2
|
+
|
|
3
|
+
La policy efectiva se resuelve con esta prioridad:
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const finalPolicy = {
|
|
7
|
+
...defaultPasswordPolicy,
|
|
8
|
+
...projectPolicy,
|
|
9
|
+
...envPolicy
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Fuentes
|
|
14
|
+
|
|
15
|
+
- `defaultPasswordPolicy`: `src/security/constants/defaultPasswordPolicy.ts`
|
|
16
|
+
- `projectPolicy`: override opcional in-memory vía `projectContext`
|
|
17
|
+
- `envPolicy`: variables declaradas en `src/config/PasswordPolicyConfig.ts`
|
|
18
|
+
|
|
19
|
+
## Endpoint
|
|
20
|
+
|
|
21
|
+
- `GET /api/auth/password-policy`
|
|
22
|
+
|
|
23
|
+
Devuelve la policy efectiva para que frontend pueda mostrar requisitos de password antes de enviar formularios.
|
|
24
|
+
|
|
25
|
+
## Validación
|
|
26
|
+
|
|
27
|
+
- Formato: `PasswordPolicySchemaFactory` genera un `ZodType<string>` dinámico
|
|
28
|
+
- Negocio: `PasswordPolicyService` aplica `preventReuse` y expone base para `expirationDays`
|
|
29
|
+
|
|
30
|
+
## Notas
|
|
31
|
+
|
|
32
|
+
- `preventReuse` usa persistencia en `user_password_history`
|
|
33
|
+
- `expirationDays` ya forma parte de la policy efectiva y del servicio, pero en esta primera versión queda informativo; no bloquea login todavía
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "3.
|
|
6
|
+
"version": "3.15.0",
|
|
7
7
|
"description": "Identity module for user management, authentication and authorization.",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
9
|
"types": "types/index.d.ts",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"license": "ISC",
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@drax/common-back": "^3.14.0",
|
|
32
|
-
"@drax/crud-back": "^3.
|
|
32
|
+
"@drax/crud-back": "^3.15.0",
|
|
33
33
|
"@drax/crud-share": "^3.14.0",
|
|
34
34
|
"@drax/email-back": "^3.1.0",
|
|
35
|
-
"@drax/identity-share": "^3.
|
|
35
|
+
"@drax/identity-share": "^3.15.0",
|
|
36
36
|
"bcryptjs": "^2.4.3",
|
|
37
37
|
"graphql": "^16.8.2",
|
|
38
38
|
"jsonwebtoken": "^9.0.2"
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
"debug": "0"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
-
"gitHead": "
|
|
66
|
+
"gitHead": "a3bd3419f580b111b26da9e6be2e1cc4c75a056e"
|
|
67
67
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
enum PasswordPolicyConfig {
|
|
2
|
+
MinLength = "PASSWORD_POLICY_MIN_LENGTH",
|
|
3
|
+
MaxLength = "PASSWORD_POLICY_MAX_LENGTH",
|
|
4
|
+
RequireUppercase = "PASSWORD_POLICY_REQUIRE_UPPERCASE",
|
|
5
|
+
RequireLowercase = "PASSWORD_POLICY_REQUIRE_LOWERCASE",
|
|
6
|
+
RequireNumber = "PASSWORD_POLICY_REQUIRE_NUMBER",
|
|
7
|
+
RequireSpecialChar = "PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR",
|
|
8
|
+
DisallowSpaces = "PASSWORD_POLICY_DISALLOW_SPACES",
|
|
9
|
+
PreventReuse = "PASSWORD_POLICY_PREVENT_REUSE",
|
|
10
|
+
ExpirationDays = "PASSWORD_POLICY_EXPIRATION_DAYS",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default PasswordPolicyConfig;
|
|
14
|
+
export {PasswordPolicyConfig};
|
|
@@ -20,6 +20,7 @@ import UserEmailService from "../services/UserEmailService.js";
|
|
|
20
20
|
import {IDraxCrudEvent, IDraxFieldFilter} from "@drax/crud-share";
|
|
21
21
|
import TenantServiceFactory from "../factory/TenantServiceFactory.js";
|
|
22
22
|
import {CustomRequest} from "@drax/crud-back/dist";
|
|
23
|
+
import PasswordPolicyServiceFactory from "../factory/PasswordPolicyServiceFactory.js";
|
|
23
24
|
|
|
24
25
|
const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
|
|
25
26
|
const AVATAR_DIR = DraxConfig.getOrLoad(IdentityConfig.AvatarDir) || 'avatar';
|
|
@@ -91,6 +92,15 @@ class UserController extends AbstractFastifyController<IUser, IUserCreate, IUser
|
|
|
91
92
|
}
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
async passwordPolicy(request, reply) {
|
|
96
|
+
try {
|
|
97
|
+
const passwordPolicyService = PasswordPolicyServiceFactory()
|
|
98
|
+
return await passwordPolicyService.getFinalPolicy()
|
|
99
|
+
} catch (e) {
|
|
100
|
+
this.handleError(e, reply)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
async me(request, reply) {
|
|
95
105
|
try {
|
|
96
106
|
if (request.authUser) {
|
|
@@ -504,4 +514,3 @@ export default UserController;
|
|
|
504
514
|
export {
|
|
505
515
|
UserController
|
|
506
516
|
}
|
|
507
|
-
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import PasswordPolicyResolver from "../resolver/PasswordPolicyResolver.js";
|
|
2
|
+
|
|
3
|
+
let passwordPolicyResolver: PasswordPolicyResolver
|
|
4
|
+
|
|
5
|
+
const PasswordPolicyResolverFactory = (): PasswordPolicyResolver => {
|
|
6
|
+
if (!passwordPolicyResolver) {
|
|
7
|
+
|
|
8
|
+
passwordPolicyResolver = new PasswordPolicyResolver()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return passwordPolicyResolver
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default PasswordPolicyResolverFactory
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import PasswordPolicyService from "../services/PasswordPolicyService.js";
|
|
2
|
+
import PasswordPolicyResolverFactory from "./PasswordPolicyResolverFactory.js";
|
|
3
|
+
import UserPasswordHistoryServiceFactory from "./UserPasswordHistoryServiceFactory.js";
|
|
4
|
+
import UserMongoRepository from "../repository/mongo/UserMongoRepository.js";
|
|
5
|
+
import UserSqliteRepository from "../repository/sqlite/UserSqliteRepository.js";
|
|
6
|
+
import {COMMON, CommonConfig, DraxConfig} from "@drax/common-back";
|
|
7
|
+
import type {IUserRepository} from "../interfaces/IUserRepository.js";
|
|
8
|
+
|
|
9
|
+
let passwordPolicyService: PasswordPolicyService
|
|
10
|
+
|
|
11
|
+
const PasswordPolicyServiceFactory = (verbose: boolean = false): PasswordPolicyService => {
|
|
12
|
+
if (!passwordPolicyService) {
|
|
13
|
+
let userRepository: IUserRepository
|
|
14
|
+
|
|
15
|
+
switch (DraxConfig.getOrLoad(CommonConfig.DbEngine)) {
|
|
16
|
+
case COMMON.DB_ENGINES.MONGODB:
|
|
17
|
+
userRepository = new UserMongoRepository()
|
|
18
|
+
break;
|
|
19
|
+
case COMMON.DB_ENGINES.SQLITE:
|
|
20
|
+
userRepository = new UserSqliteRepository(DraxConfig.getOrLoad(CommonConfig.SqliteDbFile), verbose)
|
|
21
|
+
userRepository.build()
|
|
22
|
+
break;
|
|
23
|
+
default:
|
|
24
|
+
throw new Error("DraxConfig.DB_ENGINE must be one of " + Object.values(COMMON.DB_ENGINES).join(", "));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const passwordPolicyResolver = PasswordPolicyResolverFactory()
|
|
28
|
+
passwordPolicyService = new PasswordPolicyService(
|
|
29
|
+
passwordPolicyResolver,
|
|
30
|
+
userRepository,
|
|
31
|
+
UserPasswordHistoryServiceFactory(verbose)
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return passwordPolicyService
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default PasswordPolicyServiceFactory
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {COMMON, CommonConfig, DraxConfig} from "@drax/common-back";
|
|
2
|
+
import type {IUserPasswordHistoryRepository} from "../interfaces/IUserPasswordHistoryRepository.js";
|
|
3
|
+
import UserPasswordHistoryMongoRepository from "../repository/mongo/UserPasswordHistoryMongoRepository.js";
|
|
4
|
+
import UserPasswordHistorySqliteRepository from "../repository/sqlite/UserPasswordHistorySqliteRepository.js";
|
|
5
|
+
import UserPasswordHistoryService from "../services/UserPasswordHistoryService.js";
|
|
6
|
+
|
|
7
|
+
let userPasswordHistoryService: UserPasswordHistoryService
|
|
8
|
+
|
|
9
|
+
const UserPasswordHistoryServiceFactory = (verbose: boolean = false): UserPasswordHistoryService => {
|
|
10
|
+
if (!userPasswordHistoryService) {
|
|
11
|
+
let repository: IUserPasswordHistoryRepository
|
|
12
|
+
switch (DraxConfig.getOrLoad(CommonConfig.DbEngine)) {
|
|
13
|
+
case COMMON.DB_ENGINES.MONGODB:
|
|
14
|
+
repository = new UserPasswordHistoryMongoRepository()
|
|
15
|
+
break;
|
|
16
|
+
case COMMON.DB_ENGINES.SQLITE:
|
|
17
|
+
const sqliteRepository = new UserPasswordHistorySqliteRepository(DraxConfig.getOrLoad(CommonConfig.SqliteDbFile), verbose)
|
|
18
|
+
sqliteRepository.build()
|
|
19
|
+
repository = sqliteRepository
|
|
20
|
+
break;
|
|
21
|
+
default:
|
|
22
|
+
throw new Error("DraxConfig.DB_ENGINE must be one of " + Object.values(COMMON.DB_ENGINES).join(", "));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
userPasswordHistoryService = new UserPasswordHistoryService(repository)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return userPasswordHistoryService
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default UserPasswordHistoryServiceFactory
|
|
@@ -3,6 +3,8 @@ import UserService from "../services/UserService.js";
|
|
|
3
3
|
import UserSqliteRepository from "../repository/sqlite/UserSqliteRepository.js";
|
|
4
4
|
import {IUserRepository} from "../interfaces/IUserRepository";
|
|
5
5
|
import {COMMON, CommonConfig, DraxConfig} from "@drax/common-back";
|
|
6
|
+
import PasswordPolicyServiceFactory from "./PasswordPolicyServiceFactory.js";
|
|
7
|
+
import UserPasswordHistoryServiceFactory from "./UserPasswordHistoryServiceFactory.js";
|
|
6
8
|
|
|
7
9
|
let userService: UserService
|
|
8
10
|
|
|
@@ -22,7 +24,11 @@ const UserServiceFactory = (verbose:boolean = false) : UserService => {
|
|
|
22
24
|
throw new Error("DraxConfig.DB_ENGINE must be one of " + Object.values(COMMON.DB_ENGINES).join(", "));
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
userService = new UserService(
|
|
27
|
+
userService = new UserService(
|
|
28
|
+
userRepository,
|
|
29
|
+
PasswordPolicyServiceFactory(verbose),
|
|
30
|
+
UserPasswordHistoryServiceFactory(verbose)
|
|
31
|
+
)
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
return userService
|