@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.
Files changed (68) hide show
  1. package/README.md +48 -7
  2. package/dist/api/contracts.d.ts +0 -2
  3. package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
  4. package/dist/api/migrations/1739500000000-create-security-identity.js +9 -35
  5. package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
  6. package/dist/api/migrations/1739510000000-create-security-roles.js +1 -67
  7. package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
  8. package/dist/api/migrations/1739515000000-create-security-user-roles.js +39 -0
  9. package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +1 -1
  10. package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
  11. package/dist/api/migrations/1739530000000-create-security-user.js +41 -0
  12. package/dist/api/migrations/index.d.ts +3 -2
  13. package/dist/api/migrations/index.js +7 -4
  14. package/dist/api/migrations/migrations.test.js +37 -83
  15. package/dist/api/notification-workflows.d.ts +0 -4
  16. package/dist/api/notification-workflows.js +0 -1
  17. package/dist/api/notification-workflows.test.js +1 -4
  18. package/dist/app/client.d.ts +0 -2
  19. package/dist/app/client.test.js +0 -2
  20. package/dist/nest/contracts.d.ts +0 -3
  21. package/dist/nest/dto/auth.dto.d.ts +0 -2
  22. package/dist/nest/dto/auth.dto.js +0 -10
  23. package/dist/nest/entities/app-user.entity.d.ts +0 -7
  24. package/dist/nest/entities/app-user.entity.js +0 -35
  25. package/dist/nest/entities/security-user.entity.d.ts +9 -0
  26. package/dist/nest/entities/security-user.entity.js +54 -0
  27. package/dist/nest/index.d.ts +1 -0
  28. package/dist/nest/index.js +1 -0
  29. package/dist/nest/security-auth.controller.d.ts +0 -2
  30. package/dist/nest/security-auth.controller.js +0 -2
  31. package/dist/nest/security-auth.controller.test.js +0 -4
  32. package/dist/nest/security-auth.module.js +2 -0
  33. package/dist/nest/security-auth.service.d.ts +5 -4
  34. package/dist/nest/security-auth.service.js +81 -51
  35. package/dist/nest/security-auth.service.test.js +45 -41
  36. package/dist/nest/security-workflows.module.js +2 -0
  37. package/dist/nest/security-workflows.service.d.ts +4 -2
  38. package/dist/nest/security-workflows.service.js +19 -16
  39. package/dist/nest/security-workflows.service.test.js +29 -24
  40. package/package.json +3 -3
  41. package/src/api/contracts.ts +0 -2
  42. package/src/api/migrations/1739500000000-create-security-identity.ts +11 -50
  43. package/src/api/migrations/1739510000000-create-security-roles.ts +2 -89
  44. package/src/api/migrations/1739515000000-create-security-user-roles.ts +49 -0
  45. package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +1 -1
  46. package/src/api/migrations/1739530000000-create-security-user.ts +51 -0
  47. package/src/api/migrations/index.ts +6 -3
  48. package/src/api/migrations/migrations.test.ts +48 -111
  49. package/src/api/notification-workflows.test.ts +1 -4
  50. package/src/api/notification-workflows.ts +1 -8
  51. package/src/app/client.test.ts +0 -2
  52. package/src/app/client.ts +1 -6
  53. package/src/nest/contracts.ts +1 -6
  54. package/src/nest/dto/auth.dto.ts +0 -6
  55. package/src/nest/entities/app-user.entity.ts +0 -21
  56. package/src/nest/entities/security-user.entity.ts +25 -0
  57. package/src/nest/index.ts +1 -0
  58. package/src/nest/security-auth.controller.test.ts +0 -4
  59. package/src/nest/security-auth.controller.ts +0 -4
  60. package/src/nest/security-auth.module.ts +2 -0
  61. package/src/nest/security-auth.service.test.ts +74 -43
  62. package/src/nest/security-auth.service.ts +88 -51
  63. package/src/nest/security-workflows.module.ts +2 -0
  64. package/src/nest/security-workflows.service.test.ts +31 -25
  65. package/src/nest/security-workflows.service.ts +18 -13
  66. package/dist/api/migrations/1739490000000-add-google-subject-to-user.d.ts +0 -5
  67. package/dist/api/migrations/1739490000000-add-google-subject-to-user.js +0 -14
  68. 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 usersRepo = makeRepo();
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(usersRepo, refreshTokenRepo, passwordResetRepo, rolesRepo, userRolesRepo, {
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
- usersRepo,
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, usersRepo, userRolesRepo, rolesRepo, notifier } = setup();
74
- usersRepo.findOne.mockResolvedValue(null);
75
- usersRepo.save.mockResolvedValue(makeUser());
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)(usersRepo.create).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ email: "user@example.com" }));
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, usersRepo } = setup();
90
- usersRepo.findOne.mockResolvedValue(makeUser());
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, usersRepo, userRolesRepo, rolesRepo, refreshTokenRepo } = setup();
96
+ const { service, appUsersRepo, securityUsersRepo, userRolesRepo, rolesRepo, refreshTokenRepo, } = setup();
95
97
  const user = makeUser();
96
- usersRepo.findOne.mockResolvedValue(user);
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
- usersRepo.findOne.mockResolvedValue(null);
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, usersRepo } = setup();
111
- const inactive = { ...makeUser(), isActive: false };
112
- usersRepo.findOne.mockResolvedValue(inactive);
113
- await (0, vitest_1.expect)(service.login({ email: inactive.email, password: "Secret123" })).rejects.toThrow("Account is inactive");
114
- usersRepo.findOne.mockResolvedValue({
115
- ...makeUser(),
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
- usersRepo.findOne.mockResolvedValue({
120
- ...makeUser(),
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, usersRepo, refreshTokenRepo, userRolesRepo, rolesRepo } = setup();
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
- usersRepo.findOne.mockResolvedValue(makeUser());
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, usersRepo } = setup();
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
- usersRepo.findOne.mockResolvedValue(null);
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, usersRepo } = setup();
163
- usersRepo.findOne.mockResolvedValue(makeUser());
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
- usersRepo.findOne.mockResolvedValue(null);
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, usersRepo, passwordResetRepo, notifier } = setup();
178
- usersRepo.findOne.mockResolvedValue(makeUser());
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, usersRepo, passwordResetRepo } = setup();
197
- usersRepo.findOne.mockResolvedValue(null);
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, usersRepo, userRolesRepo, rolesRepo } = setup();
214
- usersRepo.findOne
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, usersRepo } = setup();
229
- usersRepo.findOne.mockResolvedValue(null);
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 usersRepo;
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(usersRepo: Repository<AppUserEntity>, rolesRepo: Repository<SecurityRoleEntity>, userRolesRepo: Repository<SecurityUserRoleEntity>, notifier: SecurityWorkflowNotifier);
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
- usersRepo;
27
+ appUsersRepo;
28
+ securityUsersRepo;
27
29
  rolesRepo;
28
30
  userRolesRepo;
29
31
  notifier;
30
- constructor(usersRepo, rolesRepo, userRolesRepo, notifier) {
31
- this.usersRepo = usersRepo;
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.usersRepo.update({ id: userId }, { emailVerifiedAt: new Date(), emailVerificationToken: null });
38
- const user = await this.usersRepo.findOne({ where: { id: userId } });
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.usersRepo.update({ id: userId }, { adminApprovedAt: approved ? new Date() : null });
59
- const user = await this.usersRepo.findOne({ where: { id: userId } });
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("user.is_active = :isActive", { isActive: true })
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.usersRepo.update({ id: userId }, { isActive: active });
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.usersRepo.findOne({ where: { id: userId } });
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)(security_role_entity_1.SecurityRoleEntity)),
195
- __param(2, (0, typeorm_1.InjectRepository)(security_user_role_entity_1.SecurityUserRoleEntity)),
196
- __param(3, (0, common_1.Inject)(tokens_1.SECURITY_WORKFLOW_NOTIFIER)),
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 usersRepo = makeRepo();
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(usersRepo, rolesRepo, userRolesRepo, notifier);
32
- return { service, usersRepo, rolesRepo, userRolesRepo, notifier };
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, usersRepo, userRolesRepo, notifier } = setup();
40
- usersRepo.findOne.mockResolvedValue(makeUser());
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, usersRepo, userRolesRepo } = setup();
62
- usersRepo.findOne.mockResolvedValue(makeUser());
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, usersRepo } = setup();
75
- usersRepo.findOne.mockResolvedValue(null);
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, usersRepo, notifier } = setup();
80
- usersRepo.findOne.mockResolvedValue(makeUser());
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, usersRepo } = setup();
90
- usersRepo.findOne.mockResolvedValue(null);
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, usersRepo, userRolesRepo, rolesRepo } = setup();
129
- usersRepo.findOne.mockResolvedValue(makeUser());
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, usersRepo, userRolesRepo, rolesRepo } = setup();
147
- usersRepo.findOne.mockResolvedValue(makeUser());
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, usersRepo } = setup();
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)(usersRepo.update).toHaveBeenCalledWith({ id: "user-1" }, { isActive: false });
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, usersRepo } = setup();
169
- usersRepo.findOne.mockResolvedValue(null);
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.2",
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.15.5",
65
+ "@types/pg": "8.16.0",
66
66
  "@vitest/coverage-v8": "^4.0.18",
67
67
  "eslint": "9.18.0",
68
- "pg": "8.16.3",
68
+ "pg": "8.18.0",
69
69
  "prettier": "3.8.1",
70
70
  "typeorm": "0.3.28",
71
71
  "typescript": "5.9.3",
@@ -4,8 +4,6 @@ export type UserRole = string;
4
4
  export type SafeUser = {
5
5
  id: string;
6
6
  email: string;
7
- firstName: string | null;
8
- lastName: string | null;
9
7
  phone: string | null;
10
8
  roles: UserRole[];
11
9
  emailVerifiedAt: string | Date | null;
@@ -2,20 +2,13 @@ export class CreateSecurityIdentity1739500000000 {
2
2
  name = "CreateSecurityIdentity1739500000000";
3
3
 
4
4
  async up(queryRunner: {
5
- query: (sql: string, params?: unknown[]) => Promise<unknown>;
5
+ query: (sql: string) => Promise<unknown>;
6
6
  }): Promise<void> {
7
- const userTable = getSafeIdentifier(process.env.USER_TABLE, "app_user");
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 uuid_generate_v4(),
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
+ };