@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
@@ -44,8 +44,6 @@ export class SecurityAuthController {
44
44
  body: {
45
45
  email?: string;
46
46
  password?: string;
47
- firstName?: string;
48
- lastName?: string;
49
47
  },
50
48
  ) {
51
49
  if (!body.email || !body.password) {
@@ -54,8 +52,6 @@ export class SecurityAuthController {
54
52
  return this.authService.register({
55
53
  email: body.email,
56
54
  password: body.password,
57
- firstName: body.firstName ?? null,
58
- lastName: body.lastName ?? null,
59
55
  });
60
56
  }
61
57
 
@@ -5,6 +5,7 @@ import { AppUserEntity } from "./entities/app-user.entity";
5
5
  import { PasswordResetTokenEntity } from "./entities/password-reset-token.entity";
6
6
  import { RefreshTokenEntity } from "./entities/refresh-token.entity";
7
7
  import { SecurityRoleEntity } from "./entities/security-role.entity";
8
+ import { SecurityUserEntity } from "./entities/security-user.entity";
8
9
  import { SecurityUserRoleEntity } from "./entities/security-user-role.entity";
9
10
  import { SecurityAdminGuard } from "./security-admin.guard";
10
11
  import { SecurityAuthController } from "./security-auth.controller";
@@ -39,6 +40,7 @@ export class SecurityAuthModule {
39
40
  imports: [
40
41
  TypeOrmModule.forFeature([
41
42
  AppUserEntity,
43
+ SecurityUserEntity,
42
44
  RefreshTokenEntity,
43
45
  PasswordResetTokenEntity,
44
46
  SecurityRoleEntity,
@@ -39,9 +39,11 @@ const makeNotifier = () => ({
39
39
  const makeUser = () => ({
40
40
  id: "user-1",
41
41
  email: "user@example.com",
42
+ });
43
+
44
+ const makeSecurityUser = () => ({
45
+ userId: "user-1",
42
46
  passwordHash: "hashed:Secret123",
43
- firstName: "A",
44
- lastName: "B",
45
47
  emailVerifiedAt: new Date("2026-01-01T00:00:00.000Z"),
46
48
  emailVerificationToken: null,
47
49
  adminApprovedAt: new Date("2026-01-01T00:00:00.000Z"),
@@ -49,7 +51,8 @@ const makeUser = () => ({
49
51
  });
50
52
 
51
53
  const setup = () => {
52
- const usersRepo = makeRepo();
54
+ const appUsersRepo = makeRepo();
55
+ const securityUsersRepo = makeRepo();
53
56
  const refreshTokenRepo = makeRepo();
54
57
  const passwordResetRepo = makeRepo();
55
58
  const rolesRepo = makeRepo();
@@ -57,7 +60,8 @@ const setup = () => {
57
60
  const notifier = makeNotifier();
58
61
 
59
62
  const service = new SecurityAuthService(
60
- usersRepo as never,
63
+ appUsersRepo as never,
64
+ securityUsersRepo as never,
61
65
  refreshTokenRepo as never,
62
66
  passwordResetRepo as never,
63
67
  rolesRepo as never,
@@ -75,7 +79,8 @@ const setup = () => {
75
79
 
76
80
  return {
77
81
  service,
78
- usersRepo,
82
+ appUsersRepo,
83
+ securityUsersRepo,
79
84
  refreshTokenRepo,
80
85
  passwordResetRepo,
81
86
  rolesRepo,
@@ -90,29 +95,35 @@ describe("SecurityAuthService", () => {
90
95
  });
91
96
 
92
97
  it("registers user and sends verification", async () => {
93
- const { service, usersRepo, userRolesRepo, rolesRepo, notifier } = setup();
94
- usersRepo.findOne.mockResolvedValue(null);
95
- usersRepo.save.mockResolvedValue(makeUser());
98
+ const {
99
+ service,
100
+ appUsersRepo,
101
+ securityUsersRepo,
102
+ userRolesRepo,
103
+ rolesRepo,
104
+ notifier,
105
+ } = setup();
106
+ appUsersRepo.findOne.mockResolvedValue(null);
107
+ appUsersRepo.save.mockResolvedValue(makeUser());
108
+ securityUsersRepo.save.mockResolvedValue(makeSecurityUser());
96
109
  userRolesRepo.find.mockResolvedValue([]);
97
110
  rolesRepo.find.mockResolvedValue([]);
98
111
 
99
112
  const result = await service.register({
100
113
  email: "USER@example.com",
101
114
  password: "Secret123",
102
- firstName: "A",
103
- lastName: "B",
104
115
  });
105
116
 
106
117
  expect(result.success).toBe(true);
107
- expect(usersRepo.create).toHaveBeenCalledWith(
118
+ expect(appUsersRepo.create).toHaveBeenCalledWith(
108
119
  expect.objectContaining({ email: "user@example.com" }),
109
120
  );
110
121
  expect(notifier.sendEmailVerification).toHaveBeenCalled();
111
122
  });
112
123
 
113
124
  it("rejects duplicate email on register", async () => {
114
- const { service, usersRepo } = setup();
115
- usersRepo.findOne.mockResolvedValue(makeUser());
125
+ const { service, appUsersRepo } = setup();
126
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
116
127
 
117
128
  await expect(
118
129
  service.register({ email: "user@example.com", password: "Secret123" }),
@@ -120,11 +131,18 @@ describe("SecurityAuthService", () => {
120
131
  });
121
132
 
122
133
  it("handles login success and auth failures", async () => {
123
- const { service, usersRepo, userRolesRepo, rolesRepo, refreshTokenRepo } =
124
- setup();
134
+ const {
135
+ service,
136
+ appUsersRepo,
137
+ securityUsersRepo,
138
+ userRolesRepo,
139
+ rolesRepo,
140
+ refreshTokenRepo,
141
+ } = setup();
125
142
  const user = makeUser();
126
143
 
127
- usersRepo.findOne.mockResolvedValue(user);
144
+ appUsersRepo.findOne.mockResolvedValue(user);
145
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
128
146
  userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
129
147
  rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "admin" }]);
130
148
 
@@ -136,30 +154,31 @@ describe("SecurityAuthService", () => {
136
154
  expect(sign).toHaveBeenCalled();
137
155
  expect(refreshTokenRepo.save).toHaveBeenCalled();
138
156
 
139
- usersRepo.findOne.mockResolvedValue(null);
157
+ appUsersRepo.findOne.mockResolvedValue(null);
140
158
  await expect(
141
159
  service.login({ email: "none@example.com", password: "Secret123" }),
142
160
  ).rejects.toBeInstanceOf(UnauthorizedException);
143
161
  });
144
162
 
145
163
  it("blocks login when account is inactive or missing approvals", async () => {
146
- const { service, usersRepo } = setup();
147
- const inactive = { ...makeUser(), isActive: false };
148
- usersRepo.findOne.mockResolvedValue(inactive);
164
+ const { service, appUsersRepo, securityUsersRepo } = setup();
165
+ const inactive = { ...makeSecurityUser(), isActive: false };
166
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
167
+ securityUsersRepo.findOne.mockResolvedValue(inactive);
149
168
  await expect(
150
- service.login({ email: inactive.email, password: "Secret123" }),
169
+ service.login({ email: "x@example.com", password: "Secret123" }),
151
170
  ).rejects.toThrow("Account is inactive");
152
171
 
153
- usersRepo.findOne.mockResolvedValue({
154
- ...makeUser(),
172
+ securityUsersRepo.findOne.mockResolvedValue({
173
+ ...makeSecurityUser(),
155
174
  emailVerifiedAt: null,
156
175
  });
157
176
  await expect(
158
177
  service.login({ email: "x@example.com", password: "Secret123" }),
159
178
  ).rejects.toThrow("Email verification required");
160
179
 
161
- usersRepo.findOne.mockResolvedValue({
162
- ...makeUser(),
180
+ securityUsersRepo.findOne.mockResolvedValue({
181
+ ...makeSecurityUser(),
163
182
  adminApprovedAt: null,
164
183
  });
165
184
  await expect(
@@ -168,8 +187,14 @@ describe("SecurityAuthService", () => {
168
187
  });
169
188
 
170
189
  it("refreshes and revokes tokens", async () => {
171
- const { service, usersRepo, refreshTokenRepo, userRolesRepo, rolesRepo } =
172
- setup();
190
+ const {
191
+ service,
192
+ appUsersRepo,
193
+ securityUsersRepo,
194
+ refreshTokenRepo,
195
+ userRolesRepo,
196
+ rolesRepo,
197
+ } = setup();
173
198
  refreshTokenRepo.find.mockResolvedValue([
174
199
  {
175
200
  id: "rt1",
@@ -178,7 +203,8 @@ describe("SecurityAuthService", () => {
178
203
  expiresAt: new Date(Date.now() + 60_000),
179
204
  },
180
205
  ]);
181
- usersRepo.findOne.mockResolvedValue(makeUser());
206
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
207
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
182
208
  userRolesRepo.find.mockResolvedValue([]);
183
209
  rolesRepo.find.mockResolvedValue([]);
184
210
 
@@ -196,7 +222,7 @@ describe("SecurityAuthService", () => {
196
222
  });
197
223
 
198
224
  it("rejects invalid refresh token and missing refresh user", async () => {
199
- const { service, refreshTokenRepo, usersRepo } = setup();
225
+ const { service, refreshTokenRepo, appUsersRepo } = setup();
200
226
  refreshTokenRepo.find.mockResolvedValue([]);
201
227
 
202
228
  await expect(service.refresh("bad-token")).rejects.toThrow(
@@ -211,7 +237,7 @@ describe("SecurityAuthService", () => {
211
237
  expiresAt: new Date(Date.now() + 60_000),
212
238
  },
213
239
  ]);
214
- usersRepo.findOne.mockResolvedValue(null);
240
+ appUsersRepo.findOne.mockResolvedValue(null);
215
241
 
216
242
  await expect(service.refresh("token-bytes")).rejects.toThrow(
217
243
  "User not found",
@@ -219,8 +245,8 @@ describe("SecurityAuthService", () => {
219
245
  });
220
246
 
221
247
  it("changes password", async () => {
222
- const { service, usersRepo } = setup();
223
- usersRepo.findOne.mockResolvedValue(makeUser());
248
+ const { service, securityUsersRepo } = setup();
249
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
224
250
 
225
251
  await expect(
226
252
  service.changePassword({
@@ -230,7 +256,7 @@ describe("SecurityAuthService", () => {
230
256
  }),
231
257
  ).resolves.toEqual({ success: true });
232
258
 
233
- usersRepo.findOne.mockResolvedValue(null);
259
+ securityUsersRepo.findOne.mockResolvedValue(null);
234
260
  await expect(
235
261
  service.changePassword({
236
262
  userId: "missing",
@@ -241,8 +267,15 @@ describe("SecurityAuthService", () => {
241
267
  });
242
268
 
243
269
  it("handles forgot/reset password flow", async () => {
244
- const { service, usersRepo, passwordResetRepo, notifier } = setup();
245
- usersRepo.findOne.mockResolvedValue(makeUser());
270
+ const {
271
+ service,
272
+ appUsersRepo,
273
+ securityUsersRepo,
274
+ passwordResetRepo,
275
+ notifier,
276
+ } = setup();
277
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
278
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
246
279
  passwordResetRepo.findOne.mockResolvedValue({
247
280
  id: "pr1",
248
281
  userId: "user-1",
@@ -267,8 +300,8 @@ describe("SecurityAuthService", () => {
267
300
  });
268
301
 
269
302
  it("returns success for unknown forgot email and rejects bad reset token", async () => {
270
- const { service, usersRepo, passwordResetRepo } = setup();
271
- usersRepo.findOne.mockResolvedValue(null);
303
+ const { service, appUsersRepo, passwordResetRepo } = setup();
304
+ appUsersRepo.findOne.mockResolvedValue(null);
272
305
  await expect(
273
306
  service.requestForgotPassword("none@example.com"),
274
307
  ).resolves.toEqual({
@@ -293,10 +326,8 @@ describe("SecurityAuthService", () => {
293
326
  });
294
327
 
295
328
  it("verifies email token and reads user roles", async () => {
296
- const { service, usersRepo, userRolesRepo, rolesRepo } = setup();
297
- usersRepo.findOne
298
- .mockResolvedValueOnce(makeUser())
299
- .mockResolvedValueOnce(makeUser());
329
+ const { service, securityUsersRepo, userRolesRepo, rolesRepo } = setup();
330
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
300
331
  userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
301
332
  rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "coach" }]);
302
333
 
@@ -310,8 +341,8 @@ describe("SecurityAuthService", () => {
310
341
  });
311
342
 
312
343
  it("rejects invalid email verification token", async () => {
313
- const { service, usersRepo } = setup();
314
- usersRepo.findOne.mockResolvedValue(null);
344
+ const { service, securityUsersRepo } = setup();
345
+ securityUsersRepo.findOne.mockResolvedValue(null);
315
346
 
316
347
  await expect(service.verifyEmailByToken("missing")).rejects.toThrow(
317
348
  "Invalid verification token",
@@ -18,6 +18,7 @@ import { AppUserEntity } from "./entities/app-user.entity";
18
18
  import { PasswordResetTokenEntity } from "./entities/password-reset-token.entity";
19
19
  import { RefreshTokenEntity } from "./entities/refresh-token.entity";
20
20
  import { SecurityRoleEntity } from "./entities/security-role.entity";
21
+ import { SecurityUserEntity } from "./entities/security-user.entity";
21
22
  import { SecurityUserRoleEntity } from "./entities/security-user-role.entity";
22
23
  import { SECURITY_WORKFLOW_NOTIFIER } from "./tokens";
23
24
  import { SecurityWorkflowNotifier } from "./contracts";
@@ -30,7 +31,9 @@ const PASSWORD_ROUNDS = 12;
30
31
  export class SecurityAuthService {
31
32
  constructor(
32
33
  @InjectRepository(AppUserEntity)
33
- private readonly usersRepo: Repository<AppUserEntity>,
34
+ private readonly appUsersRepo: Repository<AppUserEntity>,
35
+ @InjectRepository(SecurityUserEntity)
36
+ private readonly securityUsersRepo: Repository<SecurityUserEntity>,
34
37
  @InjectRepository(RefreshTokenEntity)
35
38
  private readonly refreshTokenRepo: Repository<RefreshTokenEntity>,
36
39
  @InjectRepository(PasswordResetTokenEntity)
@@ -48,21 +51,22 @@ export class SecurityAuthService {
48
51
  async register(params: {
49
52
  email: string;
50
53
  password: string;
51
- firstName?: string | null;
52
- lastName?: string | null;
53
54
  }): Promise<RegisterResponse> {
54
55
  const email = sanitizeEmail(params.email);
55
- const existing = await this.usersRepo.findOne({ where: { email } });
56
+ const existing = await this.appUsersRepo.findOne({ where: { email } });
56
57
  if (existing) {
57
58
  throw new BadRequestException("Email already in use");
58
59
  }
59
60
 
60
- const user = await this.usersRepo.save(
61
- this.usersRepo.create({
61
+ const appUser = await this.appUsersRepo.save(
62
+ this.appUsersRepo.create({
62
63
  email,
64
+ }),
65
+ );
66
+ const securityUser = await this.securityUsersRepo.save(
67
+ this.securityUsersRepo.create({
68
+ userId: appUser.id,
63
69
  passwordHash: await hash(params.password, PASSWORD_ROUNDS),
64
- firstName: params.firstName ?? null,
65
- lastName: params.lastName ?? null,
66
70
  emailVerifiedAt: null,
67
71
  emailVerificationToken: null,
68
72
  adminApprovedAt: null,
@@ -70,17 +74,19 @@ export class SecurityAuthService {
70
74
  }),
71
75
  );
72
76
 
73
- const verificationToken = await this.createEmailVerificationToken(user.id);
77
+ const verificationToken = await this.createEmailVerificationToken(
78
+ appUser.id,
79
+ );
74
80
  if (this.notifier.sendEmailVerification) {
75
81
  await this.notifier.sendEmailVerification({
76
- email: user.email,
82
+ email: appUser.email,
77
83
  token: verificationToken,
78
84
  });
79
85
  }
80
86
 
81
87
  return {
82
88
  success: true,
83
- user: await this.toSafeUser(user),
89
+ user: await this.toSafeUser(appUser, securityUser),
84
90
  debugToken: verificationToken,
85
91
  };
86
92
  }
@@ -90,17 +96,23 @@ export class SecurityAuthService {
90
96
  password: string;
91
97
  }): Promise<AuthResponse> {
92
98
  const email = sanitizeEmail(params.email);
93
- const user = await this.usersRepo.findOne({ where: { email } });
94
- if (!user) {
99
+ const appUser = await this.appUsersRepo.findOne({ where: { email } });
100
+ if (!appUser) {
101
+ throw new UnauthorizedException("Invalid credentials");
102
+ }
103
+ const securityUser = await this.securityUsersRepo.findOne({
104
+ where: { userId: appUser.id },
105
+ });
106
+ if (!securityUser) {
95
107
  throw new UnauthorizedException("Invalid credentials");
96
108
  }
97
109
 
98
- const ok = await compare(params.password, user.passwordHash);
110
+ const ok = await compare(params.password, securityUser.passwordHash);
99
111
  if (!ok) {
100
112
  throw new UnauthorizedException("Invalid credentials");
101
113
  }
102
- this.assertCanAuthenticate(user);
103
- return this.issueTokens(user);
114
+ this.assertCanAuthenticate(securityUser);
115
+ return this.issueTokens(appUser, securityUser);
104
116
  }
105
117
 
106
118
  async refresh(refreshToken: string): Promise<AuthResponse> {
@@ -112,14 +124,20 @@ export class SecurityAuthService {
112
124
  { id: record.id },
113
125
  { revokedAt: new Date() },
114
126
  );
115
- const user = await this.usersRepo.findOne({
127
+ const appUser = await this.appUsersRepo.findOne({
116
128
  where: { id: record.userId ?? "" },
117
129
  });
118
- if (!user) {
130
+ if (!appUser) {
131
+ throw new UnauthorizedException("User not found");
132
+ }
133
+ const securityUser = await this.securityUsersRepo.findOne({
134
+ where: { userId: appUser.id },
135
+ });
136
+ if (!securityUser) {
119
137
  throw new UnauthorizedException("User not found");
120
138
  }
121
- this.assertCanAuthenticate(user);
122
- return this.issueTokens(user);
139
+ this.assertCanAuthenticate(securityUser);
140
+ return this.issueTokens(appUser, securityUser);
123
141
  }
124
142
 
125
143
  async logout(refreshToken?: string) {
@@ -141,16 +159,18 @@ export class SecurityAuthService {
141
159
  currentPassword: string;
142
160
  newPassword: string;
143
161
  }) {
144
- const user = await this.usersRepo.findOne({ where: { id: params.userId } });
145
- if (!user) {
162
+ const securityUser = await this.securityUsersRepo.findOne({
163
+ where: { userId: params.userId },
164
+ });
165
+ if (!securityUser) {
146
166
  throw new BadRequestException("User not found");
147
167
  }
148
- const ok = await compare(params.currentPassword, user.passwordHash);
168
+ const ok = await compare(params.currentPassword, securityUser.passwordHash);
149
169
  if (!ok) {
150
170
  throw new UnauthorizedException("Current password is incorrect");
151
171
  }
152
- await this.usersRepo.update(
153
- { id: user.id },
172
+ await this.securityUsersRepo.update(
173
+ { userId: securityUser.userId },
154
174
  { passwordHash: await hash(params.newPassword, PASSWORD_ROUNDS) },
155
175
  );
156
176
  return { success: true as const };
@@ -158,8 +178,14 @@ export class SecurityAuthService {
158
178
 
159
179
  async requestForgotPassword(emailInput: string) {
160
180
  const email = sanitizeEmail(emailInput);
161
- const user = await this.usersRepo.findOne({ where: { email } });
162
- if (!user) {
181
+ const appUser = await this.appUsersRepo.findOne({ where: { email } });
182
+ if (!appUser) {
183
+ return { success: true as const };
184
+ }
185
+ const securityUser = await this.securityUsersRepo.findOne({
186
+ where: { userId: appUser.id },
187
+ });
188
+ if (!securityUser) {
163
189
  return { success: true as const };
164
190
  }
165
191
  const token = randomBytes(EMAIL_TOKEN_BYTES).toString("hex");
@@ -169,14 +195,14 @@ export class SecurityAuthService {
169
195
  );
170
196
  await this.passwordResetRepo.save(
171
197
  this.passwordResetRepo.create({
172
- userId: user.id,
198
+ userId: appUser.id,
173
199
  token,
174
200
  expiresAt,
175
201
  usedAt: null,
176
202
  }),
177
203
  );
178
204
  if (this.notifier.sendPasswordReset) {
179
- await this.notifier.sendPasswordReset({ email: user.email, token });
205
+ await this.notifier.sendPasswordReset({ email: appUser.email, token });
180
206
  }
181
207
  return { success: true as const };
182
208
  }
@@ -186,8 +212,8 @@ export class SecurityAuthService {
186
212
  if (!reset || reset.usedAt || reset.expiresAt.getTime() <= Date.now()) {
187
213
  throw new BadRequestException("Invalid password reset token");
188
214
  }
189
- await this.usersRepo.update(
190
- { id: reset.userId },
215
+ await this.securityUsersRepo.update(
216
+ { userId: reset.userId },
191
217
  { passwordHash: await hash(newPassword, PASSWORD_ROUNDS) },
192
218
  );
193
219
  await this.passwordResetRepo.update(
@@ -198,14 +224,14 @@ export class SecurityAuthService {
198
224
  }
199
225
 
200
226
  async verifyEmailByToken(token: string) {
201
- const user = await this.usersRepo.findOne({
227
+ const user = await this.securityUsersRepo.findOne({
202
228
  where: { emailVerificationToken: token },
203
229
  });
204
230
  if (!user) {
205
231
  throw new BadRequestException("Invalid verification token");
206
232
  }
207
- await this.usersRepo.update(
208
- { id: user.id },
233
+ await this.securityUsersRepo.update(
234
+ { userId: user.userId },
209
235
  { emailVerifiedAt: new Date(), emailVerificationToken: null },
210
236
  );
211
237
  return { success: true as const };
@@ -215,7 +241,14 @@ export class SecurityAuthService {
215
241
  return { userId, roles: await this.getUserRoleKeys(userId) };
216
242
  }
217
243
 
218
- private assertCanAuthenticate(user: AppUserEntity) {
244
+ async getUserIdByVerificationToken(token: string): Promise<string | null> {
245
+ const user = await this.securityUsersRepo.findOne({
246
+ where: { emailVerificationToken: token },
247
+ });
248
+ return user?.userId ?? null;
249
+ }
250
+
251
+ private assertCanAuthenticate(user: SecurityUserEntity) {
219
252
  if (!user.isActive) {
220
253
  throw new UnauthorizedException("Account is inactive");
221
254
  }
@@ -230,11 +263,14 @@ export class SecurityAuthService {
230
263
  }
231
264
  }
232
265
 
233
- private async issueTokens(user: AppUserEntity): Promise<AuthResponse> {
234
- const roles = await this.getUserRoleKeys(user.id);
266
+ private async issueTokens(
267
+ appUser: AppUserEntity,
268
+ securityUser: SecurityUserEntity,
269
+ ): Promise<AuthResponse> {
270
+ const roles = await this.getUserRoleKeys(appUser.id);
235
271
  const accessTokenExpiresIn = this.options.accessTokenExpiresIn ?? "15m";
236
272
  const accessToken = sign(
237
- { sub: user.id, email: user.email, roles },
273
+ { sub: appUser.id, email: appUser.email, roles },
238
274
  this.options.jwtSecret,
239
275
  { expiresIn: accessTokenExpiresIn as SignOptions["expiresIn"] },
240
276
  );
@@ -248,7 +284,7 @@ export class SecurityAuthService {
248
284
  await this.refreshTokenRepo.save(
249
285
  this.refreshTokenRepo.create({
250
286
  id: randomUUID(),
251
- userId: user.id,
287
+ userId: appUser.id,
252
288
  tokenHash: refreshTokenHash,
253
289
  expiresAt: refreshTokenExpiresAt,
254
290
  revokedAt: null,
@@ -260,14 +296,14 @@ export class SecurityAuthService {
260
296
  accessTokenExpiresIn,
261
297
  refreshToken,
262
298
  refreshTokenExpiresAt,
263
- user: await this.toSafeUser(user),
299
+ user: await this.toSafeUser(appUser, securityUser),
264
300
  };
265
301
  }
266
302
 
267
303
  private async createEmailVerificationToken(userId: string) {
268
304
  const token = randomBytes(EMAIL_TOKEN_BYTES).toString("hex");
269
- await this.usersRepo.update(
270
- { id: userId },
305
+ await this.securityUsersRepo.update(
306
+ { userId },
271
307
  { emailVerificationToken: token },
272
308
  );
273
309
  return token;
@@ -302,18 +338,19 @@ export class SecurityAuthService {
302
338
  return roles.map((role) => normalizeRoleName(role.roleKey)).sort();
303
339
  }
304
340
 
305
- private async toSafeUser(user: AppUserEntity) {
341
+ private async toSafeUser(
342
+ appUser: AppUserEntity,
343
+ securityUser: SecurityUserEntity,
344
+ ) {
306
345
  return {
307
- id: user.id,
308
- email: user.email,
309
- firstName: user.firstName,
310
- lastName: user.lastName,
346
+ id: appUser.id,
347
+ email: appUser.email,
311
348
  phone: null,
312
- roles: await this.getUserRoleKeys(user.id),
313
- emailVerifiedAt: user.emailVerifiedAt,
349
+ roles: await this.getUserRoleKeys(appUser.id),
350
+ emailVerifiedAt: securityUser.emailVerifiedAt,
314
351
  phoneVerifiedAt: null,
315
- adminApprovedAt: user.adminApprovedAt,
316
- isActive: user.isActive,
352
+ adminApprovedAt: securityUser.adminApprovedAt,
353
+ isActive: securityUser.isActive,
317
354
  };
318
355
  }
319
356
  }
@@ -7,6 +7,7 @@ import { SecurityAdminGuard } from "./security-admin.guard";
7
7
  import { SecurityJwtGuard } from "./security-jwt.guard";
8
8
  import { AppUserEntity } from "./entities/app-user.entity";
9
9
  import { SecurityRoleEntity } from "./entities/security-role.entity";
10
+ import { SecurityUserEntity } from "./entities/security-user.entity";
10
11
  import { SecurityUserRoleEntity } from "./entities/security-user-role.entity";
11
12
  import { SecurityWorkflowsController } from "./security-workflows.controller";
12
13
  import { SecurityWorkflowsService } from "./security-workflows.service";
@@ -33,6 +34,7 @@ export class SecurityWorkflowsModule {
33
34
  imports: [
34
35
  TypeOrmModule.forFeature([
35
36
  AppUserEntity,
37
+ SecurityUserEntity,
36
38
  SecurityRoleEntity,
37
39
  SecurityUserRoleEntity,
38
40
  ]),