@flusys/nestjs-auth 1.1.0-beta → 2.0.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.
- package/README.md +590 -473
- package/cjs/config/auth.constants.js +8 -0
- package/cjs/controllers/authentication.controller.js +218 -39
- package/cjs/controllers/branch.controller.js +44 -1
- package/cjs/controllers/company-selection.controller.js +27 -20
- package/cjs/controllers/company.controller.js +44 -1
- package/cjs/controllers/user-permission.controller.js +82 -154
- package/cjs/controllers/user.controller.js +68 -15
- package/cjs/docs/auth-swagger.config.js +14 -7
- package/cjs/dtos/authentication.dto.js +16 -43
- package/cjs/dtos/index.js +0 -1
- package/cjs/dtos/user-permission.dto.js +28 -0
- package/cjs/dtos/user.dto.js +19 -4
- package/cjs/entities/user-company-permission.entity.js +0 -6
- package/cjs/interceptors/clear-token.interceptor.js +10 -23
- package/cjs/interceptors/set-token.interceptor.js +15 -55
- package/cjs/interfaces/auth-email-provider.interface.js +1 -1
- package/cjs/modules/auth.module.js +63 -147
- package/cjs/services/auth-config.service.js +22 -28
- package/cjs/services/auth-datasource.provider.js +9 -21
- package/cjs/services/auth-email.service.js +89 -85
- package/cjs/services/authentication.service.js +225 -396
- package/cjs/services/branch.service.js +9 -4
- package/cjs/services/company-selection-session.service.js +1 -1
- package/cjs/services/user-permission.service.js +43 -73
- package/cjs/services/user.service.js +78 -64
- package/cjs/strategies/jwt.strategy.js +44 -44
- package/cjs/validators/password.validator.js +45 -0
- package/config/auth.constants.d.ts +2 -0
- package/controllers/authentication.controller.d.ts +5 -3
- package/controllers/company-selection.controller.d.ts +6 -2
- package/controllers/user-permission.controller.d.ts +5 -4
- package/controllers/user.controller.d.ts +6 -0
- package/dtos/authentication.dto.d.ts +0 -4
- package/dtos/company-branch.dto.d.ts +0 -1
- package/dtos/company.dto.d.ts +0 -1
- package/dtos/index.d.ts +0 -1
- package/dtos/user-permission.dto.d.ts +4 -0
- package/dtos/user.dto.d.ts +1 -3
- package/entities/index.d.ts +3 -3
- package/entities/user-company-permission.entity.d.ts +0 -2
- package/fesm/config/auth.constants.js +6 -13
- package/fesm/controllers/authentication.controller.js +221 -42
- package/fesm/controllers/branch.controller.js +45 -2
- package/fesm/controllers/company-selection.controller.js +28 -21
- package/fesm/controllers/company.controller.js +45 -2
- package/fesm/controllers/user-permission.controller.js +84 -156
- package/fesm/controllers/user.controller.js +71 -18
- package/fesm/docs/auth-swagger.config.js +14 -7
- package/fesm/dtos/authentication.dto.js +17 -44
- package/fesm/dtos/index.js +0 -1
- package/fesm/dtos/user-permission.dto.js +28 -0
- package/fesm/dtos/user.dto.js +20 -5
- package/fesm/entities/user-company-permission.entity.js +0 -6
- package/fesm/interceptors/clear-token.interceptor.js +9 -22
- package/fesm/interceptors/set-token.interceptor.js +14 -54
- package/fesm/interfaces/auth-email-provider.interface.js +1 -1
- package/fesm/modules/auth.module.js +64 -148
- package/fesm/services/auth-config.service.js +23 -29
- package/fesm/services/auth-datasource.provider.js +10 -22
- package/fesm/services/auth-email.service.js +90 -86
- package/fesm/services/authentication.service.js +225 -396
- package/fesm/services/branch.service.js +9 -4
- package/fesm/services/company-selection-session.service.js +1 -1
- package/fesm/services/user-permission.service.js +44 -74
- package/fesm/services/user.service.js +79 -65
- package/fesm/strategies/jwt.strategy.js +44 -44
- package/fesm/validators/password.validator.js +48 -0
- package/interceptors/clear-token.interceptor.d.ts +0 -1
- package/interceptors/set-token.interceptor.d.ts +1 -1
- package/interfaces/authentication.interface.d.ts +0 -4
- package/interfaces/user-enricher.interface.d.ts +1 -4
- package/modules/auth.module.d.ts +6 -3
- package/package.json +4 -4
- package/services/auth-config.service.d.ts +6 -8
- package/services/auth-datasource.provider.d.ts +2 -4
- package/services/auth-email.service.d.ts +8 -4
- package/services/authentication.service.d.ts +15 -16
- package/services/company-selection-session.service.d.ts +1 -1
- package/services/user-permission.service.d.ts +6 -9
- package/services/user.service.d.ts +3 -0
- package/strategies/jwt.strategy.d.ts +4 -1
- package/validators/password.validator.d.ts +10 -0
- package/cjs/dtos/user-company-permission.dto.js +0 -110
- package/dtos/user-company-permission.dto.d.ts +0 -10
- 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 {
|
|
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 {
|
|
37
|
+
import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
38
38
|
export class AuthEmailService {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return this.dataSourceProvider.getRepository(User);
|
|
42
|
-
}
|
|
43
|
-
throw new Error('User repository not available');
|
|
39
|
+
isEmailEnabled() {
|
|
40
|
+
return !!this.emailProvider;
|
|
44
41
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
49
|
+
return this.dataSourceProvider.getRepository(entity);
|
|
50
|
+
}
|
|
51
|
+
generateToken() {
|
|
52
|
+
return crypto.randomBytes(32).toString('hex');
|
|
50
53
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
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.
|
|
170
|
-
const tokenRepository = await this.
|
|
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
|
-
|
|
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.
|
|
217
|
-
const userRepository = await this.
|
|
218
|
-
const tokenRepository = await this.
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
359
|
-
const pendingRepository = await this.
|
|
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)
|