@flusys/nestjs-auth 0.1.0-beta.3 → 1.0.0-rc
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 +274 -301
- package/cjs/config/auth.constants.js +22 -4
- package/cjs/controllers/authentication.controller.js +222 -38
- package/cjs/controllers/branch.controller.js +46 -4
- package/cjs/controllers/company-selection.controller.js +27 -20
- package/cjs/controllers/company.controller.js +46 -4
- package/cjs/controllers/user-permission.controller.js +40 -37
- package/cjs/controllers/user.controller.js +222 -23
- package/cjs/dtos/authentication.dto.js +119 -13
- package/cjs/dtos/user-permission.dto.js +28 -0
- package/cjs/dtos/user.dto.js +19 -4
- package/cjs/entities/index.js +5 -1
- package/cjs/entities/pending-registration.entity.js +125 -0
- package/cjs/entities/user-company-permission.entity.js +0 -6
- package/cjs/entities/user-token.entity.js +129 -0
- package/cjs/guards/auth-throttler.guard.js +53 -0
- package/cjs/interceptors/clear-token.interceptor.js +3 -6
- package/cjs/interceptors/set-token.interceptor.js +38 -31
- package/cjs/interfaces/auth-email-provider.interface.js +45 -0
- package/cjs/interfaces/authentication.interface.js +0 -3
- package/cjs/interfaces/index.js +2 -0
- package/cjs/interfaces/user-enricher.interface.js +4 -0
- package/cjs/modules/auth.module.js +8 -20
- package/cjs/services/auth-config.service.js +42 -47
- package/cjs/services/auth-datasource.provider.js +18 -34
- package/cjs/services/auth-email.service.js +471 -0
- package/cjs/services/authentication.service.js +240 -153
- package/cjs/services/branch.service.js +15 -48
- package/cjs/services/company-selection-session.service.js +1 -1
- package/cjs/services/company.service.js +7 -36
- package/cjs/services/index.js +1 -0
- package/cjs/services/user-permission.service.js +9 -29
- package/cjs/services/user.service.js +228 -107
- package/cjs/strategies/jwt.strategy.js +14 -1
- package/cjs/validators/password.validator.js +45 -0
- package/config/auth.constants.d.ts +5 -0
- package/controllers/authentication.controller.d.ts +13 -9
- package/controllers/company-selection.controller.d.ts +6 -2
- package/controllers/user.controller.d.ts +33 -0
- package/dtos/authentication.dto.d.ts +21 -3
- package/dtos/company-branch.dto.d.ts +0 -1
- package/dtos/company.dto.d.ts +0 -1
- package/dtos/user-permission.dto.d.ts +4 -0
- package/dtos/user.dto.d.ts +1 -2
- package/entities/index.d.ts +7 -3
- package/entities/pending-registration.entity.d.ts +17 -0
- package/entities/user-company-permission.entity.d.ts +0 -2
- package/entities/user-token.entity.d.ts +15 -0
- package/fesm/config/auth.constants.js +11 -8
- package/fesm/controllers/authentication.controller.js +224 -40
- package/fesm/controllers/branch.controller.js +49 -7
- package/fesm/controllers/company-selection.controller.js +28 -21
- package/fesm/controllers/company.controller.js +49 -7
- package/fesm/controllers/user-permission.controller.js +41 -38
- package/fesm/controllers/user.controller.js +225 -26
- package/fesm/dtos/authentication.dto.js +123 -111
- package/fesm/dtos/user-permission.dto.js +28 -0
- package/fesm/dtos/user.dto.js +20 -5
- package/fesm/entities/index.js +7 -1
- package/fesm/entities/pending-registration.entity.js +115 -0
- package/fesm/entities/user-company-permission.entity.js +0 -6
- package/fesm/entities/user-token.entity.js +113 -0
- package/fesm/guards/auth-throttler.guard.js +43 -0
- package/fesm/interceptors/clear-token.interceptor.js +2 -5
- package/fesm/interceptors/set-token.interceptor.js +37 -30
- package/fesm/interfaces/auth-email-provider.interface.js +38 -0
- package/fesm/interfaces/authentication.interface.js +1 -7
- package/fesm/interfaces/index.js +2 -0
- package/fesm/interfaces/user-enricher.interface.js +26 -0
- package/fesm/modules/auth.module.js +10 -22
- package/fesm/services/auth-config.service.js +42 -47
- package/fesm/services/auth-datasource.provider.js +19 -35
- package/fesm/services/auth-email.service.js +420 -0
- package/fesm/services/authentication.service.js +238 -151
- package/fesm/services/branch.service.js +16 -49
- package/fesm/services/company-selection-session.service.js +1 -1
- package/fesm/services/company.service.js +8 -37
- package/fesm/services/index.js +1 -0
- package/fesm/services/user-permission.service.js +10 -30
- package/fesm/services/user.service.js +229 -108
- package/fesm/strategies/jwt.strategy.js +14 -1
- package/fesm/validators/password.validator.js +48 -0
- package/guards/auth-throttler.guard.d.ts +7 -0
- package/interceptors/set-token.interceptor.d.ts +1 -0
- package/interfaces/auth-email-provider.interface.d.ts +6 -0
- package/interfaces/auth-module-options.interface.d.ts +4 -1
- package/interfaces/authentication.interface.d.ts +14 -0
- package/interfaces/index.d.ts +2 -0
- package/interfaces/user-enricher.interface.d.ts +64 -0
- package/interfaces/user.interface.d.ts +1 -0
- package/modules/auth.module.d.ts +0 -1
- package/package.json +4 -4
- package/services/auth-config.service.d.ts +5 -1
- package/services/auth-email.service.d.ts +51 -0
- package/services/authentication.service.d.ts +21 -12
- package/services/branch.service.d.ts +2 -5
- package/services/company-selection-session.service.d.ts +1 -1
- package/services/company.service.d.ts +1 -4
- package/services/index.d.ts +1 -0
- package/services/user-permission.service.d.ts +0 -1
- package/services/user.service.d.ts +20 -6
- package/validators/password.validator.d.ts +10 -0
|
@@ -15,7 +15,9 @@ const _common = require("@nestjs/common");
|
|
|
15
15
|
const _bcrypt = /*#__PURE__*/ _interop_require_wildcard(require("bcrypt"));
|
|
16
16
|
const _authconfigservice = require("./auth-config.service");
|
|
17
17
|
const _authdatasourceprovider = require("./auth-datasource.provider");
|
|
18
|
+
const _config = require("../config");
|
|
18
19
|
const _enums = require("../enums");
|
|
20
|
+
const _userenricherinterface = require("../interfaces/user-enricher.interface");
|
|
19
21
|
const _usercompanypermissionentity = require("../entities/user-company-permission.entity");
|
|
20
22
|
const _userentity = require("../entities/user.entity");
|
|
21
23
|
function _define_property(obj, key, value) {
|
|
@@ -87,22 +89,13 @@ function _ts_param(paramIndex, decorator) {
|
|
|
87
89
|
};
|
|
88
90
|
}
|
|
89
91
|
let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
90
|
-
|
|
91
|
-
* Resolve entity class for this service
|
|
92
|
-
* @returns User entity class
|
|
93
|
-
*/ resolveEntity() {
|
|
92
|
+
resolveEntity() {
|
|
94
93
|
return _userentity.User;
|
|
95
94
|
}
|
|
96
|
-
|
|
97
|
-
* Get DataSource provider for this service
|
|
98
|
-
* @returns AuthDataSourceProvider instance
|
|
99
|
-
*/ getDataSourceProvider() {
|
|
95
|
+
getDataSourceProvider() {
|
|
100
96
|
return this.dataSourceProvider;
|
|
101
97
|
}
|
|
102
|
-
|
|
103
|
-
* Override ensureRepositoryInitialized to also initialize permission repository and dataSource
|
|
104
|
-
* CRITICAL: Also initializes permissionRepository and dataSource for company feature support
|
|
105
|
-
*/ async ensureRepositoryInitialized() {
|
|
98
|
+
async ensureRepositoryInitialized() {
|
|
106
99
|
await super.ensureRepositoryInitialized();
|
|
107
100
|
// Initialize permission repository if company feature is enabled
|
|
108
101
|
const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
|
|
@@ -116,26 +109,16 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
116
109
|
this.dataSource = await this.getDataSourceForService();
|
|
117
110
|
}
|
|
118
111
|
}
|
|
119
|
-
//
|
|
120
|
-
// CUSTOM METHODS (Beyond RequestScopedApiService)
|
|
121
|
-
// ==========================================================================
|
|
112
|
+
// Custom Methods
|
|
122
113
|
/**
|
|
123
|
-
*
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
});
|
|
130
|
-
await this.clearCacheForId([
|
|
131
|
-
{
|
|
132
|
-
id
|
|
133
|
-
}
|
|
134
|
-
]);
|
|
114
|
+
* Validate that the requesting user owns the target profile.
|
|
115
|
+
* Users can only access/modify their own profile data.
|
|
116
|
+
*/ validateProfileOwnership(targetUserId, requestingUser) {
|
|
117
|
+
if (targetUserId !== requestingUser.id) {
|
|
118
|
+
throw new _common.ForbiddenException('Cannot access another user\'s profile');
|
|
119
|
+
}
|
|
135
120
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Verify email
|
|
138
|
-
*/ async verifyEmail(id) {
|
|
121
|
+
/** Verify email */ async verifyEmail(id) {
|
|
139
122
|
// Ensure repository is initialized for custom method
|
|
140
123
|
await this.ensureRepositoryInitialized();
|
|
141
124
|
await this.repository.update(id, {
|
|
@@ -148,9 +131,7 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
148
131
|
}
|
|
149
132
|
]);
|
|
150
133
|
}
|
|
151
|
-
/**
|
|
152
|
-
* Verify phone
|
|
153
|
-
*/ async verifyPhone(id) {
|
|
134
|
+
/** Verify phone */ async verifyPhone(id) {
|
|
154
135
|
// Ensure repository is initialized for custom method
|
|
155
136
|
await this.ensureRepositoryInitialized();
|
|
156
137
|
await this.repository.update(id, {
|
|
@@ -163,12 +144,7 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
163
144
|
}
|
|
164
145
|
]);
|
|
165
146
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Update user status
|
|
168
|
-
*
|
|
169
|
-
* When company feature is disabled: Updates User.isActive directly
|
|
170
|
-
* When company feature is enabled: Updates UserCompanyPermission.isActive for the logged user's company
|
|
171
|
-
*/ async updateStatus(id, isActive, user) {
|
|
147
|
+
/** Update user status. When company feature enabled, updates permission isActive for logged user's company. */ async updateStatus(id, isActive, user) {
|
|
172
148
|
// Ensure repository is initialized for custom method
|
|
173
149
|
await this.ensureRepositoryInitialized();
|
|
174
150
|
const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
|
|
@@ -199,50 +175,68 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
199
175
|
}
|
|
200
176
|
]);
|
|
201
177
|
}
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
// ==========================================================================
|
|
205
|
-
/**
|
|
206
|
-
* Override: Convert DTO to entity (hash password before saving)
|
|
207
|
-
*/ async convertSingleDtoToEntity(dto, user) {
|
|
178
|
+
// ApiService Overrides
|
|
179
|
+
/** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
|
|
208
180
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
209
181
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
210
|
-
// Hash password if provided
|
|
211
|
-
if (dto.password) {
|
|
212
|
-
entity.password = await _bcrypt.hash(dto.password,
|
|
182
|
+
// Hash password if provided (CreateUserDto has password field)
|
|
183
|
+
if ('password' in dto && dto.password) {
|
|
184
|
+
entity.password = await _bcrypt.hash(dto.password, _config.BCRYPT_SALT_ROUNDS);
|
|
213
185
|
}
|
|
214
186
|
return entity;
|
|
215
187
|
}
|
|
216
|
-
/**
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
188
|
+
/** Convert entity to response DTO (removes password). In raw mode with company feature, isActive comes from permission table. */ convertEntityToResponseDto(entity, isRaw) {
|
|
189
|
+
if (isRaw) {
|
|
190
|
+
return this.convertRawToResponseDto(entity);
|
|
191
|
+
}
|
|
192
|
+
return this.convertEntityToDto(entity);
|
|
193
|
+
}
|
|
194
|
+
/** Convert raw query result to response DTO */ convertRawToResponseDto(raw) {
|
|
222
195
|
// When raw mode is used with company feature, isActive comes from computed field
|
|
223
|
-
const isActive =
|
|
196
|
+
const isActive = raw.user_companyIsActive !== undefined ? Boolean(raw.user_companyIsActive) : raw.user_is_active;
|
|
224
197
|
return {
|
|
225
|
-
id:
|
|
226
|
-
name:
|
|
227
|
-
phone:
|
|
228
|
-
email:
|
|
229
|
-
profilePictureId:
|
|
198
|
+
id: raw.user_id,
|
|
199
|
+
name: raw.user_name,
|
|
200
|
+
phone: raw.user_phone,
|
|
201
|
+
email: raw.user_email,
|
|
202
|
+
profilePictureId: raw.user_profile_picture_id,
|
|
230
203
|
isActive,
|
|
231
|
-
emailVerified:
|
|
232
|
-
phoneVerified:
|
|
233
|
-
lastLoginAt:
|
|
234
|
-
additionalFields:
|
|
235
|
-
createdAt:
|
|
236
|
-
updatedAt:
|
|
237
|
-
deletedAt:
|
|
238
|
-
createdById:
|
|
239
|
-
updatedById:
|
|
240
|
-
deletedById:
|
|
204
|
+
emailVerified: raw.user_email_verified,
|
|
205
|
+
phoneVerified: raw.user_phone_verified,
|
|
206
|
+
lastLoginAt: raw.user_last_login_at,
|
|
207
|
+
additionalFields: raw.user_additional_fields,
|
|
208
|
+
createdAt: raw.user_created_at,
|
|
209
|
+
updatedAt: raw.user_updated_at,
|
|
210
|
+
deletedAt: raw.user_deleted_at,
|
|
211
|
+
createdById: raw.user_created_by_id,
|
|
212
|
+
updatedById: raw.user_updated_by_id,
|
|
213
|
+
deletedById: raw.user_deleted_by_id
|
|
241
214
|
};
|
|
242
215
|
}
|
|
243
|
-
/**
|
|
244
|
-
|
|
245
|
-
|
|
216
|
+
/** Convert entity to response DTO */ convertEntityToDto(entity) {
|
|
217
|
+
return {
|
|
218
|
+
id: entity.id,
|
|
219
|
+
name: entity.name,
|
|
220
|
+
phone: entity.phone,
|
|
221
|
+
email: entity.email,
|
|
222
|
+
profilePictureId: entity.profilePictureId,
|
|
223
|
+
isActive: entity.isActive,
|
|
224
|
+
emailVerified: entity.emailVerified,
|
|
225
|
+
phoneVerified: entity.phoneVerified,
|
|
226
|
+
lastLoginAt: entity.lastLoginAt,
|
|
227
|
+
additionalFields: entity.additionalFields,
|
|
228
|
+
createdAt: entity.createdAt,
|
|
229
|
+
updatedAt: entity.updatedAt,
|
|
230
|
+
deletedAt: entity.deletedAt,
|
|
231
|
+
createdById: entity.createdById,
|
|
232
|
+
updatedById: entity.updatedById,
|
|
233
|
+
deletedById: entity.deletedById
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
|
|
237
|
+
return entities.map((entity)=>this.convertEntityToResponseDto(entity, isRaw));
|
|
238
|
+
}
|
|
239
|
+
/** Global search - search by name, email, or phone */ async getGlobalSearchQuery(query, search, _user) {
|
|
246
240
|
query.andWhere('(user.name LIKE :search OR user.email LIKE :search OR user.phone LIKE :search)', {
|
|
247
241
|
search: `%${search}%`
|
|
248
242
|
});
|
|
@@ -251,18 +245,13 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
251
245
|
isRaw: false
|
|
252
246
|
};
|
|
253
247
|
}
|
|
254
|
-
/**
|
|
255
|
-
* Override: getExtraManipulateQuery - Add company permission-based isActive check when company feature enabled
|
|
256
|
-
*
|
|
257
|
-
* Note: When company feature is disabled at bootstrap, this method returns without modifications.
|
|
258
|
-
* All company-related filtering is skipped, and users are returned normally with isActive from User entity.
|
|
259
|
-
*
|
|
260
|
-
* When company feature is enabled:
|
|
261
|
-
* - isActive is computed from UserCompanyPermission table
|
|
262
|
-
* - Returns isRaw: true to use raw query results
|
|
263
|
-
*/ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
|
|
248
|
+
/** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
|
|
264
249
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
265
|
-
|
|
250
|
+
let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
|
|
251
|
+
// Apply user enricher query modifications if available
|
|
252
|
+
if (this.userEnricher?.enrichListQuery) {
|
|
253
|
+
await this.userEnricher.enrichListQuery(query, user);
|
|
254
|
+
}
|
|
266
255
|
// Only apply company filtering if:
|
|
267
256
|
// 1. Company feature is enabled at bootstrap
|
|
268
257
|
// 2. User has company context
|
|
@@ -315,12 +304,7 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
315
304
|
}
|
|
316
305
|
return result;
|
|
317
306
|
}
|
|
318
|
-
/**
|
|
319
|
-
* Override: insert - Create company permission when company feature is enabled
|
|
320
|
-
*
|
|
321
|
-
* Note: When company feature is disabled at bootstrap, this method falls back to parent implementation.
|
|
322
|
-
* No company permissions are created, and user is inserted normally.
|
|
323
|
-
*/ async insert(dto, user) {
|
|
307
|
+
/** Insert user. Creates company permission when company feature is enabled. */ async insert(dto, user) {
|
|
324
308
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
325
309
|
// Check if company feature is enabled
|
|
326
310
|
const isCompanyFeatureEnabled = this.authConfig.isCompanyFeatureEnabled();
|
|
@@ -388,17 +372,143 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
388
372
|
}
|
|
389
373
|
}
|
|
390
374
|
/**
|
|
391
|
-
*
|
|
392
|
-
|
|
393
|
-
|
|
375
|
+
* Get user profile with extras from enricher
|
|
376
|
+
* Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
|
|
377
|
+
*/ async getProfileWithExtras(userId, user) {
|
|
378
|
+
this.validateProfileOwnership(userId, user);
|
|
379
|
+
await this.ensureRepositoryInitialized();
|
|
380
|
+
const userEntity = await this.repository.findOne({
|
|
381
|
+
where: {
|
|
382
|
+
id: userId
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
if (!userEntity) {
|
|
386
|
+
throw new _common.NotFoundException('User not found');
|
|
387
|
+
}
|
|
388
|
+
const userData = this.convertEntityToResponseDto(userEntity, false);
|
|
389
|
+
// Get extras from enricher if available
|
|
390
|
+
let extras;
|
|
391
|
+
if (this.userEnricher?.getProfileExtras) {
|
|
392
|
+
extras = await this.userEnricher.getProfileExtras(userId, user);
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
user: userData,
|
|
396
|
+
extras
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Get enriched user list
|
|
401
|
+
* Applies enricher transformations after query execution
|
|
402
|
+
*/ async getEnrichedList(users, user) {
|
|
403
|
+
if (this.userEnricher?.enrichListItems) {
|
|
404
|
+
return this.userEnricher.enrichListItems(users, user);
|
|
405
|
+
}
|
|
406
|
+
return users;
|
|
407
|
+
}
|
|
408
|
+
// ============================================
|
|
409
|
+
// Multi-Step Profile Methods (via Enricher)
|
|
410
|
+
// ============================================
|
|
411
|
+
/**
|
|
412
|
+
* Get profile section definitions from enricher
|
|
413
|
+
*/ async getProfileSections() {
|
|
414
|
+
if (this.userEnricher?.getProfileSections) {
|
|
415
|
+
return this.userEnricher.getProfileSections();
|
|
416
|
+
}
|
|
417
|
+
return [];
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Get specific profile section data
|
|
421
|
+
*/ async getProfileSectionData(userId, sectionId, user) {
|
|
422
|
+
this.validateProfileOwnership(userId, user);
|
|
423
|
+
await this.ensureRepositoryInitialized();
|
|
424
|
+
// Verify user exists
|
|
425
|
+
const userEntity = await this.repository.findOne({
|
|
426
|
+
where: {
|
|
427
|
+
id: userId
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
if (!userEntity) {
|
|
431
|
+
throw new _common.NotFoundException('User not found');
|
|
432
|
+
}
|
|
433
|
+
if (this.userEnricher?.getProfileSectionData) {
|
|
434
|
+
return this.userEnricher.getProfileSectionData(userId, sectionId, user);
|
|
435
|
+
}
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Update specific profile section
|
|
440
|
+
*/ async updateProfileSection(userId, sectionId, data, user) {
|
|
441
|
+
this.validateProfileOwnership(userId, user);
|
|
442
|
+
await this.ensureRepositoryInitialized();
|
|
394
443
|
if (!this.dataSource) {
|
|
395
444
|
throw new _common.BadRequestException('DataSource not available');
|
|
396
445
|
}
|
|
446
|
+
// Verify user exists
|
|
447
|
+
const userEntity = await this.repository.findOne({
|
|
448
|
+
where: {
|
|
449
|
+
id: userId
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
if (!userEntity) {
|
|
453
|
+
throw new _common.NotFoundException('User not found');
|
|
454
|
+
}
|
|
455
|
+
if (!this.userEnricher?.updateProfileSection) {
|
|
456
|
+
throw new _common.BadRequestException('Profile sections not supported');
|
|
457
|
+
}
|
|
458
|
+
const queryRunner = this.dataSource.createQueryRunner();
|
|
459
|
+
await queryRunner.connect();
|
|
460
|
+
await queryRunner.startTransaction();
|
|
461
|
+
try {
|
|
462
|
+
await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
|
|
463
|
+
await queryRunner.commitTransaction();
|
|
464
|
+
await this.clearCacheForId([
|
|
465
|
+
userEntity
|
|
466
|
+
]);
|
|
467
|
+
} catch (error) {
|
|
468
|
+
await queryRunner.rollbackTransaction();
|
|
469
|
+
_utils.ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
|
|
470
|
+
entity: this.entityName,
|
|
471
|
+
sectionId
|
|
472
|
+
});
|
|
473
|
+
_utils.ErrorHandler.rethrowError(error);
|
|
474
|
+
} finally{
|
|
475
|
+
await queryRunner.release();
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Get profile completion percentage
|
|
480
|
+
*/ async getProfileCompletion(userId, user) {
|
|
481
|
+
this.validateProfileOwnership(userId, user);
|
|
482
|
+
await this.ensureRepositoryInitialized();
|
|
483
|
+
// Verify user exists
|
|
484
|
+
const userEntity = await this.repository.findOne({
|
|
485
|
+
where: {
|
|
486
|
+
id: userId
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
if (!userEntity) {
|
|
490
|
+
throw new _common.NotFoundException('User not found');
|
|
491
|
+
}
|
|
492
|
+
if (this.userEnricher?.calculateProfileCompletion) {
|
|
493
|
+
return this.userEnricher.calculateProfileCompletion(userId, user);
|
|
494
|
+
}
|
|
495
|
+
return 100; // Default to 100% if no enricher
|
|
496
|
+
}
|
|
497
|
+
/** Update user profile with password handling and extras */ async profile(dto, user) {
|
|
498
|
+
// Ensure repository and dataSource are initialized
|
|
499
|
+
await this.ensureRepositoryInitialized();
|
|
500
|
+
if (!this.dataSource) {
|
|
501
|
+
throw new _common.BadRequestException('DataSource not available');
|
|
502
|
+
}
|
|
503
|
+
const userId = dto.id ?? user.id;
|
|
504
|
+
// Validate extras if enricher provides validation
|
|
505
|
+
if (dto.extras && this.userEnricher?.validateProfileExtras) {
|
|
506
|
+
await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
|
|
507
|
+
}
|
|
397
508
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
398
509
|
await queryRunner.connect();
|
|
399
510
|
await queryRunner.startTransaction();
|
|
400
511
|
try {
|
|
401
|
-
const userId = dto.id ?? user.id;
|
|
402
512
|
const findUser = await queryRunner.manager.findOne(this.repository.target, {
|
|
403
513
|
where: {
|
|
404
514
|
id: userId
|
|
@@ -409,18 +519,28 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
409
519
|
}
|
|
410
520
|
// Handle password update
|
|
411
521
|
if (dto.password) {
|
|
412
|
-
// If updating own profile, require old password
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
522
|
+
// If updating own profile, require old password verification
|
|
523
|
+
const isOwnProfile = !dto.id || dto.id === user.id;
|
|
524
|
+
if (isOwnProfile) {
|
|
525
|
+
if (!dto.oldPassword) {
|
|
526
|
+
throw new _common.BadRequestException('Current password is required to change your password');
|
|
527
|
+
}
|
|
528
|
+
const isPasswordValid = await _bcrypt.compare(dto.oldPassword, findUser.password);
|
|
529
|
+
if (!isPasswordValid) {
|
|
530
|
+
throw new _common.BadRequestException('Current password is incorrect');
|
|
531
|
+
}
|
|
416
532
|
}
|
|
417
|
-
|
|
418
|
-
findUser.password = await _bcrypt.hash(dto.password, salt);
|
|
533
|
+
findUser.password = await _bcrypt.hash(dto.password, _config.BCRYPT_SALT_ROUNDS);
|
|
419
534
|
}
|
|
420
|
-
// Merge other fields (excluding password)
|
|
421
|
-
|
|
535
|
+
// Merge other fields (excluding password, oldPassword, extras, and sensitive flags)
|
|
536
|
+
// Users cannot self-modify emailVerified, phoneVerified, or isActive via profile update
|
|
537
|
+
const { password, oldPassword, extras, emailVerified, phoneVerified, isActive, ...restDto } = dto;
|
|
422
538
|
Object.assign(findUser, restDto);
|
|
423
539
|
await queryRunner.manager.save(this.repository.target, findUser);
|
|
540
|
+
// Handle extras update via enricher
|
|
541
|
+
if (extras && this.userEnricher?.updateProfileExtras) {
|
|
542
|
+
await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
|
|
543
|
+
}
|
|
424
544
|
await queryRunner.commitTransaction();
|
|
425
545
|
await this.clearCacheForId([
|
|
426
546
|
findUser
|
|
@@ -436,10 +556,8 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
436
556
|
await queryRunner.release();
|
|
437
557
|
}
|
|
438
558
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
// Repository will be set asynchronously by RequestScopedApiService
|
|
442
|
-
super('user', null, cacheManager, utilsService, UserService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "authConfig", void 0), _define_property(this, "logger", void 0), _define_property(this, "permissionRepository", void 0), _define_property(this, "dataSource", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig, this.logger = new _common.Logger(UserService.name);
|
|
559
|
+
constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
|
|
560
|
+
super('user', null, cacheManager, utilsService, UserService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "authConfig", void 0), _define_property(this, "userEnricher", void 0), _define_property(this, "permissionRepository", void 0), _define_property(this, "dataSource", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig, this.userEnricher = userEnricher;
|
|
443
561
|
}
|
|
444
562
|
};
|
|
445
563
|
UserService = _ts_decorate([
|
|
@@ -450,11 +568,14 @@ UserService = _ts_decorate([
|
|
|
450
568
|
_ts_param(1, (0, _common.Inject)(_modules.UtilsService)),
|
|
451
569
|
_ts_param(2, (0, _common.Inject)(_authdatasourceprovider.AuthDataSourceProvider)),
|
|
452
570
|
_ts_param(3, (0, _common.Inject)(_authconfigservice.AuthConfigService)),
|
|
571
|
+
_ts_param(4, (0, _common.Optional)()),
|
|
572
|
+
_ts_param(4, (0, _common.Inject)(_config.USER_ENRICHER)),
|
|
453
573
|
_ts_metadata("design:type", Function),
|
|
454
574
|
_ts_metadata("design:paramtypes", [
|
|
455
575
|
typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
|
|
456
576
|
typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
|
|
457
577
|
typeof _authdatasourceprovider.AuthDataSourceProvider === "undefined" ? Object : _authdatasourceprovider.AuthDataSourceProvider,
|
|
458
|
-
typeof _authconfigservice.AuthConfigService === "undefined" ? Object : _authconfigservice.AuthConfigService
|
|
578
|
+
typeof _authconfigservice.AuthConfigService === "undefined" ? Object : _authconfigservice.AuthConfigService,
|
|
579
|
+
typeof _userenricherinterface.IUserEnricher === "undefined" ? Object : _userenricherinterface.IUserEnricher
|
|
459
580
|
])
|
|
460
581
|
], UserService);
|
|
@@ -14,6 +14,7 @@ const _passport = require("@nestjs/passport");
|
|
|
14
14
|
const _passportjwt = require("passport-jwt");
|
|
15
15
|
const _authconfigservice = require("../services/auth-config.service");
|
|
16
16
|
const _authdatasourceprovider = require("../services/auth-datasource.provider");
|
|
17
|
+
const _authenticationservice = require("../services/authentication.service");
|
|
17
18
|
const _userentity = require("../entities/user.entity");
|
|
18
19
|
function _define_property(obj, key, value) {
|
|
19
20
|
if (key in obj) {
|
|
@@ -44,8 +45,9 @@ function _ts_metadata(k, v) {
|
|
|
44
45
|
}
|
|
45
46
|
let JwtStrategy = class JwtStrategy extends (0, _passport.PassportStrategy)(_passportjwt.Strategy, 'jwt') {
|
|
46
47
|
async validate(_req, payload) {
|
|
47
|
-
// Resolve request-scoped
|
|
48
|
+
// Resolve request-scoped services for this request
|
|
48
49
|
let userRepository = null;
|
|
50
|
+
let authService = null;
|
|
49
51
|
try {
|
|
50
52
|
const dataSourceProvider = await this.moduleRef.resolve(_authdatasourceprovider.AuthDataSourceProvider, undefined, {
|
|
51
53
|
strict: false
|
|
@@ -53,12 +55,23 @@ let JwtStrategy = class JwtStrategy extends (0, _passport.PassportStrategy)(_pas
|
|
|
53
55
|
if (dataSourceProvider) {
|
|
54
56
|
userRepository = await dataSourceProvider.getRepository(_userentity.User);
|
|
55
57
|
}
|
|
58
|
+
// Resolve AuthenticationService for token revocation check
|
|
59
|
+
authService = await this.moduleRef.resolve(_authenticationservice.AuthenticationService, undefined, {
|
|
60
|
+
strict: false
|
|
61
|
+
});
|
|
56
62
|
} catch (error) {
|
|
57
63
|
throw new _common.UnauthorizedException('Unable to access user data');
|
|
58
64
|
}
|
|
59
65
|
if (!userRepository) {
|
|
60
66
|
throw new _common.UnauthorizedException('User repository not available');
|
|
61
67
|
}
|
|
68
|
+
// Check if token is revoked (logged out)
|
|
69
|
+
if (authService && payload.iat) {
|
|
70
|
+
const isRevoked = await authService.isTokenRevoked(payload.id, payload.iat);
|
|
71
|
+
if (isRevoked) {
|
|
72
|
+
throw new _common.UnauthorizedException('Token has been revoked');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
62
75
|
// Find user and check if active
|
|
63
76
|
const user = await userRepository.findOne({
|
|
64
77
|
where: {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "IsStrongPassword", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return IsStrongPassword;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _classvalidator = require("class-validator");
|
|
12
|
+
function IsStrongPassword(options = {}, validationOptions) {
|
|
13
|
+
const { minLength = 8, maxLength = 128, requireUppercase = true, requireLowercase = true, requireNumber = true, requireSpecialChar = true } = options;
|
|
14
|
+
return function(object, propertyName) {
|
|
15
|
+
(0, _classvalidator.registerDecorator)({
|
|
16
|
+
name: 'isStrongPassword',
|
|
17
|
+
target: object.constructor,
|
|
18
|
+
propertyName: propertyName,
|
|
19
|
+
options: validationOptions,
|
|
20
|
+
validator: {
|
|
21
|
+
validate (value) {
|
|
22
|
+
if (typeof value !== 'string') return false;
|
|
23
|
+
const checks = [
|
|
24
|
+
value.length >= minLength,
|
|
25
|
+
value.length <= maxLength,
|
|
26
|
+
!requireUppercase || /[A-Z]/.test(value),
|
|
27
|
+
!requireLowercase || /[a-z]/.test(value),
|
|
28
|
+
!requireNumber || /[0-9]/.test(value),
|
|
29
|
+
!requireSpecialChar || /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(value)
|
|
30
|
+
];
|
|
31
|
+
return checks.every(Boolean);
|
|
32
|
+
},
|
|
33
|
+
defaultMessage (args) {
|
|
34
|
+
const requirements = [];
|
|
35
|
+
if (minLength > 0) requirements.push(`at least ${minLength} characters`);
|
|
36
|
+
if (requireUppercase) requirements.push('one uppercase letter');
|
|
37
|
+
if (requireLowercase) requirements.push('one lowercase letter');
|
|
38
|
+
if (requireNumber) requirements.push('one number');
|
|
39
|
+
if (requireSpecialChar) requirements.push('one special character (!@#$%^&*...)');
|
|
40
|
+
return `Password must contain ${requirements.join(', ')}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
export declare const AUTH_MODULE_OPTIONS = "AUTH_MODULE_OPTIONS";
|
|
2
|
+
export declare const USER_ENRICHER = "USER_ENRICHER";
|
|
3
|
+
export declare const BCRYPT_SALT_ROUNDS = 12;
|
|
4
|
+
export declare const DEFAULT_COOKIE_NAME = "fsn_refresh_token";
|
|
5
|
+
export declare const MAX_COOKIE_BYTES = 3500;
|
|
2
6
|
export declare const SESSION_CACHE_PREFIX = "auth:session";
|
|
7
|
+
export declare const TOKEN_REVOKED_PREFIX = "auth:revoked";
|
|
@@ -1,22 +1,26 @@
|
|
|
1
1
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
2
2
|
import { Request } from 'express';
|
|
3
|
-
import { ChangePasswordDto, CompanySelectionResponseDto, LoginDto, LoginResponseDto, MeResponseDto, RefreshTokenDto, RegistrationDto, RegistrationResponseDto } from '../dtos/authentication.dto';
|
|
3
|
+
import { ChangePasswordDto, CompanySelectionResponseDto, ForgotPasswordDto, LoginDto, LoginResponseDto, MeResponseDto, RefreshTokenDto, RegistrationDto, RegistrationResponseDto, ResendVerificationDto, ResetPasswordDto, VerifyEmailDto } from '../dtos/authentication.dto';
|
|
4
|
+
import { MessageResponseDto } from '@flusys/nestjs-shared/dtos';
|
|
4
5
|
import { AuthConfigService } from '../services/auth-config.service';
|
|
6
|
+
import { AuthEmailService } from '../services/auth-email.service';
|
|
5
7
|
import { AuthenticationService } from '../services/authentication.service';
|
|
6
8
|
export declare class AuthenticationController {
|
|
7
9
|
private readonly authService;
|
|
8
10
|
private readonly authConfig?;
|
|
9
|
-
|
|
11
|
+
private readonly authEmailService?;
|
|
12
|
+
constructor(authService: AuthenticationService, authConfig?: AuthConfigService, authEmailService?: AuthEmailService);
|
|
13
|
+
private buildAuthUrl;
|
|
10
14
|
login(loginDto: LoginDto): Promise<LoginResponseDto | CompanySelectionResponseDto>;
|
|
11
|
-
register(registrationDto: RegistrationDto): Promise<RegistrationResponseDto>;
|
|
15
|
+
register(req: Request, registrationDto: RegistrationDto): Promise<RegistrationResponseDto>;
|
|
12
16
|
refreshToken(req: Request, refreshTokenDto: RefreshTokenDto): Promise<{
|
|
13
17
|
accessToken: string;
|
|
14
18
|
}>;
|
|
15
19
|
getCurrentUser(user: ILoggedUserInfo): Promise<MeResponseDto>;
|
|
16
|
-
logout(userId: string): Promise<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
logout(userId: string): Promise<MessageResponseDto>;
|
|
21
|
+
changePassword(userId: string, dto: ChangePasswordDto): Promise<MessageResponseDto>;
|
|
22
|
+
forgotPassword(req: Request, dto: ForgotPasswordDto): Promise<MessageResponseDto>;
|
|
23
|
+
resetPassword(dto: ResetPasswordDto): Promise<MessageResponseDto>;
|
|
24
|
+
resendVerification(req: Request, dto: ResendVerificationDto): Promise<MessageResponseDto>;
|
|
25
|
+
verifyEmail(dto: VerifyEmailDto): Promise<MessageResponseDto | RegistrationResponseDto>;
|
|
22
26
|
}
|
|
@@ -6,9 +6,13 @@ export declare class CompanySelectionController {
|
|
|
6
6
|
select(dto: SelectDto): Promise<LoginResponseDto>;
|
|
7
7
|
switchCompany(userId: string, dto: SwitchCompanyDto): Promise<LoginResponseDto>;
|
|
8
8
|
getUserCompanies(userId: string): Promise<{
|
|
9
|
-
|
|
9
|
+
success: boolean;
|
|
10
|
+
message: string;
|
|
11
|
+
data: import("..").Company[];
|
|
10
12
|
}>;
|
|
11
13
|
getUserBranches(userId: string, companyId: string): Promise<{
|
|
12
|
-
|
|
14
|
+
success: boolean;
|
|
15
|
+
message: string;
|
|
16
|
+
data: import("..").CompanyBranch[];
|
|
13
17
|
}>;
|
|
14
18
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
2
2
|
import { CreateUserDto, UpdateUserDto, UserResponseDto } from '../dtos/user.dto';
|
|
3
|
+
import { IUser } from '../interfaces/user.interface';
|
|
3
4
|
import { UserService } from '../services/user.service';
|
|
4
5
|
declare const UserController_base: abstract new (service: UserService) => {
|
|
5
6
|
service: UserService;
|
|
@@ -14,18 +15,50 @@ declare const UserController_base: abstract new (service: UserService) => {
|
|
|
14
15
|
export declare class UserController extends UserController_base {
|
|
15
16
|
service: UserService;
|
|
16
17
|
constructor(service: UserService);
|
|
18
|
+
getProfileWithExtras(id: string, user: ILoggedUserInfo): Promise<{
|
|
19
|
+
success: boolean;
|
|
20
|
+
data: {
|
|
21
|
+
user: IUser;
|
|
22
|
+
extras?: import("..").IProfileExtras;
|
|
23
|
+
};
|
|
24
|
+
}>;
|
|
17
25
|
verifyEmail(id: string): Promise<{
|
|
26
|
+
success: boolean;
|
|
18
27
|
message: string;
|
|
19
28
|
}>;
|
|
20
29
|
verifyPhone(id: string): Promise<{
|
|
30
|
+
success: boolean;
|
|
21
31
|
message: string;
|
|
22
32
|
}>;
|
|
23
33
|
updateStatus(id: string, isActive: boolean, user: ILoggedUserInfo): Promise<{
|
|
34
|
+
success: boolean;
|
|
24
35
|
message: string;
|
|
25
36
|
}>;
|
|
26
37
|
profile(dto: UpdateUserDto, user: ILoggedUserInfo): Promise<{
|
|
38
|
+
success: boolean;
|
|
27
39
|
message: string;
|
|
28
40
|
data: string;
|
|
29
41
|
}>;
|
|
42
|
+
getProfileSections(): Promise<{
|
|
43
|
+
success: boolean;
|
|
44
|
+
message: string;
|
|
45
|
+
data: import("..").IProfileSection[];
|
|
46
|
+
}>;
|
|
47
|
+
getProfileSectionData(id: string, sectionId: string, user: ILoggedUserInfo): Promise<{
|
|
48
|
+
success: boolean;
|
|
49
|
+
message: string;
|
|
50
|
+
data: import("..").IProfileSectionData;
|
|
51
|
+
}>;
|
|
52
|
+
updateProfileSection(id: string, sectionId: string, data: Record<string, any>, user: ILoggedUserInfo): Promise<{
|
|
53
|
+
success: boolean;
|
|
54
|
+
message: string;
|
|
55
|
+
}>;
|
|
56
|
+
getProfileCompletion(id: string, user: ILoggedUserInfo): Promise<{
|
|
57
|
+
success: boolean;
|
|
58
|
+
message: string;
|
|
59
|
+
data: {
|
|
60
|
+
completionPercentage: number;
|
|
61
|
+
};
|
|
62
|
+
}>;
|
|
30
63
|
}
|
|
31
64
|
export {};
|