@scryan7371/sdr-security 0.1.2 → 0.1.4

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