@scryan7371/sdr-security 0.1.2 → 0.1.3
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/README.md +48 -7
- package/dist/api/contracts.d.ts +0 -2
- package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
- package/dist/api/migrations/1739500000000-create-security-identity.js +9 -35
- package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
- package/dist/api/migrations/1739510000000-create-security-roles.js +1 -67
- package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
- package/dist/api/migrations/1739515000000-create-security-user-roles.js +39 -0
- package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +1 -1
- package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
- package/dist/api/migrations/1739530000000-create-security-user.js +41 -0
- package/dist/api/migrations/index.d.ts +3 -2
- package/dist/api/migrations/index.js +7 -4
- package/dist/api/migrations/migrations.test.js +37 -83
- package/dist/api/notification-workflows.d.ts +0 -4
- package/dist/api/notification-workflows.js +0 -1
- package/dist/api/notification-workflows.test.js +1 -4
- package/dist/app/client.d.ts +0 -2
- package/dist/app/client.test.js +0 -2
- package/dist/nest/contracts.d.ts +0 -3
- package/dist/nest/dto/auth.dto.d.ts +0 -2
- package/dist/nest/dto/auth.dto.js +0 -10
- package/dist/nest/entities/app-user.entity.d.ts +0 -7
- package/dist/nest/entities/app-user.entity.js +0 -35
- package/dist/nest/entities/security-user.entity.d.ts +9 -0
- package/dist/nest/entities/security-user.entity.js +54 -0
- package/dist/nest/index.d.ts +1 -0
- package/dist/nest/index.js +1 -0
- package/dist/nest/security-auth.controller.d.ts +0 -2
- package/dist/nest/security-auth.controller.js +0 -2
- package/dist/nest/security-auth.controller.test.js +0 -4
- package/dist/nest/security-auth.module.js +2 -0
- package/dist/nest/security-auth.service.d.ts +5 -4
- package/dist/nest/security-auth.service.js +81 -51
- package/dist/nest/security-auth.service.test.js +45 -41
- package/dist/nest/security-workflows.module.js +2 -0
- package/dist/nest/security-workflows.service.d.ts +4 -2
- package/dist/nest/security-workflows.service.js +19 -16
- package/dist/nest/security-workflows.service.test.js +29 -24
- package/package.json +3 -3
- package/src/api/contracts.ts +0 -2
- package/src/api/migrations/1739500000000-create-security-identity.ts +11 -50
- package/src/api/migrations/1739510000000-create-security-roles.ts +2 -89
- package/src/api/migrations/1739515000000-create-security-user-roles.ts +49 -0
- package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +1 -1
- package/src/api/migrations/1739530000000-create-security-user.ts +51 -0
- package/src/api/migrations/index.ts +6 -3
- package/src/api/migrations/migrations.test.ts +48 -111
- package/src/api/notification-workflows.test.ts +1 -4
- package/src/api/notification-workflows.ts +1 -8
- package/src/app/client.test.ts +0 -2
- package/src/app/client.ts +1 -6
- package/src/nest/contracts.ts +1 -6
- package/src/nest/dto/auth.dto.ts +0 -6
- package/src/nest/entities/app-user.entity.ts +0 -21
- package/src/nest/entities/security-user.entity.ts +25 -0
- package/src/nest/index.ts +1 -0
- package/src/nest/security-auth.controller.test.ts +0 -4
- package/src/nest/security-auth.controller.ts +0 -4
- package/src/nest/security-auth.module.ts +2 -0
- package/src/nest/security-auth.service.test.ts +74 -43
- package/src/nest/security-auth.service.ts +88 -51
- package/src/nest/security-workflows.module.ts +2 -0
- package/src/nest/security-workflows.service.test.ts +31 -25
- package/src/nest/security-workflows.service.ts +18 -13
- package/dist/api/migrations/1739490000000-add-google-subject-to-user.d.ts +0 -5
- package/dist/api/migrations/1739490000000-add-google-subject-to-user.js +0 -14
- package/src/api/migrations/1739490000000-add-google-subject-to-user.ts +0 -12
|
@@ -32,22 +32,24 @@ const makeNotifier = () => ({
|
|
|
32
32
|
const makeUser = () => ({
|
|
33
33
|
id: "user-1",
|
|
34
34
|
email: "user@example.com",
|
|
35
|
+
});
|
|
36
|
+
const makeSecurityUser = () => ({
|
|
37
|
+
userId: "user-1",
|
|
35
38
|
passwordHash: "hashed:Secret123",
|
|
36
|
-
firstName: "A",
|
|
37
|
-
lastName: "B",
|
|
38
39
|
emailVerifiedAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
39
40
|
emailVerificationToken: null,
|
|
40
41
|
adminApprovedAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
41
42
|
isActive: true,
|
|
42
43
|
});
|
|
43
44
|
const setup = () => {
|
|
44
|
-
const
|
|
45
|
+
const appUsersRepo = makeRepo();
|
|
46
|
+
const securityUsersRepo = makeRepo();
|
|
45
47
|
const refreshTokenRepo = makeRepo();
|
|
46
48
|
const passwordResetRepo = makeRepo();
|
|
47
49
|
const rolesRepo = makeRepo();
|
|
48
50
|
const userRolesRepo = makeRepo();
|
|
49
51
|
const notifier = makeNotifier();
|
|
50
|
-
const service = new security_auth_service_1.SecurityAuthService(
|
|
52
|
+
const service = new security_auth_service_1.SecurityAuthService(appUsersRepo, securityUsersRepo, refreshTokenRepo, passwordResetRepo, rolesRepo, userRolesRepo, {
|
|
51
53
|
jwtSecret: "secret",
|
|
52
54
|
accessTokenExpiresIn: "15m",
|
|
53
55
|
refreshTokenExpiresInDays: 30,
|
|
@@ -57,7 +59,8 @@ const setup = () => {
|
|
|
57
59
|
}, notifier);
|
|
58
60
|
return {
|
|
59
61
|
service,
|
|
60
|
-
|
|
62
|
+
appUsersRepo,
|
|
63
|
+
securityUsersRepo,
|
|
61
64
|
refreshTokenRepo,
|
|
62
65
|
passwordResetRepo,
|
|
63
66
|
rolesRepo,
|
|
@@ -70,30 +73,30 @@ const setup = () => {
|
|
|
70
73
|
vitest_1.vi.clearAllMocks();
|
|
71
74
|
});
|
|
72
75
|
(0, vitest_1.it)("registers user and sends verification", async () => {
|
|
73
|
-
const { service,
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
const { service, appUsersRepo, securityUsersRepo, userRolesRepo, rolesRepo, notifier, } = setup();
|
|
77
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
78
|
+
appUsersRepo.save.mockResolvedValue(makeUser());
|
|
79
|
+
securityUsersRepo.save.mockResolvedValue(makeSecurityUser());
|
|
76
80
|
userRolesRepo.find.mockResolvedValue([]);
|
|
77
81
|
rolesRepo.find.mockResolvedValue([]);
|
|
78
82
|
const result = await service.register({
|
|
79
83
|
email: "USER@example.com",
|
|
80
84
|
password: "Secret123",
|
|
81
|
-
firstName: "A",
|
|
82
|
-
lastName: "B",
|
|
83
85
|
});
|
|
84
86
|
(0, vitest_1.expect)(result.success).toBe(true);
|
|
85
|
-
(0, vitest_1.expect)(
|
|
87
|
+
(0, vitest_1.expect)(appUsersRepo.create).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ email: "user@example.com" }));
|
|
86
88
|
(0, vitest_1.expect)(notifier.sendEmailVerification).toHaveBeenCalled();
|
|
87
89
|
});
|
|
88
90
|
(0, vitest_1.it)("rejects duplicate email on register", async () => {
|
|
89
|
-
const { service,
|
|
90
|
-
|
|
91
|
+
const { service, appUsersRepo } = setup();
|
|
92
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
91
93
|
await (0, vitest_1.expect)(service.register({ email: "user@example.com", password: "Secret123" })).rejects.toBeInstanceOf(common_1.BadRequestException);
|
|
92
94
|
});
|
|
93
95
|
(0, vitest_1.it)("handles login success and auth failures", async () => {
|
|
94
|
-
const { service,
|
|
96
|
+
const { service, appUsersRepo, securityUsersRepo, userRolesRepo, rolesRepo, refreshTokenRepo, } = setup();
|
|
95
97
|
const user = makeUser();
|
|
96
|
-
|
|
98
|
+
appUsersRepo.findOne.mockResolvedValue(user);
|
|
99
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
97
100
|
userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
|
|
98
101
|
rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "admin" }]);
|
|
99
102
|
const auth = await service.login({
|
|
@@ -103,27 +106,28 @@ const setup = () => {
|
|
|
103
106
|
(0, vitest_1.expect)(auth.accessToken).toBe("signed-access-token");
|
|
104
107
|
(0, vitest_1.expect)(jsonwebtoken_1.sign).toHaveBeenCalled();
|
|
105
108
|
(0, vitest_1.expect)(refreshTokenRepo.save).toHaveBeenCalled();
|
|
106
|
-
|
|
109
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
107
110
|
await (0, vitest_1.expect)(service.login({ email: "none@example.com", password: "Secret123" })).rejects.toBeInstanceOf(common_1.UnauthorizedException);
|
|
108
111
|
});
|
|
109
112
|
(0, vitest_1.it)("blocks login when account is inactive or missing approvals", async () => {
|
|
110
|
-
const { service,
|
|
111
|
-
const inactive = { ...
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
const { service, appUsersRepo, securityUsersRepo } = setup();
|
|
114
|
+
const inactive = { ...makeSecurityUser(), isActive: false };
|
|
115
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
116
|
+
securityUsersRepo.findOne.mockResolvedValue(inactive);
|
|
117
|
+
await (0, vitest_1.expect)(service.login({ email: "x@example.com", password: "Secret123" })).rejects.toThrow("Account is inactive");
|
|
118
|
+
securityUsersRepo.findOne.mockResolvedValue({
|
|
119
|
+
...makeSecurityUser(),
|
|
116
120
|
emailVerifiedAt: null,
|
|
117
121
|
});
|
|
118
122
|
await (0, vitest_1.expect)(service.login({ email: "x@example.com", password: "Secret123" })).rejects.toThrow("Email verification required");
|
|
119
|
-
|
|
120
|
-
...
|
|
123
|
+
securityUsersRepo.findOne.mockResolvedValue({
|
|
124
|
+
...makeSecurityUser(),
|
|
121
125
|
adminApprovedAt: null,
|
|
122
126
|
});
|
|
123
127
|
await (0, vitest_1.expect)(service.login({ email: "x@example.com", password: "Secret123" })).rejects.toThrow("Admin approval required");
|
|
124
128
|
});
|
|
125
129
|
(0, vitest_1.it)("refreshes and revokes tokens", async () => {
|
|
126
|
-
const { service,
|
|
130
|
+
const { service, appUsersRepo, securityUsersRepo, refreshTokenRepo, userRolesRepo, rolesRepo, } = setup();
|
|
127
131
|
refreshTokenRepo.find.mockResolvedValue([
|
|
128
132
|
{
|
|
129
133
|
id: "rt1",
|
|
@@ -132,7 +136,8 @@ const setup = () => {
|
|
|
132
136
|
expiresAt: new Date(Date.now() + 60_000),
|
|
133
137
|
},
|
|
134
138
|
]);
|
|
135
|
-
|
|
139
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
140
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
136
141
|
userRolesRepo.find.mockResolvedValue([]);
|
|
137
142
|
rolesRepo.find.mockResolvedValue([]);
|
|
138
143
|
const result = await service.refresh("token-bytes");
|
|
@@ -144,7 +149,7 @@ const setup = () => {
|
|
|
144
149
|
});
|
|
145
150
|
});
|
|
146
151
|
(0, vitest_1.it)("rejects invalid refresh token and missing refresh user", async () => {
|
|
147
|
-
const { service, refreshTokenRepo,
|
|
152
|
+
const { service, refreshTokenRepo, appUsersRepo } = setup();
|
|
148
153
|
refreshTokenRepo.find.mockResolvedValue([]);
|
|
149
154
|
await (0, vitest_1.expect)(service.refresh("bad-token")).rejects.toThrow("Invalid refresh token");
|
|
150
155
|
refreshTokenRepo.find.mockResolvedValue([
|
|
@@ -155,18 +160,18 @@ const setup = () => {
|
|
|
155
160
|
expiresAt: new Date(Date.now() + 60_000),
|
|
156
161
|
},
|
|
157
162
|
]);
|
|
158
|
-
|
|
163
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
159
164
|
await (0, vitest_1.expect)(service.refresh("token-bytes")).rejects.toThrow("User not found");
|
|
160
165
|
});
|
|
161
166
|
(0, vitest_1.it)("changes password", async () => {
|
|
162
|
-
const { service,
|
|
163
|
-
|
|
167
|
+
const { service, securityUsersRepo } = setup();
|
|
168
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
164
169
|
await (0, vitest_1.expect)(service.changePassword({
|
|
165
170
|
userId: "user-1",
|
|
166
171
|
currentPassword: "Secret123",
|
|
167
172
|
newPassword: "NewPass1",
|
|
168
173
|
})).resolves.toEqual({ success: true });
|
|
169
|
-
|
|
174
|
+
securityUsersRepo.findOne.mockResolvedValue(null);
|
|
170
175
|
await (0, vitest_1.expect)(service.changePassword({
|
|
171
176
|
userId: "missing",
|
|
172
177
|
currentPassword: "Secret123",
|
|
@@ -174,8 +179,9 @@ const setup = () => {
|
|
|
174
179
|
})).rejects.toThrow("User not found");
|
|
175
180
|
});
|
|
176
181
|
(0, vitest_1.it)("handles forgot/reset password flow", async () => {
|
|
177
|
-
const { service,
|
|
178
|
-
|
|
182
|
+
const { service, appUsersRepo, securityUsersRepo, passwordResetRepo, notifier, } = setup();
|
|
183
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
184
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
179
185
|
passwordResetRepo.findOne.mockResolvedValue({
|
|
180
186
|
id: "pr1",
|
|
181
187
|
userId: "user-1",
|
|
@@ -193,8 +199,8 @@ const setup = () => {
|
|
|
193
199
|
});
|
|
194
200
|
});
|
|
195
201
|
(0, vitest_1.it)("returns success for unknown forgot email and rejects bad reset token", async () => {
|
|
196
|
-
const { service,
|
|
197
|
-
|
|
202
|
+
const { service, appUsersRepo, passwordResetRepo } = setup();
|
|
203
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
198
204
|
await (0, vitest_1.expect)(service.requestForgotPassword("none@example.com")).resolves.toEqual({
|
|
199
205
|
success: true,
|
|
200
206
|
});
|
|
@@ -210,10 +216,8 @@ const setup = () => {
|
|
|
210
216
|
await (0, vitest_1.expect)(service.resetPassword("bad", "x")).rejects.toThrow("Invalid password reset token");
|
|
211
217
|
});
|
|
212
218
|
(0, vitest_1.it)("verifies email token and reads user roles", async () => {
|
|
213
|
-
const { service,
|
|
214
|
-
|
|
215
|
-
.mockResolvedValueOnce(makeUser())
|
|
216
|
-
.mockResolvedValueOnce(makeUser());
|
|
219
|
+
const { service, securityUsersRepo, userRolesRepo, rolesRepo } = setup();
|
|
220
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
217
221
|
userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
|
|
218
222
|
rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "coach" }]);
|
|
219
223
|
await (0, vitest_1.expect)(service.verifyEmailByToken("token-bytes")).resolves.toEqual({
|
|
@@ -225,8 +229,8 @@ const setup = () => {
|
|
|
225
229
|
});
|
|
226
230
|
});
|
|
227
231
|
(0, vitest_1.it)("rejects invalid email verification token", async () => {
|
|
228
|
-
const { service,
|
|
229
|
-
|
|
232
|
+
const { service, securityUsersRepo } = setup();
|
|
233
|
+
securityUsersRepo.findOne.mockResolvedValue(null);
|
|
230
234
|
await (0, vitest_1.expect)(service.verifyEmailByToken("missing")).rejects.toThrow("Invalid verification token");
|
|
231
235
|
});
|
|
232
236
|
(0, vitest_1.it)("handles refresh with expired matching token", async () => {
|
|
@@ -15,6 +15,7 @@ const security_admin_guard_1 = require("./security-admin.guard");
|
|
|
15
15
|
const security_jwt_guard_1 = require("./security-jwt.guard");
|
|
16
16
|
const app_user_entity_1 = require("./entities/app-user.entity");
|
|
17
17
|
const security_role_entity_1 = require("./entities/security-role.entity");
|
|
18
|
+
const security_user_entity_1 = require("./entities/security-user.entity");
|
|
18
19
|
const security_user_role_entity_1 = require("./entities/security-user-role.entity");
|
|
19
20
|
const security_workflows_controller_1 = require("./security-workflows.controller");
|
|
20
21
|
const security_workflows_service_1 = require("./security-workflows.service");
|
|
@@ -34,6 +35,7 @@ let SecurityWorkflowsModule = SecurityWorkflowsModule_1 = class SecurityWorkflow
|
|
|
34
35
|
imports: [
|
|
35
36
|
typeorm_1.TypeOrmModule.forFeature([
|
|
36
37
|
app_user_entity_1.AppUserEntity,
|
|
38
|
+
security_user_entity_1.SecurityUserEntity,
|
|
37
39
|
security_role_entity_1.SecurityRoleEntity,
|
|
38
40
|
security_user_role_entity_1.SecurityUserRoleEntity,
|
|
39
41
|
]),
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Repository } from "typeorm";
|
|
2
2
|
import { AppUserEntity } from "./entities/app-user.entity";
|
|
3
3
|
import { SecurityRoleEntity } from "./entities/security-role.entity";
|
|
4
|
+
import { SecurityUserEntity } from "./entities/security-user.entity";
|
|
4
5
|
import { SecurityUserRoleEntity } from "./entities/security-user-role.entity";
|
|
5
6
|
import { SecurityWorkflowNotifier } from "./contracts";
|
|
6
7
|
export declare class SecurityWorkflowsService {
|
|
7
|
-
private readonly
|
|
8
|
+
private readonly appUsersRepo;
|
|
9
|
+
private readonly securityUsersRepo;
|
|
8
10
|
private readonly rolesRepo;
|
|
9
11
|
private readonly userRolesRepo;
|
|
10
12
|
private readonly notifier;
|
|
11
|
-
constructor(
|
|
13
|
+
constructor(appUsersRepo: Repository<AppUserEntity>, securityUsersRepo: Repository<SecurityUserEntity>, rolesRepo: Repository<SecurityRoleEntity>, userRolesRepo: Repository<SecurityUserRoleEntity>, notifier: SecurityWorkflowNotifier);
|
|
12
14
|
markEmailVerifiedAndNotifyAdmins(userId: string): Promise<{
|
|
13
15
|
success: true;
|
|
14
16
|
notified: false;
|
|
@@ -20,22 +20,25 @@ const contracts_1 = require("../api/contracts");
|
|
|
20
20
|
const roles_1 = require("../api/roles");
|
|
21
21
|
const app_user_entity_1 = require("./entities/app-user.entity");
|
|
22
22
|
const security_role_entity_1 = require("./entities/security-role.entity");
|
|
23
|
+
const security_user_entity_1 = require("./entities/security-user.entity");
|
|
23
24
|
const security_user_role_entity_1 = require("./entities/security-user-role.entity");
|
|
24
25
|
const tokens_1 = require("./tokens");
|
|
25
26
|
let SecurityWorkflowsService = class SecurityWorkflowsService {
|
|
26
|
-
|
|
27
|
+
appUsersRepo;
|
|
28
|
+
securityUsersRepo;
|
|
27
29
|
rolesRepo;
|
|
28
30
|
userRolesRepo;
|
|
29
31
|
notifier;
|
|
30
|
-
constructor(
|
|
31
|
-
this.
|
|
32
|
+
constructor(appUsersRepo, securityUsersRepo, rolesRepo, userRolesRepo, notifier) {
|
|
33
|
+
this.appUsersRepo = appUsersRepo;
|
|
34
|
+
this.securityUsersRepo = securityUsersRepo;
|
|
32
35
|
this.rolesRepo = rolesRepo;
|
|
33
36
|
this.userRolesRepo = userRolesRepo;
|
|
34
37
|
this.notifier = notifier;
|
|
35
38
|
}
|
|
36
39
|
async markEmailVerifiedAndNotifyAdmins(userId) {
|
|
37
|
-
await this.
|
|
38
|
-
const user = await this.
|
|
40
|
+
await this.securityUsersRepo.update({ userId }, { emailVerifiedAt: new Date(), emailVerificationToken: null });
|
|
41
|
+
const user = await this.appUsersRepo.findOne({ where: { id: userId } });
|
|
39
42
|
if (!user) {
|
|
40
43
|
throw new common_1.NotFoundException("User not found");
|
|
41
44
|
}
|
|
@@ -48,15 +51,13 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
|
|
|
48
51
|
user: {
|
|
49
52
|
id: user.id,
|
|
50
53
|
email: user.email,
|
|
51
|
-
firstName: user.firstName,
|
|
52
|
-
lastName: user.lastName,
|
|
53
54
|
},
|
|
54
55
|
});
|
|
55
56
|
return { success: true, notified: true, adminEmails };
|
|
56
57
|
}
|
|
57
58
|
async setAdminApprovalAndNotifyUser(userId, approved) {
|
|
58
|
-
await this.
|
|
59
|
-
const user = await this.
|
|
59
|
+
await this.securityUsersRepo.update({ userId }, { adminApprovedAt: approved ? new Date() : null });
|
|
60
|
+
const user = await this.appUsersRepo.findOne({ where: { id: userId } });
|
|
60
61
|
if (!user) {
|
|
61
62
|
throw new common_1.NotFoundException("User not found");
|
|
62
63
|
}
|
|
@@ -65,7 +66,6 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
|
|
|
65
66
|
}
|
|
66
67
|
await this.notifier.sendUserAccountApproved({
|
|
67
68
|
email: user.email,
|
|
68
|
-
firstName: user.firstName,
|
|
69
69
|
});
|
|
70
70
|
return { success: true, notified: true };
|
|
71
71
|
}
|
|
@@ -74,8 +74,9 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
|
|
|
74
74
|
.createQueryBuilder("userRole")
|
|
75
75
|
.innerJoin("security_role", "role", "role.id = userRole.role_id")
|
|
76
76
|
.innerJoin("app_user", "user", "user.id = userRole.user_id")
|
|
77
|
+
.innerJoin("security_user", "securityUser", "securityUser.user_id = userRole.user_id")
|
|
77
78
|
.where("role.role_key = :roleKey", { roleKey: contracts_1.ADMIN_ROLE })
|
|
78
|
-
.andWhere("
|
|
79
|
+
.andWhere("securityUser.is_active = :isActive", { isActive: true })
|
|
79
80
|
.select("DISTINCT user.email", "email")
|
|
80
81
|
.getRawMany();
|
|
81
82
|
return rows.map((row) => row.email).filter(Boolean);
|
|
@@ -158,11 +159,11 @@ let SecurityWorkflowsService = class SecurityWorkflowsService {
|
|
|
158
159
|
return this.setUserRoles(userId, nextRoles);
|
|
159
160
|
}
|
|
160
161
|
async setUserActive(userId, active) {
|
|
161
|
-
await this.
|
|
162
|
+
await this.securityUsersRepo.update({ userId }, { isActive: active });
|
|
162
163
|
return { success: true, userId, active };
|
|
163
164
|
}
|
|
164
165
|
async assertUserExists(userId) {
|
|
165
|
-
const user = await this.
|
|
166
|
+
const user = await this.appUsersRepo.findOne({ where: { id: userId } });
|
|
166
167
|
if (!user) {
|
|
167
168
|
throw new common_1.NotFoundException("User not found");
|
|
168
169
|
}
|
|
@@ -191,10 +192,12 @@ exports.SecurityWorkflowsService = SecurityWorkflowsService;
|
|
|
191
192
|
exports.SecurityWorkflowsService = SecurityWorkflowsService = __decorate([
|
|
192
193
|
(0, common_1.Injectable)(),
|
|
193
194
|
__param(0, (0, typeorm_1.InjectRepository)(app_user_entity_1.AppUserEntity)),
|
|
194
|
-
__param(1, (0, typeorm_1.InjectRepository)(
|
|
195
|
-
__param(2, (0, typeorm_1.InjectRepository)(
|
|
196
|
-
__param(3, (0,
|
|
195
|
+
__param(1, (0, typeorm_1.InjectRepository)(security_user_entity_1.SecurityUserEntity)),
|
|
196
|
+
__param(2, (0, typeorm_1.InjectRepository)(security_role_entity_1.SecurityRoleEntity)),
|
|
197
|
+
__param(3, (0, typeorm_1.InjectRepository)(security_user_role_entity_1.SecurityUserRoleEntity)),
|
|
198
|
+
__param(4, (0, common_1.Inject)(tokens_1.SECURITY_WORKFLOW_NOTIFIER)),
|
|
197
199
|
__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
200
|
+
typeorm_2.Repository,
|
|
198
201
|
typeorm_2.Repository,
|
|
199
202
|
typeorm_2.Repository, Object])
|
|
200
203
|
], SecurityWorkflowsService);
|
|
@@ -19,25 +19,31 @@ const makeNotifier = () => ({
|
|
|
19
19
|
const makeUser = () => ({
|
|
20
20
|
id: "user-1",
|
|
21
21
|
email: "user@example.com",
|
|
22
|
-
firstName: "A",
|
|
23
|
-
lastName: "B",
|
|
24
22
|
isActive: true,
|
|
25
23
|
});
|
|
26
24
|
const setup = () => {
|
|
27
|
-
const
|
|
25
|
+
const appUsersRepo = makeRepo();
|
|
26
|
+
const securityUsersRepo = makeRepo();
|
|
28
27
|
const rolesRepo = makeRepo();
|
|
29
28
|
const userRolesRepo = makeRepo();
|
|
30
29
|
const notifier = makeNotifier();
|
|
31
|
-
const service = new security_workflows_service_1.SecurityWorkflowsService(
|
|
32
|
-
return {
|
|
30
|
+
const service = new security_workflows_service_1.SecurityWorkflowsService(appUsersRepo, securityUsersRepo, rolesRepo, userRolesRepo, notifier);
|
|
31
|
+
return {
|
|
32
|
+
service,
|
|
33
|
+
appUsersRepo,
|
|
34
|
+
securityUsersRepo,
|
|
35
|
+
rolesRepo,
|
|
36
|
+
userRolesRepo,
|
|
37
|
+
notifier,
|
|
38
|
+
};
|
|
33
39
|
};
|
|
34
40
|
(0, vitest_1.describe)("SecurityWorkflowsService", () => {
|
|
35
41
|
(0, vitest_1.beforeEach)(() => {
|
|
36
42
|
vitest_1.vi.clearAllMocks();
|
|
37
43
|
});
|
|
38
44
|
(0, vitest_1.it)("marks email verified and notifies admins", async () => {
|
|
39
|
-
const { service,
|
|
40
|
-
|
|
45
|
+
const { service, appUsersRepo, userRolesRepo, notifier } = setup();
|
|
46
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
41
47
|
const getRawMany = vitest_1.vi
|
|
42
48
|
.fn()
|
|
43
49
|
.mockResolvedValue([{ email: "admin@example.com" }]);
|
|
@@ -58,8 +64,8 @@ const setup = () => {
|
|
|
58
64
|
(0, vitest_1.expect)(notifier.sendAdminsUserEmailVerified).toHaveBeenCalled();
|
|
59
65
|
});
|
|
60
66
|
(0, vitest_1.it)("returns not-notified when no admins are present", async () => {
|
|
61
|
-
const { service,
|
|
62
|
-
|
|
67
|
+
const { service, appUsersRepo, userRolesRepo } = setup();
|
|
68
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
63
69
|
const qb = {
|
|
64
70
|
innerJoin: vitest_1.vi.fn().mockReturnThis(),
|
|
65
71
|
where: vitest_1.vi.fn().mockReturnThis(),
|
|
@@ -71,23 +77,22 @@ const setup = () => {
|
|
|
71
77
|
await (0, vitest_1.expect)(service.markEmailVerifiedAndNotifyAdmins("user-1")).resolves.toEqual({ success: true, notified: false, adminEmails: [] });
|
|
72
78
|
});
|
|
73
79
|
(0, vitest_1.it)("throws when user is missing during verification flow", async () => {
|
|
74
|
-
const { service,
|
|
75
|
-
|
|
80
|
+
const { service, appUsersRepo } = setup();
|
|
81
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
76
82
|
await (0, vitest_1.expect)(service.markEmailVerifiedAndNotifyAdmins("missing")).rejects.toBeInstanceOf(common_1.NotFoundException);
|
|
77
83
|
});
|
|
78
84
|
(0, vitest_1.it)("handles admin approval notifications", async () => {
|
|
79
|
-
const { service,
|
|
80
|
-
|
|
85
|
+
const { service, appUsersRepo, notifier } = setup();
|
|
86
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
81
87
|
await (0, vitest_1.expect)(service.setAdminApprovalAndNotifyUser("user-1", false)).resolves.toEqual({ success: true, notified: false });
|
|
82
88
|
await (0, vitest_1.expect)(service.setAdminApprovalAndNotifyUser("user-1", true)).resolves.toEqual({ success: true, notified: true });
|
|
83
89
|
(0, vitest_1.expect)(notifier.sendUserAccountApproved).toHaveBeenCalledWith({
|
|
84
90
|
email: "user@example.com",
|
|
85
|
-
firstName: "A",
|
|
86
91
|
});
|
|
87
92
|
});
|
|
88
93
|
(0, vitest_1.it)("throws when approval target user is missing", async () => {
|
|
89
|
-
const { service,
|
|
90
|
-
|
|
94
|
+
const { service, appUsersRepo } = setup();
|
|
95
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
91
96
|
await (0, vitest_1.expect)(service.setAdminApprovalAndNotifyUser("missing", true)).rejects.toBeInstanceOf(common_1.NotFoundException);
|
|
92
97
|
});
|
|
93
98
|
(0, vitest_1.it)("manages role catalog and protected role removal", async () => {
|
|
@@ -125,8 +130,8 @@ const setup = () => {
|
|
|
125
130
|
(0, vitest_1.expect)(userRolesRepo.delete).toHaveBeenCalledWith({ roleId: "r2" });
|
|
126
131
|
});
|
|
127
132
|
(0, vitest_1.it)("gets and sets user roles", async () => {
|
|
128
|
-
const { service,
|
|
129
|
-
|
|
133
|
+
const { service, appUsersRepo, userRolesRepo, rolesRepo } = setup();
|
|
134
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
130
135
|
userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
|
|
131
136
|
rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "ADMIN" }]);
|
|
132
137
|
await (0, vitest_1.expect)(service.getUserRoles("user-1")).resolves.toEqual({
|
|
@@ -143,8 +148,8 @@ const setup = () => {
|
|
|
143
148
|
(0, vitest_1.expect)(userRolesRepo.save).toHaveBeenCalled();
|
|
144
149
|
});
|
|
145
150
|
(0, vitest_1.it)("assigns and removes role from user", async () => {
|
|
146
|
-
const { service,
|
|
147
|
-
|
|
151
|
+
const { service, appUsersRepo, userRolesRepo, rolesRepo } = setup();
|
|
152
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
148
153
|
userRolesRepo.find.mockResolvedValue([]);
|
|
149
154
|
rolesRepo.find.mockResolvedValue([]);
|
|
150
155
|
await service.assignRoleToUser("user-1", "coach");
|
|
@@ -156,17 +161,17 @@ const setup = () => {
|
|
|
156
161
|
(0, vitest_1.expect)(userRolesRepo.delete).toHaveBeenCalledWith({ userId: "user-1" });
|
|
157
162
|
});
|
|
158
163
|
(0, vitest_1.it)("sets user active state", async () => {
|
|
159
|
-
const { service,
|
|
164
|
+
const { service, securityUsersRepo } = setup();
|
|
160
165
|
await (0, vitest_1.expect)(service.setUserActive("user-1", false)).resolves.toEqual({
|
|
161
166
|
success: true,
|
|
162
167
|
userId: "user-1",
|
|
163
168
|
active: false,
|
|
164
169
|
});
|
|
165
|
-
(0, vitest_1.expect)(
|
|
170
|
+
(0, vitest_1.expect)(securityUsersRepo.update).toHaveBeenCalledWith({ userId: "user-1" }, { isActive: false });
|
|
166
171
|
});
|
|
167
172
|
(0, vitest_1.it)("throws when role operations target missing user", async () => {
|
|
168
|
-
const { service,
|
|
169
|
-
|
|
173
|
+
const { service, appUsersRepo } = setup();
|
|
174
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
170
175
|
await (0, vitest_1.expect)(service.getUserRoles("missing")).rejects.toBeInstanceOf(common_1.NotFoundException);
|
|
171
176
|
await (0, vitest_1.expect)(service.setUserRoles("missing", ["ADMIN"])).rejects.toBeInstanceOf(common_1.NotFoundException);
|
|
172
177
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scryan7371/sdr-security",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Reusable auth/security capability for API and app clients.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"@nestjs/typeorm": "11.0.0",
|
|
63
63
|
"@types/jsonwebtoken": "9.0.10",
|
|
64
64
|
"@types/node": "^25.2.3",
|
|
65
|
-
"@types/pg": "8.
|
|
65
|
+
"@types/pg": "8.16.0",
|
|
66
66
|
"@vitest/coverage-v8": "^4.0.18",
|
|
67
67
|
"eslint": "9.18.0",
|
|
68
|
-
"pg": "8.
|
|
68
|
+
"pg": "8.18.0",
|
|
69
69
|
"prettier": "3.8.1",
|
|
70
70
|
"typeorm": "0.3.28",
|
|
71
71
|
"typescript": "5.9.3",
|
package/src/api/contracts.ts
CHANGED
|
@@ -2,20 +2,13 @@ export class CreateSecurityIdentity1739500000000 {
|
|
|
2
2
|
name = "CreateSecurityIdentity1739500000000";
|
|
3
3
|
|
|
4
4
|
async up(queryRunner: {
|
|
5
|
-
query: (sql: string
|
|
5
|
+
query: (sql: string) => Promise<unknown>;
|
|
6
6
|
}): Promise<void> {
|
|
7
|
-
const
|
|
8
|
-
const userSchema = getSafeIdentifier(
|
|
9
|
-
process.env.USER_TABLE_SCHEMA,
|
|
10
|
-
"public",
|
|
11
|
-
);
|
|
12
|
-
const userTableRef = `"${userSchema}"."${userTable}"`;
|
|
13
|
-
|
|
14
|
-
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS "uuid-ossp"`);
|
|
7
|
+
const userTableRef = getUserTableReference();
|
|
15
8
|
|
|
16
9
|
await queryRunner.query(`
|
|
17
10
|
CREATE TABLE IF NOT EXISTS "security_identity" (
|
|
18
|
-
"id" uuid PRIMARY KEY NOT NULL DEFAULT
|
|
11
|
+
"id" uuid PRIMARY KEY NOT NULL DEFAULT uuidv7(),
|
|
19
12
|
"user_id" varchar NOT NULL,
|
|
20
13
|
"provider" varchar NOT NULL,
|
|
21
14
|
"provider_subject" varchar NOT NULL,
|
|
@@ -31,46 +24,6 @@ export class CreateSecurityIdentity1739500000000 {
|
|
|
31
24
|
await queryRunner.query(
|
|
32
25
|
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_security_identity_user_provider" ON "security_identity" ("user_id", "provider")`,
|
|
33
26
|
);
|
|
34
|
-
|
|
35
|
-
const hasGoogleSubjectColumn = (await queryRunner.query(
|
|
36
|
-
`
|
|
37
|
-
SELECT 1
|
|
38
|
-
FROM information_schema.columns
|
|
39
|
-
WHERE table_schema = $1
|
|
40
|
-
AND table_name = $2
|
|
41
|
-
AND column_name = 'google_subject'
|
|
42
|
-
LIMIT 1
|
|
43
|
-
`,
|
|
44
|
-
[userSchema, userTable],
|
|
45
|
-
)) as Array<{ "?column?": number }>;
|
|
46
|
-
|
|
47
|
-
if (hasGoogleSubjectColumn.length > 0) {
|
|
48
|
-
await queryRunner.query(`
|
|
49
|
-
INSERT INTO "security_identity" (
|
|
50
|
-
"user_id",
|
|
51
|
-
"provider",
|
|
52
|
-
"provider_subject",
|
|
53
|
-
"created_at",
|
|
54
|
-
"updated_at"
|
|
55
|
-
)
|
|
56
|
-
SELECT
|
|
57
|
-
"id",
|
|
58
|
-
'google',
|
|
59
|
-
"google_subject",
|
|
60
|
-
now(),
|
|
61
|
-
now()
|
|
62
|
-
FROM ${userTableRef}
|
|
63
|
-
WHERE "google_subject" IS NOT NULL
|
|
64
|
-
ON CONFLICT ("provider", "provider_subject") DO NOTHING
|
|
65
|
-
`);
|
|
66
|
-
|
|
67
|
-
await queryRunner.query(
|
|
68
|
-
`DROP INDEX IF EXISTS "IDX_app_user_google_subject"`,
|
|
69
|
-
);
|
|
70
|
-
await queryRunner.query(
|
|
71
|
-
`ALTER TABLE ${userTableRef} DROP COLUMN IF EXISTS "google_subject"`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
27
|
}
|
|
75
28
|
|
|
76
29
|
async down(queryRunner: {
|
|
@@ -93,3 +46,11 @@ const getSafeIdentifier = (value: string | undefined, fallback: string) => {
|
|
|
93
46
|
}
|
|
94
47
|
return resolved;
|
|
95
48
|
};
|
|
49
|
+
|
|
50
|
+
const getUserTableReference = () => {
|
|
51
|
+
const table = getSafeIdentifier(process.env.USER_TABLE, "app_user");
|
|
52
|
+
const schema = process.env.USER_TABLE_SCHEMA
|
|
53
|
+
? getSafeIdentifier(process.env.USER_TABLE_SCHEMA, "public")
|
|
54
|
+
: "public";
|
|
55
|
+
return `"${schema}"."${table}"`;
|
|
56
|
+
};
|