@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.
- package/README.md +48 -7
- package/dist/api/contracts.d.ts +0 -2
- package/dist/api/migrations/1700000000001-add-refresh-tokens.js +4 -2
- package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
- package/dist/api/migrations/1739500000000-create-security-identity.js +12 -36
- package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
- package/dist/api/migrations/1739510000000-create-security-roles.js +3 -68
- package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
- package/dist/api/migrations/1739515000000-create-security-user-roles.js +42 -0
- package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +4 -2
- package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
- package/dist/api/migrations/1739530000000-create-security-user.js +42 -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/integration/database.integration.test.js +1 -1
- 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 +1 -36
- package/dist/nest/entities/password-reset-token.entity.d.ts +1 -0
- package/dist/nest/entities/password-reset-token.entity.js +14 -2
- package/dist/nest/entities/refresh-token.entity.js +2 -2
- package/dist/nest/entities/security-role.entity.d.ts +1 -0
- package/dist/nest/entities/security-role.entity.js +13 -1
- package/dist/nest/entities/security-user-role.entity.d.ts +1 -0
- package/dist/nest/entities/security-user-role.entity.js +14 -2
- 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 +85 -52
- package/dist/nest/security-auth.service.test.js +48 -42
- 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 +23 -16
- package/dist/nest/security-workflows.service.test.js +29 -24
- package/package.json +5 -4
- package/src/api/contracts.ts +0 -2
- package/src/api/migrations/1700000000001-add-refresh-tokens.ts +4 -2
- package/src/api/migrations/1739500000000-create-security-identity.ts +14 -51
- package/src/api/migrations/1739510000000-create-security-roles.ts +4 -90
- package/src/api/migrations/1739515000000-create-security-user-roles.ts +52 -0
- package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +4 -2
- package/src/api/migrations/1739530000000-create-security-user.ts +52 -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/integration/database.integration.test.ts +1 -1
- package/src/nest/contracts.ts +1 -6
- package/src/nest/dto/auth.dto.ts +0 -6
- package/src/nest/entities/app-user.entity.ts +2 -23
- package/src/nest/entities/password-reset-token.entity.ts +12 -3
- package/src/nest/entities/refresh-token.entity.ts +2 -2
- package/src/nest/entities/security-role.entity.ts +10 -2
- package/src/nest/entities/security-user-role.entity.ts +11 -3
- 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 +78 -44
- package/src/nest/security-auth.service.ts +93 -53
- 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 +22 -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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
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,
|
|
115
|
-
|
|
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 {
|
|
124
|
-
|
|
137
|
+
const {
|
|
138
|
+
service,
|
|
139
|
+
appUsersRepo,
|
|
140
|
+
securityUsersRepo,
|
|
141
|
+
userRolesRepo,
|
|
142
|
+
rolesRepo,
|
|
143
|
+
refreshTokenRepo,
|
|
144
|
+
} = setup();
|
|
125
145
|
const user = makeUser();
|
|
126
146
|
|
|
127
|
-
|
|
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
|
-
|
|
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,
|
|
147
|
-
const inactive = { ...
|
|
148
|
-
|
|
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:
|
|
172
|
+
service.login({ email: "x@example.com", password: "Secret123" }),
|
|
151
173
|
).rejects.toThrow("Account is inactive");
|
|
152
174
|
|
|
153
|
-
|
|
154
|
-
...
|
|
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
|
-
|
|
162
|
-
...
|
|
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 {
|
|
172
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
223
|
-
|
|
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
|
-
|
|
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 {
|
|
245
|
-
|
|
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,
|
|
271
|
-
|
|
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,
|
|
297
|
-
|
|
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,
|
|
314
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
61
|
-
this.
|
|
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(
|
|
79
|
+
const verificationToken = await this.createEmailVerificationToken(
|
|
80
|
+
appUser.id,
|
|
81
|
+
);
|
|
74
82
|
if (this.notifier.sendEmailVerification) {
|
|
75
83
|
await this.notifier.sendEmailVerification({
|
|
76
|
-
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(
|
|
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
|
|
94
|
-
if (!
|
|
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,
|
|
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(
|
|
103
|
-
return this.issueTokens(
|
|
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
|
|
129
|
+
const appUser = await this.appUsersRepo.findOne({
|
|
116
130
|
where: { id: record.userId ?? "" },
|
|
117
131
|
});
|
|
118
|
-
if (!
|
|
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(
|
|
122
|
-
return this.issueTokens(
|
|
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
|
|
145
|
-
|
|
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,
|
|
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.
|
|
153
|
-
{
|
|
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
|
|
162
|
-
if (!
|
|
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
|
-
|
|
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:
|
|
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.
|
|
190
|
-
{
|
|
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.
|
|
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.
|
|
208
|
-
{
|
|
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
|
-
|
|
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(
|
|
234
|
-
|
|
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:
|
|
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:
|
|
251
|
-
userId:
|
|
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(
|
|
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.
|
|
270
|
-
{
|
|
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(
|
|
344
|
+
private async toSafeUser(
|
|
345
|
+
appUser: AppUserEntity,
|
|
346
|
+
securityUser: SecurityUserEntity,
|
|
347
|
+
) {
|
|
306
348
|
return {
|
|
307
|
-
id:
|
|
308
|
-
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(
|
|
313
|
-
emailVerifiedAt:
|
|
352
|
+
roles: await this.getUserRoleKeys(appUser.id),
|
|
353
|
+
emailVerifiedAt: securityUser.emailVerifiedAt,
|
|
314
354
|
phoneVerifiedAt: null,
|
|
315
|
-
adminApprovedAt:
|
|
316
|
-
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
|
]),
|