@scryan7371/sdr-security 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -7
- package/dist/api/contracts.d.ts +0 -2
- package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
- package/dist/api/migrations/1739500000000-create-security-identity.js +9 -35
- package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
- package/dist/api/migrations/1739510000000-create-security-roles.js +1 -67
- package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
- package/dist/api/migrations/1739515000000-create-security-user-roles.js +39 -0
- package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +1 -1
- package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
- package/dist/api/migrations/1739530000000-create-security-user.js +41 -0
- package/dist/api/migrations/index.d.ts +3 -2
- package/dist/api/migrations/index.js +7 -4
- package/dist/api/migrations/migrations.test.js +37 -83
- package/dist/api/notification-workflows.d.ts +0 -4
- package/dist/api/notification-workflows.js +0 -1
- package/dist/api/notification-workflows.test.js +1 -4
- package/dist/app/client.d.ts +0 -2
- package/dist/app/client.test.js +0 -2
- package/dist/nest/contracts.d.ts +0 -3
- package/dist/nest/dto/auth.dto.d.ts +0 -2
- package/dist/nest/dto/auth.dto.js +0 -10
- package/dist/nest/entities/app-user.entity.d.ts +0 -7
- package/dist/nest/entities/app-user.entity.js +0 -35
- package/dist/nest/entities/security-user.entity.d.ts +9 -0
- package/dist/nest/entities/security-user.entity.js +54 -0
- package/dist/nest/index.d.ts +1 -0
- package/dist/nest/index.js +1 -0
- package/dist/nest/security-auth.controller.d.ts +0 -2
- package/dist/nest/security-auth.controller.js +0 -2
- package/dist/nest/security-auth.controller.test.js +0 -4
- package/dist/nest/security-auth.module.js +2 -0
- package/dist/nest/security-auth.service.d.ts +5 -4
- package/dist/nest/security-auth.service.js +81 -51
- package/dist/nest/security-auth.service.test.js +45 -41
- package/dist/nest/security-workflows.module.js +2 -0
- package/dist/nest/security-workflows.service.d.ts +4 -2
- package/dist/nest/security-workflows.service.js +19 -16
- package/dist/nest/security-workflows.service.test.js +29 -24
- package/package.json +3 -3
- package/src/api/contracts.ts +0 -2
- package/src/api/migrations/1739500000000-create-security-identity.ts +11 -50
- package/src/api/migrations/1739510000000-create-security-roles.ts +2 -89
- package/src/api/migrations/1739515000000-create-security-user-roles.ts +49 -0
- package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +1 -1
- package/src/api/migrations/1739530000000-create-security-user.ts +51 -0
- package/src/api/migrations/index.ts +6 -3
- package/src/api/migrations/migrations.test.ts +48 -111
- package/src/api/notification-workflows.test.ts +1 -4
- package/src/api/notification-workflows.ts +1 -8
- package/src/app/client.test.ts +0 -2
- package/src/app/client.ts +1 -6
- package/src/nest/contracts.ts +1 -6
- package/src/nest/dto/auth.dto.ts +0 -6
- package/src/nest/entities/app-user.entity.ts +0 -21
- package/src/nest/entities/security-user.entity.ts +25 -0
- package/src/nest/index.ts +1 -0
- package/src/nest/security-auth.controller.test.ts +0 -4
- package/src/nest/security-auth.controller.ts +0 -4
- package/src/nest/security-auth.module.ts +2 -0
- package/src/nest/security-auth.service.test.ts +74 -43
- package/src/nest/security-auth.service.ts +88 -51
- package/src/nest/security-workflows.module.ts +2 -0
- package/src/nest/security-workflows.service.test.ts +31 -25
- package/src/nest/security-workflows.service.ts +18 -13
- package/dist/api/migrations/1739490000000-add-google-subject-to-user.d.ts +0 -5
- package/dist/api/migrations/1739490000000-add-google-subject-to-user.js +0 -14
- package/src/api/migrations/1739490000000-add-google-subject-to-user.ts +0 -12
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
94
|
-
|
|
95
|
-
|
|
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(
|
|
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,
|
|
115
|
-
|
|
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 {
|
|
124
|
-
|
|
134
|
+
const {
|
|
135
|
+
service,
|
|
136
|
+
appUsersRepo,
|
|
137
|
+
securityUsersRepo,
|
|
138
|
+
userRolesRepo,
|
|
139
|
+
rolesRepo,
|
|
140
|
+
refreshTokenRepo,
|
|
141
|
+
} = setup();
|
|
125
142
|
const user = makeUser();
|
|
126
143
|
|
|
127
|
-
|
|
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
|
-
|
|
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,
|
|
147
|
-
const inactive = { ...
|
|
148
|
-
|
|
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:
|
|
169
|
+
service.login({ email: "x@example.com", password: "Secret123" }),
|
|
151
170
|
).rejects.toThrow("Account is inactive");
|
|
152
171
|
|
|
153
|
-
|
|
154
|
-
...
|
|
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
|
-
|
|
162
|
-
...
|
|
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 {
|
|
172
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
223
|
-
|
|
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
|
-
|
|
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 {
|
|
245
|
-
|
|
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,
|
|
271
|
-
|
|
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,
|
|
297
|
-
|
|
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,
|
|
314
|
-
|
|
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
|
|
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.
|
|
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
|
|
61
|
-
this.
|
|
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(
|
|
77
|
+
const verificationToken = await this.createEmailVerificationToken(
|
|
78
|
+
appUser.id,
|
|
79
|
+
);
|
|
74
80
|
if (this.notifier.sendEmailVerification) {
|
|
75
81
|
await this.notifier.sendEmailVerification({
|
|
76
|
-
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(
|
|
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
|
|
94
|
-
if (!
|
|
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,
|
|
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(
|
|
103
|
-
return this.issueTokens(
|
|
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
|
|
127
|
+
const appUser = await this.appUsersRepo.findOne({
|
|
116
128
|
where: { id: record.userId ?? "" },
|
|
117
129
|
});
|
|
118
|
-
if (!
|
|
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(
|
|
122
|
-
return this.issueTokens(
|
|
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
|
|
145
|
-
|
|
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,
|
|
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.
|
|
153
|
-
{
|
|
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
|
|
162
|
-
if (!
|
|
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:
|
|
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:
|
|
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.
|
|
190
|
-
{
|
|
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.
|
|
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.
|
|
208
|
-
{
|
|
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
|
-
|
|
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(
|
|
234
|
-
|
|
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:
|
|
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:
|
|
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(
|
|
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.
|
|
270
|
-
{
|
|
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(
|
|
341
|
+
private async toSafeUser(
|
|
342
|
+
appUser: AppUserEntity,
|
|
343
|
+
securityUser: SecurityUserEntity,
|
|
344
|
+
) {
|
|
306
345
|
return {
|
|
307
|
-
id:
|
|
308
|
-
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(
|
|
313
|
-
emailVerifiedAt:
|
|
349
|
+
roles: await this.getUserRoleKeys(appUser.id),
|
|
350
|
+
emailVerifiedAt: securityUser.emailVerifiedAt,
|
|
314
351
|
phoneVerifiedAt: null,
|
|
315
|
-
adminApprovedAt:
|
|
316
|
-
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
|
]),
|