@flusys/nestjs-auth 3.0.0 → 4.0.0-lts
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 +1 -1
- package/cjs/config/index.js +1 -0
- package/cjs/config/message-keys.js +150 -0
- package/cjs/controllers/authentication.controller.js +9 -4
- package/cjs/controllers/branch.controller.js +1 -0
- package/cjs/controllers/company-selection.controller.js +3 -0
- package/cjs/controllers/company.controller.js +1 -0
- package/cjs/controllers/email-verification.controller.js +7 -2
- package/cjs/controllers/user-permission.controller.js +9 -1
- package/cjs/controllers/user.controller.js +16 -4
- package/cjs/dtos/authentication.dto.js +8 -0
- package/cjs/dtos/user-permission.dto.js +14 -0
- package/cjs/interceptors/set-token.interceptor.js +14 -5
- package/cjs/services/auth-config.service.js +9 -8
- package/cjs/services/auth-datasource.provider.js +7 -4
- package/cjs/services/auth-email.service.js +155 -45
- package/cjs/services/authentication.service.js +306 -71
- package/cjs/services/branch.service.js +1 -1
- package/cjs/services/company-selection-session.service.js +25 -6
- package/cjs/services/company.service.js +1 -1
- package/cjs/services/user-permission.service.js +52 -4
- package/cjs/services/user.service.js +122 -26
- package/cjs/strategies/jwt.strategy.js +18 -4
- package/config/index.d.ts +1 -0
- package/config/message-keys.d.ts +218 -0
- package/controllers/company-selection.controller.d.ts +2 -0
- package/controllers/user-permission.controller.d.ts +4 -0
- package/controllers/user.controller.d.ts +10 -0
- package/dtos/authentication.dto.d.ts +1 -0
- package/dtos/user-permission.dto.d.ts +2 -0
- package/entities/index.d.ts +10 -1
- package/fesm/config/index.js +1 -0
- package/fesm/config/message-keys.js +118 -0
- package/fesm/controllers/authentication.controller.js +9 -3
- package/fesm/controllers/branch.controller.js +1 -0
- package/fesm/controllers/company-selection.controller.js +3 -0
- package/fesm/controllers/company.controller.js +1 -0
- package/fesm/controllers/email-verification.controller.js +7 -2
- package/fesm/controllers/user-permission.controller.js +9 -1
- package/fesm/controllers/user.controller.js +16 -4
- package/fesm/dtos/authentication.dto.js +8 -0
- package/fesm/dtos/user-permission.dto.js +14 -0
- package/fesm/interceptors/set-token.interceptor.js +14 -4
- package/fesm/services/auth-config.service.js +10 -9
- package/fesm/services/auth-datasource.provider.js +7 -4
- package/fesm/services/auth-email.service.js +156 -45
- package/fesm/services/authentication.service.js +297 -61
- package/fesm/services/branch.service.js +1 -1
- package/fesm/services/company-selection-session.service.js +25 -6
- package/fesm/services/company.service.js +1 -1
- package/fesm/services/user-permission.service.js +52 -4
- package/fesm/services/user.service.js +122 -25
- package/fesm/strategies/jwt.strategy.js +18 -4
- package/interfaces/auth-module-options.interface.d.ts +2 -2
- package/interfaces/authentication.interface.d.ts +1 -0
- package/modules/auth.module.d.ts +2 -1
- package/package.json +6 -4
- package/services/auth-config.service.d.ts +0 -2
- package/services/auth-datasource.provider.d.ts +1 -3
- package/services/auth-email.service.d.ts +6 -3
- package/services/authentication.service.d.ts +5 -2
- package/services/branch.service.d.ts +2 -1
- package/services/user.service.d.ts +7 -7
|
@@ -26,6 +26,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { createApiController, USER_PERMISSIONS } from '@flusys/nestjs-shared/classes';
|
|
29
|
+
import { USER_MESSAGES } from '../config';
|
|
29
30
|
import { CurrentUser, RequirePermission } from '@flusys/nestjs-shared/decorators';
|
|
30
31
|
import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
|
|
31
32
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
@@ -34,6 +35,7 @@ import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiTags } from '@nestjs
|
|
|
34
35
|
import { CreateUserDto, UpdateUserDto, UserResponseDto } from '../dtos/user.dto';
|
|
35
36
|
import { UserService } from '../services/user.service';
|
|
36
37
|
export class UserController extends createApiController(CreateUserDto, UpdateUserDto, UserResponseDto, {
|
|
38
|
+
entityName: 'user',
|
|
37
39
|
security: {
|
|
38
40
|
insert: {
|
|
39
41
|
level: 'permission',
|
|
@@ -83,6 +85,8 @@ export class UserController extends createApiController(CreateUserDto, UpdateUse
|
|
|
83
85
|
const data = await this.service.getProfileWithExtras(id, user);
|
|
84
86
|
return {
|
|
85
87
|
success: true,
|
|
88
|
+
message: 'Profile extras retrieved successfully',
|
|
89
|
+
messageKey: USER_MESSAGES.PROFILE_EXTRAS_SUCCESS,
|
|
86
90
|
data
|
|
87
91
|
};
|
|
88
92
|
}
|
|
@@ -90,21 +94,24 @@ export class UserController extends createApiController(CreateUserDto, UpdateUse
|
|
|
90
94
|
await this.service.verifyEmail(id);
|
|
91
95
|
return {
|
|
92
96
|
success: true,
|
|
93
|
-
message: 'Email verified successfully'
|
|
97
|
+
message: 'Email verified successfully',
|
|
98
|
+
messageKey: USER_MESSAGES.EMAIL_VERIFY_SUCCESS
|
|
94
99
|
};
|
|
95
100
|
}
|
|
96
101
|
async verifyPhone(id) {
|
|
97
102
|
await this.service.verifyPhone(id);
|
|
98
103
|
return {
|
|
99
104
|
success: true,
|
|
100
|
-
message: 'Phone verified successfully'
|
|
105
|
+
message: 'Phone verified successfully',
|
|
106
|
+
messageKey: USER_MESSAGES.PHONE_VERIFY_SUCCESS
|
|
101
107
|
};
|
|
102
108
|
}
|
|
103
109
|
async updateStatus(id, isActive, user) {
|
|
104
110
|
await this.service.updateStatus(id, isActive, user);
|
|
105
111
|
return {
|
|
106
112
|
success: true,
|
|
107
|
-
message: 'Status updated successfully'
|
|
113
|
+
message: 'Status updated successfully',
|
|
114
|
+
messageKey: USER_MESSAGES.STATUS_UPDATE_SUCCESS
|
|
108
115
|
};
|
|
109
116
|
}
|
|
110
117
|
async profile(dto, user) {
|
|
@@ -112,6 +119,7 @@ export class UserController extends createApiController(CreateUserDto, UpdateUse
|
|
|
112
119
|
return {
|
|
113
120
|
success: true,
|
|
114
121
|
message: 'Profile updated successfully',
|
|
122
|
+
messageKey: USER_MESSAGES.PROFILE_UPDATE_SUCCESS,
|
|
115
123
|
data
|
|
116
124
|
};
|
|
117
125
|
}
|
|
@@ -120,6 +128,7 @@ export class UserController extends createApiController(CreateUserDto, UpdateUse
|
|
|
120
128
|
return {
|
|
121
129
|
success: true,
|
|
122
130
|
message: 'Profile sections retrieved successfully',
|
|
131
|
+
messageKey: USER_MESSAGES.PROFILE_SECTIONS_SUCCESS,
|
|
123
132
|
data: sections
|
|
124
133
|
};
|
|
125
134
|
}
|
|
@@ -128,6 +137,7 @@ export class UserController extends createApiController(CreateUserDto, UpdateUse
|
|
|
128
137
|
return {
|
|
129
138
|
success: true,
|
|
130
139
|
message: 'Profile section data retrieved successfully',
|
|
140
|
+
messageKey: USER_MESSAGES.PROFILE_SECTION_DATA_SUCCESS,
|
|
131
141
|
data
|
|
132
142
|
};
|
|
133
143
|
}
|
|
@@ -135,7 +145,8 @@ export class UserController extends createApiController(CreateUserDto, UpdateUse
|
|
|
135
145
|
await this.service.updateProfileSection(id, sectionId, data, user);
|
|
136
146
|
return {
|
|
137
147
|
success: true,
|
|
138
|
-
message: 'Section updated successfully'
|
|
148
|
+
message: 'Section updated successfully',
|
|
149
|
+
messageKey: USER_MESSAGES.PROFILE_SECTION_UPDATE_SUCCESS
|
|
139
150
|
};
|
|
140
151
|
}
|
|
141
152
|
async getProfileCompletion(id, user) {
|
|
@@ -143,6 +154,7 @@ export class UserController extends createApiController(CreateUserDto, UpdateUse
|
|
|
143
154
|
return {
|
|
144
155
|
success: true,
|
|
145
156
|
message: 'Profile completion retrieved successfully',
|
|
157
|
+
messageKey: USER_MESSAGES.PROFILE_COMPLETION_SUCCESS,
|
|
146
158
|
data: {
|
|
147
159
|
completionPercentage: percentage
|
|
148
160
|
}
|
|
@@ -613,6 +613,7 @@ export class LoginResponseRequiresEmailVerificationDto {
|
|
|
613
613
|
_define_property(this, "requiresEmailVerification", void 0);
|
|
614
614
|
_define_property(this, "email", void 0);
|
|
615
615
|
_define_property(this, "message", void 0);
|
|
616
|
+
_define_property(this, "messageKey", void 0);
|
|
616
617
|
}
|
|
617
618
|
}
|
|
618
619
|
_ts_decorate([
|
|
@@ -633,6 +634,13 @@ _ts_decorate([
|
|
|
633
634
|
}),
|
|
634
635
|
_ts_metadata("design:type", String)
|
|
635
636
|
], LoginResponseRequiresEmailVerificationDto.prototype, "message", void 0);
|
|
637
|
+
_ts_decorate([
|
|
638
|
+
ApiProperty({
|
|
639
|
+
example: 'auth.login.email.not.verified',
|
|
640
|
+
description: 'Translation key for localization'
|
|
641
|
+
}),
|
|
642
|
+
_ts_metadata("design:type", String)
|
|
643
|
+
], LoginResponseRequiresEmailVerificationDto.prototype, "messageKey", void 0);
|
|
636
644
|
export class ForgotPasswordDto {
|
|
637
645
|
constructor(){
|
|
638
646
|
_define_property(this, "email", void 0);
|
|
@@ -235,6 +235,7 @@ _ts_decorate([
|
|
|
235
235
|
constructor(){
|
|
236
236
|
_define_property(this, "success", void 0);
|
|
237
237
|
_define_property(this, "message", void 0);
|
|
238
|
+
_define_property(this, "messageKey", void 0);
|
|
238
239
|
_define_property(this, "data", void 0);
|
|
239
240
|
}
|
|
240
241
|
}
|
|
@@ -250,6 +251,12 @@ _ts_decorate([
|
|
|
250
251
|
}),
|
|
251
252
|
_ts_metadata("design:type", String)
|
|
252
253
|
], UserCompaniesResponseDto.prototype, "message", void 0);
|
|
254
|
+
_ts_decorate([
|
|
255
|
+
ApiProperty({
|
|
256
|
+
example: 'userPermission.company.list.success'
|
|
257
|
+
}),
|
|
258
|
+
_ts_metadata("design:type", String)
|
|
259
|
+
], UserCompaniesResponseDto.prototype, "messageKey", void 0);
|
|
253
260
|
_ts_decorate([
|
|
254
261
|
ApiProperty({
|
|
255
262
|
description: 'List of companies the user has access to',
|
|
@@ -281,6 +288,7 @@ _ts_decorate([
|
|
|
281
288
|
constructor(){
|
|
282
289
|
_define_property(this, "success", void 0);
|
|
283
290
|
_define_property(this, "message", void 0);
|
|
291
|
+
_define_property(this, "messageKey", void 0);
|
|
284
292
|
_define_property(this, "data", void 0);
|
|
285
293
|
}
|
|
286
294
|
}
|
|
@@ -296,6 +304,12 @@ _ts_decorate([
|
|
|
296
304
|
}),
|
|
297
305
|
_ts_metadata("design:type", String)
|
|
298
306
|
], UserBranchesResponseDto.prototype, "message", void 0);
|
|
307
|
+
_ts_decorate([
|
|
308
|
+
ApiProperty({
|
|
309
|
+
example: 'userPermission.branch.list.success'
|
|
310
|
+
}),
|
|
311
|
+
_ts_metadata("design:type", String)
|
|
312
|
+
], UserBranchesResponseDto.prototype, "messageKey", void 0);
|
|
299
313
|
_ts_decorate([
|
|
300
314
|
ApiProperty({
|
|
301
315
|
description: 'List of branches the user has access to',
|
|
@@ -26,6 +26,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { buildCookieOptions, isBrowserRequest, parseDurationToMs } from '@flusys/nestjs-shared/utils';
|
|
29
|
+
import { AUTH_MESSAGES } from '../config';
|
|
29
30
|
import { Inject, Injectable, Optional, PayloadTooLargeException } from '@nestjs/common';
|
|
30
31
|
import { Buffer } from 'buffer';
|
|
31
32
|
import { catchError, map, throwError } from 'rxjs';
|
|
@@ -42,10 +43,13 @@ export class SetToken {
|
|
|
42
43
|
const req = context.switchToHttp().getRequest();
|
|
43
44
|
const res = context.switchToHttp().getResponse();
|
|
44
45
|
return next.handle().pipe(map((value)=>{
|
|
45
|
-
if (!
|
|
46
|
+
if (typeof value !== 'object' || value === null || !('refreshToken' in value)) {
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
const response = value;
|
|
46
50
|
if (isBrowserRequest(req)) {
|
|
47
|
-
this.setRefreshTokenCookie(req, res,
|
|
48
|
-
const { refreshToken, ...responseWithoutToken } =
|
|
51
|
+
this.setRefreshTokenCookie(req, res, String(response.refreshToken));
|
|
52
|
+
const { refreshToken, ...responseWithoutToken } = response;
|
|
49
53
|
return responseWithoutToken;
|
|
50
54
|
}
|
|
51
55
|
return value;
|
|
@@ -55,7 +59,13 @@ export class SetToken {
|
|
|
55
59
|
const rawCookieString = `${this.cookieName}=${tokenValue}; HttpOnly`;
|
|
56
60
|
const estimatedBytes = Buffer.byteLength(rawCookieString, 'utf8');
|
|
57
61
|
if (estimatedBytes > MAX_COOKIE_BYTES) {
|
|
58
|
-
throw new PayloadTooLargeException(
|
|
62
|
+
throw new PayloadTooLargeException({
|
|
63
|
+
message: `Refresh token too large (≈${estimatedBytes} bytes)`,
|
|
64
|
+
messageKey: AUTH_MESSAGES.TOKEN_TOO_LARGE,
|
|
65
|
+
messageParams: {
|
|
66
|
+
size: estimatedBytes
|
|
67
|
+
}
|
|
68
|
+
});
|
|
59
69
|
}
|
|
60
70
|
res.cookie(this.cookieName, tokenValue, {
|
|
61
71
|
httpOnly: true,
|
|
@@ -26,7 +26,8 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { envConfig } from '@flusys/nestjs-core/config';
|
|
29
|
-
import {
|
|
29
|
+
import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
|
|
30
|
+
import { Inject, Injectable, InternalServerErrorException, Optional } from '@nestjs/common';
|
|
30
31
|
import { AUTH_MODULE_OPTIONS } from '../config/auth.constants';
|
|
31
32
|
import { AuthModuleOptions } from '../interfaces/auth-module-options.interface';
|
|
32
33
|
export class AuthConfigService {
|
|
@@ -47,7 +48,10 @@ export class AuthConfigService {
|
|
|
47
48
|
// JWT Configuration
|
|
48
49
|
getJwtSecret() {
|
|
49
50
|
if (!this.config.jwtSecret) {
|
|
50
|
-
throw new InternalServerErrorException(
|
|
51
|
+
throw new InternalServerErrorException({
|
|
52
|
+
message: '[AuthModule] JWT_SECRET is required - set via environment or module config',
|
|
53
|
+
messageKey: SYSTEM_MESSAGES.CONFIG_REQUIRED
|
|
54
|
+
});
|
|
51
55
|
}
|
|
52
56
|
return this.config.jwtSecret;
|
|
53
57
|
}
|
|
@@ -59,11 +63,10 @@ export class AuthConfigService {
|
|
|
59
63
|
return this.config.refreshTokenSecret;
|
|
60
64
|
}
|
|
61
65
|
if (envConfig.isProduction()) {
|
|
62
|
-
throw new InternalServerErrorException(
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.logger.warn('REFRESH_TOKEN_SECRET not configured - using JWT_SECRET as fallback. ' + 'For better security, configure a separate refresh token secret.');
|
|
66
|
+
throw new InternalServerErrorException({
|
|
67
|
+
message: '[AuthModule] REFRESH_TOKEN_SECRET is required in production - using the same secret for access and refresh tokens is a security risk',
|
|
68
|
+
messageKey: SYSTEM_MESSAGES.CONFIG_REQUIRED
|
|
69
|
+
});
|
|
67
70
|
}
|
|
68
71
|
return this.getJwtSecret();
|
|
69
72
|
}
|
|
@@ -78,10 +81,8 @@ export class AuthConfigService {
|
|
|
78
81
|
return this.config.frontendUrl;
|
|
79
82
|
}
|
|
80
83
|
constructor(injectedOptions){
|
|
81
|
-
_define_property(this, "logger", new Logger(AuthConfigService.name));
|
|
82
84
|
_define_property(this, "options", void 0);
|
|
83
85
|
_define_property(this, "config", void 0);
|
|
84
|
-
_define_property(this, "refreshTokenSecretWarned", false);
|
|
85
86
|
this.options = injectedOptions ?? {
|
|
86
87
|
global: false,
|
|
87
88
|
includeController: true
|
|
@@ -25,8 +25,8 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import { MultiTenantDataSourceService } from '@flusys/nestjs-shared
|
|
29
|
-
import { Inject, Injectable, InternalServerErrorException,
|
|
28
|
+
import { MultiTenantDataSourceService, SYSTEM_MESSAGES } from '@flusys/nestjs-shared';
|
|
29
|
+
import { Inject, Injectable, InternalServerErrorException, Optional, Scope } from '@nestjs/common';
|
|
30
30
|
import { REQUEST } from '@nestjs/core';
|
|
31
31
|
import { Request } from 'express';
|
|
32
32
|
import { AuthModuleOptions } from '../interfaces/auth-module-options.interface';
|
|
@@ -40,7 +40,10 @@ export class AuthDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
40
40
|
const lockPromise = (async ()=>{
|
|
41
41
|
const config = this.getDefaultDatabaseConfig();
|
|
42
42
|
if (!config) {
|
|
43
|
-
throw new InternalServerErrorException(
|
|
43
|
+
throw new InternalServerErrorException({
|
|
44
|
+
message: 'Default database config is not available',
|
|
45
|
+
messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
46
|
+
});
|
|
44
47
|
}
|
|
45
48
|
const ds = await this.createDataSourceFromConfig(config);
|
|
46
49
|
AuthDataSourceProvider.singleDataSource = ds;
|
|
@@ -103,7 +106,7 @@ export class AuthDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
103
106
|
return super.createDataSourceFromConfig(config, await this.getAuthEntities());
|
|
104
107
|
}
|
|
105
108
|
constructor(authOptions, request){
|
|
106
|
-
super(AuthDataSourceProvider.buildParentOptions(authOptions), request), _define_property(this, "authOptions", void 0),
|
|
109
|
+
super(AuthDataSourceProvider.buildParentOptions(authOptions), request), _define_property(this, "authOptions", void 0), this.authOptions = authOptions;
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
_define_property(AuthDataSourceProvider, "tenantConnections", new Map());
|
|
@@ -25,7 +25,10 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
28
|
+
import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
|
|
29
|
+
import { LogAction } from '@flusys/nestjs-shared';
|
|
30
|
+
import { AUTH_MESSAGES, EMAIL_VERIFICATION_MESSAGES, USER_MESSAGES } from '../config';
|
|
31
|
+
import { BadRequestException, Inject, Injectable, InternalServerErrorException, Optional, Scope } from '@nestjs/common';
|
|
29
32
|
import * as bcrypt from 'bcrypt';
|
|
30
33
|
import * as crypto from 'crypto';
|
|
31
34
|
import { LessThan } from 'typeorm';
|
|
@@ -41,7 +44,13 @@ export class AuthEmailService {
|
|
|
41
44
|
}
|
|
42
45
|
async getRepository(entity) {
|
|
43
46
|
if (!this.dataSourceProvider) {
|
|
44
|
-
throw new InternalServerErrorException(
|
|
47
|
+
throw new InternalServerErrorException({
|
|
48
|
+
message: `${String(entity)} repository not available`,
|
|
49
|
+
messageKey: SYSTEM_MESSAGES.REPOSITORY_NOT_AVAILABLE,
|
|
50
|
+
messageParams: {
|
|
51
|
+
entity: String(entity)
|
|
52
|
+
}
|
|
53
|
+
});
|
|
45
54
|
}
|
|
46
55
|
return this.dataSourceProvider.getRepository(entity);
|
|
47
56
|
}
|
|
@@ -51,29 +60,36 @@ export class AuthEmailService {
|
|
|
51
60
|
hashToken(token) {
|
|
52
61
|
return crypto.createHash('sha256').update(token).digest('hex');
|
|
53
62
|
}
|
|
54
|
-
getErrorMessage(error) {
|
|
55
|
-
return error instanceof Error ? error.message : 'Unknown error';
|
|
56
|
-
}
|
|
57
63
|
assertEmailEnabled() {
|
|
58
64
|
if (!this.emailProvider) {
|
|
59
|
-
throw new BadRequestException(
|
|
65
|
+
throw new BadRequestException({
|
|
66
|
+
message: 'Email service is not configured',
|
|
67
|
+
messageKey: AUTH_MESSAGES.EMAIL_NOT_CONFIGURED
|
|
68
|
+
});
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
|
-
async sendVerificationEmailSafe(email, token, verifyUrl
|
|
71
|
+
async sendVerificationEmailSafe(email, token, verifyUrl) {
|
|
63
72
|
try {
|
|
64
73
|
await this.emailProvider.sendVerificationEmail(email, token, verifyUrl);
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
} catch {
|
|
75
|
+
throw new BadRequestException({
|
|
76
|
+
message: 'Failed to send verification email. Please try again.',
|
|
77
|
+
messageKey: AUTH_MESSAGES.EMAIL_SEND_FAILED
|
|
78
|
+
});
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
|
-
validateTokenNotExpiredOrUsed(token, expiredMessage, usedMessage) {
|
|
81
|
+
validateTokenNotExpiredOrUsed(token, expiredMessage, expiredMessageKey, usedMessage, usedMessageKey) {
|
|
72
82
|
if (token.expiresAt < new Date()) {
|
|
73
|
-
throw new BadRequestException(
|
|
83
|
+
throw new BadRequestException({
|
|
84
|
+
message: expiredMessage,
|
|
85
|
+
messageKey: expiredMessageKey
|
|
86
|
+
});
|
|
74
87
|
}
|
|
75
|
-
if (usedMessage && token.usedAt) {
|
|
76
|
-
throw new BadRequestException(
|
|
88
|
+
if (usedMessage && usedMessageKey && token.usedAt) {
|
|
89
|
+
throw new BadRequestException({
|
|
90
|
+
message: usedMessage,
|
|
91
|
+
messageKey: usedMessageKey
|
|
92
|
+
});
|
|
77
93
|
}
|
|
78
94
|
}
|
|
79
95
|
async hasPendingRegistration(email) {
|
|
@@ -97,7 +113,10 @@ export class AuthEmailService {
|
|
|
97
113
|
}
|
|
98
114
|
});
|
|
99
115
|
if (existingUser) {
|
|
100
|
-
throw new BadRequestException(
|
|
116
|
+
throw new BadRequestException({
|
|
117
|
+
message: 'Registration failed. Please try again or contact support.',
|
|
118
|
+
messageKey: AUTH_MESSAGES.REGISTER_FAILED
|
|
119
|
+
});
|
|
101
120
|
}
|
|
102
121
|
// Delete any existing pending registration for this email
|
|
103
122
|
await pendingRepository.delete({
|
|
@@ -121,10 +140,11 @@ export class AuthEmailService {
|
|
|
121
140
|
await pendingRepository.save(pending);
|
|
122
141
|
// Send verification email with raw token (user receives this)
|
|
123
142
|
const verifyUrl = `${data.verifyUrlBase}?token=${token}`;
|
|
124
|
-
await this.sendVerificationEmailSafe(data.email, token, verifyUrl
|
|
143
|
+
await this.sendVerificationEmailSafe(data.email, token, verifyUrl);
|
|
125
144
|
return {
|
|
126
145
|
success: true,
|
|
127
|
-
message: 'Please check your email to verify your registration.'
|
|
146
|
+
message: 'Please check your email to verify your registration.',
|
|
147
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.VERIFICATION_SENT
|
|
128
148
|
};
|
|
129
149
|
}
|
|
130
150
|
async forgotPassword(email, resetUrlBase) {
|
|
@@ -138,10 +158,10 @@ export class AuthEmailService {
|
|
|
138
158
|
});
|
|
139
159
|
// Always return success to prevent email enumeration
|
|
140
160
|
if (!user || !user.isActive) {
|
|
141
|
-
this.logger.warn(`Password reset requested for non-existent/inactive email: ${email}`);
|
|
142
161
|
return {
|
|
143
162
|
success: true,
|
|
144
|
-
message: 'If the email exists, a password reset link has been sent.'
|
|
163
|
+
message: 'If the email exists, a password reset link has been sent.',
|
|
164
|
+
messageKey: AUTH_MESSAGES.PASSWORD_RESET_EMAIL_SENT
|
|
145
165
|
};
|
|
146
166
|
}
|
|
147
167
|
// Invalidate existing password reset tokens for this user
|
|
@@ -163,14 +183,16 @@ export class AuthEmailService {
|
|
|
163
183
|
const resetUrl = `${resetUrlBase}?token=${token}`;
|
|
164
184
|
try {
|
|
165
185
|
await this.emailProvider.sendPasswordResetEmail(email, token, resetUrl);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
186
|
+
} catch {
|
|
187
|
+
throw new BadRequestException({
|
|
188
|
+
message: 'Failed to process request. Please try again later.',
|
|
189
|
+
messageKey: SYSTEM_MESSAGES.INTERNAL_ERROR
|
|
190
|
+
});
|
|
170
191
|
}
|
|
171
192
|
return {
|
|
172
193
|
success: true,
|
|
173
|
-
message: 'If the email exists, a password reset link has been sent.'
|
|
194
|
+
message: 'If the email exists, a password reset link has been sent.',
|
|
195
|
+
messageKey: AUTH_MESSAGES.PASSWORD_RESET_EMAIL_SENT
|
|
174
196
|
};
|
|
175
197
|
}
|
|
176
198
|
async resetPassword(token, newPassword) {
|
|
@@ -185,16 +207,22 @@ export class AuthEmailService {
|
|
|
185
207
|
}
|
|
186
208
|
});
|
|
187
209
|
if (!userToken) {
|
|
188
|
-
throw new BadRequestException(
|
|
210
|
+
throw new BadRequestException({
|
|
211
|
+
message: 'Invalid or expired reset token',
|
|
212
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.VERIFICATION_TOKEN_INVALID
|
|
213
|
+
});
|
|
189
214
|
}
|
|
190
|
-
this.validateTokenNotExpiredOrUsed(userToken, 'Reset token has expired. Please request a new one.', 'This reset token has already been used.');
|
|
215
|
+
this.validateTokenNotExpiredOrUsed(userToken, 'Reset token has expired. Please request a new one.', AUTH_MESSAGES.PASSWORD_RESET_TOKEN_EXPIRED, 'This reset token has already been used.', AUTH_MESSAGES.PASSWORD_RESET_TOKEN_USED);
|
|
191
216
|
const user = await userRepository.findOne({
|
|
192
217
|
where: {
|
|
193
218
|
id: userToken.userId
|
|
194
219
|
}
|
|
195
220
|
});
|
|
196
221
|
if (!user) {
|
|
197
|
-
throw new BadRequestException(
|
|
222
|
+
throw new BadRequestException({
|
|
223
|
+
message: 'User not found',
|
|
224
|
+
messageKey: USER_MESSAGES.NOT_FOUND
|
|
225
|
+
});
|
|
198
226
|
}
|
|
199
227
|
// Update password
|
|
200
228
|
user.password = await bcrypt.hash(newPassword, BCRYPT_SALT_ROUNDS);
|
|
@@ -202,10 +230,10 @@ export class AuthEmailService {
|
|
|
202
230
|
// Mark token as used
|
|
203
231
|
userToken.usedAt = new Date();
|
|
204
232
|
await tokenRepository.save(userToken);
|
|
205
|
-
this.logger.log(`Password reset successful for user: ${user.email}`);
|
|
206
233
|
return {
|
|
207
234
|
success: true,
|
|
208
|
-
message: 'Password has been reset successfully.'
|
|
235
|
+
message: 'Password has been reset successfully.',
|
|
236
|
+
messageKey: AUTH_MESSAGES.PASSWORD_RESET_SUCCESS
|
|
209
237
|
};
|
|
210
238
|
}
|
|
211
239
|
async verifyEmail(token) {
|
|
@@ -225,7 +253,10 @@ export class AuthEmailService {
|
|
|
225
253
|
await pendingRepository.delete({
|
|
226
254
|
id: pending.id
|
|
227
255
|
});
|
|
228
|
-
throw new BadRequestException(
|
|
256
|
+
throw new BadRequestException({
|
|
257
|
+
message: 'Verification token has expired. Please register again.',
|
|
258
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.VERIFICATION_TOKEN_EXPIRED
|
|
259
|
+
});
|
|
229
260
|
}
|
|
230
261
|
// Delete pending registration (will be consumed by caller to create user)
|
|
231
262
|
await pendingRepository.delete({
|
|
@@ -244,18 +275,25 @@ export class AuthEmailService {
|
|
|
244
275
|
}
|
|
245
276
|
});
|
|
246
277
|
if (!userToken) {
|
|
247
|
-
throw new BadRequestException(
|
|
278
|
+
throw new BadRequestException({
|
|
279
|
+
message: 'Invalid or expired verification token',
|
|
280
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.VERIFICATION_TOKEN_INVALID
|
|
281
|
+
});
|
|
248
282
|
}
|
|
249
283
|
if (userToken.expiresAt < new Date()) {
|
|
250
284
|
await tokenRepository.delete({
|
|
251
285
|
id: userToken.id
|
|
252
286
|
});
|
|
253
|
-
throw new BadRequestException(
|
|
287
|
+
throw new BadRequestException({
|
|
288
|
+
message: 'Verification token has expired. Please request a new one.',
|
|
289
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.VERIFICATION_TOKEN_EXPIRED
|
|
290
|
+
});
|
|
254
291
|
}
|
|
255
292
|
if (userToken.usedAt) {
|
|
256
293
|
return {
|
|
257
294
|
type: 'verified',
|
|
258
|
-
message: 'Email has already been verified.'
|
|
295
|
+
message: 'Email has already been verified.',
|
|
296
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.VERIFICATION_SUCCESS
|
|
259
297
|
};
|
|
260
298
|
}
|
|
261
299
|
const user = await userRepository.findOne({
|
|
@@ -264,7 +302,10 @@ export class AuthEmailService {
|
|
|
264
302
|
}
|
|
265
303
|
});
|
|
266
304
|
if (!user) {
|
|
267
|
-
throw new BadRequestException(
|
|
305
|
+
throw new BadRequestException({
|
|
306
|
+
message: 'User not found',
|
|
307
|
+
messageKey: USER_MESSAGES.NOT_FOUND
|
|
308
|
+
});
|
|
268
309
|
}
|
|
269
310
|
// Mark email as verified
|
|
270
311
|
user.emailVerified = true;
|
|
@@ -272,10 +313,10 @@ export class AuthEmailService {
|
|
|
272
313
|
// Mark token as used
|
|
273
314
|
userToken.usedAt = new Date();
|
|
274
315
|
await tokenRepository.save(userToken);
|
|
275
|
-
this.logger.log(`Email verified for user: ${user.email}`);
|
|
276
316
|
return {
|
|
277
317
|
type: 'verified',
|
|
278
|
-
message: 'Email has been verified successfully.'
|
|
318
|
+
message: 'Email has been verified successfully.',
|
|
319
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.VERIFICATION_SUCCESS
|
|
279
320
|
};
|
|
280
321
|
}
|
|
281
322
|
async resendVerificationEmail(email, verifyUrlBase) {
|
|
@@ -295,10 +336,11 @@ export class AuthEmailService {
|
|
|
295
336
|
pending.expiresAt = new Date(Date.now() + this.EMAIL_VERIFICATION_EXPIRY);
|
|
296
337
|
await pendingRepository.save(pending);
|
|
297
338
|
const verifyUrl = `${verifyUrlBase}?token=${token}`;
|
|
298
|
-
await this.sendVerificationEmailSafe(email, token, verifyUrl
|
|
339
|
+
await this.sendVerificationEmailSafe(email, token, verifyUrl);
|
|
299
340
|
return {
|
|
300
341
|
success: true,
|
|
301
|
-
message: 'If the email requires verification, a link has been sent.'
|
|
342
|
+
message: 'If the email requires verification, a link has been sent.',
|
|
343
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.RESEND_SUCCESS
|
|
302
344
|
};
|
|
303
345
|
}
|
|
304
346
|
// Check existing user
|
|
@@ -310,7 +352,8 @@ export class AuthEmailService {
|
|
|
310
352
|
if (!user || user.emailVerified) {
|
|
311
353
|
return {
|
|
312
354
|
success: true,
|
|
313
|
-
message: 'If the email requires verification, a link has been sent.'
|
|
355
|
+
message: 'If the email requires verification, a link has been sent.',
|
|
356
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.RESEND_SUCCESS
|
|
314
357
|
};
|
|
315
358
|
}
|
|
316
359
|
// Invalidate existing tokens and create new one
|
|
@@ -327,10 +370,11 @@ export class AuthEmailService {
|
|
|
327
370
|
});
|
|
328
371
|
await tokenRepository.save(userToken);
|
|
329
372
|
const verifyUrl = `${verifyUrlBase}?token=${token}`;
|
|
330
|
-
await this.sendVerificationEmailSafe(user.email, token, verifyUrl
|
|
373
|
+
await this.sendVerificationEmailSafe(user.email, token, verifyUrl);
|
|
331
374
|
return {
|
|
332
375
|
success: true,
|
|
333
|
-
message: 'If the email requires verification, a link has been sent.'
|
|
376
|
+
message: 'If the email requires verification, a link has been sent.',
|
|
377
|
+
messageKey: EMAIL_VERIFICATION_MESSAGES.RESEND_SUCCESS
|
|
334
378
|
};
|
|
335
379
|
}
|
|
336
380
|
async cleanupExpired() {
|
|
@@ -345,7 +389,6 @@ export class AuthEmailService {
|
|
|
345
389
|
});
|
|
346
390
|
const deletedTokens = tokenResult.affected ?? 0;
|
|
347
391
|
const deletedPending = pendingResult.affected ?? 0;
|
|
348
|
-
this.logger.log(`Cleaned up ${deletedTokens} expired tokens and ${deletedPending} expired pending registrations`);
|
|
349
392
|
return {
|
|
350
393
|
deletedTokens,
|
|
351
394
|
deletedPending
|
|
@@ -354,16 +397,84 @@ export class AuthEmailService {
|
|
|
354
397
|
constructor(dataSourceProvider, emailProvider){
|
|
355
398
|
_define_property(this, "dataSourceProvider", void 0);
|
|
356
399
|
_define_property(this, "emailProvider", void 0);
|
|
357
|
-
_define_property(this, "logger", void 0);
|
|
358
400
|
_define_property(this, "PASSWORD_RESET_EXPIRY", void 0); // 1 hour
|
|
359
401
|
_define_property(this, "EMAIL_VERIFICATION_EXPIRY", void 0); // 24 hours
|
|
360
402
|
this.dataSourceProvider = dataSourceProvider;
|
|
361
403
|
this.emailProvider = emailProvider;
|
|
362
|
-
this.logger = new Logger(AuthEmailService.name);
|
|
363
404
|
this.PASSWORD_RESET_EXPIRY = 60 * 60 * 1000;
|
|
364
405
|
this.EMAIL_VERIFICATION_EXPIRY = 24 * 60 * 60 * 1000;
|
|
365
406
|
}
|
|
366
407
|
}
|
|
408
|
+
_ts_decorate([
|
|
409
|
+
LogAction({
|
|
410
|
+
action: 'auth.createPendingRegistration',
|
|
411
|
+
module: 'auth',
|
|
412
|
+
includeParams: false
|
|
413
|
+
}),
|
|
414
|
+
_ts_metadata("design:type", Function),
|
|
415
|
+
_ts_metadata("design:paramtypes", [
|
|
416
|
+
Object
|
|
417
|
+
]),
|
|
418
|
+
_ts_metadata("design:returntype", Promise)
|
|
419
|
+
], AuthEmailService.prototype, "createPendingRegistration", null);
|
|
420
|
+
_ts_decorate([
|
|
421
|
+
LogAction({
|
|
422
|
+
action: 'auth.forgotPassword',
|
|
423
|
+
module: 'auth',
|
|
424
|
+
includeParams: false
|
|
425
|
+
}),
|
|
426
|
+
_ts_metadata("design:type", Function),
|
|
427
|
+
_ts_metadata("design:paramtypes", [
|
|
428
|
+
String,
|
|
429
|
+
String
|
|
430
|
+
]),
|
|
431
|
+
_ts_metadata("design:returntype", Promise)
|
|
432
|
+
], AuthEmailService.prototype, "forgotPassword", null);
|
|
433
|
+
_ts_decorate([
|
|
434
|
+
LogAction({
|
|
435
|
+
action: 'auth.resetPassword',
|
|
436
|
+
module: 'auth',
|
|
437
|
+
includeParams: false
|
|
438
|
+
}),
|
|
439
|
+
_ts_metadata("design:type", Function),
|
|
440
|
+
_ts_metadata("design:paramtypes", [
|
|
441
|
+
String,
|
|
442
|
+
String
|
|
443
|
+
]),
|
|
444
|
+
_ts_metadata("design:returntype", Promise)
|
|
445
|
+
], AuthEmailService.prototype, "resetPassword", null);
|
|
446
|
+
_ts_decorate([
|
|
447
|
+
LogAction({
|
|
448
|
+
action: 'auth.verifyEmailToken',
|
|
449
|
+
module: 'auth'
|
|
450
|
+
}),
|
|
451
|
+
_ts_metadata("design:type", Function),
|
|
452
|
+
_ts_metadata("design:paramtypes", [
|
|
453
|
+
String
|
|
454
|
+
]),
|
|
455
|
+
_ts_metadata("design:returntype", Promise)
|
|
456
|
+
], AuthEmailService.prototype, "verifyEmail", null);
|
|
457
|
+
_ts_decorate([
|
|
458
|
+
LogAction({
|
|
459
|
+
action: 'auth.sendVerificationEmail',
|
|
460
|
+
module: 'auth'
|
|
461
|
+
}),
|
|
462
|
+
_ts_metadata("design:type", Function),
|
|
463
|
+
_ts_metadata("design:paramtypes", [
|
|
464
|
+
String,
|
|
465
|
+
String
|
|
466
|
+
]),
|
|
467
|
+
_ts_metadata("design:returntype", Promise)
|
|
468
|
+
], AuthEmailService.prototype, "resendVerificationEmail", null);
|
|
469
|
+
_ts_decorate([
|
|
470
|
+
LogAction({
|
|
471
|
+
action: 'auth.cleanupExpiredTokens',
|
|
472
|
+
module: 'auth'
|
|
473
|
+
}),
|
|
474
|
+
_ts_metadata("design:type", Function),
|
|
475
|
+
_ts_metadata("design:paramtypes", []),
|
|
476
|
+
_ts_metadata("design:returntype", Promise)
|
|
477
|
+
], AuthEmailService.prototype, "cleanupExpired", null);
|
|
367
478
|
AuthEmailService = _ts_decorate([
|
|
368
479
|
Injectable({
|
|
369
480
|
scope: Scope.REQUEST
|