@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
package/src/index.ts CHANGED
@@ -5,6 +5,8 @@ import TenantServiceFactory from "./factory/TenantServiceFactory.js";
5
5
  import UserApiKeyServiceFactory from "./factory/UserApiKeyServiceFactory.js";
6
6
  import UserLoginFailServiceFactory from "./factory/UserLoginFailServiceFactory.js";
7
7
  import UserSessionServiceFactory from "./factory/UserSessionServiceFactory.js";
8
+ import PasswordPolicyServiceFactory from "./factory/PasswordPolicyServiceFactory.js";
9
+ import UserPasswordHistoryServiceFactory from "./factory/UserPasswordHistoryServiceFactory.js";
8
10
 
9
11
  import RoleService from "./services/RoleService.js";
10
12
  import UserService from "./services/UserService.js";
@@ -13,6 +15,9 @@ import PermissionService from "./services/PermissionService.js";
13
15
  import UserApiKeyService from "./services/UserApiKeyService.js";
14
16
  import UserSessionService from "./services/UserSessionService.js";
15
17
  import UserLoginFailService from "./services/UserLoginFailService.js";
18
+ import UserPasswordHistoryService from "./services/UserPasswordHistoryService.js";
19
+ import PasswordPolicyService from "./services/PasswordPolicyService.js";
20
+ import PasswordPolicyResolver from "./resolver/PasswordPolicyResolver.js";
16
21
 
17
22
  import Rbac from "./rbac/Rbac.js";
18
23
 
@@ -29,6 +34,7 @@ import {rbacMiddleware} from "./middleware/rbacMiddleware.js";
29
34
  import {apiKeyMiddleware} from "./middleware/apiKeyMiddleware.js";
30
35
 
31
36
  import IdentityConfig from "./config/IdentityConfig.js";
37
+ import PasswordPolicyConfig from "./config/PasswordPolicyConfig.js";
32
38
  import BadCredentialsError from "./errors/BadCredentialsError.js";
33
39
 
34
40
  import CreateUserIfNotExist from "./setup/CreateUserIfNotExist.js";
@@ -37,6 +43,7 @@ import CreateOrUpdateRole from "./setup/CreateOrUpdateRole.js";
37
43
  import LoadPermissions from "./setup/LoadPermissions.js";
38
44
  import LoadIdentityConfigFromEnv from "./setup/LoadIdentityConfigFromEnv.js";
39
45
  import RecoveryUserPassword from "./setup/RecoveryUserPassword.js";
46
+ import SetProjectPasswordPolicy from "./setup/SetProjectPasswordPolicy.js";
40
47
 
41
48
  import type {IRoleRepository} from "./interfaces/IRoleRepository";
42
49
  import type {ITenantRepository} from "./interfaces/ITenantRepository";
@@ -44,6 +51,7 @@ import type {IUserRepository} from "./interfaces/IUserRepository";
44
51
  import type {IUserApiKeyRepository} from "./interfaces/IUserApiKeyRepository";
45
52
  import type {IUserLoginFailRepository} from "./interfaces/IUserLoginFailRepository";
46
53
  import type {IUserSessionRepository} from "./interfaces/IUserSessionRepository";
54
+ import type {IUserPasswordHistoryRepository} from "./interfaces/IUserPasswordHistoryRepository";
47
55
 
48
56
  import {RoleModel, RoleMongoSchema} from "./models/RoleModel.js";
49
57
  import {TenantModel, TenantMongoSchema} from "./models/TenantModel.js";
@@ -51,6 +59,7 @@ import {UserModel, UserMongoSchema} from "./models/UserModel.js";
51
59
  import {UserApiKeyModel, UserApiKeyMongoSchema} from "./models/UserApiKeyModel.js";
52
60
  import {UserSessionModel,UserSessionMongoSchema} from "./models/UserSessionModel.js";
53
61
  import {UserLoginFailModel,UserLoginFailMongoSchema} from "./models/UserLoginFailModel.js";
62
+ import {UserPasswordHistoryModel, UserPasswordHistoryMongoSchema} from "./models/UserPasswordHistoryModel.js";
54
63
 
55
64
 
56
65
  import RoleMongoRepository from "./repository/mongo/RoleMongoRepository.js";
@@ -59,6 +68,7 @@ import UserMongoRepository from "./repository/mongo/UserMongoRepository.js";
59
68
  import UserApiKeyMongoRepository from "./repository/mongo/UserApiKeyMongoRepository.js";
60
69
  import UserSessionMongoRepository from "./repository/mongo/UserSessionMongoRepository.js";
61
70
  import UserLoginFailMongoRepository from "./repository/mongo/UserLoginFailMongoRepository.js";
71
+ import UserPasswordHistoryMongoRepository from "./repository/mongo/UserPasswordHistoryMongoRepository.js";
62
72
 
63
73
  import RoleSqliteRepository from "./repository/sqlite/RoleSqliteRepository.js";
64
74
  import TenantSqliteRepository from "./repository/sqlite/TenantSqliteRepository.js";
@@ -66,6 +76,7 @@ import UserSqliteRepository from "./repository/sqlite/UserSqliteRepository.js";
66
76
  import UserApiKeySqliteRepository from "./repository/sqlite/UserApiKeySqliteRepository.js";
67
77
  import UserLoginFailSqliteRepository from "./repository/sqlite/UserLoginFailSqliteRepository.js";
68
78
  import UserSessionSqliteRepository from "./repository/sqlite/UserSessionSqliteRepository.js";
79
+ import UserPasswordHistorySqliteRepository from "./repository/sqlite/UserPasswordHistorySqliteRepository.js";
69
80
 
70
81
 
71
82
  import {RolePermissions} from "./permissions/RolePermissions.js";
@@ -81,6 +92,8 @@ import {RoleSchema, RoleBaseSchema} from "./schemas/RoleSchema.js";
81
92
  import {UserApiKeySchema, UserApiKeyBaseSchema} from "./schemas/UserApiKeySchema.js";
82
93
  import {UserLoginFailBaseSchema, UserLoginFailSchema} from "./schemas/UserLoginFailSchema.js";
83
94
  import {UserSessionBaseSchema, UserSessionSchema} from "./schemas/UserSessionSchema.js";
95
+ import {defaultPasswordPolicy} from "./policies/defaultPasswordPolicy.js";
96
+ import {PasswordPolicySchema} from "./schemas/PasswordPolicySchema.js";
84
97
 
85
98
 
86
99
  const graphqlMergeResult = await GraphqlMerge()
@@ -95,6 +108,7 @@ export type {
95
108
  IUserApiKeyRepository,
96
109
  IUserLoginFailRepository,
97
110
  IUserSessionRepository,
111
+ IUserPasswordHistoryRepository,
98
112
  }
99
113
 
100
114
  export {
@@ -118,6 +132,9 @@ export {
118
132
  UserApiKeyService,
119
133
  UserSessionService,
120
134
  UserLoginFailService,
135
+ UserPasswordHistoryService,
136
+ PasswordPolicyService,
137
+ PasswordPolicyResolver,
121
138
  PermissionService,
122
139
  Rbac,
123
140
 
@@ -128,6 +145,8 @@ export {
128
145
  UserApiKeyServiceFactory,
129
146
  UserSessionServiceFactory,
130
147
  UserLoginFailServiceFactory,
148
+ UserPasswordHistoryServiceFactory,
149
+ PasswordPolicyServiceFactory,
131
150
 
132
151
  //GQL
133
152
  identityTypeDefs,
@@ -163,6 +182,7 @@ export {
163
182
  UserApiKeyMongoRepository,
164
183
  UserSessionMongoRepository,
165
184
  UserLoginFailMongoRepository,
185
+ UserPasswordHistoryMongoRepository,
166
186
 
167
187
  //Mongo Models
168
188
  RoleModel,
@@ -171,6 +191,7 @@ export {
171
191
  UserApiKeyModel,
172
192
  UserSessionModel,
173
193
  UserLoginFailModel,
194
+ UserPasswordHistoryModel,
174
195
 
175
196
  RoleMongoSchema,
176
197
  TenantMongoSchema,
@@ -178,6 +199,7 @@ export {
178
199
  UserApiKeyMongoSchema,
179
200
  UserSessionMongoSchema,
180
201
  UserLoginFailMongoSchema,
202
+ UserPasswordHistoryMongoSchema,
181
203
 
182
204
  //Sqlite Repositories
183
205
  RoleSqliteRepository,
@@ -186,12 +208,16 @@ export {
186
208
  UserApiKeySqliteRepository,
187
209
  UserLoginFailSqliteRepository,
188
210
  UserSessionSqliteRepository,
211
+ UserPasswordHistorySqliteRepository,
189
212
 
190
213
  //Config
191
214
  IdentityConfig,
215
+ PasswordPolicyConfig,
192
216
 
193
217
  //Errors
194
218
  BadCredentialsError,
219
+ defaultPasswordPolicy,
220
+ PasswordPolicySchema,
195
221
 
196
222
  //Setup
197
223
  LoadIdentityConfigFromEnv,
@@ -199,7 +225,6 @@ export {
199
225
  CreateOrUpdateRole,
200
226
  CreateUserIfNotExist,
201
227
  CreateTenantIfNotExist,
202
- RecoveryUserPassword
228
+ RecoveryUserPassword,
229
+ SetProjectPasswordPolicy
203
230
  }
204
-
205
-
@@ -0,0 +1,21 @@
1
+ interface IUserPasswordHistory {
2
+ _id?: string
3
+ id?: string
4
+ user: string
5
+ passwordHash: string
6
+ createdAt?: string | Date
7
+ updatedAt?: string | Date
8
+ }
9
+
10
+ interface IUserPasswordHistoryCreate {
11
+ _id?: string
12
+ id?: string
13
+ user: string
14
+ passwordHash: string
15
+ createdAt?: string | Date
16
+ }
17
+
18
+ export type {
19
+ IUserPasswordHistory,
20
+ IUserPasswordHistoryCreate
21
+ }
@@ -0,0 +1,8 @@
1
+ import type {IUserPasswordHistory, IUserPasswordHistoryCreate} from "./IUserPasswordHistory.js";
2
+
3
+ interface IUserPasswordHistoryRepository {
4
+ create(data: IUserPasswordHistoryCreate): Promise<IUserPasswordHistory>
5
+ findLatestByUserId(userId: string, limit: number): Promise<IUserPasswordHistory[]>
6
+ }
7
+
8
+ export type {IUserPasswordHistoryRepository}
@@ -0,0 +1,42 @@
1
+ import {mongoose} from "@drax/common-back";
2
+ import {PaginateModel} from "mongoose";
3
+ import uniqueValidator from "mongoose-unique-validator";
4
+ import mongoosePaginate from "mongoose-paginate-v2";
5
+ import type {IUserPasswordHistory} from "../interfaces/IUserPasswordHistory.js";
6
+
7
+ const UserPasswordHistoryMongoSchema = new mongoose.Schema<IUserPasswordHistory>({
8
+ user: {type: String, required: true, index: true},
9
+ passwordHash: {type: String, required: true, index: false}
10
+ }, {timestamps: true});
11
+
12
+ UserPasswordHistoryMongoSchema.plugin(uniqueValidator, {message: "validation.unique"});
13
+ UserPasswordHistoryMongoSchema.plugin(mongoosePaginate);
14
+
15
+ UserPasswordHistoryMongoSchema.virtual("id").get(function () {
16
+ return this._id.toString();
17
+ });
18
+
19
+ UserPasswordHistoryMongoSchema.set("toJSON", {getters: true, virtuals: true});
20
+ UserPasswordHistoryMongoSchema.set("toObject", {getters: true, virtuals: true});
21
+
22
+ const MODEL_NAME = "UserPasswordHistory";
23
+ const COLLECTION_NAME = "user_password_history";
24
+
25
+ let UserPasswordHistoryModel;
26
+
27
+ try {
28
+ UserPasswordHistoryModel = mongoose.model<IUserPasswordHistory, PaginateModel<IUserPasswordHistory>>(MODEL_NAME, UserPasswordHistoryMongoSchema, COLLECTION_NAME);
29
+ } catch (e) {
30
+ if (e.name === "OverwriteModelError") {
31
+ UserPasswordHistoryModel = mongoose.model<IUserPasswordHistory, PaginateModel<IUserPasswordHistory>>(MODEL_NAME);
32
+ } else {
33
+ throw e;
34
+ }
35
+ }
36
+
37
+ export {
38
+ UserPasswordHistoryMongoSchema,
39
+ UserPasswordHistoryModel
40
+ }
41
+
42
+ export default UserPasswordHistoryModel
@@ -0,0 +1,17 @@
1
+ import type {IPasswordPolicy} from "@drax/identity-share/dist";
2
+
3
+ const defaultPasswordPolicy: IPasswordPolicy = {
4
+ minLength: 8,
5
+ maxLength: 64,
6
+ requireUppercase: true,
7
+ requireLowercase: true,
8
+ requireNumber: true,
9
+ requireSpecialChar: false,
10
+ disallowSpaces: true,
11
+ preventReuse: 3,
12
+ expirationDays: null
13
+ }
14
+
15
+ export {
16
+ defaultPasswordPolicy
17
+ }
@@ -0,0 +1,25 @@
1
+ import {AbstractMongoRepository} from "@drax/crud-back";
2
+ import type {IUserPasswordHistoryRepository} from "../../interfaces/IUserPasswordHistoryRepository.js";
3
+ import type {IUserPasswordHistory, IUserPasswordHistoryCreate} from "../../interfaces/IUserPasswordHistory.js";
4
+ import {UserPasswordHistoryModel} from "../../models/UserPasswordHistoryModel.js";
5
+
6
+ class UserPasswordHistoryMongoRepository extends AbstractMongoRepository<IUserPasswordHistory, IUserPasswordHistoryCreate, IUserPasswordHistoryCreate> implements IUserPasswordHistoryRepository {
7
+ constructor() {
8
+ super();
9
+ this._model = UserPasswordHistoryModel;
10
+ this._searchFields = ["user"];
11
+ this._populateFields = ["user"];
12
+ }
13
+
14
+ async findLatestByUserId(userId: string, limit: number): Promise<IUserPasswordHistory[]> {
15
+ return UserPasswordHistoryModel
16
+ .find({user: userId})
17
+ .sort({createdAt: -1})
18
+ .limit(limit)
19
+ .lean(true)
20
+ .exec() as Promise<IUserPasswordHistory[]>
21
+ }
22
+ }
23
+
24
+ export default UserPasswordHistoryMongoRepository
25
+ export {UserPasswordHistoryMongoRepository}
@@ -0,0 +1,47 @@
1
+ import type {SqliteTableField} from "@drax/common-back";
2
+ import {AbstractSqliteRepository} from "@drax/crud-back";
3
+ import type {IUserPasswordHistoryRepository} from "../../interfaces/IUserPasswordHistoryRepository.js";
4
+ import type {IUserPasswordHistory, IUserPasswordHistoryCreate} from "../../interfaces/IUserPasswordHistory.js";
5
+
6
+ class UserPasswordHistorySqliteRepository extends AbstractSqliteRepository<IUserPasswordHistory, IUserPasswordHistoryCreate, IUserPasswordHistoryCreate> implements IUserPasswordHistoryRepository {
7
+ protected db: any;
8
+ protected tableName: string = "user_password_history";
9
+ protected dataBaseFile: string;
10
+ protected searchFields: string[] = [];
11
+ protected booleanFields: string[] = [];
12
+ protected identifier: string = "_id";
13
+ protected populateFields = [
14
+ {field: "user", table: "users", identifier: "_id"}
15
+ ]
16
+ protected tableFields: SqliteTableField[] = [
17
+ {name: "user", type: "TEXT", unique: false, primary: false},
18
+ {name: "passwordHash", type: "TEXT", unique: false, primary: false},
19
+ {name: "createdAt", type: "TEXT", unique: false, primary: false},
20
+ {name: "updatedAt", type: "TEXT", unique: false, primary: false},
21
+ ]
22
+ protected verbose: boolean;
23
+
24
+ async prepareData(): Promise<void> {
25
+ }
26
+
27
+ async prepareItem(item: IUserPasswordHistory): Promise<void> {
28
+ if (item.createdAt && typeof item.createdAt === "string") {
29
+ item.createdAt = new Date(item.createdAt)
30
+ }
31
+
32
+ if (item.updatedAt && typeof item.updatedAt === "string") {
33
+ item.updatedAt = new Date(item.updatedAt)
34
+ }
35
+ }
36
+
37
+ async findLatestByUserId(userId: string, limit: number): Promise<IUserPasswordHistory[]> {
38
+ const rows = this.db.prepare(`SELECT * FROM ${this.tableName} WHERE user = ? ORDER BY createdAt DESC LIMIT ?`).all(userId, limit);
39
+ for (const row of rows) {
40
+ await this.decorate(row)
41
+ }
42
+ return rows
43
+ }
44
+ }
45
+
46
+ export default UserPasswordHistorySqliteRepository
47
+ export {UserPasswordHistorySqliteRepository}
@@ -0,0 +1,33 @@
1
+ import type {IPasswordPolicy, IPasswordPolicyProject} from "@drax/identity-share";
2
+ import {defaultPasswordPolicy} from "../policies/defaultPasswordPolicy.js";
3
+ import {PartialPasswordPolicySchema, PasswordPolicySchema} from "../schemas/PasswordPolicySchema.js";
4
+ import getPasswordEnvPolicy from "../utils/getPasswordEnvPolicy.js";
5
+
6
+ class PasswordPolicyResolver {
7
+
8
+ private projectPolicy : IPasswordPolicyProject = {};
9
+
10
+ setProjectPolicy (projectPolicy : IPasswordPolicyProject){
11
+ this.projectPolicy = projectPolicy;
12
+ }
13
+
14
+ async resolve(): Promise<IPasswordPolicy> {
15
+ const projectPolicy = await this.getProjectPolicy()
16
+ const envPolicy = getPasswordEnvPolicy()
17
+
18
+ return PasswordPolicySchema.parse({
19
+ ...defaultPasswordPolicy,
20
+ ...projectPolicy,
21
+ ...envPolicy
22
+ }) as IPasswordPolicy
23
+ }
24
+
25
+ private async getProjectPolicy(): Promise<Partial<IPasswordPolicy>> {
26
+ return this.projectPolicy
27
+ ? PartialPasswordPolicySchema.parse(this.projectPolicy)
28
+ : {}
29
+ }
30
+ }
31
+
32
+ export default PasswordPolicyResolver
33
+ export {PasswordPolicyResolver}
@@ -10,6 +10,7 @@ import {
10
10
  } from "../schemas/PasswordSchema.js";
11
11
  import {SwitchTenantBodyRequestSchema, SwitchTenantBodyResponseSchema} from "../schemas/SwitchTenantSchema.js";
12
12
  import zod from "zod"
13
+ import {PasswordPolicySchema} from "../schemas/PasswordPolicySchema.js";
13
14
 
14
15
  async function UserRoutes(fastify, options) {
15
16
 
@@ -44,6 +45,16 @@ async function UserRoutes(fastify, options) {
44
45
  },
45
46
  (req, rep) => controller.auth(req, rep))
46
47
 
48
+ fastify.get('/api/auth/password-policy', {
49
+ schema: {
50
+ tags: ['Auth'],
51
+ response: {
52
+ 200: zod.toJSONSchema(PasswordPolicySchema),
53
+ 500: schemas.jsonErrorBodyResponse,
54
+ },
55
+ },
56
+ }, (req, rep) => controller.passwordPolicy(req, rep))
57
+
47
58
  fastify.get('/api/auth/me', {
48
59
  schema: {
49
60
  tags: ['Auth'],
@@ -0,0 +1,29 @@
1
+ import z from "zod";
2
+
3
+ const PasswordPolicySchemaBase = z.object({
4
+ minLength: z.number().int().min(1),
5
+ maxLength: z.number().int().min(1),
6
+ requireUppercase: z.boolean(),
7
+ requireLowercase: z.boolean(),
8
+ requireNumber: z.boolean(),
9
+ requireSpecialChar: z.boolean(),
10
+ disallowSpaces: z.boolean(),
11
+ preventReuse: z.number().int().min(0),
12
+ expirationDays: z.number().int().min(1).nullable(),
13
+ })
14
+
15
+ const PasswordPolicySchema = PasswordPolicySchemaBase.refine((
16
+ policy) =>
17
+ policy.maxLength >= policy.minLength,
18
+ {
19
+ message: "validation.password.maxLength",
20
+ path: ["maxLength"]
21
+ }
22
+ );
23
+
24
+ const PartialPasswordPolicySchema = PasswordPolicySchemaBase.partial();
25
+
26
+ export {
27
+ PasswordPolicySchema,
28
+ PartialPasswordPolicySchema
29
+ }
@@ -8,9 +8,7 @@ const RegisterBodyRequestSchema = z.object({
8
8
  email: email("validation.email.invalid"),
9
9
  phone: string({ error: "validation.required" }).optional(),
10
10
  password: string({ error: "validation.required" })
11
- .min(1, "validation.required")
12
- .min(8, "validation.password.min8")
13
- .max(64, "validation.password.max64"),
11
+ .min(1, "validation.required"),
14
12
  });
15
13
 
16
14
  const RegisterBodyResponseSchema = z.object({
@@ -17,9 +17,7 @@ const UserBaseSchema = object({
17
17
 
18
18
  const UserCreateSchema = UserBaseSchema.extend({
19
19
  password: string({error: "validation.required"})
20
- .min(1, "validation.required")
21
- .min(8, "validation.password.min8")
22
- .max(64, "validation.password.max64"),
20
+ .min(1, "validation.required"),
23
21
  });
24
22
 
25
23
 
@@ -0,0 +1,184 @@
1
+ import {randomInt} from "crypto";
2
+ import {ZodError, ZodType} from "zod";
3
+ import {ValidationError, ZodErrorToValidationError} from "@drax/common-back";
4
+ import type {IUser} from "@drax/identity-share";
5
+ import type {IUserPasswordHistory} from "../interfaces/IUserPasswordHistory";
6
+ import type {IPasswordPolicy} from "@drax/identity-share";
7
+ import type UserPasswordHistoryService from "./UserPasswordHistoryService";
8
+ import type {IUserRepository} from "../interfaces/IUserRepository";
9
+ import AuthUtils from "../utils/AuthUtils.js";
10
+ import PasswordPolicyResolver from "../resolver/PasswordPolicyResolver.js";
11
+ import PasswordPolicySchemaFactory from "../utils/PasswordPolicySchemaFactory.js";
12
+
13
+ interface IValidatePasswordOptions {
14
+ field?: string
15
+ userId?: string
16
+ currentPasswordHash?: string
17
+ }
18
+
19
+ class PasswordPolicyService {
20
+ constructor(
21
+ private readonly resolver: PasswordPolicyResolver,
22
+ private readonly userRepository?: IUserRepository,
23
+ private readonly userPasswordHistoryService?: UserPasswordHistoryService
24
+ ) {
25
+ }
26
+
27
+ async getFinalPolicy(): Promise<IPasswordPolicy> {
28
+ return this.resolver.resolve()
29
+ }
30
+
31
+ async getPasswordSchema(): Promise<ZodType<string>> {
32
+ const policy = await this.getFinalPolicy()
33
+ return PasswordPolicySchemaFactory.create(policy)
34
+ }
35
+
36
+ async validatePassword(password: string, options?: IValidatePasswordOptions): Promise<void> {
37
+ const field = options?.field || "password"
38
+ try {
39
+ const schema = await this.getPasswordSchema()
40
+ await schema.parseAsync(password)
41
+ } catch (e) {
42
+ if (e instanceof ZodError) {
43
+ const validationError = ZodErrorToValidationError(e, {[field]: password})
44
+ validationError.errors = validationError.errors.map((error) => ({...error, field}))
45
+ throw validationError
46
+ }
47
+ throw e
48
+ }
49
+
50
+ if (options?.userId) {
51
+ await this.validateReuse(password, options.userId, {
52
+ field,
53
+ currentPasswordHash: options.currentPasswordHash
54
+ })
55
+ }
56
+ }
57
+
58
+ async generateCompatiblePassword(): Promise<string> {
59
+ const policy = await this.getFinalPolicy()
60
+ const uppercaseChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
61
+ const lowercaseChars = "abcdefghijkmnopqrstuvwxyz"
62
+ const numericChars = "0123456789"
63
+ const specialChars = "!@#$%&*_-+="
64
+ const fallbackSpecial = policy.disallowSpaces ? "!" : " "
65
+ const combinedChars = [
66
+ uppercaseChars,
67
+ lowercaseChars,
68
+ numericChars,
69
+ policy.requireSpecialChar ? specialChars : "",
70
+ policy.disallowSpaces ? "" : " "
71
+ ].join("") || `${uppercaseChars}${lowercaseChars}${numericChars}${fallbackSpecial}`
72
+
73
+ const chars: string[] = []
74
+
75
+ if (policy.requireUppercase) {
76
+ chars.push(this.randomChar(uppercaseChars))
77
+ }
78
+ if (policy.requireLowercase) {
79
+ chars.push(this.randomChar(lowercaseChars))
80
+ }
81
+ if (policy.requireNumber) {
82
+ chars.push(this.randomChar(numericChars))
83
+ }
84
+ if (policy.requireSpecialChar) {
85
+ chars.push(this.randomChar(specialChars))
86
+ }
87
+ if (!chars.length) {
88
+ chars.push(this.randomChar(`${uppercaseChars}${lowercaseChars}${numericChars}`))
89
+ }
90
+
91
+ while (chars.length < policy.minLength) {
92
+ chars.push(this.randomChar(combinedChars))
93
+ }
94
+
95
+ const password = this.shuffle(chars).join("").slice(0, policy.maxLength)
96
+ await this.validatePassword(password)
97
+ return password
98
+ }
99
+
100
+ async getPasswordExpiration(user: IUser): Promise<{expired: boolean, expiresAt: Date | null}> {
101
+ const policy = await this.getFinalPolicy()
102
+ if (!policy.expirationDays) {
103
+ return {expired: false, expiresAt: null}
104
+ }
105
+
106
+ const lastPasswordChange = await this.getLastPasswordChangeDate(user)
107
+ if (!lastPasswordChange) {
108
+ return {expired: false, expiresAt: null}
109
+ }
110
+
111
+ const expiresAt = new Date(lastPasswordChange)
112
+ expiresAt.setDate(expiresAt.getDate() + policy.expirationDays)
113
+
114
+ return {
115
+ expired: expiresAt.getTime() <= Date.now(),
116
+ expiresAt
117
+ }
118
+ }
119
+
120
+ async recordPassword(userId: string, passwordHash: string): Promise<void> {
121
+ await this.userPasswordHistoryService?.create(userId, passwordHash)
122
+ }
123
+
124
+ private async validateReuse(password: string, userId: string, options?: {field: string, currentPasswordHash?: string}): Promise<void> {
125
+ const policy = await this.getFinalPolicy()
126
+ if (!policy.preventReuse) {
127
+ return
128
+ }
129
+
130
+ const field = options?.field || "password"
131
+ const recentHashes = await this.getRecentPasswordHashes(userId, policy.preventReuse, options?.currentPasswordHash)
132
+ const reused = recentHashes.some((item) => AuthUtils.checkPassword(password, item.passwordHash))
133
+ if (reused) {
134
+ throw new ValidationError([{field, reason: "validation.password.preventReuse"}])
135
+ }
136
+ }
137
+
138
+ private async getRecentPasswordHashes(userId: string, limit: number, currentPasswordHash?: string): Promise<IUserPasswordHistory[]> {
139
+ const recent = await this.userPasswordHistoryService?.findLatestByUserId(userId, limit) || []
140
+ const hashes = [...recent]
141
+
142
+ if (currentPasswordHash && !hashes.some((item) => item.passwordHash === currentPasswordHash)) {
143
+ hashes.unshift({user: userId, passwordHash: currentPasswordHash})
144
+ } else if (!currentPasswordHash && this.userRepository) {
145
+ const user = await this.userRepository.findByIdWithPassword(userId)
146
+ if (user?.password && !hashes.some((item) => item.passwordHash === user.password)) {
147
+ hashes.unshift({user: userId, passwordHash: user.password})
148
+ }
149
+ }
150
+
151
+ return hashes.slice(0, limit)
152
+ }
153
+
154
+ private async getLastPasswordChangeDate(user: IUser): Promise<Date | null> {
155
+ if (!user?._id || !this.userPasswordHistoryService) {
156
+ return user?.updatedAt ? new Date(user.updatedAt) : user?.createdAt ? new Date(user.createdAt) : null
157
+ }
158
+
159
+ const latest = await this.userPasswordHistoryService.findLatestByUserId(user._id.toString(), 1)
160
+ if (latest[0]?.createdAt) {
161
+ return new Date(latest[0].createdAt)
162
+ }
163
+
164
+ return user?.updatedAt ? new Date(user.updatedAt) : user?.createdAt ? new Date(user.createdAt) : null
165
+ }
166
+
167
+ private randomChar(source: string): string {
168
+ return source[randomInt(0, source.length)]
169
+ }
170
+
171
+ private shuffle(chars: string[]): string[] {
172
+ const items = [...chars]
173
+ for (let i = items.length - 1; i > 0; i -= 1) {
174
+ const j = randomInt(0, i + 1)
175
+ const temp = items[i]
176
+ items[i] = items[j]
177
+ items[j] = temp
178
+ }
179
+ return items
180
+ }
181
+ }
182
+
183
+ export default PasswordPolicyService
184
+ export {PasswordPolicyService}
@@ -0,0 +1,23 @@
1
+ import type {IUserPasswordHistoryRepository} from "../interfaces/IUserPasswordHistoryRepository.js";
2
+ import type {IUserPasswordHistory} from "../interfaces/IUserPasswordHistory.js";
3
+
4
+ class UserPasswordHistoryService {
5
+ constructor(private readonly repository: IUserPasswordHistoryRepository) {
6
+ }
7
+
8
+ async create(userId: string, passwordHash: string): Promise<IUserPasswordHistory> {
9
+ return this.repository.create({
10
+ user: userId,
11
+ passwordHash
12
+ })
13
+ }
14
+
15
+ async findLatestByUserId(userId: string, limit: number): Promise<IUserPasswordHistory[]> {
16
+ if (limit <= 0) {
17
+ return []
18
+ }
19
+ return this.repository.findLatestByUserId(userId, limit)
20
+ }
21
+ }
22
+
23
+ export default UserPasswordHistoryService