@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
|
@@ -17,6 +17,7 @@ const common_1 = require("@nestjs/common");
|
|
|
17
17
|
const crypto_1 = require("crypto");
|
|
18
18
|
const bcryptjs_1 = require("bcryptjs");
|
|
19
19
|
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
20
|
+
const uuid_1 = require("uuid");
|
|
20
21
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
21
22
|
const typeorm_2 = require("typeorm");
|
|
22
23
|
const roles_1 = require("../api/roles");
|
|
@@ -26,21 +27,24 @@ const app_user_entity_1 = require("./entities/app-user.entity");
|
|
|
26
27
|
const password_reset_token_entity_1 = require("./entities/password-reset-token.entity");
|
|
27
28
|
const refresh_token_entity_1 = require("./entities/refresh-token.entity");
|
|
28
29
|
const security_role_entity_1 = require("./entities/security-role.entity");
|
|
30
|
+
const security_user_entity_1 = require("./entities/security-user.entity");
|
|
29
31
|
const security_user_role_entity_1 = require("./entities/security-user-role.entity");
|
|
30
32
|
const tokens_1 = require("./tokens");
|
|
31
33
|
const EMAIL_TOKEN_BYTES = 24;
|
|
32
34
|
const REFRESH_TOKEN_BYTES = 32;
|
|
33
35
|
const PASSWORD_ROUNDS = 12;
|
|
34
36
|
let SecurityAuthService = class SecurityAuthService {
|
|
35
|
-
|
|
37
|
+
appUsersRepo;
|
|
38
|
+
securityUsersRepo;
|
|
36
39
|
refreshTokenRepo;
|
|
37
40
|
passwordResetRepo;
|
|
38
41
|
rolesRepo;
|
|
39
42
|
userRolesRepo;
|
|
40
43
|
options;
|
|
41
44
|
notifier;
|
|
42
|
-
constructor(
|
|
43
|
-
this.
|
|
45
|
+
constructor(appUsersRepo, securityUsersRepo, refreshTokenRepo, passwordResetRepo, rolesRepo, userRolesRepo, options, notifier) {
|
|
46
|
+
this.appUsersRepo = appUsersRepo;
|
|
47
|
+
this.securityUsersRepo = securityUsersRepo;
|
|
44
48
|
this.refreshTokenRepo = refreshTokenRepo;
|
|
45
49
|
this.passwordResetRepo = passwordResetRepo;
|
|
46
50
|
this.rolesRepo = rolesRepo;
|
|
@@ -50,45 +54,53 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
50
54
|
}
|
|
51
55
|
async register(params) {
|
|
52
56
|
const email = (0, validation_1.sanitizeEmail)(params.email);
|
|
53
|
-
const existing = await this.
|
|
57
|
+
const existing = await this.appUsersRepo.findOne({ where: { email } });
|
|
54
58
|
if (existing) {
|
|
55
59
|
throw new common_1.BadRequestException("Email already in use");
|
|
56
60
|
}
|
|
57
|
-
const
|
|
61
|
+
const appUser = await this.appUsersRepo.save(this.appUsersRepo.create({
|
|
62
|
+
id: (0, uuid_1.v7)(),
|
|
58
63
|
email,
|
|
64
|
+
}));
|
|
65
|
+
const securityUser = await this.securityUsersRepo.save(this.securityUsersRepo.create({
|
|
66
|
+
userId: appUser.id,
|
|
59
67
|
passwordHash: await (0, bcryptjs_1.hash)(params.password, PASSWORD_ROUNDS),
|
|
60
|
-
firstName: params.firstName ?? null,
|
|
61
|
-
lastName: params.lastName ?? null,
|
|
62
68
|
emailVerifiedAt: null,
|
|
63
69
|
emailVerificationToken: null,
|
|
64
70
|
adminApprovedAt: null,
|
|
65
71
|
isActive: true,
|
|
66
72
|
}));
|
|
67
|
-
const verificationToken = await this.createEmailVerificationToken(
|
|
73
|
+
const verificationToken = await this.createEmailVerificationToken(appUser.id);
|
|
68
74
|
if (this.notifier.sendEmailVerification) {
|
|
69
75
|
await this.notifier.sendEmailVerification({
|
|
70
|
-
email:
|
|
76
|
+
email: appUser.email,
|
|
71
77
|
token: verificationToken,
|
|
72
78
|
});
|
|
73
79
|
}
|
|
74
80
|
return {
|
|
75
81
|
success: true,
|
|
76
|
-
user: await this.toSafeUser(
|
|
82
|
+
user: await this.toSafeUser(appUser, securityUser),
|
|
77
83
|
debugToken: verificationToken,
|
|
78
84
|
};
|
|
79
85
|
}
|
|
80
86
|
async login(params) {
|
|
81
87
|
const email = (0, validation_1.sanitizeEmail)(params.email);
|
|
82
|
-
const
|
|
83
|
-
if (!
|
|
88
|
+
const appUser = await this.appUsersRepo.findOne({ where: { email } });
|
|
89
|
+
if (!appUser) {
|
|
90
|
+
throw new common_1.UnauthorizedException("Invalid credentials");
|
|
91
|
+
}
|
|
92
|
+
const securityUser = await this.securityUsersRepo.findOne({
|
|
93
|
+
where: { userId: appUser.id },
|
|
94
|
+
});
|
|
95
|
+
if (!securityUser) {
|
|
84
96
|
throw new common_1.UnauthorizedException("Invalid credentials");
|
|
85
97
|
}
|
|
86
|
-
const ok = await (0, bcryptjs_1.compare)(params.password,
|
|
98
|
+
const ok = await (0, bcryptjs_1.compare)(params.password, securityUser.passwordHash);
|
|
87
99
|
if (!ok) {
|
|
88
100
|
throw new common_1.UnauthorizedException("Invalid credentials");
|
|
89
101
|
}
|
|
90
|
-
this.assertCanAuthenticate(
|
|
91
|
-
return this.issueTokens(
|
|
102
|
+
this.assertCanAuthenticate(securityUser);
|
|
103
|
+
return this.issueTokens(appUser, securityUser);
|
|
92
104
|
}
|
|
93
105
|
async refresh(refreshToken) {
|
|
94
106
|
const record = await this.findValidRefreshToken(refreshToken);
|
|
@@ -96,14 +108,20 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
96
108
|
throw new common_1.UnauthorizedException("Invalid refresh token");
|
|
97
109
|
}
|
|
98
110
|
await this.refreshTokenRepo.update({ id: record.id }, { revokedAt: new Date() });
|
|
99
|
-
const
|
|
111
|
+
const appUser = await this.appUsersRepo.findOne({
|
|
100
112
|
where: { id: record.userId ?? "" },
|
|
101
113
|
});
|
|
102
|
-
if (!
|
|
114
|
+
if (!appUser) {
|
|
103
115
|
throw new common_1.UnauthorizedException("User not found");
|
|
104
116
|
}
|
|
105
|
-
this.
|
|
106
|
-
|
|
117
|
+
const securityUser = await this.securityUsersRepo.findOne({
|
|
118
|
+
where: { userId: appUser.id },
|
|
119
|
+
});
|
|
120
|
+
if (!securityUser) {
|
|
121
|
+
throw new common_1.UnauthorizedException("User not found");
|
|
122
|
+
}
|
|
123
|
+
this.assertCanAuthenticate(securityUser);
|
|
124
|
+
return this.issueTokens(appUser, securityUser);
|
|
107
125
|
}
|
|
108
126
|
async logout(refreshToken) {
|
|
109
127
|
if (!refreshToken) {
|
|
@@ -116,34 +134,43 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
116
134
|
return { success: true };
|
|
117
135
|
}
|
|
118
136
|
async changePassword(params) {
|
|
119
|
-
const
|
|
120
|
-
|
|
137
|
+
const securityUser = await this.securityUsersRepo.findOne({
|
|
138
|
+
where: { userId: params.userId },
|
|
139
|
+
});
|
|
140
|
+
if (!securityUser) {
|
|
121
141
|
throw new common_1.BadRequestException("User not found");
|
|
122
142
|
}
|
|
123
|
-
const ok = await (0, bcryptjs_1.compare)(params.currentPassword,
|
|
143
|
+
const ok = await (0, bcryptjs_1.compare)(params.currentPassword, securityUser.passwordHash);
|
|
124
144
|
if (!ok) {
|
|
125
145
|
throw new common_1.UnauthorizedException("Current password is incorrect");
|
|
126
146
|
}
|
|
127
|
-
await this.
|
|
147
|
+
await this.securityUsersRepo.update({ userId: securityUser.userId }, { passwordHash: await (0, bcryptjs_1.hash)(params.newPassword, PASSWORD_ROUNDS) });
|
|
128
148
|
return { success: true };
|
|
129
149
|
}
|
|
130
150
|
async requestForgotPassword(emailInput) {
|
|
131
151
|
const email = (0, validation_1.sanitizeEmail)(emailInput);
|
|
132
|
-
const
|
|
133
|
-
if (!
|
|
152
|
+
const appUser = await this.appUsersRepo.findOne({ where: { email } });
|
|
153
|
+
if (!appUser) {
|
|
154
|
+
return { success: true };
|
|
155
|
+
}
|
|
156
|
+
const securityUser = await this.securityUsersRepo.findOne({
|
|
157
|
+
where: { userId: appUser.id },
|
|
158
|
+
});
|
|
159
|
+
if (!securityUser) {
|
|
134
160
|
return { success: true };
|
|
135
161
|
}
|
|
136
162
|
const token = (0, crypto_1.randomBytes)(EMAIL_TOKEN_BYTES).toString("hex");
|
|
137
163
|
const expiresAt = new Date(Date.now() +
|
|
138
164
|
(this.options.passwordResetTokenExpiresInMinutes ?? 30) * 60_000);
|
|
139
165
|
await this.passwordResetRepo.save(this.passwordResetRepo.create({
|
|
140
|
-
|
|
166
|
+
id: (0, uuid_1.v7)(),
|
|
167
|
+
userId: appUser.id,
|
|
141
168
|
token,
|
|
142
169
|
expiresAt,
|
|
143
170
|
usedAt: null,
|
|
144
171
|
}));
|
|
145
172
|
if (this.notifier.sendPasswordReset) {
|
|
146
|
-
await this.notifier.sendPasswordReset({ email:
|
|
173
|
+
await this.notifier.sendPasswordReset({ email: appUser.email, token });
|
|
147
174
|
}
|
|
148
175
|
return { success: true };
|
|
149
176
|
}
|
|
@@ -152,23 +179,29 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
152
179
|
if (!reset || reset.usedAt || reset.expiresAt.getTime() <= Date.now()) {
|
|
153
180
|
throw new common_1.BadRequestException("Invalid password reset token");
|
|
154
181
|
}
|
|
155
|
-
await this.
|
|
182
|
+
await this.securityUsersRepo.update({ userId: reset.userId }, { passwordHash: await (0, bcryptjs_1.hash)(newPassword, PASSWORD_ROUNDS) });
|
|
156
183
|
await this.passwordResetRepo.update({ id: reset.id }, { usedAt: new Date() });
|
|
157
184
|
return { success: true };
|
|
158
185
|
}
|
|
159
186
|
async verifyEmailByToken(token) {
|
|
160
|
-
const user = await this.
|
|
187
|
+
const user = await this.securityUsersRepo.findOne({
|
|
161
188
|
where: { emailVerificationToken: token },
|
|
162
189
|
});
|
|
163
190
|
if (!user) {
|
|
164
191
|
throw new common_1.BadRequestException("Invalid verification token");
|
|
165
192
|
}
|
|
166
|
-
await this.
|
|
193
|
+
await this.securityUsersRepo.update({ userId: user.userId }, { emailVerifiedAt: new Date(), emailVerificationToken: null });
|
|
167
194
|
return { success: true };
|
|
168
195
|
}
|
|
169
196
|
async getMyRoles(userId) {
|
|
170
197
|
return { userId, roles: await this.getUserRoleKeys(userId) };
|
|
171
198
|
}
|
|
199
|
+
async getUserIdByVerificationToken(token) {
|
|
200
|
+
const user = await this.securityUsersRepo.findOne({
|
|
201
|
+
where: { emailVerificationToken: token },
|
|
202
|
+
});
|
|
203
|
+
return user?.userId ?? null;
|
|
204
|
+
}
|
|
172
205
|
assertCanAuthenticate(user) {
|
|
173
206
|
if (!user.isActive) {
|
|
174
207
|
throw new common_1.UnauthorizedException("Account is inactive");
|
|
@@ -181,17 +214,17 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
181
214
|
throw new common_1.UnauthorizedException("Admin approval required");
|
|
182
215
|
}
|
|
183
216
|
}
|
|
184
|
-
async issueTokens(
|
|
185
|
-
const roles = await this.getUserRoleKeys(
|
|
217
|
+
async issueTokens(appUser, securityUser) {
|
|
218
|
+
const roles = await this.getUserRoleKeys(appUser.id);
|
|
186
219
|
const accessTokenExpiresIn = this.options.accessTokenExpiresIn ?? "15m";
|
|
187
|
-
const accessToken = (0, jsonwebtoken_1.sign)({ sub:
|
|
220
|
+
const accessToken = (0, jsonwebtoken_1.sign)({ sub: appUser.id, email: appUser.email, roles }, this.options.jwtSecret, { expiresIn: accessTokenExpiresIn });
|
|
188
221
|
const refreshToken = (0, crypto_1.randomBytes)(REFRESH_TOKEN_BYTES).toString("hex");
|
|
189
222
|
const refreshTokenHash = await (0, bcryptjs_1.hash)(refreshToken, PASSWORD_ROUNDS);
|
|
190
223
|
const refreshTokenExpiresAt = new Date(Date.now() +
|
|
191
224
|
(this.options.refreshTokenExpiresInDays ?? 30) * 24 * 60 * 60 * 1000);
|
|
192
225
|
await this.refreshTokenRepo.save(this.refreshTokenRepo.create({
|
|
193
|
-
id: (0,
|
|
194
|
-
userId:
|
|
226
|
+
id: (0, uuid_1.v7)(),
|
|
227
|
+
userId: appUser.id,
|
|
195
228
|
tokenHash: refreshTokenHash,
|
|
196
229
|
expiresAt: refreshTokenExpiresAt,
|
|
197
230
|
revokedAt: null,
|
|
@@ -201,12 +234,12 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
201
234
|
accessTokenExpiresIn,
|
|
202
235
|
refreshToken,
|
|
203
236
|
refreshTokenExpiresAt,
|
|
204
|
-
user: await this.toSafeUser(
|
|
237
|
+
user: await this.toSafeUser(appUser, securityUser),
|
|
205
238
|
};
|
|
206
239
|
}
|
|
207
240
|
async createEmailVerificationToken(userId) {
|
|
208
241
|
const token = (0, crypto_1.randomBytes)(EMAIL_TOKEN_BYTES).toString("hex");
|
|
209
|
-
await this.
|
|
242
|
+
await this.securityUsersRepo.update({ userId }, { emailVerificationToken: token });
|
|
210
243
|
return token;
|
|
211
244
|
}
|
|
212
245
|
async findValidRefreshToken(token, onlyUnexpired = true) {
|
|
@@ -236,18 +269,16 @@ let SecurityAuthService = class SecurityAuthService {
|
|
|
236
269
|
const roles = await this.rolesRepo.find({ where: { id: (0, typeorm_2.In)(roleIds) } });
|
|
237
270
|
return roles.map((role) => (0, roles_1.normalizeRoleName)(role.roleKey)).sort();
|
|
238
271
|
}
|
|
239
|
-
async toSafeUser(
|
|
272
|
+
async toSafeUser(appUser, securityUser) {
|
|
240
273
|
return {
|
|
241
|
-
id:
|
|
242
|
-
email:
|
|
243
|
-
firstName: user.firstName,
|
|
244
|
-
lastName: user.lastName,
|
|
274
|
+
id: appUser.id,
|
|
275
|
+
email: appUser.email,
|
|
245
276
|
phone: null,
|
|
246
|
-
roles: await this.getUserRoleKeys(
|
|
247
|
-
emailVerifiedAt:
|
|
277
|
+
roles: await this.getUserRoleKeys(appUser.id),
|
|
278
|
+
emailVerifiedAt: securityUser.emailVerifiedAt,
|
|
248
279
|
phoneVerifiedAt: null,
|
|
249
|
-
adminApprovedAt:
|
|
250
|
-
isActive:
|
|
280
|
+
adminApprovedAt: securityUser.adminApprovedAt,
|
|
281
|
+
isActive: securityUser.isActive,
|
|
251
282
|
};
|
|
252
283
|
}
|
|
253
284
|
};
|
|
@@ -255,15 +286,17 @@ exports.SecurityAuthService = SecurityAuthService;
|
|
|
255
286
|
exports.SecurityAuthService = SecurityAuthService = __decorate([
|
|
256
287
|
(0, common_1.Injectable)(),
|
|
257
288
|
__param(0, (0, typeorm_1.InjectRepository)(app_user_entity_1.AppUserEntity)),
|
|
258
|
-
__param(1, (0, typeorm_1.InjectRepository)(
|
|
259
|
-
__param(2, (0, typeorm_1.InjectRepository)(
|
|
260
|
-
__param(3, (0, typeorm_1.InjectRepository)(
|
|
261
|
-
__param(4, (0, typeorm_1.InjectRepository)(
|
|
262
|
-
__param(5, (0,
|
|
263
|
-
__param(6, (0, common_1.Inject)(
|
|
289
|
+
__param(1, (0, typeorm_1.InjectRepository)(security_user_entity_1.SecurityUserEntity)),
|
|
290
|
+
__param(2, (0, typeorm_1.InjectRepository)(refresh_token_entity_1.RefreshTokenEntity)),
|
|
291
|
+
__param(3, (0, typeorm_1.InjectRepository)(password_reset_token_entity_1.PasswordResetTokenEntity)),
|
|
292
|
+
__param(4, (0, typeorm_1.InjectRepository)(security_role_entity_1.SecurityRoleEntity)),
|
|
293
|
+
__param(5, (0, typeorm_1.InjectRepository)(security_user_role_entity_1.SecurityUserRoleEntity)),
|
|
294
|
+
__param(6, (0, common_1.Inject)(security_auth_constants_1.SECURITY_AUTH_OPTIONS)),
|
|
295
|
+
__param(7, (0, common_1.Inject)(tokens_1.SECURITY_WORKFLOW_NOTIFIER)),
|
|
264
296
|
__metadata("design:paramtypes", [typeorm_2.Repository,
|
|
265
297
|
typeorm_2.Repository,
|
|
266
298
|
typeorm_2.Repository,
|
|
267
299
|
typeorm_2.Repository,
|
|
300
|
+
typeorm_2.Repository,
|
|
268
301
|
typeorm_2.Repository, Object, Object])
|
|
269
302
|
], SecurityAuthService);
|
|
@@ -8,11 +8,13 @@ vitest_1.vi.mock("bcryptjs", () => ({
|
|
|
8
8
|
}));
|
|
9
9
|
vitest_1.vi.mock("crypto", () => ({
|
|
10
10
|
randomBytes: vitest_1.vi.fn(() => ({ toString: () => "token-bytes" })),
|
|
11
|
-
randomUUID: vitest_1.vi.fn(() => "uuid-1"),
|
|
12
11
|
}));
|
|
13
12
|
vitest_1.vi.mock("jsonwebtoken", () => ({
|
|
14
13
|
sign: vitest_1.vi.fn(() => "signed-access-token"),
|
|
15
14
|
}));
|
|
15
|
+
vitest_1.vi.mock("uuid", () => ({
|
|
16
|
+
v7: vitest_1.vi.fn(() => "uuid-1"),
|
|
17
|
+
}));
|
|
16
18
|
const bcryptjs_1 = require("bcryptjs");
|
|
17
19
|
const jsonwebtoken_1 = require("jsonwebtoken");
|
|
18
20
|
const security_auth_service_1 = require("./security-auth.service");
|
|
@@ -32,22 +34,24 @@ const makeNotifier = () => ({
|
|
|
32
34
|
const makeUser = () => ({
|
|
33
35
|
id: "user-1",
|
|
34
36
|
email: "user@example.com",
|
|
37
|
+
});
|
|
38
|
+
const makeSecurityUser = () => ({
|
|
39
|
+
userId: "user-1",
|
|
35
40
|
passwordHash: "hashed:Secret123",
|
|
36
|
-
firstName: "A",
|
|
37
|
-
lastName: "B",
|
|
38
41
|
emailVerifiedAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
39
42
|
emailVerificationToken: null,
|
|
40
43
|
adminApprovedAt: new Date("2026-01-01T00:00:00.000Z"),
|
|
41
44
|
isActive: true,
|
|
42
45
|
});
|
|
43
46
|
const setup = () => {
|
|
44
|
-
const
|
|
47
|
+
const appUsersRepo = makeRepo();
|
|
48
|
+
const securityUsersRepo = makeRepo();
|
|
45
49
|
const refreshTokenRepo = makeRepo();
|
|
46
50
|
const passwordResetRepo = makeRepo();
|
|
47
51
|
const rolesRepo = makeRepo();
|
|
48
52
|
const userRolesRepo = makeRepo();
|
|
49
53
|
const notifier = makeNotifier();
|
|
50
|
-
const service = new security_auth_service_1.SecurityAuthService(
|
|
54
|
+
const service = new security_auth_service_1.SecurityAuthService(appUsersRepo, securityUsersRepo, refreshTokenRepo, passwordResetRepo, rolesRepo, userRolesRepo, {
|
|
51
55
|
jwtSecret: "secret",
|
|
52
56
|
accessTokenExpiresIn: "15m",
|
|
53
57
|
refreshTokenExpiresInDays: 30,
|
|
@@ -57,7 +61,8 @@ const setup = () => {
|
|
|
57
61
|
}, notifier);
|
|
58
62
|
return {
|
|
59
63
|
service,
|
|
60
|
-
|
|
64
|
+
appUsersRepo,
|
|
65
|
+
securityUsersRepo,
|
|
61
66
|
refreshTokenRepo,
|
|
62
67
|
passwordResetRepo,
|
|
63
68
|
rolesRepo,
|
|
@@ -70,30 +75,30 @@ const setup = () => {
|
|
|
70
75
|
vitest_1.vi.clearAllMocks();
|
|
71
76
|
});
|
|
72
77
|
(0, vitest_1.it)("registers user and sends verification", async () => {
|
|
73
|
-
const { service,
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
const { service, appUsersRepo, securityUsersRepo, userRolesRepo, rolesRepo, notifier, } = setup();
|
|
79
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
80
|
+
appUsersRepo.save.mockResolvedValue(makeUser());
|
|
81
|
+
securityUsersRepo.save.mockResolvedValue(makeSecurityUser());
|
|
76
82
|
userRolesRepo.find.mockResolvedValue([]);
|
|
77
83
|
rolesRepo.find.mockResolvedValue([]);
|
|
78
84
|
const result = await service.register({
|
|
79
85
|
email: "USER@example.com",
|
|
80
86
|
password: "Secret123",
|
|
81
|
-
firstName: "A",
|
|
82
|
-
lastName: "B",
|
|
83
87
|
});
|
|
84
88
|
(0, vitest_1.expect)(result.success).toBe(true);
|
|
85
|
-
(0, vitest_1.expect)(
|
|
89
|
+
(0, vitest_1.expect)(appUsersRepo.create).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ email: "user@example.com" }));
|
|
86
90
|
(0, vitest_1.expect)(notifier.sendEmailVerification).toHaveBeenCalled();
|
|
87
91
|
});
|
|
88
92
|
(0, vitest_1.it)("rejects duplicate email on register", async () => {
|
|
89
|
-
const { service,
|
|
90
|
-
|
|
93
|
+
const { service, appUsersRepo } = setup();
|
|
94
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
91
95
|
await (0, vitest_1.expect)(service.register({ email: "user@example.com", password: "Secret123" })).rejects.toBeInstanceOf(common_1.BadRequestException);
|
|
92
96
|
});
|
|
93
97
|
(0, vitest_1.it)("handles login success and auth failures", async () => {
|
|
94
|
-
const { service,
|
|
98
|
+
const { service, appUsersRepo, securityUsersRepo, userRolesRepo, rolesRepo, refreshTokenRepo, } = setup();
|
|
95
99
|
const user = makeUser();
|
|
96
|
-
|
|
100
|
+
appUsersRepo.findOne.mockResolvedValue(user);
|
|
101
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
97
102
|
userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
|
|
98
103
|
rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "admin" }]);
|
|
99
104
|
const auth = await service.login({
|
|
@@ -103,27 +108,28 @@ const setup = () => {
|
|
|
103
108
|
(0, vitest_1.expect)(auth.accessToken).toBe("signed-access-token");
|
|
104
109
|
(0, vitest_1.expect)(jsonwebtoken_1.sign).toHaveBeenCalled();
|
|
105
110
|
(0, vitest_1.expect)(refreshTokenRepo.save).toHaveBeenCalled();
|
|
106
|
-
|
|
111
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
107
112
|
await (0, vitest_1.expect)(service.login({ email: "none@example.com", password: "Secret123" })).rejects.toBeInstanceOf(common_1.UnauthorizedException);
|
|
108
113
|
});
|
|
109
114
|
(0, vitest_1.it)("blocks login when account is inactive or missing approvals", async () => {
|
|
110
|
-
const { service,
|
|
111
|
-
const inactive = { ...
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
const { service, appUsersRepo, securityUsersRepo } = setup();
|
|
116
|
+
const inactive = { ...makeSecurityUser(), isActive: false };
|
|
117
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
118
|
+
securityUsersRepo.findOne.mockResolvedValue(inactive);
|
|
119
|
+
await (0, vitest_1.expect)(service.login({ email: "x@example.com", password: "Secret123" })).rejects.toThrow("Account is inactive");
|
|
120
|
+
securityUsersRepo.findOne.mockResolvedValue({
|
|
121
|
+
...makeSecurityUser(),
|
|
116
122
|
emailVerifiedAt: null,
|
|
117
123
|
});
|
|
118
124
|
await (0, vitest_1.expect)(service.login({ email: "x@example.com", password: "Secret123" })).rejects.toThrow("Email verification required");
|
|
119
|
-
|
|
120
|
-
...
|
|
125
|
+
securityUsersRepo.findOne.mockResolvedValue({
|
|
126
|
+
...makeSecurityUser(),
|
|
121
127
|
adminApprovedAt: null,
|
|
122
128
|
});
|
|
123
129
|
await (0, vitest_1.expect)(service.login({ email: "x@example.com", password: "Secret123" })).rejects.toThrow("Admin approval required");
|
|
124
130
|
});
|
|
125
131
|
(0, vitest_1.it)("refreshes and revokes tokens", async () => {
|
|
126
|
-
const { service,
|
|
132
|
+
const { service, appUsersRepo, securityUsersRepo, refreshTokenRepo, userRolesRepo, rolesRepo, } = setup();
|
|
127
133
|
refreshTokenRepo.find.mockResolvedValue([
|
|
128
134
|
{
|
|
129
135
|
id: "rt1",
|
|
@@ -132,7 +138,8 @@ const setup = () => {
|
|
|
132
138
|
expiresAt: new Date(Date.now() + 60_000),
|
|
133
139
|
},
|
|
134
140
|
]);
|
|
135
|
-
|
|
141
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
142
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
136
143
|
userRolesRepo.find.mockResolvedValue([]);
|
|
137
144
|
rolesRepo.find.mockResolvedValue([]);
|
|
138
145
|
const result = await service.refresh("token-bytes");
|
|
@@ -144,7 +151,7 @@ const setup = () => {
|
|
|
144
151
|
});
|
|
145
152
|
});
|
|
146
153
|
(0, vitest_1.it)("rejects invalid refresh token and missing refresh user", async () => {
|
|
147
|
-
const { service, refreshTokenRepo,
|
|
154
|
+
const { service, refreshTokenRepo, appUsersRepo } = setup();
|
|
148
155
|
refreshTokenRepo.find.mockResolvedValue([]);
|
|
149
156
|
await (0, vitest_1.expect)(service.refresh("bad-token")).rejects.toThrow("Invalid refresh token");
|
|
150
157
|
refreshTokenRepo.find.mockResolvedValue([
|
|
@@ -155,18 +162,18 @@ const setup = () => {
|
|
|
155
162
|
expiresAt: new Date(Date.now() + 60_000),
|
|
156
163
|
},
|
|
157
164
|
]);
|
|
158
|
-
|
|
165
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
159
166
|
await (0, vitest_1.expect)(service.refresh("token-bytes")).rejects.toThrow("User not found");
|
|
160
167
|
});
|
|
161
168
|
(0, vitest_1.it)("changes password", async () => {
|
|
162
|
-
const { service,
|
|
163
|
-
|
|
169
|
+
const { service, securityUsersRepo } = setup();
|
|
170
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
164
171
|
await (0, vitest_1.expect)(service.changePassword({
|
|
165
172
|
userId: "user-1",
|
|
166
173
|
currentPassword: "Secret123",
|
|
167
174
|
newPassword: "NewPass1",
|
|
168
175
|
})).resolves.toEqual({ success: true });
|
|
169
|
-
|
|
176
|
+
securityUsersRepo.findOne.mockResolvedValue(null);
|
|
170
177
|
await (0, vitest_1.expect)(service.changePassword({
|
|
171
178
|
userId: "missing",
|
|
172
179
|
currentPassword: "Secret123",
|
|
@@ -174,8 +181,9 @@ const setup = () => {
|
|
|
174
181
|
})).rejects.toThrow("User not found");
|
|
175
182
|
});
|
|
176
183
|
(0, vitest_1.it)("handles forgot/reset password flow", async () => {
|
|
177
|
-
const { service,
|
|
178
|
-
|
|
184
|
+
const { service, appUsersRepo, securityUsersRepo, passwordResetRepo, notifier, } = setup();
|
|
185
|
+
appUsersRepo.findOne.mockResolvedValue(makeUser());
|
|
186
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
179
187
|
passwordResetRepo.findOne.mockResolvedValue({
|
|
180
188
|
id: "pr1",
|
|
181
189
|
userId: "user-1",
|
|
@@ -193,8 +201,8 @@ const setup = () => {
|
|
|
193
201
|
});
|
|
194
202
|
});
|
|
195
203
|
(0, vitest_1.it)("returns success for unknown forgot email and rejects bad reset token", async () => {
|
|
196
|
-
const { service,
|
|
197
|
-
|
|
204
|
+
const { service, appUsersRepo, passwordResetRepo } = setup();
|
|
205
|
+
appUsersRepo.findOne.mockResolvedValue(null);
|
|
198
206
|
await (0, vitest_1.expect)(service.requestForgotPassword("none@example.com")).resolves.toEqual({
|
|
199
207
|
success: true,
|
|
200
208
|
});
|
|
@@ -210,10 +218,8 @@ const setup = () => {
|
|
|
210
218
|
await (0, vitest_1.expect)(service.resetPassword("bad", "x")).rejects.toThrow("Invalid password reset token");
|
|
211
219
|
});
|
|
212
220
|
(0, vitest_1.it)("verifies email token and reads user roles", async () => {
|
|
213
|
-
const { service,
|
|
214
|
-
|
|
215
|
-
.mockResolvedValueOnce(makeUser())
|
|
216
|
-
.mockResolvedValueOnce(makeUser());
|
|
221
|
+
const { service, securityUsersRepo, userRolesRepo, rolesRepo } = setup();
|
|
222
|
+
securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
|
|
217
223
|
userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
|
|
218
224
|
rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "coach" }]);
|
|
219
225
|
await (0, vitest_1.expect)(service.verifyEmailByToken("token-bytes")).resolves.toEqual({
|
|
@@ -225,8 +231,8 @@ const setup = () => {
|
|
|
225
231
|
});
|
|
226
232
|
});
|
|
227
233
|
(0, vitest_1.it)("rejects invalid email verification token", async () => {
|
|
228
|
-
const { service,
|
|
229
|
-
|
|
234
|
+
const { service, securityUsersRepo } = setup();
|
|
235
|
+
securityUsersRepo.findOne.mockResolvedValue(null);
|
|
230
236
|
await (0, vitest_1.expect)(service.verifyEmailByToken("missing")).rejects.toThrow("Invalid verification token");
|
|
231
237
|
});
|
|
232
238
|
(0, vitest_1.it)("handles refresh with expired matching token", async () => {
|
|
@@ -15,6 +15,7 @@ const security_admin_guard_1 = require("./security-admin.guard");
|
|
|
15
15
|
const security_jwt_guard_1 = require("./security-jwt.guard");
|
|
16
16
|
const app_user_entity_1 = require("./entities/app-user.entity");
|
|
17
17
|
const security_role_entity_1 = require("./entities/security-role.entity");
|
|
18
|
+
const security_user_entity_1 = require("./entities/security-user.entity");
|
|
18
19
|
const security_user_role_entity_1 = require("./entities/security-user-role.entity");
|
|
19
20
|
const security_workflows_controller_1 = require("./security-workflows.controller");
|
|
20
21
|
const security_workflows_service_1 = require("./security-workflows.service");
|
|
@@ -34,6 +35,7 @@ let SecurityWorkflowsModule = SecurityWorkflowsModule_1 = class SecurityWorkflow
|
|
|
34
35
|
imports: [
|
|
35
36
|
typeorm_1.TypeOrmModule.forFeature([
|
|
36
37
|
app_user_entity_1.AppUserEntity,
|
|
38
|
+
security_user_entity_1.SecurityUserEntity,
|
|
37
39
|
security_role_entity_1.SecurityRoleEntity,
|
|
38
40
|
security_user_role_entity_1.SecurityUserRoleEntity,
|
|
39
41
|
]),
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { Repository } from "typeorm";
|
|
2
2
|
import { AppUserEntity } from "./entities/app-user.entity";
|
|
3
3
|
import { SecurityRoleEntity } from "./entities/security-role.entity";
|
|
4
|
+
import { SecurityUserEntity } from "./entities/security-user.entity";
|
|
4
5
|
import { SecurityUserRoleEntity } from "./entities/security-user-role.entity";
|
|
5
6
|
import { SecurityWorkflowNotifier } from "./contracts";
|
|
6
7
|
export declare class SecurityWorkflowsService {
|
|
7
|
-
private readonly
|
|
8
|
+
private readonly appUsersRepo;
|
|
9
|
+
private readonly securityUsersRepo;
|
|
8
10
|
private readonly rolesRepo;
|
|
9
11
|
private readonly userRolesRepo;
|
|
10
12
|
private readonly notifier;
|
|
11
|
-
constructor(
|
|
13
|
+
constructor(appUsersRepo: Repository<AppUserEntity>, securityUsersRepo: Repository<SecurityUserEntity>, rolesRepo: Repository<SecurityRoleEntity>, userRolesRepo: Repository<SecurityUserRoleEntity>, notifier: SecurityWorkflowNotifier);
|
|
12
14
|
markEmailVerifiedAndNotifyAdmins(userId: string): Promise<{
|
|
13
15
|
success: true;
|
|
14
16
|
notified: false;
|