@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.
Files changed (133) hide show
  1. package/dist/config/PasswordPolicyConfig.js +14 -0
  2. package/dist/controllers/UserController.js +10 -0
  3. package/dist/factory/PasswordPolicyResolverFactory.js +9 -0
  4. package/dist/factory/PasswordPolicyServiceFactory.js +27 -0
  5. package/dist/factory/UserPasswordHistoryServiceFactory.js +25 -0
  6. package/dist/factory/UserServiceFactory.js +3 -1
  7. package/dist/index.js +20 -8
  8. package/dist/interfaces/IUserPasswordHistory.js +1 -0
  9. package/dist/interfaces/IUserPasswordHistoryRepository.js +1 -0
  10. package/dist/models/UserPasswordHistoryModel.js +30 -0
  11. package/dist/policies/defaultPasswordPolicy.js +12 -0
  12. package/dist/repository/mongo/UserPasswordHistoryMongoRepository.js +20 -0
  13. package/dist/repository/sqlite/UserPasswordHistorySqliteRepository.js +38 -0
  14. package/dist/resolver/PasswordPolicyResolver.js +27 -0
  15. package/dist/routes/UserRoutes.js +10 -0
  16. package/dist/schemas/PasswordPolicySchema.js +18 -0
  17. package/dist/schemas/RegisterSchema.js +1 -3
  18. package/dist/schemas/UserSchema.js +1 -3
  19. package/dist/security/constants/defaultPasswordPolicy.js +12 -0
  20. package/dist/security/interfaces/IPasswordPolicy.js +1 -0
  21. package/dist/security/interfaces/IPasswordPolicyProjectContext.js +1 -0
  22. package/dist/security/schemas/PasswordPolicySchema.js +18 -0
  23. package/dist/security/services/PasswordPolicyResolver.js +21 -0
  24. package/dist/security/services/PasswordPolicyService.js +147 -0
  25. package/dist/security/utils/PasswordPolicySchemaFactory.js +36 -0
  26. package/dist/security/utils/getPasswordEnvPolicy.js +19 -0
  27. package/dist/services/PasswordPolicyService.js +147 -0
  28. package/dist/services/UserPasswordHistoryService.js +18 -0
  29. package/dist/services/UserService.js +34 -9
  30. package/dist/setup/LoadIdentityConfigFromEnv.js +10 -0
  31. package/dist/setup/SetProjectPasswordPolicy.js +7 -0
  32. package/dist/utils/PasswordPolicySchemaFactory.js +36 -0
  33. package/dist/utils/getPasswordEnvPolicy.js +19 -0
  34. package/docs/password-policy.md +33 -0
  35. package/package.json +4 -4
  36. package/src/config/PasswordPolicyConfig.ts +14 -0
  37. package/src/controllers/UserController.ts +10 -1
  38. package/src/factory/PasswordPolicyResolverFactory.ts +14 -0
  39. package/src/factory/PasswordPolicyServiceFactory.ts +38 -0
  40. package/src/factory/UserPasswordHistoryServiceFactory.ts +31 -0
  41. package/src/factory/UserServiceFactory.ts +7 -1
  42. package/src/index.ts +28 -3
  43. package/src/interfaces/IUserPasswordHistory.ts +21 -0
  44. package/src/interfaces/IUserPasswordHistoryRepository.ts +8 -0
  45. package/src/models/UserPasswordHistoryModel.ts +42 -0
  46. package/src/policies/defaultPasswordPolicy.ts +17 -0
  47. package/src/repository/mongo/UserPasswordHistoryMongoRepository.ts +25 -0
  48. package/src/repository/sqlite/UserPasswordHistorySqliteRepository.ts +47 -0
  49. package/src/resolver/PasswordPolicyResolver.ts +33 -0
  50. package/src/routes/UserRoutes.ts +11 -0
  51. package/src/schemas/PasswordPolicySchema.ts +29 -0
  52. package/src/schemas/RegisterSchema.ts +1 -3
  53. package/src/schemas/UserSchema.ts +1 -3
  54. package/src/services/PasswordPolicyService.ts +184 -0
  55. package/src/services/UserPasswordHistoryService.ts +23 -0
  56. package/src/services/UserService.ts +38 -9
  57. package/src/setup/LoadIdentityConfigFromEnv.ts +11 -0
  58. package/src/setup/SetProjectPasswordPolicy.ts +12 -0
  59. package/src/utils/PasswordPolicySchemaFactory.ts +47 -0
  60. package/src/utils/getPasswordEnvPolicy.ts +25 -0
  61. package/test/data-obj/users/root-mongo-user.ts +1 -1
  62. package/test/data-obj/users/root-sqlite-user.ts +1 -1
  63. package/test/endpoints/data/users-data.ts +3 -3
  64. package/test/endpoints/password-policy-route.test.ts +33 -0
  65. package/test/endpoints/user-route.test.ts +17 -4
  66. package/test/security/password-policy-resolver.test.ts +55 -0
  67. package/test/security/password-policy-schema-factory.test.ts +40 -0
  68. package/test/services/user-service.test.ts +218 -31
  69. package/test/setup/TestSetup.ts +22 -4
  70. package/test/setup/data/basic-user.ts +1 -1
  71. package/test/setup/data/root-user.ts +1 -1
  72. package/tsconfig.tsbuildinfo +1 -1
  73. package/types/config/PasswordPolicyConfig.d.ts +14 -0
  74. package/types/config/PasswordPolicyConfig.d.ts.map +1 -0
  75. package/types/controllers/UserController.d.ts +1 -0
  76. package/types/controllers/UserController.d.ts.map +1 -1
  77. package/types/factory/PasswordPolicyResolverFactory.d.ts +4 -0
  78. package/types/factory/PasswordPolicyResolverFactory.d.ts.map +1 -0
  79. package/types/factory/PasswordPolicyServiceFactory.d.ts +4 -0
  80. package/types/factory/PasswordPolicyServiceFactory.d.ts.map +1 -0
  81. package/types/factory/UserPasswordHistoryServiceFactory.d.ts +4 -0
  82. package/types/factory/UserPasswordHistoryServiceFactory.d.ts.map +1 -0
  83. package/types/factory/UserServiceFactory.d.ts.map +1 -1
  84. package/types/index.d.ts +15 -2
  85. package/types/index.d.ts.map +1 -1
  86. package/types/interfaces/IUserPasswordHistory.d.ts +17 -0
  87. package/types/interfaces/IUserPasswordHistory.d.ts.map +1 -0
  88. package/types/interfaces/IUserPasswordHistoryRepository.d.ts +7 -0
  89. package/types/interfaces/IUserPasswordHistoryRepository.d.ts.map +1 -0
  90. package/types/models/UserPasswordHistoryModel.d.ts +15 -0
  91. package/types/models/UserPasswordHistoryModel.d.ts.map +1 -0
  92. package/types/policies/defaultPasswordPolicy.d.ts +4 -0
  93. package/types/policies/defaultPasswordPolicy.d.ts.map +1 -0
  94. package/types/repository/mongo/UserPasswordHistoryMongoRepository.d.ts +10 -0
  95. package/types/repository/mongo/UserPasswordHistoryMongoRepository.d.ts.map +1 -0
  96. package/types/repository/sqlite/UserPasswordHistorySqliteRepository.d.ts +25 -0
  97. package/types/repository/sqlite/UserPasswordHistorySqliteRepository.d.ts.map +1 -0
  98. package/types/resolver/PasswordPolicyResolver.d.ts +10 -0
  99. package/types/resolver/PasswordPolicyResolver.d.ts.map +1 -0
  100. package/types/routes/UserRoutes.d.ts.map +1 -1
  101. package/types/schemas/PasswordPolicySchema.d.ts +25 -0
  102. package/types/schemas/PasswordPolicySchema.d.ts.map +1 -0
  103. package/types/schemas/RegisterSchema.d.ts.map +1 -1
  104. package/types/schemas/UserSchema.d.ts.map +1 -1
  105. package/types/security/constants/defaultPasswordPolicy.d.ts +4 -0
  106. package/types/security/constants/defaultPasswordPolicy.d.ts.map +1 -0
  107. package/types/security/interfaces/IPasswordPolicy.d.ts +13 -0
  108. package/types/security/interfaces/IPasswordPolicy.d.ts.map +1 -0
  109. package/types/security/interfaces/IPasswordPolicyProjectContext.d.ts +6 -0
  110. package/types/security/interfaces/IPasswordPolicyProjectContext.d.ts.map +1 -0
  111. package/types/security/schemas/PasswordPolicySchema.d.ts +25 -0
  112. package/types/security/schemas/PasswordPolicySchema.d.ts.map +1 -0
  113. package/types/security/services/PasswordPolicyResolver.d.ts +9 -0
  114. package/types/security/services/PasswordPolicyResolver.d.ts.map +1 -0
  115. package/types/security/services/PasswordPolicyService.d.ts +35 -0
  116. package/types/security/services/PasswordPolicyService.d.ts.map +1 -0
  117. package/types/security/utils/PasswordPolicySchemaFactory.d.ts +9 -0
  118. package/types/security/utils/PasswordPolicySchemaFactory.d.ts.map +1 -0
  119. package/types/security/utils/getPasswordEnvPolicy.d.ts +5 -0
  120. package/types/security/utils/getPasswordEnvPolicy.d.ts.map +1 -0
  121. package/types/services/PasswordPolicyService.d.ts +34 -0
  122. package/types/services/PasswordPolicyService.d.ts.map +1 -0
  123. package/types/services/UserPasswordHistoryService.d.ts +10 -0
  124. package/types/services/UserPasswordHistoryService.d.ts.map +1 -0
  125. package/types/services/UserService.d.ts +5 -1
  126. package/types/services/UserService.d.ts.map +1 -1
  127. package/types/setup/LoadIdentityConfigFromEnv.d.ts.map +1 -1
  128. package/types/setup/SetProjectPasswordPolicy.d.ts +5 -0
  129. package/types/setup/SetProjectPasswordPolicy.d.ts.map +1 -0
  130. package/types/utils/PasswordPolicySchemaFactory.d.ts +9 -0
  131. package/types/utils/PasswordPolicySchemaFactory.d.ts.map +1 -0
  132. package/types/utils/getPasswordEnvPolicy.d.ts +5 -0
  133. package/types/utils/getPasswordEnvPolicy.d.ts.map +1 -0
@@ -11,12 +11,20 @@ import {AbstractService} from "@drax/crud-back";
11
11
  import {randomUUID} from "crypto"
12
12
  import UserLoginFailServiceFactory from "../factory/UserLoginFailServiceFactory.js";
13
13
  import UserSessionServiceFactory from "../factory/UserSessionServiceFactory.js";
14
+ import type PasswordPolicyService from "./PasswordPolicyService";
15
+ import PasswordPolicyServiceFactory from "../factory/PasswordPolicyServiceFactory.js";
16
+ import type UserPasswordHistoryService from "./UserPasswordHistoryService.js";
17
+ import UserPasswordHistoryServiceFactory from "../factory/UserPasswordHistoryServiceFactory.js";
14
18
 
15
19
  class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
16
20
 
17
21
  _repository: IUserRepository
18
22
 
19
- constructor(userRepository: IUserRepository) {
23
+ constructor(
24
+ userRepository: IUserRepository,
25
+ private readonly passwordPolicyService: PasswordPolicyService = PasswordPolicyServiceFactory(),
26
+ private readonly userPasswordHistoryService: UserPasswordHistoryService = UserPasswordHistoryServiceFactory()
27
+ ) {
20
28
  super(userRepository, UserBaseSchema);
21
29
  this._repository = userRepository;
22
30
  }
@@ -75,7 +83,7 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
75
83
  user = await this.findByEmail(email)
76
84
 
77
85
  if (!user && createIfNotFound) {
78
- userData.password = userData.password ? userData.password : randomUUID()
86
+ userData.password = userData.password ? userData.password : await this.passwordPolicyService.generateCompatiblePassword()
79
87
  userData.active = userData.active === undefined ? true : userData.active
80
88
  user = await this.create(userData)
81
89
  }
@@ -105,8 +113,14 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
105
113
  async changeUserPassword(userId: string, newPassword: string):Promise<IUser> {
106
114
  const user = await this._repository.findByIdWithPassword(userId)
107
115
  if (user) {
108
- newPassword = AuthUtils.hashPassword(newPassword)
109
- await this._repository.changePassword(userId, newPassword)
116
+ await this.passwordPolicyService.validatePassword(newPassword, {
117
+ field: 'newPassword',
118
+ userId,
119
+ currentPasswordHash: user.password
120
+ })
121
+ const newPasswordHash = AuthUtils.hashPassword(newPassword)
122
+ await this._repository.changePassword(userId, newPasswordHash)
123
+ await this.userPasswordHistoryService.create(userId, newPasswordHash)
110
124
  delete user.password
111
125
  return user
112
126
  } else {
@@ -124,8 +138,14 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
124
138
  }
125
139
 
126
140
  if (AuthUtils.checkPassword(currentPassword, user.password)) {
127
- newPassword = AuthUtils.hashPassword(newPassword)
128
- await this._repository.changePassword(userId, newPassword)
141
+ await this.passwordPolicyService.validatePassword(newPassword, {
142
+ field: 'newPassword',
143
+ userId,
144
+ currentPasswordHash: user.password
145
+ })
146
+ const newPasswordHash = AuthUtils.hashPassword(newPassword)
147
+ await this._repository.changePassword(userId, newPasswordHash)
148
+ await this.userPasswordHistoryService.create(userId, newPasswordHash)
129
149
  delete user.password
130
150
  return user
131
151
  } else {
@@ -167,8 +187,14 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
167
187
  try {
168
188
  const user = await this._repository.findByRecoveryCode(recoveryCode)
169
189
  if (user && user.active) {
170
- newPassword = AuthUtils.hashPassword(newPassword)
171
- await this._repository.changePassword(user._id, newPassword)
190
+ await this.passwordPolicyService.validatePassword(newPassword, {
191
+ field: 'newPassword',
192
+ userId: user._id.toString(),
193
+ currentPasswordHash: user.password
194
+ })
195
+ const newPasswordHash = AuthUtils.hashPassword(newPassword)
196
+ await this._repository.changePassword(user._id, newPasswordHash)
197
+ await this.userPasswordHistoryService.create(user._id.toString(), newPasswordHash)
172
198
  await this._repository.updatePartial(user._id, {recoveryCode: null})
173
199
  return user
174
200
  } else {
@@ -237,10 +263,13 @@ class UserService extends AbstractService<IUser, IUserCreate, IUserUpdate> {
237
263
  userData.tenant = userData.tenant === "" ? null : userData.tenant
238
264
 
239
265
  await UserCreateSchema.parseAsync(userData)
266
+ await this.passwordPolicyService.validatePassword(userData.password)
240
267
 
241
- userData.password = AuthUtils.hashPassword(userData.password.trim())
268
+ const passwordHash = AuthUtils.hashPassword(userData.password.trim())
269
+ userData.password = passwordHash
242
270
 
243
271
  const user: IUser = await this._repository.create(userData)
272
+ await this.userPasswordHistoryService.create(user._id.toString(), passwordHash)
244
273
  return user
245
274
  } catch (e) {
246
275
  console.error("Error creating user", 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
 
4
5
  function LoadIdentityConfigFromEnv() {
5
6
 
@@ -10,6 +11,16 @@ function LoadIdentityConfigFromEnv() {
10
11
 
11
12
  DraxConfig.set(IdentityConfig.RbacCacheTTL, process.env[IdentityConfig.RbacCacheTTL])
12
13
  DraxConfig.set(IdentityConfig.AvatarDir, process.env[IdentityConfig.AvatarDir])
14
+
15
+ DraxConfig.set(PasswordPolicyConfig.MinLength, process.env[PasswordPolicyConfig.MinLength])
16
+ DraxConfig.set(PasswordPolicyConfig.MaxLength, process.env[PasswordPolicyConfig.MaxLength])
17
+ DraxConfig.set(PasswordPolicyConfig.RequireUppercase, process.env[PasswordPolicyConfig.RequireUppercase])
18
+ DraxConfig.set(PasswordPolicyConfig.RequireLowercase, process.env[PasswordPolicyConfig.RequireLowercase])
19
+ DraxConfig.set(PasswordPolicyConfig.RequireNumber, process.env[PasswordPolicyConfig.RequireNumber])
20
+ DraxConfig.set(PasswordPolicyConfig.RequireSpecialChar, process.env[PasswordPolicyConfig.RequireSpecialChar])
21
+ DraxConfig.set(PasswordPolicyConfig.DisallowSpaces, process.env[PasswordPolicyConfig.DisallowSpaces])
22
+ DraxConfig.set(PasswordPolicyConfig.PreventReuse, process.env[PasswordPolicyConfig.PreventReuse])
23
+ DraxConfig.set(PasswordPolicyConfig.ExpirationDays, process.env[PasswordPolicyConfig.ExpirationDays])
13
24
  }
14
25
 
15
26
  export default LoadIdentityConfigFromEnv
@@ -0,0 +1,12 @@
1
+ import type {IPasswordPolicyProject} from "@drax/identity-share";
2
+ import PasswordPolicyResolverFactory from "../factory/PasswordPolicyResolverFactory.js";
3
+
4
+
5
+ function SetProjectPasswordPolicy(projectPasswordPolicy: IPasswordPolicyProject){
6
+
7
+ const passwordPolicyResolver = PasswordPolicyResolverFactory()
8
+ passwordPolicyResolver.setProjectPolicy(projectPasswordPolicy)
9
+ }
10
+
11
+ export default SetProjectPasswordPolicy
12
+ export {SetProjectPasswordPolicy}
@@ -0,0 +1,47 @@
1
+ import z from "zod";
2
+ import type {IPasswordPolicy} from "@drax/identity-share";
3
+
4
+ class PasswordPolicySchemaFactory {
5
+ private static cache = new Map<string, z.ZodType<string>>()
6
+
7
+ static create(policy: IPasswordPolicy): z.ZodType<string> {
8
+ const cacheKey = JSON.stringify(policy)
9
+ const cached = this.cache.get(cacheKey)
10
+ if (cached) {
11
+ return cached
12
+ }
13
+
14
+ let schema = z.string({error: "validation.required"})
15
+ .min(1, "validation.required")
16
+ .min(policy.minLength, "validation.password.minLength")
17
+ .max(policy.maxLength, "validation.password.maxLength")
18
+
19
+ if (policy.requireUppercase) {
20
+ schema = schema.regex(/[A-Z]/, "validation.password.requireUppercase")
21
+ }
22
+
23
+ if (policy.requireLowercase) {
24
+ schema = schema.regex(/[a-z]/, "validation.password.requireLowercase")
25
+ }
26
+
27
+ if (policy.requireNumber) {
28
+ schema = schema.regex(/[0-9]/, "validation.password.requireNumber")
29
+ }
30
+
31
+ if (policy.requireSpecialChar) {
32
+ schema = schema.regex(/[^A-Za-z0-9]/, "validation.password.requireSpecialChar")
33
+ }
34
+
35
+ if (policy.disallowSpaces) {
36
+ schema = schema.refine((value) => !/\s/.test(value), {
37
+ message: "validation.password.disallowSpaces"
38
+ })
39
+ }
40
+
41
+ this.cache.set(cacheKey, schema)
42
+ return schema
43
+ }
44
+ }
45
+
46
+ export default PasswordPolicySchemaFactory
47
+ export {PasswordPolicySchemaFactory}
@@ -0,0 +1,25 @@
1
+ import type {IPasswordPolicy} from "@drax/identity-share";
2
+ import {DraxConfig} from "@drax/common-back";
3
+ import PasswordPolicyConfig from "../config/PasswordPolicyConfig.js";
4
+ import {PartialPasswordPolicySchema} from "../schemas/PasswordPolicySchema.js";
5
+
6
+ function getPasswordEnvPolicy(): Partial<IPasswordPolicy> {
7
+ const envPolicy = {
8
+ minLength: DraxConfig.getOrLoad(PasswordPolicyConfig.MinLength, "number"),
9
+ maxLength: DraxConfig.getOrLoad(PasswordPolicyConfig.MaxLength, "number"),
10
+ requireUppercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireUppercase, "boolean"),
11
+ requireLowercase: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireLowercase, "boolean"),
12
+ requireNumber: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireNumber, "boolean"),
13
+ requireSpecialChar: DraxConfig.getOrLoad(PasswordPolicyConfig.RequireSpecialChar, "boolean"),
14
+ disallowSpaces: DraxConfig.getOrLoad(PasswordPolicyConfig.DisallowSpaces, "boolean"),
15
+ preventReuse: DraxConfig.getOrLoad(PasswordPolicyConfig.PreventReuse, "number"),
16
+ expirationDays: DraxConfig.getOrLoad(PasswordPolicyConfig.ExpirationDays, "number")
17
+ }
18
+
19
+ return PartialPasswordPolicySchema.parse(
20
+ Object.fromEntries(Object.entries(envPolicy).filter(([, value]) => value !== undefined))
21
+ )
22
+ }
23
+
24
+ export default getPasswordEnvPolicy
25
+ export {getPasswordEnvPolicy}
@@ -6,7 +6,7 @@ const user = {
6
6
  groups: [],
7
7
  username: "root",
8
8
  email: "root@example.com",
9
- password: "12345678",
9
+ password: "Root1234",
10
10
  name: "root",
11
11
  phone: "123456789",
12
12
  role: "646a661e44c93567c23d8d62",
@@ -6,7 +6,7 @@ const user = {
6
6
  groups: [],
7
7
  username: "root",
8
8
  email: "root@example.com",
9
- password: "123",
9
+ password: "Root1234",
10
10
  name: "root",
11
11
  phone: "123456789",
12
12
  avatar: "asd",
@@ -2,7 +2,7 @@ import {IUserCreate} from "@drax/identity-share";
2
2
 
3
3
  const USER1: IUserCreate = {
4
4
  active: true,
5
- password: "12345678",
5
+ password: "User1234",
6
6
  phone: "",
7
7
  role: "",
8
8
  name: "John Wick",
@@ -11,7 +11,7 @@ const USER1: IUserCreate = {
11
11
  }
12
12
  const USER2: IUserCreate = {
13
13
  active: true,
14
- password: "12345678",
14
+ password: "User1234",
15
15
  phone: "",
16
16
  role: "",
17
17
  name: "John Rambo",
@@ -20,7 +20,7 @@ const USER2: IUserCreate = {
20
20
  }
21
21
  const USER3: IUserCreate = {
22
22
  active: true,
23
- password: "12345678",
23
+ password: "User1234",
24
24
  phone: "",
25
25
  role: "",
26
26
  name: "John Depp",
@@ -0,0 +1,33 @@
1
+ import {afterAll, beforeAll, describe, expect, it} from "vitest";
2
+ import {LoadIdentityConfigFromEnv} from "../../src/setup/LoadIdentityConfigFromEnv.js";
3
+ import {TestSetup} from "../setup/TestSetup";
4
+
5
+ describe("Password Policy Route Test", () => {
6
+ const testSetup = new TestSetup("sqlite")
7
+
8
+ beforeAll(async () => {
9
+ await testSetup.setup()
10
+ process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR = "true"
11
+ process.env.PASSWORD_POLICY_MIN_LENGTH = "18"
12
+ LoadIdentityConfigFromEnv()
13
+ })
14
+
15
+ afterAll(async () => {
16
+ delete process.env.PASSWORD_POLICY_MIN_LENGTH
17
+ delete process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR
18
+ await testSetup.dropAndClose()
19
+ })
20
+
21
+ it("returns the final effective policy", async () => {
22
+ const response = await testSetup.fastifyInstance.inject({
23
+ method: "GET",
24
+ url: "/api/auth/password-policy"
25
+ })
26
+
27
+ expect(response.statusCode).toBe(200)
28
+ const body = response.json()
29
+ expect(body.minLength).toBe(18)
30
+ expect(body.requireSpecialChar).toBe(true)
31
+ expect(body.requireUppercase).toBe(true)
32
+ })
33
+ })
@@ -6,7 +6,7 @@ import {USER1} from "./data/users-data"
6
6
 
7
7
  describe("User Route Test", async function () {
8
8
 
9
- let testSetup = new TestSetup()
9
+ let testSetup = new TestSetup("sqlite")
10
10
 
11
11
  beforeAll(async () => {
12
12
  await testSetup.setup()
@@ -91,7 +91,7 @@ describe("User Route Test", async function () {
91
91
 
92
92
  const items = await getResp.json();
93
93
  expect(getResp.statusCode).toBe(200);
94
- expect(items[0].name).toBe(USER1.name);
94
+ expect(items.some((item) => item._id === result._id && item.name === USER1.name)).toBe(true);
95
95
  })
96
96
 
97
97
 
@@ -123,7 +123,7 @@ describe("User Route Test", async function () {
123
123
  const respPassword = await testSetup.fastifyInstance.inject({
124
124
  method: 'POST',
125
125
  url: '/api/users/password/change',
126
- payload: {currentPassword: "basic.123", newPassword: "newpass"},
126
+ payload: {currentPassword: "Basic1234", newPassword: "Newpass123"},
127
127
  headers: {Authorization: `Bearer ${accessToken}`}
128
128
  });
129
129
 
@@ -141,7 +141,7 @@ describe("User Route Test", async function () {
141
141
  const respPassword = await testSetup.fastifyInstance.inject({
142
142
  method: 'POST',
143
143
  url: '/api/users/password/change/'+testSetup.rootUser._id,
144
- payload: {currentPassword: "root.123", newPassword: "newpass"},
144
+ payload: {currentPassword: "Root1234", newPassword: "Newpass123"},
145
145
  headers: {Authorization: `Bearer ${accessToken}`}
146
146
  });
147
147
 
@@ -152,5 +152,18 @@ describe("User Route Test", async function () {
152
152
 
153
153
  })
154
154
 
155
+ it("should return effective password policy", async () => {
156
+ const resp = await testSetup.fastifyInstance.inject({
157
+ method: 'GET',
158
+ url: '/api/auth/password-policy',
159
+ });
160
+
161
+ const result = await resp.json();
162
+ expect(resp.statusCode).toBe(200);
163
+ expect(result.minLength).toBe(8);
164
+ expect(result.requireUppercase).toBe(true);
165
+ expect(result.preventReuse).toBe(3);
166
+ })
167
+
155
168
 
156
169
  })
@@ -0,0 +1,55 @@
1
+ import {afterEach, describe, expect, it} from "vitest";
2
+ import {DraxConfig} from "@drax/common-back";
3
+ import PasswordPolicyResolver from "../../src/resolver/PasswordPolicyResolver.js";
4
+ import PasswordPolicyConfig from "../../src/config/PasswordPolicyConfig.js";
5
+
6
+ describe("PasswordPolicyResolver", () => {
7
+ afterEach(() => {
8
+ delete process.env.PASSWORD_POLICY_MIN_LENGTH
9
+ delete process.env.PASSWORD_POLICY_MAX_LENGTH
10
+ delete process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR
11
+ DraxConfig.set(PasswordPolicyConfig.MinLength, undefined)
12
+ DraxConfig.set(PasswordPolicyConfig.MaxLength, undefined)
13
+ DraxConfig.set(PasswordPolicyConfig.RequireSpecialChar, undefined)
14
+ })
15
+
16
+ it("uses default policy when there are no overrides", async () => {
17
+ const resolver = new PasswordPolicyResolver()
18
+ const policy = await resolver.resolve()
19
+
20
+ expect(policy.minLength).toBe(8)
21
+ expect(policy.maxLength).toBe(64)
22
+ expect(policy.requireUppercase).toBe(true)
23
+ })
24
+
25
+ it("project policy overrides default policy", async () => {
26
+ const resolver = new PasswordPolicyResolver()
27
+ resolver.setProjectPolicy({minLength: 12, requireUppercase: false, requireSpecialChar: true})
28
+ const policy = await resolver.resolve()
29
+
30
+ expect(policy.minLength).toBe(12)
31
+ expect(policy.requireSpecialChar).toBe(true)
32
+ expect(policy.requireUppercase).toBe(false)
33
+ })
34
+
35
+ it("env policy overrides project policy", async () => {
36
+ process.env.PASSWORD_POLICY_MIN_LENGTH = "16"
37
+ process.env.PASSWORD_POLICY_REQUIRE_SPECIAL_CHAR = "false"
38
+
39
+ const resolver = new PasswordPolicyResolver()
40
+ const policy = await resolver.resolve()
41
+
42
+ expect(policy.minLength).toBe(16)
43
+ expect(policy.requireSpecialChar).toBe(false)
44
+ })
45
+
46
+ it("ignores undefined env overrides", async () => {
47
+ process.env.PASSWORD_POLICY_MIN_LENGTH = ""
48
+
49
+ const resolver = new PasswordPolicyResolver()
50
+ resolver.setProjectPolicy({minLength: 14})
51
+ const policy = await resolver.resolve()
52
+
53
+ expect(policy.minLength).toBe(14)
54
+ })
55
+ })
@@ -0,0 +1,40 @@
1
+ import {describe, expect, it} from "vitest";
2
+ import PasswordPolicySchemaFactory from "../../src/utils/PasswordPolicySchemaFactory.js";
3
+ import {defaultPasswordPolicy} from "../../src/policies/defaultPasswordPolicy.js";
4
+
5
+ describe("PasswordPolicySchemaFactory", () => {
6
+ it("validates minLength", async () => {
7
+ const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
8
+ await expect(schema.parseAsync("Abc1234")).rejects.toThrow()
9
+ })
10
+
11
+ it("validates maxLength", async () => {
12
+ const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, maxLength: 8})
13
+ await expect(schema.parseAsync("Abcd12345")).rejects.toThrow()
14
+ })
15
+
16
+ it("validates uppercase", async () => {
17
+ const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
18
+ await expect(schema.parseAsync("lowercase1")).rejects.toThrow()
19
+ })
20
+
21
+ it("validates lowercase", async () => {
22
+ const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
23
+ await expect(schema.parseAsync("UPPERCASE1")).rejects.toThrow()
24
+ })
25
+
26
+ it("validates number", async () => {
27
+ const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
28
+ await expect(schema.parseAsync("NoNumbers")).rejects.toThrow()
29
+ })
30
+
31
+ it("validates specialChar", async () => {
32
+ const schema = PasswordPolicySchemaFactory.create({...defaultPasswordPolicy, requireSpecialChar: true})
33
+ await expect(schema.parseAsync("NoSpecial1")).rejects.toThrow()
34
+ })
35
+
36
+ it("validates disallowSpaces", async () => {
37
+ const schema = PasswordPolicySchemaFactory.create(defaultPasswordPolicy)
38
+ await expect(schema.parseAsync("Space 123A")).rejects.toThrow()
39
+ })
40
+ })