@flusys/nestjs-auth 1.1.0-beta → 1.1.0

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 (94) hide show
  1. package/README.md +633 -468
  2. package/cjs/config/auth.constants.js +8 -0
  3. package/cjs/controllers/authentication.controller.js +175 -167
  4. package/cjs/controllers/branch.controller.js +44 -1
  5. package/cjs/controllers/company-selection.controller.js +27 -20
  6. package/cjs/controllers/company.controller.js +44 -1
  7. package/cjs/controllers/email-verification.controller.js +220 -0
  8. package/cjs/controllers/index.js +1 -0
  9. package/cjs/controllers/user-permission.controller.js +82 -154
  10. package/cjs/controllers/user.controller.js +68 -15
  11. package/cjs/docs/auth-swagger.config.js +14 -7
  12. package/cjs/dtos/authentication.dto.js +16 -43
  13. package/cjs/dtos/index.js +0 -1
  14. package/cjs/dtos/user-permission.dto.js +28 -0
  15. package/cjs/dtos/user.dto.js +19 -4
  16. package/cjs/entities/index.js +16 -17
  17. package/cjs/entities/user-company-permission.entity.js +3 -9
  18. package/cjs/interceptors/clear-token.interceptor.js +10 -23
  19. package/cjs/interceptors/set-token.interceptor.js +15 -55
  20. package/cjs/interfaces/auth-email-provider.interface.js +1 -1
  21. package/cjs/modules/auth.module.js +79 -154
  22. package/cjs/services/auth-config.service.js +33 -35
  23. package/cjs/services/auth-datasource.provider.js +18 -42
  24. package/cjs/services/auth-email.service.js +89 -85
  25. package/cjs/services/authentication.service.js +248 -414
  26. package/cjs/services/branch.service.js +9 -4
  27. package/cjs/services/company-selection-session.service.js +1 -1
  28. package/cjs/services/user-permission.service.js +43 -73
  29. package/cjs/services/user.service.js +78 -64
  30. package/cjs/strategies/jwt.strategy.js +44 -44
  31. package/cjs/validators/password.validator.js +45 -0
  32. package/config/auth.constants.d.ts +2 -0
  33. package/controllers/authentication.controller.d.ts +4 -10
  34. package/controllers/company-selection.controller.d.ts +6 -2
  35. package/controllers/email-verification.controller.d.ts +20 -0
  36. package/controllers/index.d.ts +1 -0
  37. package/controllers/user-permission.controller.d.ts +5 -4
  38. package/controllers/user.controller.d.ts +6 -0
  39. package/dtos/authentication.dto.d.ts +0 -4
  40. package/dtos/company-branch.dto.d.ts +0 -1
  41. package/dtos/company.dto.d.ts +0 -1
  42. package/dtos/index.d.ts +0 -1
  43. package/dtos/user-permission.dto.d.ts +4 -0
  44. package/dtos/user.dto.d.ts +1 -3
  45. package/entities/index.d.ts +5 -10
  46. package/entities/user-company-permission.entity.d.ts +1 -3
  47. package/fesm/config/auth.constants.js +6 -13
  48. package/fesm/controllers/authentication.controller.js +178 -170
  49. package/fesm/controllers/branch.controller.js +45 -2
  50. package/fesm/controllers/company-selection.controller.js +28 -21
  51. package/fesm/controllers/company.controller.js +45 -2
  52. package/fesm/controllers/email-verification.controller.js +210 -0
  53. package/fesm/controllers/index.js +1 -0
  54. package/fesm/controllers/user-permission.controller.js +84 -156
  55. package/fesm/controllers/user.controller.js +71 -18
  56. package/fesm/docs/auth-swagger.config.js +14 -7
  57. package/fesm/dtos/authentication.dto.js +17 -44
  58. package/fesm/dtos/index.js +0 -1
  59. package/fesm/dtos/user-permission.dto.js +28 -0
  60. package/fesm/dtos/user.dto.js +20 -5
  61. package/fesm/entities/index.js +17 -9
  62. package/fesm/entities/user-company-permission.entity.js +3 -9
  63. package/fesm/interceptors/clear-token.interceptor.js +9 -22
  64. package/fesm/interceptors/set-token.interceptor.js +14 -54
  65. package/fesm/interfaces/auth-email-provider.interface.js +1 -1
  66. package/fesm/modules/auth.module.js +82 -157
  67. package/fesm/services/auth-config.service.js +34 -36
  68. package/fesm/services/auth-datasource.provider.js +19 -43
  69. package/fesm/services/auth-email.service.js +90 -86
  70. package/fesm/services/authentication.service.js +248 -414
  71. package/fesm/services/branch.service.js +9 -4
  72. package/fesm/services/company-selection-session.service.js +1 -1
  73. package/fesm/services/user-permission.service.js +44 -74
  74. package/fesm/services/user.service.js +79 -65
  75. package/fesm/strategies/jwt.strategy.js +44 -44
  76. package/fesm/validators/password.validator.js +48 -0
  77. package/interceptors/clear-token.interceptor.d.ts +0 -1
  78. package/interceptors/set-token.interceptor.d.ts +1 -1
  79. package/interfaces/authentication.interface.d.ts +0 -4
  80. package/interfaces/user-enricher.interface.d.ts +1 -4
  81. package/modules/auth.module.d.ts +9 -4
  82. package/package.json +4 -4
  83. package/services/auth-config.service.d.ts +7 -8
  84. package/services/auth-datasource.provider.d.ts +3 -4
  85. package/services/auth-email.service.d.ts +8 -4
  86. package/services/authentication.service.d.ts +16 -16
  87. package/services/company-selection-session.service.d.ts +1 -1
  88. package/services/user-permission.service.d.ts +6 -9
  89. package/services/user.service.d.ts +3 -0
  90. package/strategies/jwt.strategy.d.ts +4 -1
  91. package/validators/password.validator.d.ts +10 -0
  92. package/cjs/dtos/user-company-permission.dto.js +0 -110
  93. package/dtos/user-company-permission.dto.d.ts +0 -10
  94. package/fesm/dtos/user-company-permission.dto.js +0 -98
@@ -25,52 +25,77 @@ function _ts_param(paramIndex, decorator) {
25
25
  decorator(target, key, paramIndex);
26
26
  };
27
27
  }
28
- import { BadRequestException, Inject, Injectable, Logger, Optional, Scope } from '@nestjs/common';
28
+ import { BadRequestException, Inject, Injectable, InternalServerErrorException, Logger, Optional, Scope } from '@nestjs/common';
29
29
  import * as bcrypt from 'bcrypt';
30
30
  import * as crypto from 'crypto';
31
31
  import { LessThan } from 'typeorm';
32
- import { AuthDataSourceProvider } from './auth-datasource.provider';
32
+ import { BCRYPT_SALT_ROUNDS } from '../config';
33
33
  import { PendingRegistration } from '../entities/pending-registration.entity';
34
34
  import { TokenType, UserToken } from '../entities/user-token.entity';
35
35
  import { User } from '../entities/user.entity';
36
36
  import { AUTH_EMAIL_PROVIDER, IAuthEmailProvider } from '../interfaces';
37
- import { BCRYPT_SALT_ROUNDS } from '../config';
37
+ import { AuthDataSourceProvider } from './auth-datasource.provider';
38
38
  export class AuthEmailService {
39
- async getUserRepository() {
40
- if (this.dataSourceProvider) {
41
- return this.dataSourceProvider.getRepository(User);
42
- }
43
- throw new Error('User repository not available');
39
+ isEmailEnabled() {
40
+ return !!this.emailProvider;
44
41
  }
45
- async getUserTokenRepository() {
46
- if (this.dataSourceProvider) {
47
- return this.dataSourceProvider.getRepository(UserToken);
42
+ // ============================================
43
+ // Private Helpers
44
+ // ============================================
45
+ async getRepository(entity) {
46
+ if (!this.dataSourceProvider) {
47
+ throw new InternalServerErrorException(`${String(entity)} repository not available`);
48
48
  }
49
- throw new Error('UserToken repository not available');
49
+ return this.dataSourceProvider.getRepository(entity);
50
+ }
51
+ generateToken() {
52
+ return crypto.randomBytes(32).toString('hex');
50
53
  }
51
- async getPendingRegistrationRepository() {
52
- if (this.dataSourceProvider) {
53
- return this.dataSourceProvider.getRepository(PendingRegistration);
54
+ /** Hash token for secure storage - prevents token theft if database is compromised */ hashToken(token) {
55
+ return crypto.createHash('sha256').update(token).digest('hex');
56
+ }
57
+ getErrorMessage(error) {
58
+ return error instanceof Error ? error.message : 'Unknown error';
59
+ }
60
+ assertEmailEnabled() {
61
+ if (!this.emailProvider) {
62
+ throw new BadRequestException('Email service is not configured');
54
63
  }
55
- throw new Error('PendingRegistration repository not available');
56
64
  }
57
- isEmailEnabled() {
58
- return !!this.emailProvider;
65
+ async sendVerificationEmailSafe(email, token, verifyUrl, logContext) {
66
+ try {
67
+ await this.emailProvider.sendVerificationEmail(email, token, verifyUrl);
68
+ this.logger.log(`${logContext} verification email sent to: ${email}`);
69
+ } catch (error) {
70
+ this.logger.error(`Failed to send verification email: ${this.getErrorMessage(error)}`);
71
+ throw new BadRequestException('Failed to send verification email. Please try again.');
72
+ }
59
73
  }
60
- generateToken() {
61
- return crypto.randomBytes(32).toString('hex');
74
+ validateTokenNotExpiredOrUsed(token, expiredMessage, usedMessage) {
75
+ if (token.expiresAt < new Date()) {
76
+ throw new BadRequestException(expiredMessage);
77
+ }
78
+ if (usedMessage && token.usedAt) {
79
+ throw new BadRequestException(usedMessage);
80
+ }
62
81
  }
63
82
  // ============================================
64
83
  // Pending Registration (New Users)
65
84
  // ============================================
66
- /**
67
- * Create pending registration - stores data until email verified
68
- */ async createPendingRegistration(data) {
69
- if (!this.emailProvider) {
70
- throw new BadRequestException('Email service is not configured');
71
- }
72
- const userRepository = await this.getUserRepository();
73
- const pendingRepository = await this.getPendingRegistrationRepository();
85
+ async hasPendingRegistration(email) {
86
+ if (!this.isEmailEnabled()) return false;
87
+ const pendingRepository = await this.getRepository(PendingRegistration);
88
+ const pending = await pendingRepository.findOne({
89
+ where: {
90
+ email
91
+ }
92
+ });
93
+ return !!pending && pending.expiresAt > new Date();
94
+ }
95
+ async createPendingRegistration(data) {
96
+ this.assertEmailEnabled();
97
+ const userRepository = await this.getRepository(User);
98
+ const pendingRepository = await this.getRepository(PendingRegistration);
74
99
  // Check if user already exists
75
100
  const existingUser = await userRepository.findOne({
76
101
  where: {
@@ -84,8 +109,9 @@ export class AuthEmailService {
84
109
  await pendingRepository.delete({
85
110
  email: data.email
86
111
  });
87
- // Create pending registration
112
+ // Create pending registration with hashed token for secure storage
88
113
  const token = this.generateToken();
114
+ const tokenHash = this.hashToken(token);
89
115
  const passwordHash = await bcrypt.hash(data.password, BCRYPT_SALT_ROUNDS);
90
116
  const expiresAt = new Date(Date.now() + this.EMAIL_VERIFICATION_EXPIRY);
91
117
  const pending = pendingRepository.create({
@@ -93,20 +119,14 @@ export class AuthEmailService {
93
119
  name: data.name ?? null,
94
120
  phone: data.phone ?? null,
95
121
  passwordHash,
96
- token,
122
+ token: tokenHash,
97
123
  expiresAt,
98
124
  companyData: data.companyData ?? null
99
125
  });
100
126
  await pendingRepository.save(pending);
101
- // Send verification email
127
+ // Send verification email with raw token (user receives this)
102
128
  const verifyUrl = `${data.verifyUrlBase}?token=${token}`;
103
- try {
104
- await this.emailProvider.sendVerificationEmail(data.email, token, verifyUrl);
105
- this.logger.log(`Registration verification email sent to: ${data.email}`);
106
- } catch (error) {
107
- this.logger.error(`Failed to send verification email: ${error.message}`);
108
- throw new BadRequestException('Failed to send verification email. Please try again.');
109
- }
129
+ await this.sendVerificationEmailSafe(data.email, token, verifyUrl, 'Registration');
110
130
  return {
111
131
  success: true,
112
132
  message: 'Please check your email to verify your registration.'
@@ -118,11 +138,9 @@ export class AuthEmailService {
118
138
  /**
119
139
  * Request password reset - sends reset email
120
140
  */ async forgotPassword(email, resetUrlBase) {
121
- if (!this.emailProvider) {
122
- throw new BadRequestException('Email service is not configured');
123
- }
124
- const userRepository = await this.getUserRepository();
125
- const tokenRepository = await this.getUserTokenRepository();
141
+ this.assertEmailEnabled();
142
+ const userRepository = await this.getRepository(User);
143
+ const tokenRepository = await this.getRepository(UserToken);
126
144
  const user = await userRepository.findOne({
127
145
  where: {
128
146
  email
@@ -141,22 +159,24 @@ export class AuthEmailService {
141
159
  userId: user.id,
142
160
  type: TokenType.PASSWORD_RESET
143
161
  });
144
- // Create new token
162
+ // Create new token with hashed storage
145
163
  const token = this.generateToken();
164
+ const tokenHash = this.hashToken(token);
146
165
  const userToken = tokenRepository.create({
147
166
  userId: user.id,
148
167
  type: TokenType.PASSWORD_RESET,
149
- token,
168
+ token: tokenHash,
150
169
  expiresAt: new Date(Date.now() + this.PASSWORD_RESET_EXPIRY)
151
170
  });
152
171
  await tokenRepository.save(userToken);
153
- // Send email
172
+ // Send email with raw token
154
173
  const resetUrl = `${resetUrlBase}?token=${token}`;
155
174
  try {
156
175
  await this.emailProvider.sendPasswordResetEmail(email, token, resetUrl);
157
176
  this.logger.log(`Password reset email sent to: ${email}`);
158
177
  } catch (error) {
159
- this.logger.error(`Failed to send password reset email: ${error.message}`);
178
+ this.logger.error(`Failed to send password reset email: ${this.getErrorMessage(error)}`);
179
+ throw new BadRequestException('Failed to process request. Please try again later.');
160
180
  }
161
181
  return {
162
182
  success: true,
@@ -166,26 +186,20 @@ export class AuthEmailService {
166
186
  /**
167
187
  * Reset password using token
168
188
  */ async resetPassword(token, newPassword) {
169
- const userRepository = await this.getUserRepository();
170
- const tokenRepository = await this.getUserTokenRepository();
189
+ const userRepository = await this.getRepository(User);
190
+ const tokenRepository = await this.getRepository(UserToken);
191
+ // Hash the incoming token to compare with stored hash
192
+ const tokenHash = this.hashToken(token);
171
193
  const userToken = await tokenRepository.findOne({
172
194
  where: {
173
- token,
195
+ token: tokenHash,
174
196
  type: TokenType.PASSWORD_RESET
175
197
  }
176
198
  });
177
199
  if (!userToken) {
178
200
  throw new BadRequestException('Invalid or expired reset token');
179
201
  }
180
- if (userToken.expiresAt < new Date()) {
181
- await tokenRepository.delete({
182
- id: userToken.id
183
- });
184
- throw new BadRequestException('Reset token has expired. Please request a new one.');
185
- }
186
- if (userToken.usedAt) {
187
- throw new BadRequestException('This reset token has already been used.');
188
- }
202
+ this.validateTokenNotExpiredOrUsed(userToken, 'Reset token has expired. Please request a new one.', 'This reset token has already been used.');
189
203
  const user = await userRepository.findOne({
190
204
  where: {
191
205
  id: userToken.userId
@@ -213,13 +227,15 @@ export class AuthEmailService {
213
227
  * Verify email using token
214
228
  * Handles both pending registration and existing user verification
215
229
  */ async verifyEmail(token) {
216
- const pendingRepository = await this.getPendingRegistrationRepository();
217
- const userRepository = await this.getUserRepository();
218
- const tokenRepository = await this.getUserTokenRepository();
230
+ const pendingRepository = await this.getRepository(PendingRegistration);
231
+ const userRepository = await this.getRepository(User);
232
+ const tokenRepository = await this.getRepository(UserToken);
233
+ // Hash the incoming token to compare with stored hash
234
+ const tokenHash = this.hashToken(token);
219
235
  // Check for pending registration first
220
236
  const pending = await pendingRepository.findOne({
221
237
  where: {
222
- token
238
+ token: tokenHash
223
239
  }
224
240
  });
225
241
  if (pending) {
@@ -241,7 +257,7 @@ export class AuthEmailService {
241
257
  // Check for existing user email verification token
242
258
  const userToken = await tokenRepository.findOne({
243
259
  where: {
244
- token,
260
+ token: tokenHash,
245
261
  type: TokenType.EMAIL_VERIFICATION
246
262
  }
247
263
  });
@@ -283,12 +299,10 @@ export class AuthEmailService {
283
299
  /**
284
300
  * Resend verification email - handles both pending registrations and existing users
285
301
  */ async resendVerificationEmail(email, verifyUrlBase) {
286
- if (!this.emailProvider) {
287
- throw new BadRequestException('Email service is not configured');
288
- }
289
- const pendingRepository = await this.getPendingRegistrationRepository();
290
- const userRepository = await this.getUserRepository();
291
- const tokenRepository = await this.getUserTokenRepository();
302
+ this.assertEmailEnabled();
303
+ const pendingRepository = await this.getRepository(PendingRegistration);
304
+ const userRepository = await this.getRepository(User);
305
+ const tokenRepository = await this.getRepository(UserToken);
292
306
  // Check pending registration first
293
307
  const pending = await pendingRepository.findOne({
294
308
  where: {
@@ -297,16 +311,11 @@ export class AuthEmailService {
297
311
  });
298
312
  if (pending) {
299
313
  const token = this.generateToken();
300
- pending.token = token;
314
+ pending.token = this.hashToken(token);
301
315
  pending.expiresAt = new Date(Date.now() + this.EMAIL_VERIFICATION_EXPIRY);
302
316
  await pendingRepository.save(pending);
303
317
  const verifyUrl = `${verifyUrlBase}?token=${token}`;
304
- try {
305
- await this.emailProvider.sendVerificationEmail(email, token, verifyUrl);
306
- this.logger.log(`Re-sent registration verification email to: ${email}`);
307
- } catch (error) {
308
- this.logger.error(`Failed to resend verification email: ${error.message}`);
309
- }
318
+ await this.sendVerificationEmailSafe(email, token, verifyUrl, 'Re-sent registration');
310
319
  return {
311
320
  success: true,
312
321
  message: 'If the email requires verification, a link has been sent.'
@@ -333,17 +342,12 @@ export class AuthEmailService {
333
342
  const userToken = tokenRepository.create({
334
343
  userId: user.id,
335
344
  type: TokenType.EMAIL_VERIFICATION,
336
- token,
345
+ token: this.hashToken(token),
337
346
  expiresAt: new Date(Date.now() + this.EMAIL_VERIFICATION_EXPIRY)
338
347
  });
339
348
  await tokenRepository.save(userToken);
340
349
  const verifyUrl = `${verifyUrlBase}?token=${token}`;
341
- try {
342
- await this.emailProvider.sendVerificationEmail(user.email, token, verifyUrl);
343
- this.logger.log(`Verification email sent to: ${user.email}`);
344
- } catch (error) {
345
- this.logger.error(`Failed to send verification email: ${error.message}`);
346
- }
350
+ await this.sendVerificationEmailSafe(user.email, token, verifyUrl, 'Verification');
347
351
  return {
348
352
  success: true,
349
353
  message: 'If the email requires verification, a link has been sent.'
@@ -355,8 +359,8 @@ export class AuthEmailService {
355
359
  /**
356
360
  * Clean up expired tokens and pending registrations
357
361
  */ async cleanupExpired() {
358
- const tokenRepository = await this.getUserTokenRepository();
359
- const pendingRepository = await this.getPendingRegistrationRepository();
362
+ const tokenRepository = await this.getRepository(UserToken);
363
+ const pendingRepository = await this.getRepository(PendingRegistration);
360
364
  const now = new Date();
361
365
  const tokenResult = await tokenRepository.delete({
362
366
  expiresAt: LessThan(now)