@drax/identity-back 3.13.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/UserApiKeyController.js +8 -119
- 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/graphql/resolvers/user-api-key.resolvers.js +2 -3
- package/dist/index.js +20 -8
- package/dist/interfaces/IUserPasswordHistory.js +1 -0
- package/dist/interfaces/IUserPasswordHistoryRepository.js +1 -0
- package/dist/middleware/apiKeyMiddleware.js +1 -1
- package/dist/middleware/rbacMiddleware.js +1 -1
- package/dist/models/UserPasswordHistoryModel.js +30 -0
- package/dist/permissions/UserApiKeyPermissions.js +1 -2
- 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 +6 -6
- package/src/config/PasswordPolicyConfig.ts +14 -0
- package/src/controllers/UserApiKeyController.ts +15 -129
- 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/graphql/resolvers/user-api-key.resolvers.ts +2 -3
- package/src/index.ts +28 -3
- package/src/interfaces/IUserPasswordHistory.ts +21 -0
- package/src/interfaces/IUserPasswordHistoryRepository.ts +8 -0
- package/src/middleware/apiKeyMiddleware.ts +1 -1
- package/src/middleware/rbacMiddleware.ts +1 -1
- package/src/models/UserPasswordHistoryModel.ts +42 -0
- package/src/permissions/UserApiKeyPermissions.ts +1 -2
- 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/UserApiKeyController.d.ts +10 -6
- package/types/controllers/UserApiKeyController.d.ts.map +1 -1
- 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/graphql/resolvers/user-api-key.resolvers.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/permissions/UserApiKeyPermissions.d.ts +1 -2
- package/types/permissions/UserApiKeyPermissions.d.ts.map +1 -1
- package/types/permissions/index.d.ts +0 -2
- package/types/permissions/index.d.ts.map +1 -1
- 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
|
@@ -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
|
|
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: "
|
|
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: "
|
|
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
|
+
})
|
|
@@ -1,38 +1,173 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {beforeEach, describe, expect, it} from "vitest"
|
|
2
2
|
import UserService from "../../src/services/UserService";
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {IRole} from "../../../identity-share/src/interfaces/IRole";
|
|
6
|
-
import UserMongoRepository from "../../src/repository/mongo/UserMongoRepository";
|
|
7
|
-
import {IUserRepository} from "../../src/interfaces/IUserRepository";
|
|
3
|
+
import type {IUserRepository} from "../../src/interfaces/IUserRepository";
|
|
4
|
+
import type {IUser, IUserCreate, IUserUpdate} from "@drax/identity-share";
|
|
8
5
|
import {ValidationError} from "@drax/common-back";
|
|
6
|
+
import PasswordPolicyResolver from "../../src/resolver/PasswordPolicyResolver";
|
|
7
|
+
import PasswordPolicyService from "../../src/services/PasswordPolicyService";
|
|
8
|
+
import type {IUserPasswordHistory} from "../../src/interfaces/IUserPasswordHistory";
|
|
9
|
+
import type {IUserPasswordHistoryRepository} from "../../src/interfaces/IUserPasswordHistoryRepository";
|
|
10
|
+
import UserPasswordHistoryService from "../../src/services/UserPasswordHistoryService";
|
|
11
|
+
|
|
12
|
+
class InMemoryUserRepository implements IUserRepository {
|
|
13
|
+
private items = new Map<string, IUser>()
|
|
14
|
+
|
|
15
|
+
async create(data: IUserCreate): Promise<IUser> {
|
|
16
|
+
const user: IUser = {...data, _id: data._id || "user-1", role: data.role as any, tenant: data.tenant as any} as IUser
|
|
17
|
+
this.items.set(user._id.toString(), user)
|
|
18
|
+
return {...user}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async update(id: string, data: IUserUpdate): Promise<IUser> {
|
|
22
|
+
const current = this.items.get(id)
|
|
23
|
+
const updated = {...current, ...data, _id: id} as IUser
|
|
24
|
+
this.items.set(id, updated)
|
|
25
|
+
return {...updated}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async updatePartial(id: string, data: Partial<IUserUpdate & IUser>): Promise<IUser> {
|
|
29
|
+
const current = this.items.get(id)
|
|
30
|
+
const updated = {...current, ...data, _id: id} as IUser
|
|
31
|
+
this.items.set(id, updated)
|
|
32
|
+
return {...updated}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async delete(id: string): Promise<boolean> {
|
|
36
|
+
return this.items.delete(id)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async findById(id: string): Promise<IUser | null> {
|
|
40
|
+
const user = this.items.get(id)
|
|
41
|
+
if (!user) {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
const safeUser = {...user}
|
|
45
|
+
delete safeUser.password
|
|
46
|
+
return safeUser
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async findByIdWithPassword(id: string): Promise<IUser | null> {
|
|
50
|
+
const user = this.items.get(id)
|
|
51
|
+
return user ? {...user} : null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async findByUsername(username: string): Promise<IUser | null> {
|
|
55
|
+
const user = [...this.items.values()].find((item) => item.username === username)
|
|
56
|
+
if (!user) {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
const safeUser = {...user}
|
|
60
|
+
delete safeUser.password
|
|
61
|
+
return safeUser
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async findByUsernameWithPassword(username: string): Promise<IUser | null> {
|
|
65
|
+
const user = [...this.items.values()].find((item) => item.username === username)
|
|
66
|
+
return user ? {...user} : null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async findByEmail(email: string): Promise<IUser | null> {
|
|
70
|
+
const user = [...this.items.values()].find((item) => item.email === email)
|
|
71
|
+
return user ? {...user} : null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async changePassword(id: string, password: string): Promise<Boolean> {
|
|
75
|
+
const current = this.items.get(id)
|
|
76
|
+
this.items.set(id, {...current, password} as IUser)
|
|
77
|
+
return true
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async changeAvatar(id: string, avatarUrl: string): Promise<Boolean> {
|
|
81
|
+
const current = this.items.get(id)
|
|
82
|
+
this.items.set(id, {...current, avatar: avatarUrl} as IUser)
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async findByEmailCode(code: string): Promise<IUser | null> {
|
|
87
|
+
const user = [...this.items.values()].find((item) => item.emailCode === code)
|
|
88
|
+
return user ? {...user} : null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async findByPhoneCode(code: string): Promise<IUser | null> {
|
|
92
|
+
const user = [...this.items.values()].find((item) => item.phoneCode === code)
|
|
93
|
+
return user ? {...user} : null
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async findByRecoveryCode(code: string): Promise<IUser | null> {
|
|
97
|
+
const user = [...this.items.values()].find((item) => item.recoveryCode === code)
|
|
98
|
+
return user ? {...user} : null
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async paginate(): Promise<any> {
|
|
102
|
+
return {items: [...this.items.values()], page: 1, limit: 10, total: this.items.size}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async search(): Promise<IUser[]> {
|
|
106
|
+
return [...this.items.values()]
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async groupBy(): Promise<any[]> {
|
|
110
|
+
return []
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async export(): Promise<IUser[]> {
|
|
114
|
+
return [...this.items.values()]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async count(): Promise<number> {
|
|
118
|
+
return this.items.size
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
build(): void {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class InMemoryUserPasswordHistoryRepository implements IUserPasswordHistoryRepository {
|
|
126
|
+
private items: IUserPasswordHistory[] = []
|
|
127
|
+
|
|
128
|
+
async create(data: IUserPasswordHistory): Promise<IUserPasswordHistory> {
|
|
129
|
+
const created = {...data, _id: `${this.items.length + 1}`, createdAt: new Date()}
|
|
130
|
+
this.items.unshift(created)
|
|
131
|
+
return created
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async findLatestByUserId(userId: string, limit: number): Promise<IUserPasswordHistory[]> {
|
|
135
|
+
return this.items.filter((item) => item.user === userId).slice(0, limit)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
9
138
|
|
|
10
139
|
describe("UserServiceTest", function () {
|
|
11
|
-
let userRepository: IUserRepository
|
|
12
|
-
let userService
|
|
13
|
-
let
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
userAdminData = (await import("../data-obj/users/root-mongo-user")).default
|
|
21
|
-
return
|
|
22
|
-
})
|
|
140
|
+
let userRepository: IUserRepository
|
|
141
|
+
let userService: UserService
|
|
142
|
+
let userAdminData: IUserCreate
|
|
143
|
+
|
|
144
|
+
beforeEach(async () => {
|
|
145
|
+
userRepository = new InMemoryUserRepository()
|
|
146
|
+
const userPasswordHistoryService = new UserPasswordHistoryService(new InMemoryUserPasswordHistoryRepository())
|
|
147
|
+
const passwordPolicyService = new PasswordPolicyService(new PasswordPolicyResolver(), userRepository, userPasswordHistoryService)
|
|
148
|
+
userService = new UserService(userRepository, passwordPolicyService, userPasswordHistoryService)
|
|
23
149
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
150
|
+
userAdminData = {
|
|
151
|
+
_id: "user-1",
|
|
152
|
+
active: true,
|
|
153
|
+
groups: [],
|
|
154
|
+
username: "root",
|
|
155
|
+
email: "root@example.com",
|
|
156
|
+
password: "Root1234",
|
|
157
|
+
name: "root",
|
|
158
|
+
phone: "123456789",
|
|
159
|
+
role: "role-1",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await userService.create({...userAdminData})
|
|
27
163
|
})
|
|
28
164
|
|
|
29
165
|
it("should create user", async function () {
|
|
30
|
-
const userData = {...userAdminData}
|
|
166
|
+
const userData = {...userAdminData, _id: "user-2", username: "admin2", email: "admin2@example.com"}
|
|
31
167
|
let userCreated = await userService.create(userData)
|
|
32
|
-
expect(userCreated.username).toBe(
|
|
168
|
+
expect(userCreated.username).toBe(userData.username)
|
|
33
169
|
})
|
|
34
170
|
|
|
35
|
-
|
|
36
171
|
it("should find one user", async function () {
|
|
37
172
|
let user = await userService.findById(userAdminData._id)
|
|
38
173
|
expect(user.username).toBe(userAdminData.username)
|
|
@@ -42,31 +177,83 @@ describe("UserServiceTest", function () {
|
|
|
42
177
|
const userData = {...userAdminData}
|
|
43
178
|
const newName = "AdminUpdated"
|
|
44
179
|
userData.name = newName
|
|
45
|
-
let userUpdated = await userService.update(userAdminData._id, userData)
|
|
180
|
+
let userUpdated = await userService.update(userAdminData._id, userData as any)
|
|
46
181
|
expect(userUpdated.name).toBe(newName)
|
|
47
182
|
})
|
|
48
183
|
|
|
49
184
|
it("should fail create user with short password", async function () {
|
|
50
|
-
let userData = {...userAdminData, password: "123"}
|
|
185
|
+
let userData = {...userAdminData, _id: "user-3", username: "shortpass", email: "shortpass@example.com", password: "123"}
|
|
186
|
+
|
|
187
|
+
await expect(async () => {
|
|
188
|
+
await userService.create(userData)
|
|
189
|
+
}).rejects.toSatisfy((err) => {
|
|
190
|
+
expect(err).toBeInstanceOf(ValidationError)
|
|
191
|
+
expect(err.errors[0].field).toBe('password')
|
|
192
|
+
expect(err.errors[0].reason).toBe('validation.password.minLength')
|
|
193
|
+
return true;
|
|
194
|
+
});
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it("create rejects password without uppercase according to policy", async function () {
|
|
198
|
+
let userData = {...userAdminData, _id: "user-4", username: "no-uppercase", email: "nouppercase@example.com", password: "lowercase1"}
|
|
51
199
|
|
|
52
200
|
await expect(async () => {
|
|
53
201
|
await userService.create(userData)
|
|
54
202
|
}).rejects.toSatisfy((err) => {
|
|
55
203
|
expect(err).toBeInstanceOf(ValidationError)
|
|
56
204
|
expect(err.errors[0].field).toBe('password')
|
|
57
|
-
expect(err.errors[0].reason).toBe('validation.password.
|
|
205
|
+
expect(err.errors[0].reason).toBe('validation.password.requireUppercase')
|
|
58
206
|
return true;
|
|
59
207
|
});
|
|
60
208
|
})
|
|
61
209
|
|
|
210
|
+
it("changeUserPassword rejects password that does not meet policy", async function () {
|
|
211
|
+
const userId = userAdminData._id
|
|
212
|
+
await expect(async () => {
|
|
213
|
+
await userService.changeUserPassword(userId, "lowercase1")
|
|
214
|
+
}).rejects.toSatisfy((err) => {
|
|
215
|
+
expect(err).toBeInstanceOf(ValidationError)
|
|
216
|
+
expect(err.errors[0].field).toBe('newPassword')
|
|
217
|
+
expect(err.errors[0].reason).toBe('validation.password.requireUppercase')
|
|
218
|
+
return true;
|
|
219
|
+
});
|
|
220
|
+
})
|
|
62
221
|
|
|
63
|
-
it("
|
|
222
|
+
it("changeOwnPassword rejects password that does not meet policy", async function () {
|
|
64
223
|
const userId = userAdminData._id
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
224
|
+
await expect(async () => {
|
|
225
|
+
await userService.changeOwnPassword(userId, "Root1234", "short1A")
|
|
226
|
+
}).rejects.toSatisfy((err) => {
|
|
227
|
+
expect(err).toBeInstanceOf(ValidationError)
|
|
228
|
+
expect(err.errors[0].field).toBe('newPassword')
|
|
229
|
+
expect(err.errors[0].reason).toBe('validation.password.minLength')
|
|
230
|
+
return true;
|
|
231
|
+
});
|
|
68
232
|
})
|
|
69
233
|
|
|
234
|
+
it("changeUserPasswordByCode rejects password that does not meet policy", async function () {
|
|
235
|
+
const code = await userService.recoveryCode(userAdminData.email)
|
|
236
|
+
await expect(async () => {
|
|
237
|
+
await userService.changeUserPasswordByCode(code, "lowercase1")
|
|
238
|
+
}).rejects.toSatisfy((err) => {
|
|
239
|
+
expect(err).toBeInstanceOf(ValidationError)
|
|
240
|
+
expect(err.errors[0].field).toBe('newPassword')
|
|
241
|
+
expect(err.errors[0].reason).toBe('validation.password.requireUppercase')
|
|
242
|
+
return true;
|
|
243
|
+
});
|
|
244
|
+
})
|
|
70
245
|
|
|
246
|
+
it("preventReuse rejects a recently used password", async function () {
|
|
247
|
+
const userId = userAdminData._id
|
|
248
|
+
await userService.changeUserPassword(userId, "SecondPass1")
|
|
71
249
|
|
|
250
|
+
await expect(async () => {
|
|
251
|
+
await userService.changeUserPassword(userId, "Root1234")
|
|
252
|
+
}).rejects.toSatisfy((err) => {
|
|
253
|
+
expect(err).toBeInstanceOf(ValidationError)
|
|
254
|
+
expect(err.errors[0].field).toBe('newPassword')
|
|
255
|
+
expect(err.errors[0].reason).toBe('validation.password.preventReuse')
|
|
256
|
+
return true;
|
|
257
|
+
});
|
|
258
|
+
})
|
|
72
259
|
})
|
package/test/setup/TestSetup.ts
CHANGED
|
@@ -20,6 +20,7 @@ import basicUserData from "./data/basic-user";
|
|
|
20
20
|
|
|
21
21
|
import {IUser, IRole} from "@drax/identity-share";
|
|
22
22
|
import MongoInMemory from "./MongoInMemory";
|
|
23
|
+
import {randomUUID} from "crypto";
|
|
23
24
|
|
|
24
25
|
class TestSetup {
|
|
25
26
|
|
|
@@ -29,8 +30,12 @@ class TestSetup {
|
|
|
29
30
|
private _basicUser: IUser;
|
|
30
31
|
private _adminRole: IRole;
|
|
31
32
|
private _restrictedRole: IRole;
|
|
33
|
+
private readonly dbEngine: "mongo" | "sqlite";
|
|
34
|
+
private readonly sqliteFile: string;
|
|
32
35
|
|
|
33
|
-
constructor() {
|
|
36
|
+
constructor(dbEngine: "mongo" | "sqlite" = "mongo", sqliteFile?: string) {
|
|
37
|
+
this.dbEngine = dbEngine
|
|
38
|
+
this.sqliteFile = sqliteFile || `/tmp/drax-identity-back-${randomUUID()}.sqlite`
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
async setup() {
|
|
@@ -45,8 +50,11 @@ class TestSetup {
|
|
|
45
50
|
|
|
46
51
|
setupEnvironmentVariables() {
|
|
47
52
|
// Define environment variables
|
|
48
|
-
process.env.DRAX_DB_ENGINE =
|
|
53
|
+
process.env.DRAX_DB_ENGINE = this.dbEngine;
|
|
49
54
|
process.env.DRAX_JWT_SECRET = "xxx";
|
|
55
|
+
if (this.dbEngine === "sqlite") {
|
|
56
|
+
process.env.DRAX_SQLITE_FILE = this.sqliteFile;
|
|
57
|
+
}
|
|
50
58
|
}
|
|
51
59
|
|
|
52
60
|
setupConfig() {
|
|
@@ -80,6 +88,9 @@ class TestSetup {
|
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
async setupMongoInMemoryAndConnect() {
|
|
91
|
+
if (this.dbEngine !== "mongo") {
|
|
92
|
+
return
|
|
93
|
+
}
|
|
83
94
|
this._mongoInMemory = new MongoInMemory();
|
|
84
95
|
await this._mongoInMemory.connect();
|
|
85
96
|
}
|
|
@@ -95,11 +106,18 @@ class TestSetup {
|
|
|
95
106
|
}
|
|
96
107
|
|
|
97
108
|
async dropData() {
|
|
98
|
-
|
|
109
|
+
if (this._mongoInMemory) {
|
|
110
|
+
await this._mongoInMemory.dropData()
|
|
111
|
+
}
|
|
99
112
|
}
|
|
100
113
|
|
|
101
114
|
async dropAndClose() {
|
|
102
|
-
|
|
115
|
+
if (this._fastifyInstance) {
|
|
116
|
+
await this._fastifyInstance.close()
|
|
117
|
+
}
|
|
118
|
+
if (this._mongoInMemory) {
|
|
119
|
+
await this._mongoInMemory.dropAndClose()
|
|
120
|
+
}
|
|
103
121
|
}
|
|
104
122
|
|
|
105
123
|
async login(username: string, password: string): Promise<{accessToken: string}> {
|