@flusys/nestjs-auth 1.0.0-rc → 1.0.1
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 +530 -396
- package/cjs/controllers/authentication.controller.js +160 -28
- package/cjs/controllers/user-permission.controller.js +49 -136
- package/cjs/docs/auth-swagger.config.js +14 -7
- package/cjs/dtos/authentication.dto.js +0 -30
- package/cjs/dtos/index.js +0 -1
- package/cjs/entities/user-company-permission.entity.js +3 -3
- package/cjs/interceptors/clear-token.interceptor.js +8 -18
- package/cjs/interceptors/set-token.interceptor.js +8 -55
- package/cjs/interfaces/auth-email-provider.interface.js +1 -1
- package/cjs/modules/auth.module.js +63 -129
- package/cjs/services/auth-config.service.js +6 -29
- package/cjs/services/auth-datasource.provider.js +8 -20
- package/cjs/services/auth-email.service.js +72 -87
- package/cjs/services/authentication.service.js +211 -355
- package/cjs/services/branch.service.js +9 -5
- package/cjs/services/user-permission.service.js +43 -71
- package/cjs/services/user.service.js +18 -47
- package/cjs/strategies/jwt.strategy.js +44 -44
- package/controllers/authentication.controller.d.ts +5 -3
- package/controllers/user-permission.controller.d.ts +5 -4
- package/dtos/authentication.dto.d.ts +0 -4
- package/dtos/index.d.ts +0 -1
- package/entities/index.d.ts +3 -3
- package/entities/user-company-permission.entity.d.ts +1 -1
- package/fesm/controllers/authentication.controller.js +162 -30
- package/fesm/controllers/user-permission.controller.js +50 -137
- package/fesm/docs/auth-swagger.config.js +14 -7
- package/fesm/dtos/authentication.dto.js +0 -30
- package/fesm/dtos/index.js +0 -1
- package/fesm/entities/user-company-permission.entity.js +3 -3
- package/fesm/interceptors/clear-token.interceptor.js +8 -18
- package/fesm/interceptors/set-token.interceptor.js +8 -55
- package/fesm/interfaces/auth-email-provider.interface.js +1 -1
- package/fesm/modules/auth.module.js +63 -129
- package/fesm/services/auth-config.service.js +6 -29
- package/fesm/services/auth-datasource.provider.js +8 -20
- package/fesm/services/auth-email.service.js +72 -87
- package/fesm/services/authentication.service.js +211 -355
- package/fesm/services/branch.service.js +9 -5
- package/fesm/services/user-permission.service.js +44 -72
- package/fesm/services/user.service.js +18 -47
- package/fesm/strategies/jwt.strategy.js +44 -44
- package/interceptors/clear-token.interceptor.d.ts +0 -1
- package/interceptors/set-token.interceptor.d.ts +1 -2
- package/interfaces/authentication.interface.d.ts +0 -4
- package/modules/auth.module.d.ts +6 -2
- package/package.json +3 -3
- package/services/auth-config.service.d.ts +2 -7
- package/services/auth-datasource.provider.d.ts +2 -4
- package/services/auth-email.service.d.ts +7 -4
- package/services/authentication.service.d.ts +14 -8
- package/services/user-permission.service.d.ts +6 -8
- package/services/user.service.d.ts +1 -1
- package/strategies/jwt.strategy.d.ts +4 -1
- package/cjs/dtos/user-company-permission.dto.js +0 -110
- package/cjs/guards/auth-throttler.guard.js +0 -53
- package/dtos/user-company-permission.dto.d.ts +0 -10
- package/fesm/dtos/user-company-permission.dto.js +0 -98
- package/fesm/guards/auth-throttler.guard.js +0 -43
- package/guards/auth-throttler.guard.d.ts +0 -7
|
@@ -29,51 +29,73 @@ import { BadRequestException, Inject, Injectable, InternalServerErrorException,
|
|
|
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
|
-
async getUserRepository() {
|
|
40
|
-
if (this.dataSourceProvider) {
|
|
41
|
-
return this.dataSourceProvider.getRepository(User);
|
|
42
|
-
}
|
|
43
|
-
throw new InternalServerErrorException('User repository not available');
|
|
44
|
-
}
|
|
45
|
-
async getUserTokenRepository() {
|
|
46
|
-
if (this.dataSourceProvider) {
|
|
47
|
-
return this.dataSourceProvider.getRepository(UserToken);
|
|
48
|
-
}
|
|
49
|
-
throw new InternalServerErrorException('UserToken repository not available');
|
|
50
|
-
}
|
|
51
|
-
async getPendingRegistrationRepository() {
|
|
52
|
-
if (this.dataSourceProvider) {
|
|
53
|
-
return this.dataSourceProvider.getRepository(PendingRegistration);
|
|
54
|
-
}
|
|
55
|
-
throw new InternalServerErrorException('PendingRegistration repository not available');
|
|
56
|
-
}
|
|
57
39
|
isEmailEnabled() {
|
|
58
40
|
return !!this.emailProvider;
|
|
59
41
|
}
|
|
42
|
+
// ============================================
|
|
43
|
+
// Private Helpers
|
|
44
|
+
// ============================================
|
|
45
|
+
async getRepository(entity) {
|
|
46
|
+
if (!this.dataSourceProvider) {
|
|
47
|
+
throw new InternalServerErrorException(`${String(entity)} repository not available`);
|
|
48
|
+
}
|
|
49
|
+
return this.dataSourceProvider.getRepository(entity);
|
|
50
|
+
}
|
|
60
51
|
generateToken() {
|
|
61
52
|
return crypto.randomBytes(32).toString('hex');
|
|
62
53
|
}
|
|
63
54
|
/** Hash token for secure storage - prevents token theft if database is compromised */ hashToken(token) {
|
|
64
55
|
return crypto.createHash('sha256').update(token).digest('hex');
|
|
65
56
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* Create pending registration - stores data until email verified
|
|
71
|
-
*/ async createPendingRegistration(data) {
|
|
57
|
+
getErrorMessage(error) {
|
|
58
|
+
return error instanceof Error ? error.message : 'Unknown error';
|
|
59
|
+
}
|
|
60
|
+
assertEmailEnabled() {
|
|
72
61
|
if (!this.emailProvider) {
|
|
73
62
|
throw new BadRequestException('Email service is not configured');
|
|
74
63
|
}
|
|
75
|
-
|
|
76
|
-
|
|
64
|
+
}
|
|
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
|
+
}
|
|
73
|
+
}
|
|
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
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// ============================================
|
|
83
|
+
// Pending Registration (New Users)
|
|
84
|
+
// ============================================
|
|
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);
|
|
77
99
|
// Check if user already exists
|
|
78
100
|
const existingUser = await userRepository.findOne({
|
|
79
101
|
where: {
|
|
@@ -104,14 +126,7 @@ export class AuthEmailService {
|
|
|
104
126
|
await pendingRepository.save(pending);
|
|
105
127
|
// Send verification email with raw token (user receives this)
|
|
106
128
|
const verifyUrl = `${data.verifyUrlBase}?token=${token}`;
|
|
107
|
-
|
|
108
|
-
await this.emailProvider.sendVerificationEmail(data.email, token, verifyUrl);
|
|
109
|
-
this.logger.log(`Registration verification email sent to: ${data.email}`);
|
|
110
|
-
} catch (error) {
|
|
111
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
112
|
-
this.logger.error(`Failed to send verification email: ${errorMessage}`);
|
|
113
|
-
throw new BadRequestException('Failed to send verification email. Please try again.');
|
|
114
|
-
}
|
|
129
|
+
await this.sendVerificationEmailSafe(data.email, token, verifyUrl, 'Registration');
|
|
115
130
|
return {
|
|
116
131
|
success: true,
|
|
117
132
|
message: 'Please check your email to verify your registration.'
|
|
@@ -123,11 +138,9 @@ export class AuthEmailService {
|
|
|
123
138
|
/**
|
|
124
139
|
* Request password reset - sends reset email
|
|
125
140
|
*/ async forgotPassword(email, resetUrlBase) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const userRepository = await this.getUserRepository();
|
|
130
|
-
const tokenRepository = await this.getUserTokenRepository();
|
|
141
|
+
this.assertEmailEnabled();
|
|
142
|
+
const userRepository = await this.getRepository(User);
|
|
143
|
+
const tokenRepository = await this.getRepository(UserToken);
|
|
131
144
|
const user = await userRepository.findOne({
|
|
132
145
|
where: {
|
|
133
146
|
email
|
|
@@ -162,9 +175,7 @@ export class AuthEmailService {
|
|
|
162
175
|
await this.emailProvider.sendPasswordResetEmail(email, token, resetUrl);
|
|
163
176
|
this.logger.log(`Password reset email sent to: ${email}`);
|
|
164
177
|
} catch (error) {
|
|
165
|
-
|
|
166
|
-
this.logger.error(`Failed to send password reset email: ${errorMessage}`);
|
|
167
|
-
// Don't silently fail - but still use same message to prevent enumeration
|
|
178
|
+
this.logger.error(`Failed to send password reset email: ${this.getErrorMessage(error)}`);
|
|
168
179
|
throw new BadRequestException('Failed to process request. Please try again later.');
|
|
169
180
|
}
|
|
170
181
|
return {
|
|
@@ -175,8 +186,8 @@ export class AuthEmailService {
|
|
|
175
186
|
/**
|
|
176
187
|
* Reset password using token
|
|
177
188
|
*/ async resetPassword(token, newPassword) {
|
|
178
|
-
const userRepository = await this.
|
|
179
|
-
const tokenRepository = await this.
|
|
189
|
+
const userRepository = await this.getRepository(User);
|
|
190
|
+
const tokenRepository = await this.getRepository(UserToken);
|
|
180
191
|
// Hash the incoming token to compare with stored hash
|
|
181
192
|
const tokenHash = this.hashToken(token);
|
|
182
193
|
const userToken = await tokenRepository.findOne({
|
|
@@ -188,15 +199,7 @@ export class AuthEmailService {
|
|
|
188
199
|
if (!userToken) {
|
|
189
200
|
throw new BadRequestException('Invalid or expired reset token');
|
|
190
201
|
}
|
|
191
|
-
|
|
192
|
-
await tokenRepository.delete({
|
|
193
|
-
id: userToken.id
|
|
194
|
-
});
|
|
195
|
-
throw new BadRequestException('Reset token has expired. Please request a new one.');
|
|
196
|
-
}
|
|
197
|
-
if (userToken.usedAt) {
|
|
198
|
-
throw new BadRequestException('This reset token has already been used.');
|
|
199
|
-
}
|
|
202
|
+
this.validateTokenNotExpiredOrUsed(userToken, 'Reset token has expired. Please request a new one.', 'This reset token has already been used.');
|
|
200
203
|
const user = await userRepository.findOne({
|
|
201
204
|
where: {
|
|
202
205
|
id: userToken.userId
|
|
@@ -224,9 +227,9 @@ export class AuthEmailService {
|
|
|
224
227
|
* Verify email using token
|
|
225
228
|
* Handles both pending registration and existing user verification
|
|
226
229
|
*/ async verifyEmail(token) {
|
|
227
|
-
const pendingRepository = await this.
|
|
228
|
-
const userRepository = await this.
|
|
229
|
-
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);
|
|
230
233
|
// Hash the incoming token to compare with stored hash
|
|
231
234
|
const tokenHash = this.hashToken(token);
|
|
232
235
|
// Check for pending registration first
|
|
@@ -251,7 +254,7 @@ export class AuthEmailService {
|
|
|
251
254
|
pending
|
|
252
255
|
};
|
|
253
256
|
}
|
|
254
|
-
// Check for existing user email verification token
|
|
257
|
+
// Check for existing user email verification token
|
|
255
258
|
const userToken = await tokenRepository.findOne({
|
|
256
259
|
where: {
|
|
257
260
|
token: tokenHash,
|
|
@@ -296,12 +299,10 @@ export class AuthEmailService {
|
|
|
296
299
|
/**
|
|
297
300
|
* Resend verification email - handles both pending registrations and existing users
|
|
298
301
|
*/ async resendVerificationEmail(email, verifyUrlBase) {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const
|
|
303
|
-
const userRepository = await this.getUserRepository();
|
|
304
|
-
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);
|
|
305
306
|
// Check pending registration first
|
|
306
307
|
const pending = await pendingRepository.findOne({
|
|
307
308
|
where: {
|
|
@@ -310,19 +311,11 @@ export class AuthEmailService {
|
|
|
310
311
|
});
|
|
311
312
|
if (pending) {
|
|
312
313
|
const token = this.generateToken();
|
|
313
|
-
|
|
314
|
-
pending.token = tokenHash; // Store hashed token
|
|
314
|
+
pending.token = this.hashToken(token);
|
|
315
315
|
pending.expiresAt = new Date(Date.now() + this.EMAIL_VERIFICATION_EXPIRY);
|
|
316
316
|
await pendingRepository.save(pending);
|
|
317
317
|
const verifyUrl = `${verifyUrlBase}?token=${token}`;
|
|
318
|
-
|
|
319
|
-
await this.emailProvider.sendVerificationEmail(email, token, verifyUrl);
|
|
320
|
-
this.logger.log(`Re-sent registration verification email to: ${email}`);
|
|
321
|
-
} catch (error) {
|
|
322
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
323
|
-
this.logger.error(`Failed to resend verification email: ${errorMessage}`);
|
|
324
|
-
throw new BadRequestException('Failed to send verification email. Please try again.');
|
|
325
|
-
}
|
|
318
|
+
await this.sendVerificationEmailSafe(email, token, verifyUrl, 'Re-sent registration');
|
|
326
319
|
return {
|
|
327
320
|
success: true,
|
|
328
321
|
message: 'If the email requires verification, a link has been sent.'
|
|
@@ -346,23 +339,15 @@ export class AuthEmailService {
|
|
|
346
339
|
type: TokenType.EMAIL_VERIFICATION
|
|
347
340
|
});
|
|
348
341
|
const token = this.generateToken();
|
|
349
|
-
const tokenHash = this.hashToken(token);
|
|
350
342
|
const userToken = tokenRepository.create({
|
|
351
343
|
userId: user.id,
|
|
352
344
|
type: TokenType.EMAIL_VERIFICATION,
|
|
353
|
-
token:
|
|
345
|
+
token: this.hashToken(token),
|
|
354
346
|
expiresAt: new Date(Date.now() + this.EMAIL_VERIFICATION_EXPIRY)
|
|
355
347
|
});
|
|
356
348
|
await tokenRepository.save(userToken);
|
|
357
349
|
const verifyUrl = `${verifyUrlBase}?token=${token}`;
|
|
358
|
-
|
|
359
|
-
await this.emailProvider.sendVerificationEmail(user.email, token, verifyUrl);
|
|
360
|
-
this.logger.log(`Verification email sent to: ${user.email}`);
|
|
361
|
-
} catch (error) {
|
|
362
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
363
|
-
this.logger.error(`Failed to send verification email: ${errorMessage}`);
|
|
364
|
-
throw new BadRequestException('Failed to send verification email. Please try again.');
|
|
365
|
-
}
|
|
350
|
+
await this.sendVerificationEmailSafe(user.email, token, verifyUrl, 'Verification');
|
|
366
351
|
return {
|
|
367
352
|
success: true,
|
|
368
353
|
message: 'If the email requires verification, a link has been sent.'
|
|
@@ -374,8 +359,8 @@ export class AuthEmailService {
|
|
|
374
359
|
/**
|
|
375
360
|
* Clean up expired tokens and pending registrations
|
|
376
361
|
*/ async cleanupExpired() {
|
|
377
|
-
const tokenRepository = await this.
|
|
378
|
-
const pendingRepository = await this.
|
|
362
|
+
const tokenRepository = await this.getRepository(UserToken);
|
|
363
|
+
const pendingRepository = await this.getRepository(PendingRegistration);
|
|
379
364
|
const now = new Date();
|
|
380
365
|
const tokenResult = await tokenRepository.delete({
|
|
381
366
|
expiresAt: LessThan(now)
|