@scryan7371/sdr-security 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +216 -13
  2. package/dist/api/contracts.d.ts +12 -2
  3. package/dist/api/index.d.ts +1 -0
  4. package/dist/api/index.js +1 -0
  5. package/dist/api/migrations/1739500000000-create-security-identity.d.ts +1 -1
  6. package/dist/api/migrations/1739500000000-create-security-identity.js +9 -35
  7. package/dist/api/migrations/1739510000000-create-security-roles.d.ts +1 -1
  8. package/dist/api/migrations/1739510000000-create-security-roles.js +1 -67
  9. package/dist/api/migrations/1739515000000-create-security-user-roles.d.ts +9 -0
  10. package/dist/api/migrations/1739515000000-create-security-user-roles.js +39 -0
  11. package/dist/api/migrations/1739520000000-create-password-reset-tokens.d.ts +9 -0
  12. package/dist/api/migrations/1739520000000-create-password-reset-tokens.js +42 -0
  13. package/dist/api/migrations/1739530000000-create-security-user.d.ts +9 -0
  14. package/dist/api/migrations/1739530000000-create-security-user.js +41 -0
  15. package/dist/api/migrations/index.d.ts +4 -2
  16. package/dist/api/migrations/index.js +10 -4
  17. package/dist/api/migrations/migrations.test.d.ts +1 -0
  18. package/dist/api/migrations/migrations.test.js +88 -0
  19. package/dist/api/notification-workflows.d.ts +31 -0
  20. package/dist/api/notification-workflows.js +22 -0
  21. package/dist/api/notification-workflows.test.d.ts +1 -0
  22. package/dist/api/notification-workflows.test.js +63 -0
  23. package/dist/api/validation.test.d.ts +1 -0
  24. package/dist/api/validation.test.js +20 -0
  25. package/dist/app/client.d.ts +17 -4
  26. package/dist/app/client.js +38 -11
  27. package/dist/app/client.test.d.ts +1 -0
  28. package/dist/app/client.test.js +130 -0
  29. package/dist/index.test.d.ts +1 -0
  30. package/dist/index.test.js +10 -0
  31. package/dist/integration/database.integration.test.d.ts +1 -0
  32. package/dist/integration/database.integration.test.js +158 -0
  33. package/dist/nest/contracts.d.ts +21 -0
  34. package/dist/nest/contracts.js +2 -0
  35. package/dist/nest/dto/auth.dto.d.ts +25 -0
  36. package/dist/nest/dto/auth.dto.js +89 -0
  37. package/dist/nest/dto/workflows.dto.d.ts +16 -0
  38. package/dist/nest/dto/workflows.dto.js +58 -0
  39. package/dist/nest/entities/app-user.entity.d.ts +4 -0
  40. package/dist/nest/entities/app-user.entity.js +29 -0
  41. package/dist/nest/entities/password-reset-token.entity.d.ts +8 -0
  42. package/dist/nest/entities/password-reset-token.entity.js +49 -0
  43. package/dist/nest/entities/refresh-token.entity.d.ts +8 -0
  44. package/dist/nest/entities/refresh-token.entity.js +49 -0
  45. package/dist/nest/entities/security-role.entity.d.ts +6 -0
  46. package/dist/nest/entities/security-role.entity.js +39 -0
  47. package/dist/nest/entities/security-user-role.entity.d.ts +5 -0
  48. package/dist/nest/entities/security-user-role.entity.js +34 -0
  49. package/dist/nest/entities/security-user.entity.d.ts +9 -0
  50. package/dist/nest/entities/security-user.entity.js +54 -0
  51. package/dist/nest/index.d.ts +19 -0
  52. package/dist/nest/index.js +35 -0
  53. package/dist/nest/index.test.d.ts +1 -0
  54. package/dist/nest/index.test.js +14 -0
  55. package/dist/nest/security-admin.guard.d.ts +4 -0
  56. package/dist/nest/security-admin.guard.js +25 -0
  57. package/dist/nest/security-admin.guard.test.d.ts +1 -0
  58. package/dist/nest/security-admin.guard.test.js +24 -0
  59. package/dist/nest/security-auth.constants.d.ts +1 -0
  60. package/dist/nest/security-auth.constants.js +4 -0
  61. package/dist/nest/security-auth.controller.d.ts +51 -0
  62. package/dist/nest/security-auth.controller.js +177 -0
  63. package/dist/nest/security-auth.controller.test.d.ts +1 -0
  64. package/dist/nest/security-auth.controller.test.js +87 -0
  65. package/dist/nest/security-auth.module.d.ts +9 -0
  66. package/dist/nest/security-auth.module.js +70 -0
  67. package/dist/nest/security-auth.options.d.ts +8 -0
  68. package/dist/nest/security-auth.options.js +2 -0
  69. package/dist/nest/security-auth.service.d.ts +60 -0
  70. package/dist/nest/security-auth.service.js +299 -0
  71. package/dist/nest/security-auth.service.test.d.ts +1 -0
  72. package/dist/nest/security-auth.service.test.js +249 -0
  73. package/dist/nest/security-jwt.guard.d.ts +7 -0
  74. package/dist/nest/security-jwt.guard.js +46 -0
  75. package/dist/nest/security-jwt.guard.test.d.ts +1 -0
  76. package/dist/nest/security-jwt.guard.test.js +51 -0
  77. package/dist/nest/security-modules.test.d.ts +1 -0
  78. package/dist/nest/security-modules.test.js +61 -0
  79. package/dist/nest/security-workflows.controller.d.ts +72 -0
  80. package/dist/nest/security-workflows.controller.js +187 -0
  81. package/dist/nest/security-workflows.controller.test.d.ts +1 -0
  82. package/dist/nest/security-workflows.controller.test.js +87 -0
  83. package/dist/nest/security-workflows.module.d.ts +9 -0
  84. package/dist/nest/security-workflows.module.js +61 -0
  85. package/dist/nest/security-workflows.service.d.ts +69 -0
  86. package/dist/nest/security-workflows.service.js +203 -0
  87. package/dist/nest/security-workflows.service.test.d.ts +1 -0
  88. package/dist/nest/security-workflows.service.test.js +178 -0
  89. package/dist/nest/swagger.d.ts +2 -0
  90. package/dist/nest/swagger.js +16 -0
  91. package/dist/nest/swagger.test.d.ts +1 -0
  92. package/dist/nest/swagger.test.js +21 -0
  93. package/dist/nest/tokens.d.ts +1 -0
  94. package/dist/nest/tokens.js +4 -0
  95. package/package.json +45 -4
  96. package/src/api/contracts.ts +11 -2
  97. package/src/api/index.ts +1 -0
  98. package/src/api/migrations/1739500000000-create-security-identity.ts +11 -50
  99. package/src/api/migrations/1739510000000-create-security-roles.ts +2 -89
  100. package/src/api/migrations/1739515000000-create-security-user-roles.ts +49 -0
  101. package/src/api/migrations/1739520000000-create-password-reset-tokens.ts +57 -0
  102. package/src/api/migrations/1739530000000-create-security-user.ts +51 -0
  103. package/src/api/migrations/index.ts +9 -3
  104. package/src/api/migrations/migrations.test.ts +145 -0
  105. package/src/api/notification-workflows.test.ts +78 -0
  106. package/src/api/notification-workflows.ts +38 -0
  107. package/src/api/validation.test.ts +21 -0
  108. package/src/app/client.test.ts +157 -0
  109. package/src/app/client.ts +74 -18
  110. package/src/index.test.ts +9 -0
  111. package/src/integration/database.integration.test.ts +205 -0
  112. package/src/nest/contracts.ts +20 -0
  113. package/src/nest/dto/auth.dto.ts +48 -0
  114. package/src/nest/dto/workflows.dto.ts +29 -0
  115. package/src/nest/entities/app-user.entity.ts +10 -0
  116. package/src/nest/entities/password-reset-token.entity.ts +27 -0
  117. package/src/nest/entities/refresh-token.entity.ts +22 -0
  118. package/src/nest/entities/security-role.entity.ts +16 -0
  119. package/src/nest/entities/security-user-role.entity.ts +13 -0
  120. package/src/nest/entities/security-user.entity.ts +25 -0
  121. package/src/nest/index.test.ts +20 -0
  122. package/src/nest/index.ts +19 -0
  123. package/src/nest/security-admin.guard.test.ts +31 -0
  124. package/src/nest/security-admin.guard.ts +21 -0
  125. package/src/nest/security-auth.constants.ts +1 -0
  126. package/src/nest/security-auth.controller.test.ts +128 -0
  127. package/src/nest/security-auth.controller.ts +148 -0
  128. package/src/nest/security-auth.module.ts +65 -0
  129. package/src/nest/security-auth.options.ts +8 -0
  130. package/src/nest/security-auth.service.test.ts +368 -0
  131. package/src/nest/security-auth.service.ts +356 -0
  132. package/src/nest/security-jwt.guard.test.ts +65 -0
  133. package/src/nest/security-jwt.guard.ts +47 -0
  134. package/src/nest/security-modules.test.ts +79 -0
  135. package/src/nest/security-workflows.controller.test.ts +119 -0
  136. package/src/nest/security-workflows.controller.ts +149 -0
  137. package/src/nest/security-workflows.module.ts +56 -0
  138. package/src/nest/security-workflows.service.test.ts +238 -0
  139. package/src/nest/security-workflows.service.ts +220 -0
  140. package/src/nest/swagger.test.ts +27 -0
  141. package/src/nest/swagger.ts +18 -0
  142. package/src/nest/tokens.ts +1 -0
  143. package/dist/api/migrations/1739490000000-add-google-subject-to-user.d.ts +0 -5
  144. package/dist/api/migrations/1739490000000-add-google-subject-to-user.js +0 -14
  145. package/src/api/migrations/1739490000000-add-google-subject-to-user.ts +0 -12
@@ -0,0 +1,60 @@
1
+ import { Repository } from "typeorm";
2
+ import { AuthResponse, RegisterResponse } from "../api/contracts";
3
+ import { SecurityAuthModuleOptions } from "./security-auth.options";
4
+ import { AppUserEntity } from "./entities/app-user.entity";
5
+ import { PasswordResetTokenEntity } from "./entities/password-reset-token.entity";
6
+ import { RefreshTokenEntity } from "./entities/refresh-token.entity";
7
+ import { SecurityRoleEntity } from "./entities/security-role.entity";
8
+ import { SecurityUserEntity } from "./entities/security-user.entity";
9
+ import { SecurityUserRoleEntity } from "./entities/security-user-role.entity";
10
+ import { SecurityWorkflowNotifier } from "./contracts";
11
+ export declare class SecurityAuthService {
12
+ private readonly appUsersRepo;
13
+ private readonly securityUsersRepo;
14
+ private readonly refreshTokenRepo;
15
+ private readonly passwordResetRepo;
16
+ private readonly rolesRepo;
17
+ private readonly userRolesRepo;
18
+ private readonly options;
19
+ private readonly notifier;
20
+ constructor(appUsersRepo: Repository<AppUserEntity>, securityUsersRepo: Repository<SecurityUserEntity>, refreshTokenRepo: Repository<RefreshTokenEntity>, passwordResetRepo: Repository<PasswordResetTokenEntity>, rolesRepo: Repository<SecurityRoleEntity>, userRolesRepo: Repository<SecurityUserRoleEntity>, options: SecurityAuthModuleOptions, notifier: SecurityWorkflowNotifier);
21
+ register(params: {
22
+ email: string;
23
+ password: string;
24
+ }): Promise<RegisterResponse>;
25
+ login(params: {
26
+ email: string;
27
+ password: string;
28
+ }): Promise<AuthResponse>;
29
+ refresh(refreshToken: string): Promise<AuthResponse>;
30
+ logout(refreshToken?: string): Promise<{
31
+ success: true;
32
+ }>;
33
+ changePassword(params: {
34
+ userId: string;
35
+ currentPassword: string;
36
+ newPassword: string;
37
+ }): Promise<{
38
+ success: true;
39
+ }>;
40
+ requestForgotPassword(emailInput: string): Promise<{
41
+ success: true;
42
+ }>;
43
+ resetPassword(token: string, newPassword: string): Promise<{
44
+ success: true;
45
+ }>;
46
+ verifyEmailByToken(token: string): Promise<{
47
+ success: true;
48
+ }>;
49
+ getMyRoles(userId: string): Promise<{
50
+ userId: string;
51
+ roles: string[];
52
+ }>;
53
+ getUserIdByVerificationToken(token: string): Promise<string | null>;
54
+ private assertCanAuthenticate;
55
+ private issueTokens;
56
+ private createEmailVerificationToken;
57
+ private findValidRefreshToken;
58
+ private getUserRoleKeys;
59
+ private toSafeUser;
60
+ }
@@ -0,0 +1,299 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SecurityAuthService = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const crypto_1 = require("crypto");
18
+ const bcryptjs_1 = require("bcryptjs");
19
+ const jsonwebtoken_1 = require("jsonwebtoken");
20
+ const typeorm_1 = require("@nestjs/typeorm");
21
+ const typeorm_2 = require("typeorm");
22
+ const roles_1 = require("../api/roles");
23
+ const validation_1 = require("../api/validation");
24
+ const security_auth_constants_1 = require("./security-auth.constants");
25
+ const app_user_entity_1 = require("./entities/app-user.entity");
26
+ const password_reset_token_entity_1 = require("./entities/password-reset-token.entity");
27
+ const refresh_token_entity_1 = require("./entities/refresh-token.entity");
28
+ const security_role_entity_1 = require("./entities/security-role.entity");
29
+ const security_user_entity_1 = require("./entities/security-user.entity");
30
+ const security_user_role_entity_1 = require("./entities/security-user-role.entity");
31
+ const tokens_1 = require("./tokens");
32
+ const EMAIL_TOKEN_BYTES = 24;
33
+ const REFRESH_TOKEN_BYTES = 32;
34
+ const PASSWORD_ROUNDS = 12;
35
+ let SecurityAuthService = class SecurityAuthService {
36
+ appUsersRepo;
37
+ securityUsersRepo;
38
+ refreshTokenRepo;
39
+ passwordResetRepo;
40
+ rolesRepo;
41
+ userRolesRepo;
42
+ options;
43
+ notifier;
44
+ constructor(appUsersRepo, securityUsersRepo, refreshTokenRepo, passwordResetRepo, rolesRepo, userRolesRepo, options, notifier) {
45
+ this.appUsersRepo = appUsersRepo;
46
+ this.securityUsersRepo = securityUsersRepo;
47
+ this.refreshTokenRepo = refreshTokenRepo;
48
+ this.passwordResetRepo = passwordResetRepo;
49
+ this.rolesRepo = rolesRepo;
50
+ this.userRolesRepo = userRolesRepo;
51
+ this.options = options;
52
+ this.notifier = notifier;
53
+ }
54
+ async register(params) {
55
+ const email = (0, validation_1.sanitizeEmail)(params.email);
56
+ const existing = await this.appUsersRepo.findOne({ where: { email } });
57
+ if (existing) {
58
+ throw new common_1.BadRequestException("Email already in use");
59
+ }
60
+ const appUser = await this.appUsersRepo.save(this.appUsersRepo.create({
61
+ email,
62
+ }));
63
+ const securityUser = await this.securityUsersRepo.save(this.securityUsersRepo.create({
64
+ userId: appUser.id,
65
+ passwordHash: await (0, bcryptjs_1.hash)(params.password, PASSWORD_ROUNDS),
66
+ emailVerifiedAt: null,
67
+ emailVerificationToken: null,
68
+ adminApprovedAt: null,
69
+ isActive: true,
70
+ }));
71
+ const verificationToken = await this.createEmailVerificationToken(appUser.id);
72
+ if (this.notifier.sendEmailVerification) {
73
+ await this.notifier.sendEmailVerification({
74
+ email: appUser.email,
75
+ token: verificationToken,
76
+ });
77
+ }
78
+ return {
79
+ success: true,
80
+ user: await this.toSafeUser(appUser, securityUser),
81
+ debugToken: verificationToken,
82
+ };
83
+ }
84
+ async login(params) {
85
+ const email = (0, validation_1.sanitizeEmail)(params.email);
86
+ const appUser = await this.appUsersRepo.findOne({ where: { email } });
87
+ if (!appUser) {
88
+ throw new common_1.UnauthorizedException("Invalid credentials");
89
+ }
90
+ const securityUser = await this.securityUsersRepo.findOne({
91
+ where: { userId: appUser.id },
92
+ });
93
+ if (!securityUser) {
94
+ throw new common_1.UnauthorizedException("Invalid credentials");
95
+ }
96
+ const ok = await (0, bcryptjs_1.compare)(params.password, securityUser.passwordHash);
97
+ if (!ok) {
98
+ throw new common_1.UnauthorizedException("Invalid credentials");
99
+ }
100
+ this.assertCanAuthenticate(securityUser);
101
+ return this.issueTokens(appUser, securityUser);
102
+ }
103
+ async refresh(refreshToken) {
104
+ const record = await this.findValidRefreshToken(refreshToken);
105
+ if (!record) {
106
+ throw new common_1.UnauthorizedException("Invalid refresh token");
107
+ }
108
+ await this.refreshTokenRepo.update({ id: record.id }, { revokedAt: new Date() });
109
+ const appUser = await this.appUsersRepo.findOne({
110
+ where: { id: record.userId ?? "" },
111
+ });
112
+ if (!appUser) {
113
+ throw new common_1.UnauthorizedException("User not found");
114
+ }
115
+ const securityUser = await this.securityUsersRepo.findOne({
116
+ where: { userId: appUser.id },
117
+ });
118
+ if (!securityUser) {
119
+ throw new common_1.UnauthorizedException("User not found");
120
+ }
121
+ this.assertCanAuthenticate(securityUser);
122
+ return this.issueTokens(appUser, securityUser);
123
+ }
124
+ async logout(refreshToken) {
125
+ if (!refreshToken) {
126
+ return { success: true };
127
+ }
128
+ const record = await this.findValidRefreshToken(refreshToken, false);
129
+ if (record && !record.revokedAt) {
130
+ await this.refreshTokenRepo.update({ id: record.id }, { revokedAt: new Date() });
131
+ }
132
+ return { success: true };
133
+ }
134
+ async changePassword(params) {
135
+ const securityUser = await this.securityUsersRepo.findOne({
136
+ where: { userId: params.userId },
137
+ });
138
+ if (!securityUser) {
139
+ throw new common_1.BadRequestException("User not found");
140
+ }
141
+ const ok = await (0, bcryptjs_1.compare)(params.currentPassword, securityUser.passwordHash);
142
+ if (!ok) {
143
+ throw new common_1.UnauthorizedException("Current password is incorrect");
144
+ }
145
+ await this.securityUsersRepo.update({ userId: securityUser.userId }, { passwordHash: await (0, bcryptjs_1.hash)(params.newPassword, PASSWORD_ROUNDS) });
146
+ return { success: true };
147
+ }
148
+ async requestForgotPassword(emailInput) {
149
+ const email = (0, validation_1.sanitizeEmail)(emailInput);
150
+ const appUser = await this.appUsersRepo.findOne({ where: { email } });
151
+ if (!appUser) {
152
+ return { success: true };
153
+ }
154
+ const securityUser = await this.securityUsersRepo.findOne({
155
+ where: { userId: appUser.id },
156
+ });
157
+ if (!securityUser) {
158
+ return { success: true };
159
+ }
160
+ const token = (0, crypto_1.randomBytes)(EMAIL_TOKEN_BYTES).toString("hex");
161
+ const expiresAt = new Date(Date.now() +
162
+ (this.options.passwordResetTokenExpiresInMinutes ?? 30) * 60_000);
163
+ await this.passwordResetRepo.save(this.passwordResetRepo.create({
164
+ userId: appUser.id,
165
+ token,
166
+ expiresAt,
167
+ usedAt: null,
168
+ }));
169
+ if (this.notifier.sendPasswordReset) {
170
+ await this.notifier.sendPasswordReset({ email: appUser.email, token });
171
+ }
172
+ return { success: true };
173
+ }
174
+ async resetPassword(token, newPassword) {
175
+ const reset = await this.passwordResetRepo.findOne({ where: { token } });
176
+ if (!reset || reset.usedAt || reset.expiresAt.getTime() <= Date.now()) {
177
+ throw new common_1.BadRequestException("Invalid password reset token");
178
+ }
179
+ await this.securityUsersRepo.update({ userId: reset.userId }, { passwordHash: await (0, bcryptjs_1.hash)(newPassword, PASSWORD_ROUNDS) });
180
+ await this.passwordResetRepo.update({ id: reset.id }, { usedAt: new Date() });
181
+ return { success: true };
182
+ }
183
+ async verifyEmailByToken(token) {
184
+ const user = await this.securityUsersRepo.findOne({
185
+ where: { emailVerificationToken: token },
186
+ });
187
+ if (!user) {
188
+ throw new common_1.BadRequestException("Invalid verification token");
189
+ }
190
+ await this.securityUsersRepo.update({ userId: user.userId }, { emailVerifiedAt: new Date(), emailVerificationToken: null });
191
+ return { success: true };
192
+ }
193
+ async getMyRoles(userId) {
194
+ return { userId, roles: await this.getUserRoleKeys(userId) };
195
+ }
196
+ async getUserIdByVerificationToken(token) {
197
+ const user = await this.securityUsersRepo.findOne({
198
+ where: { emailVerificationToken: token },
199
+ });
200
+ return user?.userId ?? null;
201
+ }
202
+ assertCanAuthenticate(user) {
203
+ if (!user.isActive) {
204
+ throw new common_1.UnauthorizedException("Account is inactive");
205
+ }
206
+ if ((this.options.requireEmailVerification ?? true) &&
207
+ !user.emailVerifiedAt) {
208
+ throw new common_1.UnauthorizedException("Email verification required");
209
+ }
210
+ if ((this.options.requireAdminApproval ?? true) && !user.adminApprovedAt) {
211
+ throw new common_1.UnauthorizedException("Admin approval required");
212
+ }
213
+ }
214
+ async issueTokens(appUser, securityUser) {
215
+ const roles = await this.getUserRoleKeys(appUser.id);
216
+ const accessTokenExpiresIn = this.options.accessTokenExpiresIn ?? "15m";
217
+ const accessToken = (0, jsonwebtoken_1.sign)({ sub: appUser.id, email: appUser.email, roles }, this.options.jwtSecret, { expiresIn: accessTokenExpiresIn });
218
+ const refreshToken = (0, crypto_1.randomBytes)(REFRESH_TOKEN_BYTES).toString("hex");
219
+ const refreshTokenHash = await (0, bcryptjs_1.hash)(refreshToken, PASSWORD_ROUNDS);
220
+ const refreshTokenExpiresAt = new Date(Date.now() +
221
+ (this.options.refreshTokenExpiresInDays ?? 30) * 24 * 60 * 60 * 1000);
222
+ await this.refreshTokenRepo.save(this.refreshTokenRepo.create({
223
+ id: (0, crypto_1.randomUUID)(),
224
+ userId: appUser.id,
225
+ tokenHash: refreshTokenHash,
226
+ expiresAt: refreshTokenExpiresAt,
227
+ revokedAt: null,
228
+ }));
229
+ return {
230
+ accessToken,
231
+ accessTokenExpiresIn,
232
+ refreshToken,
233
+ refreshTokenExpiresAt,
234
+ user: await this.toSafeUser(appUser, securityUser),
235
+ };
236
+ }
237
+ async createEmailVerificationToken(userId) {
238
+ const token = (0, crypto_1.randomBytes)(EMAIL_TOKEN_BYTES).toString("hex");
239
+ await this.securityUsersRepo.update({ userId }, { emailVerificationToken: token });
240
+ return token;
241
+ }
242
+ async findValidRefreshToken(token, onlyUnexpired = true) {
243
+ const candidates = await this.refreshTokenRepo.find({
244
+ where: { revokedAt: (0, typeorm_2.IsNull)() },
245
+ order: { createdAt: "DESC" },
246
+ take: 50,
247
+ });
248
+ for (const candidate of candidates) {
249
+ const match = await (0, bcryptjs_1.compare)(token, candidate.tokenHash);
250
+ if (!match) {
251
+ continue;
252
+ }
253
+ if (onlyUnexpired && candidate.expiresAt.getTime() <= Date.now()) {
254
+ return null;
255
+ }
256
+ return candidate;
257
+ }
258
+ return null;
259
+ }
260
+ async getUserRoleKeys(userId) {
261
+ const assignments = await this.userRolesRepo.find({ where: { userId } });
262
+ if (assignments.length === 0) {
263
+ return [];
264
+ }
265
+ const roleIds = assignments.map((assignment) => assignment.roleId);
266
+ const roles = await this.rolesRepo.find({ where: { id: (0, typeorm_2.In)(roleIds) } });
267
+ return roles.map((role) => (0, roles_1.normalizeRoleName)(role.roleKey)).sort();
268
+ }
269
+ async toSafeUser(appUser, securityUser) {
270
+ return {
271
+ id: appUser.id,
272
+ email: appUser.email,
273
+ phone: null,
274
+ roles: await this.getUserRoleKeys(appUser.id),
275
+ emailVerifiedAt: securityUser.emailVerifiedAt,
276
+ phoneVerifiedAt: null,
277
+ adminApprovedAt: securityUser.adminApprovedAt,
278
+ isActive: securityUser.isActive,
279
+ };
280
+ }
281
+ };
282
+ exports.SecurityAuthService = SecurityAuthService;
283
+ exports.SecurityAuthService = SecurityAuthService = __decorate([
284
+ (0, common_1.Injectable)(),
285
+ __param(0, (0, typeorm_1.InjectRepository)(app_user_entity_1.AppUserEntity)),
286
+ __param(1, (0, typeorm_1.InjectRepository)(security_user_entity_1.SecurityUserEntity)),
287
+ __param(2, (0, typeorm_1.InjectRepository)(refresh_token_entity_1.RefreshTokenEntity)),
288
+ __param(3, (0, typeorm_1.InjectRepository)(password_reset_token_entity_1.PasswordResetTokenEntity)),
289
+ __param(4, (0, typeorm_1.InjectRepository)(security_role_entity_1.SecurityRoleEntity)),
290
+ __param(5, (0, typeorm_1.InjectRepository)(security_user_role_entity_1.SecurityUserRoleEntity)),
291
+ __param(6, (0, common_1.Inject)(security_auth_constants_1.SECURITY_AUTH_OPTIONS)),
292
+ __param(7, (0, common_1.Inject)(tokens_1.SECURITY_WORKFLOW_NOTIFIER)),
293
+ __metadata("design:paramtypes", [typeorm_2.Repository,
294
+ typeorm_2.Repository,
295
+ typeorm_2.Repository,
296
+ typeorm_2.Repository,
297
+ typeorm_2.Repository,
298
+ typeorm_2.Repository, Object, Object])
299
+ ], SecurityAuthService);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const common_1 = require("@nestjs/common");
4
+ const vitest_1 = require("vitest");
5
+ vitest_1.vi.mock("bcryptjs", () => ({
6
+ hash: vitest_1.vi.fn(async (value) => `hashed:${value}`),
7
+ compare: vitest_1.vi.fn(async (plain, hashed) => hashed === `hashed:${plain}`),
8
+ }));
9
+ vitest_1.vi.mock("crypto", () => ({
10
+ randomBytes: vitest_1.vi.fn(() => ({ toString: () => "token-bytes" })),
11
+ randomUUID: vitest_1.vi.fn(() => "uuid-1"),
12
+ }));
13
+ vitest_1.vi.mock("jsonwebtoken", () => ({
14
+ sign: vitest_1.vi.fn(() => "signed-access-token"),
15
+ }));
16
+ const bcryptjs_1 = require("bcryptjs");
17
+ const jsonwebtoken_1 = require("jsonwebtoken");
18
+ const security_auth_service_1 = require("./security-auth.service");
19
+ const makeRepo = () => ({
20
+ findOne: vitest_1.vi.fn(),
21
+ save: vitest_1.vi.fn(async (value) => value),
22
+ create: vitest_1.vi.fn((value) => value),
23
+ update: vitest_1.vi.fn(async () => ({ affected: 1 })),
24
+ find: vitest_1.vi.fn(async () => []),
25
+ });
26
+ const makeNotifier = () => ({
27
+ sendEmailVerification: vitest_1.vi.fn(async () => undefined),
28
+ sendPasswordReset: vitest_1.vi.fn(async () => undefined),
29
+ sendAdminsUserEmailVerified: vitest_1.vi.fn(async () => undefined),
30
+ sendUserAccountApproved: vitest_1.vi.fn(async () => undefined),
31
+ });
32
+ const makeUser = () => ({
33
+ id: "user-1",
34
+ email: "user@example.com",
35
+ });
36
+ const makeSecurityUser = () => ({
37
+ userId: "user-1",
38
+ passwordHash: "hashed:Secret123",
39
+ emailVerifiedAt: new Date("2026-01-01T00:00:00.000Z"),
40
+ emailVerificationToken: null,
41
+ adminApprovedAt: new Date("2026-01-01T00:00:00.000Z"),
42
+ isActive: true,
43
+ });
44
+ const setup = () => {
45
+ const appUsersRepo = makeRepo();
46
+ const securityUsersRepo = makeRepo();
47
+ const refreshTokenRepo = makeRepo();
48
+ const passwordResetRepo = makeRepo();
49
+ const rolesRepo = makeRepo();
50
+ const userRolesRepo = makeRepo();
51
+ const notifier = makeNotifier();
52
+ const service = new security_auth_service_1.SecurityAuthService(appUsersRepo, securityUsersRepo, refreshTokenRepo, passwordResetRepo, rolesRepo, userRolesRepo, {
53
+ jwtSecret: "secret",
54
+ accessTokenExpiresIn: "15m",
55
+ refreshTokenExpiresInDays: 30,
56
+ requireEmailVerification: true,
57
+ requireAdminApproval: true,
58
+ passwordResetTokenExpiresInMinutes: 30,
59
+ }, notifier);
60
+ return {
61
+ service,
62
+ appUsersRepo,
63
+ securityUsersRepo,
64
+ refreshTokenRepo,
65
+ passwordResetRepo,
66
+ rolesRepo,
67
+ userRolesRepo,
68
+ notifier,
69
+ };
70
+ };
71
+ (0, vitest_1.describe)("SecurityAuthService", () => {
72
+ (0, vitest_1.beforeEach)(() => {
73
+ vitest_1.vi.clearAllMocks();
74
+ });
75
+ (0, vitest_1.it)("registers user and sends verification", async () => {
76
+ const { service, appUsersRepo, securityUsersRepo, userRolesRepo, rolesRepo, notifier, } = setup();
77
+ appUsersRepo.findOne.mockResolvedValue(null);
78
+ appUsersRepo.save.mockResolvedValue(makeUser());
79
+ securityUsersRepo.save.mockResolvedValue(makeSecurityUser());
80
+ userRolesRepo.find.mockResolvedValue([]);
81
+ rolesRepo.find.mockResolvedValue([]);
82
+ const result = await service.register({
83
+ email: "USER@example.com",
84
+ password: "Secret123",
85
+ });
86
+ (0, vitest_1.expect)(result.success).toBe(true);
87
+ (0, vitest_1.expect)(appUsersRepo.create).toHaveBeenCalledWith(vitest_1.expect.objectContaining({ email: "user@example.com" }));
88
+ (0, vitest_1.expect)(notifier.sendEmailVerification).toHaveBeenCalled();
89
+ });
90
+ (0, vitest_1.it)("rejects duplicate email on register", async () => {
91
+ const { service, appUsersRepo } = setup();
92
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
93
+ await (0, vitest_1.expect)(service.register({ email: "user@example.com", password: "Secret123" })).rejects.toBeInstanceOf(common_1.BadRequestException);
94
+ });
95
+ (0, vitest_1.it)("handles login success and auth failures", async () => {
96
+ const { service, appUsersRepo, securityUsersRepo, userRolesRepo, rolesRepo, refreshTokenRepo, } = setup();
97
+ const user = makeUser();
98
+ appUsersRepo.findOne.mockResolvedValue(user);
99
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
100
+ userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
101
+ rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "admin" }]);
102
+ const auth = await service.login({
103
+ email: user.email,
104
+ password: "Secret123",
105
+ });
106
+ (0, vitest_1.expect)(auth.accessToken).toBe("signed-access-token");
107
+ (0, vitest_1.expect)(jsonwebtoken_1.sign).toHaveBeenCalled();
108
+ (0, vitest_1.expect)(refreshTokenRepo.save).toHaveBeenCalled();
109
+ appUsersRepo.findOne.mockResolvedValue(null);
110
+ await (0, vitest_1.expect)(service.login({ email: "none@example.com", password: "Secret123" })).rejects.toBeInstanceOf(common_1.UnauthorizedException);
111
+ });
112
+ (0, vitest_1.it)("blocks login when account is inactive or missing approvals", async () => {
113
+ const { service, appUsersRepo, securityUsersRepo } = setup();
114
+ const inactive = { ...makeSecurityUser(), isActive: false };
115
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
116
+ securityUsersRepo.findOne.mockResolvedValue(inactive);
117
+ await (0, vitest_1.expect)(service.login({ email: "x@example.com", password: "Secret123" })).rejects.toThrow("Account is inactive");
118
+ securityUsersRepo.findOne.mockResolvedValue({
119
+ ...makeSecurityUser(),
120
+ emailVerifiedAt: null,
121
+ });
122
+ await (0, vitest_1.expect)(service.login({ email: "x@example.com", password: "Secret123" })).rejects.toThrow("Email verification required");
123
+ securityUsersRepo.findOne.mockResolvedValue({
124
+ ...makeSecurityUser(),
125
+ adminApprovedAt: null,
126
+ });
127
+ await (0, vitest_1.expect)(service.login({ email: "x@example.com", password: "Secret123" })).rejects.toThrow("Admin approval required");
128
+ });
129
+ (0, vitest_1.it)("refreshes and revokes tokens", async () => {
130
+ const { service, appUsersRepo, securityUsersRepo, refreshTokenRepo, userRolesRepo, rolesRepo, } = setup();
131
+ refreshTokenRepo.find.mockResolvedValue([
132
+ {
133
+ id: "rt1",
134
+ userId: "user-1",
135
+ tokenHash: "hashed:token-bytes",
136
+ expiresAt: new Date(Date.now() + 60_000),
137
+ },
138
+ ]);
139
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
140
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
141
+ userRolesRepo.find.mockResolvedValue([]);
142
+ rolesRepo.find.mockResolvedValue([]);
143
+ const result = await service.refresh("token-bytes");
144
+ (0, vitest_1.expect)(result.accessToken).toBe("signed-access-token");
145
+ (0, vitest_1.expect)(refreshTokenRepo.update).toHaveBeenCalledWith({ id: "rt1" }, vitest_1.expect.objectContaining({ revokedAt: vitest_1.expect.any(Date) }));
146
+ await (0, vitest_1.expect)(service.logout()).resolves.toEqual({ success: true });
147
+ await (0, vitest_1.expect)(service.logout("token-bytes")).resolves.toEqual({
148
+ success: true,
149
+ });
150
+ });
151
+ (0, vitest_1.it)("rejects invalid refresh token and missing refresh user", async () => {
152
+ const { service, refreshTokenRepo, appUsersRepo } = setup();
153
+ refreshTokenRepo.find.mockResolvedValue([]);
154
+ await (0, vitest_1.expect)(service.refresh("bad-token")).rejects.toThrow("Invalid refresh token");
155
+ refreshTokenRepo.find.mockResolvedValue([
156
+ {
157
+ id: "rt1",
158
+ userId: "missing",
159
+ tokenHash: "hashed:token-bytes",
160
+ expiresAt: new Date(Date.now() + 60_000),
161
+ },
162
+ ]);
163
+ appUsersRepo.findOne.mockResolvedValue(null);
164
+ await (0, vitest_1.expect)(service.refresh("token-bytes")).rejects.toThrow("User not found");
165
+ });
166
+ (0, vitest_1.it)("changes password", async () => {
167
+ const { service, securityUsersRepo } = setup();
168
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
169
+ await (0, vitest_1.expect)(service.changePassword({
170
+ userId: "user-1",
171
+ currentPassword: "Secret123",
172
+ newPassword: "NewPass1",
173
+ })).resolves.toEqual({ success: true });
174
+ securityUsersRepo.findOne.mockResolvedValue(null);
175
+ await (0, vitest_1.expect)(service.changePassword({
176
+ userId: "missing",
177
+ currentPassword: "Secret123",
178
+ newPassword: "NewPass1",
179
+ })).rejects.toThrow("User not found");
180
+ });
181
+ (0, vitest_1.it)("handles forgot/reset password flow", async () => {
182
+ const { service, appUsersRepo, securityUsersRepo, passwordResetRepo, notifier, } = setup();
183
+ appUsersRepo.findOne.mockResolvedValue(makeUser());
184
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
185
+ passwordResetRepo.findOne.mockResolvedValue({
186
+ id: "pr1",
187
+ userId: "user-1",
188
+ token: "token-bytes",
189
+ usedAt: null,
190
+ expiresAt: new Date(Date.now() + 60_000),
191
+ });
192
+ await (0, vitest_1.expect)(service.requestForgotPassword("user@example.com")).resolves.toEqual({
193
+ success: true,
194
+ });
195
+ (0, vitest_1.expect)(passwordResetRepo.save).toHaveBeenCalled();
196
+ (0, vitest_1.expect)(notifier.sendPasswordReset).toHaveBeenCalled();
197
+ await (0, vitest_1.expect)(service.resetPassword("token-bytes", "NewPass1")).resolves.toEqual({
198
+ success: true,
199
+ });
200
+ });
201
+ (0, vitest_1.it)("returns success for unknown forgot email and rejects bad reset token", async () => {
202
+ const { service, appUsersRepo, passwordResetRepo } = setup();
203
+ appUsersRepo.findOne.mockResolvedValue(null);
204
+ await (0, vitest_1.expect)(service.requestForgotPassword("none@example.com")).resolves.toEqual({
205
+ success: true,
206
+ });
207
+ passwordResetRepo.findOne.mockResolvedValue(null);
208
+ await (0, vitest_1.expect)(service.resetPassword("bad", "x")).rejects.toThrow("Invalid password reset token");
209
+ passwordResetRepo.findOne.mockResolvedValue({
210
+ id: "pr1",
211
+ userId: "user-1",
212
+ token: "bad",
213
+ usedAt: new Date(),
214
+ expiresAt: new Date(Date.now() + 60_000),
215
+ });
216
+ await (0, vitest_1.expect)(service.resetPassword("bad", "x")).rejects.toThrow("Invalid password reset token");
217
+ });
218
+ (0, vitest_1.it)("verifies email token and reads user roles", async () => {
219
+ const { service, securityUsersRepo, userRolesRepo, rolesRepo } = setup();
220
+ securityUsersRepo.findOne.mockResolvedValue(makeSecurityUser());
221
+ userRolesRepo.find.mockResolvedValue([{ roleId: "r1" }]);
222
+ rolesRepo.find.mockResolvedValue([{ id: "r1", roleKey: "coach" }]);
223
+ await (0, vitest_1.expect)(service.verifyEmailByToken("token-bytes")).resolves.toEqual({
224
+ success: true,
225
+ });
226
+ await (0, vitest_1.expect)(service.getMyRoles("user-1")).resolves.toEqual({
227
+ userId: "user-1",
228
+ roles: ["COACH"],
229
+ });
230
+ });
231
+ (0, vitest_1.it)("rejects invalid email verification token", async () => {
232
+ const { service, securityUsersRepo } = setup();
233
+ securityUsersRepo.findOne.mockResolvedValue(null);
234
+ await (0, vitest_1.expect)(service.verifyEmailByToken("missing")).rejects.toThrow("Invalid verification token");
235
+ });
236
+ (0, vitest_1.it)("handles refresh with expired matching token", async () => {
237
+ const { service, refreshTokenRepo } = setup();
238
+ refreshTokenRepo.find.mockResolvedValue([
239
+ {
240
+ id: "rt1",
241
+ userId: "user-1",
242
+ tokenHash: "hashed:token-bytes",
243
+ expiresAt: new Date(Date.now() - 1),
244
+ },
245
+ ]);
246
+ await (0, vitest_1.expect)(service.refresh("token-bytes")).rejects.toThrow("Invalid refresh token");
247
+ (0, vitest_1.expect)(bcryptjs_1.compare).toHaveBeenCalled();
248
+ });
249
+ });
@@ -0,0 +1,7 @@
1
+ import { CanActivate, ExecutionContext } from "@nestjs/common";
2
+ import { SecurityAuthModuleOptions } from "./security-auth.options";
3
+ export declare class SecurityJwtGuard implements CanActivate {
4
+ private readonly options;
5
+ constructor(options: SecurityAuthModuleOptions);
6
+ canActivate(context: ExecutionContext): boolean;
7
+ }
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
12
+ return function (target, key) { decorator(target, key, paramIndex); }
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SecurityJwtGuard = void 0;
16
+ const common_1 = require("@nestjs/common");
17
+ const jsonwebtoken_1 = require("jsonwebtoken");
18
+ const security_auth_constants_1 = require("./security-auth.constants");
19
+ let SecurityJwtGuard = class SecurityJwtGuard {
20
+ options;
21
+ constructor(options) {
22
+ this.options = options;
23
+ }
24
+ canActivate(context) {
25
+ const request = context.switchToHttp().getRequest();
26
+ const header = request.headers?.authorization ?? request.headers?.Authorization;
27
+ if (!header || !header.startsWith("Bearer ")) {
28
+ throw new common_1.UnauthorizedException("Missing bearer token");
29
+ }
30
+ const token = header.slice("Bearer ".length);
31
+ try {
32
+ const payload = (0, jsonwebtoken_1.verify)(token, this.options.jwtSecret);
33
+ request.user = payload;
34
+ return true;
35
+ }
36
+ catch {
37
+ throw new common_1.UnauthorizedException("Invalid token");
38
+ }
39
+ }
40
+ };
41
+ exports.SecurityJwtGuard = SecurityJwtGuard;
42
+ exports.SecurityJwtGuard = SecurityJwtGuard = __decorate([
43
+ (0, common_1.Injectable)(),
44
+ __param(0, (0, common_1.Inject)(security_auth_constants_1.SECURITY_AUTH_OPTIONS)),
45
+ __metadata("design:paramtypes", [Object])
46
+ ], SecurityJwtGuard);
@@ -0,0 +1 @@
1
+ export {};