@flusys/nestjs-auth 1.0.0-beta → 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 -291
- package/cjs/config/auth.constants.js +12 -0
- package/cjs/controllers/authentication.controller.js +219 -10
- 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 +40 -25
- package/cjs/controllers/user.controller.js +211 -7
- 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/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 +23 -2
- package/cjs/services/auth-datasource.provider.js +5 -3
- package/cjs/services/auth-email.service.js +471 -0
- package/cjs/services/authentication.service.js +169 -77
- package/cjs/services/branch.service.js +6 -5
- package/cjs/services/company-selection-session.service.js +1 -1
- package/cjs/services/index.js +1 -0
- package/cjs/services/user-permission.service.js +0 -2
- package/cjs/services/user.service.js +213 -34
- package/cjs/validators/password.validator.js +45 -0
- package/config/auth.constants.d.ts +3 -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 +6 -2
- package/fesm/controllers/authentication.controller.js +221 -12
- 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 +41 -26
- package/fesm/controllers/user.controller.js +214 -10
- package/fesm/dtos/authentication.dto.js +106 -14
- 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 -1
- 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 +24 -3
- package/fesm/services/auth-datasource.provider.js +6 -4
- package/fesm/services/auth-email.service.js +420 -0
- package/fesm/services/authentication.service.js +169 -77
- package/fesm/services/branch.service.js +6 -5
- package/fesm/services/company-selection-session.service.js +1 -1
- package/fesm/services/index.js +1 -0
- package/fesm/services/user-permission.service.js +1 -3
- package/fesm/services/user.service.js +215 -36
- 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 +16 -11
- package/services/company-selection-session.service.d.ts +1 -1
- package/services/index.d.ts +1 -0
- package/services/user-permission.service.d.ts +0 -1
- package/services/user.service.d.ts +19 -3
- package/validators/password.validator.d.ts +10 -0
|
@@ -28,12 +28,13 @@ function _ts_param(paramIndex, decorator) {
|
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
30
|
import { ErrorHandler } from '@flusys/nestjs-shared/utils';
|
|
31
|
-
import { BadRequestException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
|
|
31
|
+
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException, Optional, Scope } from '@nestjs/common';
|
|
32
32
|
import * as bcrypt from 'bcrypt';
|
|
33
33
|
import { AuthConfigService } from './auth-config.service';
|
|
34
34
|
import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
35
|
-
import { BCRYPT_SALT_ROUNDS } from '../config';
|
|
35
|
+
import { BCRYPT_SALT_ROUNDS, USER_ENRICHER } from '../config';
|
|
36
36
|
import { PermissionType } from '../enums';
|
|
37
|
+
import { IUserEnricher } from '../interfaces/user-enricher.interface';
|
|
37
38
|
import { UserCompanyPermission } from '../entities/user-company-permission.entity';
|
|
38
39
|
import { User } from '../entities/user.entity';
|
|
39
40
|
export class UserService extends RequestScopedApiService {
|
|
@@ -58,6 +59,14 @@ export class UserService extends RequestScopedApiService {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
// Custom Methods
|
|
62
|
+
/**
|
|
63
|
+
* Validate that the requesting user owns the target profile.
|
|
64
|
+
* Users can only access/modify their own profile data.
|
|
65
|
+
*/ validateProfileOwnership(targetUserId, requestingUser) {
|
|
66
|
+
if (targetUserId !== requestingUser.id) {
|
|
67
|
+
throw new ForbiddenException('Cannot access another user\'s profile');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
61
70
|
/** Verify email */ async verifyEmail(id) {
|
|
62
71
|
// Ensure repository is initialized for custom method
|
|
63
72
|
await this.ensureRepositoryInitialized();
|
|
@@ -119,33 +128,58 @@ export class UserService extends RequestScopedApiService {
|
|
|
119
128
|
/** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
|
|
120
129
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
121
130
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
122
|
-
// Hash password if provided
|
|
123
|
-
if (dto.password) {
|
|
131
|
+
// Hash password if provided (CreateUserDto has password field)
|
|
132
|
+
if ('password' in dto && dto.password) {
|
|
124
133
|
entity.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
125
134
|
}
|
|
126
135
|
return entity;
|
|
127
136
|
}
|
|
128
137
|
/** Convert entity to response DTO (removes password). In raw mode with company feature, isActive comes from permission table. */ convertEntityToResponseDto(entity, isRaw) {
|
|
138
|
+
if (isRaw) {
|
|
139
|
+
return this.convertRawToResponseDto(entity);
|
|
140
|
+
}
|
|
141
|
+
return this.convertEntityToDto(entity);
|
|
142
|
+
}
|
|
143
|
+
/** Convert raw query result to response DTO */ convertRawToResponseDto(raw) {
|
|
129
144
|
// When raw mode is used with company feature, isActive comes from computed field
|
|
130
|
-
|
|
131
|
-
const isActive = isRaw && entity.user_companyIsActive !== undefined ? Boolean(entity.user_companyIsActive) : isRaw ? entity.user_is_active : entity.isActive;
|
|
145
|
+
const isActive = raw.user_companyIsActive !== undefined ? Boolean(raw.user_companyIsActive) : raw.user_is_active;
|
|
132
146
|
return {
|
|
133
|
-
id:
|
|
134
|
-
name:
|
|
135
|
-
phone:
|
|
136
|
-
email:
|
|
137
|
-
profilePictureId:
|
|
147
|
+
id: raw.user_id,
|
|
148
|
+
name: raw.user_name,
|
|
149
|
+
phone: raw.user_phone,
|
|
150
|
+
email: raw.user_email,
|
|
151
|
+
profilePictureId: raw.user_profile_picture_id,
|
|
138
152
|
isActive,
|
|
139
|
-
emailVerified:
|
|
140
|
-
phoneVerified:
|
|
141
|
-
lastLoginAt:
|
|
142
|
-
additionalFields:
|
|
143
|
-
createdAt:
|
|
144
|
-
updatedAt:
|
|
145
|
-
deletedAt:
|
|
146
|
-
createdById:
|
|
147
|
-
updatedById:
|
|
148
|
-
deletedById:
|
|
153
|
+
emailVerified: raw.user_email_verified,
|
|
154
|
+
phoneVerified: raw.user_phone_verified,
|
|
155
|
+
lastLoginAt: raw.user_last_login_at,
|
|
156
|
+
additionalFields: raw.user_additional_fields,
|
|
157
|
+
createdAt: raw.user_created_at,
|
|
158
|
+
updatedAt: raw.user_updated_at,
|
|
159
|
+
deletedAt: raw.user_deleted_at,
|
|
160
|
+
createdById: raw.user_created_by_id,
|
|
161
|
+
updatedById: raw.user_updated_by_id,
|
|
162
|
+
deletedById: raw.user_deleted_by_id
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/** Convert entity to response DTO */ convertEntityToDto(entity) {
|
|
166
|
+
return {
|
|
167
|
+
id: entity.id,
|
|
168
|
+
name: entity.name,
|
|
169
|
+
phone: entity.phone,
|
|
170
|
+
email: entity.email,
|
|
171
|
+
profilePictureId: entity.profilePictureId,
|
|
172
|
+
isActive: entity.isActive,
|
|
173
|
+
emailVerified: entity.emailVerified,
|
|
174
|
+
phoneVerified: entity.phoneVerified,
|
|
175
|
+
lastLoginAt: entity.lastLoginAt,
|
|
176
|
+
additionalFields: entity.additionalFields,
|
|
177
|
+
createdAt: entity.createdAt,
|
|
178
|
+
updatedAt: entity.updatedAt,
|
|
179
|
+
deletedAt: entity.deletedAt,
|
|
180
|
+
createdById: entity.createdById,
|
|
181
|
+
updatedById: entity.updatedById,
|
|
182
|
+
deletedById: entity.deletedById
|
|
149
183
|
};
|
|
150
184
|
}
|
|
151
185
|
/** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
|
|
@@ -162,7 +196,11 @@ export class UserService extends RequestScopedApiService {
|
|
|
162
196
|
}
|
|
163
197
|
/** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
|
|
164
198
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
165
|
-
|
|
199
|
+
let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
|
|
200
|
+
// Apply user enricher query modifications if available
|
|
201
|
+
if (this.userEnricher?.enrichListQuery) {
|
|
202
|
+
await this.userEnricher.enrichListQuery(query, user);
|
|
203
|
+
}
|
|
166
204
|
// Only apply company filtering if:
|
|
167
205
|
// 1. Company feature is enabled at bootstrap
|
|
168
206
|
// 2. User has company context
|
|
@@ -282,16 +320,144 @@ export class UserService extends RequestScopedApiService {
|
|
|
282
320
|
await queryRunner.release();
|
|
283
321
|
}
|
|
284
322
|
}
|
|
285
|
-
/**
|
|
286
|
-
|
|
323
|
+
/**
|
|
324
|
+
* Get user profile with extras from enricher
|
|
325
|
+
* Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
|
|
326
|
+
*/ async getProfileWithExtras(userId, user) {
|
|
327
|
+
this.validateProfileOwnership(userId, user);
|
|
328
|
+
await this.ensureRepositoryInitialized();
|
|
329
|
+
const userEntity = await this.repository.findOne({
|
|
330
|
+
where: {
|
|
331
|
+
id: userId
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
if (!userEntity) {
|
|
335
|
+
throw new NotFoundException('User not found');
|
|
336
|
+
}
|
|
337
|
+
const userData = this.convertEntityToResponseDto(userEntity, false);
|
|
338
|
+
// Get extras from enricher if available
|
|
339
|
+
let extras;
|
|
340
|
+
if (this.userEnricher?.getProfileExtras) {
|
|
341
|
+
extras = await this.userEnricher.getProfileExtras(userId, user);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
user: userData,
|
|
345
|
+
extras
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Get enriched user list
|
|
350
|
+
* Applies enricher transformations after query execution
|
|
351
|
+
*/ async getEnrichedList(users, user) {
|
|
352
|
+
if (this.userEnricher?.enrichListItems) {
|
|
353
|
+
return this.userEnricher.enrichListItems(users, user);
|
|
354
|
+
}
|
|
355
|
+
return users;
|
|
356
|
+
}
|
|
357
|
+
// ============================================
|
|
358
|
+
// Multi-Step Profile Methods (via Enricher)
|
|
359
|
+
// ============================================
|
|
360
|
+
/**
|
|
361
|
+
* Get profile section definitions from enricher
|
|
362
|
+
*/ async getProfileSections() {
|
|
363
|
+
if (this.userEnricher?.getProfileSections) {
|
|
364
|
+
return this.userEnricher.getProfileSections();
|
|
365
|
+
}
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Get specific profile section data
|
|
370
|
+
*/ async getProfileSectionData(userId, sectionId, user) {
|
|
371
|
+
this.validateProfileOwnership(userId, user);
|
|
372
|
+
await this.ensureRepositoryInitialized();
|
|
373
|
+
// Verify user exists
|
|
374
|
+
const userEntity = await this.repository.findOne({
|
|
375
|
+
where: {
|
|
376
|
+
id: userId
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
if (!userEntity) {
|
|
380
|
+
throw new NotFoundException('User not found');
|
|
381
|
+
}
|
|
382
|
+
if (this.userEnricher?.getProfileSectionData) {
|
|
383
|
+
return this.userEnricher.getProfileSectionData(userId, sectionId, user);
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Update specific profile section
|
|
389
|
+
*/ async updateProfileSection(userId, sectionId, data, user) {
|
|
390
|
+
this.validateProfileOwnership(userId, user);
|
|
391
|
+
await this.ensureRepositoryInitialized();
|
|
287
392
|
if (!this.dataSource) {
|
|
288
393
|
throw new BadRequestException('DataSource not available');
|
|
289
394
|
}
|
|
395
|
+
// Verify user exists
|
|
396
|
+
const userEntity = await this.repository.findOne({
|
|
397
|
+
where: {
|
|
398
|
+
id: userId
|
|
399
|
+
}
|
|
400
|
+
});
|
|
401
|
+
if (!userEntity) {
|
|
402
|
+
throw new NotFoundException('User not found');
|
|
403
|
+
}
|
|
404
|
+
if (!this.userEnricher?.updateProfileSection) {
|
|
405
|
+
throw new BadRequestException('Profile sections not supported');
|
|
406
|
+
}
|
|
407
|
+
const queryRunner = this.dataSource.createQueryRunner();
|
|
408
|
+
await queryRunner.connect();
|
|
409
|
+
await queryRunner.startTransaction();
|
|
410
|
+
try {
|
|
411
|
+
await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
|
|
412
|
+
await queryRunner.commitTransaction();
|
|
413
|
+
await this.clearCacheForId([
|
|
414
|
+
userEntity
|
|
415
|
+
]);
|
|
416
|
+
} catch (error) {
|
|
417
|
+
await queryRunner.rollbackTransaction();
|
|
418
|
+
ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
|
|
419
|
+
entity: this.entityName,
|
|
420
|
+
sectionId
|
|
421
|
+
});
|
|
422
|
+
ErrorHandler.rethrowError(error);
|
|
423
|
+
} finally{
|
|
424
|
+
await queryRunner.release();
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Get profile completion percentage
|
|
429
|
+
*/ async getProfileCompletion(userId, user) {
|
|
430
|
+
this.validateProfileOwnership(userId, user);
|
|
431
|
+
await this.ensureRepositoryInitialized();
|
|
432
|
+
// Verify user exists
|
|
433
|
+
const userEntity = await this.repository.findOne({
|
|
434
|
+
where: {
|
|
435
|
+
id: userId
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
if (!userEntity) {
|
|
439
|
+
throw new NotFoundException('User not found');
|
|
440
|
+
}
|
|
441
|
+
if (this.userEnricher?.calculateProfileCompletion) {
|
|
442
|
+
return this.userEnricher.calculateProfileCompletion(userId, user);
|
|
443
|
+
}
|
|
444
|
+
return 100; // Default to 100% if no enricher
|
|
445
|
+
}
|
|
446
|
+
/** Update user profile with password handling and extras */ async profile(dto, user) {
|
|
447
|
+
// Ensure repository and dataSource are initialized
|
|
448
|
+
await this.ensureRepositoryInitialized();
|
|
449
|
+
if (!this.dataSource) {
|
|
450
|
+
throw new BadRequestException('DataSource not available');
|
|
451
|
+
}
|
|
452
|
+
const userId = dto.id ?? user.id;
|
|
453
|
+
// Validate extras if enricher provides validation
|
|
454
|
+
if (dto.extras && this.userEnricher?.validateProfileExtras) {
|
|
455
|
+
await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
|
|
456
|
+
}
|
|
290
457
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
291
458
|
await queryRunner.connect();
|
|
292
459
|
await queryRunner.startTransaction();
|
|
293
460
|
try {
|
|
294
|
-
const userId = dto.id ?? user.id;
|
|
295
461
|
const findUser = await queryRunner.manager.findOne(this.repository.target, {
|
|
296
462
|
where: {
|
|
297
463
|
id: userId
|
|
@@ -302,18 +468,28 @@ export class UserService extends RequestScopedApiService {
|
|
|
302
468
|
}
|
|
303
469
|
// Handle password update
|
|
304
470
|
if (dto.password) {
|
|
305
|
-
// If updating own profile, require old password
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
471
|
+
// If updating own profile, require old password verification
|
|
472
|
+
const isOwnProfile = !dto.id || dto.id === user.id;
|
|
473
|
+
if (isOwnProfile) {
|
|
474
|
+
if (!dto.oldPassword) {
|
|
475
|
+
throw new BadRequestException('Current password is required to change your password');
|
|
476
|
+
}
|
|
477
|
+
const isPasswordValid = await bcrypt.compare(dto.oldPassword, findUser.password);
|
|
478
|
+
if (!isPasswordValid) {
|
|
479
|
+
throw new BadRequestException('Current password is incorrect');
|
|
480
|
+
}
|
|
309
481
|
}
|
|
310
|
-
|
|
311
|
-
findUser.password = await bcrypt.hash(dto.password, salt);
|
|
482
|
+
findUser.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
312
483
|
}
|
|
313
|
-
// Merge other fields (excluding password)
|
|
314
|
-
|
|
484
|
+
// Merge other fields (excluding password, oldPassword, extras, and sensitive flags)
|
|
485
|
+
// Users cannot self-modify emailVerified, phoneVerified, or isActive via profile update
|
|
486
|
+
const { password, oldPassword, extras, emailVerified, phoneVerified, isActive, ...restDto } = dto;
|
|
315
487
|
Object.assign(findUser, restDto);
|
|
316
488
|
await queryRunner.manager.save(this.repository.target, findUser);
|
|
489
|
+
// Handle extras update via enricher
|
|
490
|
+
if (extras && this.userEnricher?.updateProfileExtras) {
|
|
491
|
+
await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
|
|
492
|
+
}
|
|
317
493
|
await queryRunner.commitTransaction();
|
|
318
494
|
await this.clearCacheForId([
|
|
319
495
|
findUser
|
|
@@ -329,8 +505,8 @@ export class UserService extends RequestScopedApiService {
|
|
|
329
505
|
await queryRunner.release();
|
|
330
506
|
}
|
|
331
507
|
}
|
|
332
|
-
constructor(cacheManager, utilsService, dataSourceProvider, authConfig){
|
|
333
|
-
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, "permissionRepository", void 0), _define_property(this, "dataSource", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.dataSourceProvider = dataSourceProvider, this.authConfig = authConfig;
|
|
508
|
+
constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
|
|
509
|
+
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;
|
|
334
510
|
}
|
|
335
511
|
}
|
|
336
512
|
UserService = _ts_decorate([
|
|
@@ -341,11 +517,14 @@ UserService = _ts_decorate([
|
|
|
341
517
|
_ts_param(1, Inject(UtilsService)),
|
|
342
518
|
_ts_param(2, Inject(AuthDataSourceProvider)),
|
|
343
519
|
_ts_param(3, Inject(AuthConfigService)),
|
|
520
|
+
_ts_param(4, Optional()),
|
|
521
|
+
_ts_param(4, Inject(USER_ENRICHER)),
|
|
344
522
|
_ts_metadata("design:type", Function),
|
|
345
523
|
_ts_metadata("design:paramtypes", [
|
|
346
524
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
347
525
|
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
348
526
|
typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
|
|
349
|
-
typeof AuthConfigService === "undefined" ? Object : AuthConfigService
|
|
527
|
+
typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
|
|
528
|
+
typeof IUserEnricher === "undefined" ? Object : IUserEnricher
|
|
350
529
|
])
|
|
351
530
|
], UserService);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { registerDecorator } from 'class-validator';
|
|
2
|
+
/**
|
|
3
|
+
* Strong password validation decorator.
|
|
4
|
+
* Validates that passwords meet complexity requirements to prevent weak passwords.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* @IsStrongPassword()
|
|
9
|
+
* password!: string;
|
|
10
|
+
*
|
|
11
|
+
* // With custom options
|
|
12
|
+
* @IsStrongPassword({ minLength: 12, requireSpecialChar: false })
|
|
13
|
+
* password!: string;
|
|
14
|
+
* ```
|
|
15
|
+
*/ export function IsStrongPassword(options = {}, validationOptions) {
|
|
16
|
+
const { minLength = 8, maxLength = 128, requireUppercase = true, requireLowercase = true, requireNumber = true, requireSpecialChar = true } = options;
|
|
17
|
+
return function(object, propertyName) {
|
|
18
|
+
registerDecorator({
|
|
19
|
+
name: 'isStrongPassword',
|
|
20
|
+
target: object.constructor,
|
|
21
|
+
propertyName: propertyName,
|
|
22
|
+
options: validationOptions,
|
|
23
|
+
validator: {
|
|
24
|
+
validate (value) {
|
|
25
|
+
if (typeof value !== 'string') return false;
|
|
26
|
+
const checks = [
|
|
27
|
+
value.length >= minLength,
|
|
28
|
+
value.length <= maxLength,
|
|
29
|
+
!requireUppercase || /[A-Z]/.test(value),
|
|
30
|
+
!requireLowercase || /[a-z]/.test(value),
|
|
31
|
+
!requireNumber || /[0-9]/.test(value),
|
|
32
|
+
!requireSpecialChar || /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(value)
|
|
33
|
+
];
|
|
34
|
+
return checks.every(Boolean);
|
|
35
|
+
},
|
|
36
|
+
defaultMessage (args) {
|
|
37
|
+
const requirements = [];
|
|
38
|
+
if (minLength > 0) requirements.push(`at least ${minLength} characters`);
|
|
39
|
+
if (requireUppercase) requirements.push('one uppercase letter');
|
|
40
|
+
if (requireLowercase) requirements.push('one lowercase letter');
|
|
41
|
+
if (requireNumber) requirements.push('one number');
|
|
42
|
+
if (requireSpecialChar) requirements.push('one special character (!@#$%^&*...)');
|
|
43
|
+
return `Password must contain ${requirements.join(', ')}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ExecutionContext } from '@nestjs/common';
|
|
2
|
+
import { ThrottlerGuard } from '@nestjs/throttler';
|
|
3
|
+
export declare class AuthThrottlerGuard extends ThrottlerGuard {
|
|
4
|
+
protected getTracker(req: Record<string, unknown>): Promise<string>;
|
|
5
|
+
protected throwThrottlingException(): Promise<void>;
|
|
6
|
+
protected shouldSkip(context: ExecutionContext): Promise<boolean>;
|
|
7
|
+
}
|
|
@@ -5,6 +5,7 @@ export declare class SetToken implements NestInterceptor {
|
|
|
5
5
|
private readonly authConfig?;
|
|
6
6
|
constructor(authConfig?: AuthConfigService);
|
|
7
7
|
private get cookieName();
|
|
8
|
+
private getMaxAgeMs;
|
|
8
9
|
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
9
10
|
private isBrowserRequest;
|
|
10
11
|
private setRefreshTokenCookie;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export interface IAuthEmailProvider {
|
|
2
|
+
sendPasswordResetEmail(email: string, token: string, resetUrl: string): Promise<void>;
|
|
3
|
+
sendVerificationEmail(email: string, token: string, verifyUrl: string): Promise<void>;
|
|
4
|
+
sendWelcomeEmail?(email: string, name: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
export declare const AUTH_EMAIL_PROVIDER = "AUTH_EMAIL_PROVIDER";
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
|
|
2
|
-
import { ModuleMetadata, Type } from '@nestjs/common';
|
|
2
|
+
import { ModuleMetadata, Provider, Type } from '@nestjs/common';
|
|
3
3
|
export interface IAuthModuleConfig extends IDataSourceServiceOptions {
|
|
4
4
|
jwtSecret?: string;
|
|
5
5
|
jwtExpiration?: string;
|
|
6
6
|
refreshTokenSecret?: string;
|
|
7
7
|
refreshTokenExpiration?: string;
|
|
8
8
|
refreshTokenCookieName?: string;
|
|
9
|
+
frontendUrl?: string;
|
|
9
10
|
}
|
|
10
11
|
export interface AuthModuleOptions extends IDynamicModuleConfig {
|
|
11
12
|
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
12
13
|
config?: IAuthModuleConfig;
|
|
14
|
+
providers?: Provider[];
|
|
13
15
|
}
|
|
14
16
|
export interface AuthOptionsFactory extends IModuleOptionsFactory<IAuthModuleConfig> {
|
|
15
17
|
createAuthOptions(): Promise<IAuthModuleConfig> | IAuthModuleConfig;
|
|
@@ -21,4 +23,5 @@ export interface AuthModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>,
|
|
|
21
23
|
inject?: any[];
|
|
22
24
|
useClass?: Type<AuthOptionsFactory>;
|
|
23
25
|
useExisting?: Type<AuthOptionsFactory>;
|
|
26
|
+
providers?: Provider[];
|
|
24
27
|
}
|
|
@@ -51,3 +51,17 @@ export interface LoginResponseRequiresSelection {
|
|
|
51
51
|
user: IUserMini;
|
|
52
52
|
companies: CompanyWithBranches[];
|
|
53
53
|
}
|
|
54
|
+
export interface RegistrationResponse {
|
|
55
|
+
accessToken?: string;
|
|
56
|
+
refreshToken?: string;
|
|
57
|
+
user: IUserMini;
|
|
58
|
+
company?: ICompanyMini;
|
|
59
|
+
branch?: ICompanyBranchMini;
|
|
60
|
+
companyFeatureEnabled?: boolean;
|
|
61
|
+
requiresEmailVerification?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface LoginResponseRequiresEmailVerification {
|
|
64
|
+
requiresEmailVerification: true;
|
|
65
|
+
email: string;
|
|
66
|
+
message: string;
|
|
67
|
+
}
|
package/interfaces/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from './authentication.interface';
|
|
2
|
+
export * from './auth-email-provider.interface';
|
|
2
3
|
export * from './company-branch.interface';
|
|
3
4
|
export * from './company.interface';
|
|
4
5
|
export * from './user.interface';
|
|
6
|
+
export * from './user-enricher.interface';
|
|
5
7
|
export * from './auth-module-options.interface';
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
2
|
+
import { QueryRunner, SelectQueryBuilder } from 'typeorm';
|
|
3
|
+
import { User } from '../entities/user.entity';
|
|
4
|
+
import { IUser } from './user.interface';
|
|
5
|
+
export interface IProfileSection {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
order: number;
|
|
9
|
+
icon?: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
hasFileUpload?: boolean;
|
|
12
|
+
editPermission?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface IProfileSectionData {
|
|
15
|
+
sectionId: string;
|
|
16
|
+
data: Record<string, any>;
|
|
17
|
+
isComplete: boolean;
|
|
18
|
+
files?: IProfileFile[];
|
|
19
|
+
}
|
|
20
|
+
export interface IProfileFile {
|
|
21
|
+
field: string;
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
url?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface IProfileExtras {
|
|
28
|
+
roles?: IProfileRole[];
|
|
29
|
+
actions?: IProfileAction[];
|
|
30
|
+
sections?: IProfileSectionData[];
|
|
31
|
+
completionPercentage?: number;
|
|
32
|
+
}
|
|
33
|
+
export interface IProfileRole {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
description?: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface IProfileAction {
|
|
39
|
+
id: string;
|
|
40
|
+
name: string;
|
|
41
|
+
code: string;
|
|
42
|
+
description?: string | null;
|
|
43
|
+
}
|
|
44
|
+
export type IEnrichedUser = IUser & IProfileExtras;
|
|
45
|
+
export interface IUserEnricher {
|
|
46
|
+
enrichListQuery?(query: SelectQueryBuilder<User>, user: ILoggedUserInfo | null): Promise<{
|
|
47
|
+
extraFields: string[];
|
|
48
|
+
}>;
|
|
49
|
+
enrichListItems?(users: IUser[], user: ILoggedUserInfo | null): Promise<IEnrichedUser[]>;
|
|
50
|
+
getProfileExtras?(userId: string, user: ILoggedUserInfo): Promise<IProfileExtras>;
|
|
51
|
+
updateProfileExtras?(userId: string, extras: Record<string, any>, queryRunner: QueryRunner): Promise<void>;
|
|
52
|
+
validateProfileExtras?(userId: string, extras: Record<string, any>, user: ILoggedUserInfo): Promise<void>;
|
|
53
|
+
getProfileSections?(): IProfileSection[];
|
|
54
|
+
getProfileSectionData?(userId: string, sectionId: string, user: ILoggedUserInfo): Promise<IProfileSectionData>;
|
|
55
|
+
updateProfileSection?(userId: string, sectionId: string, data: Record<string, any>, queryRunner: QueryRunner): Promise<void>;
|
|
56
|
+
handleSectionFileUpload?(userId: string, sectionId: string, field: string, fileInfo: {
|
|
57
|
+
name: string;
|
|
58
|
+
url: string;
|
|
59
|
+
size: number;
|
|
60
|
+
type: string;
|
|
61
|
+
}, queryRunner: QueryRunner): Promise<IProfileFile>;
|
|
62
|
+
handleSectionFileDelete?(userId: string, sectionId: string, field: string, fileId: string, queryRunner: QueryRunner): Promise<void>;
|
|
63
|
+
calculateProfileCompletion?(userId: string, user: ILoggedUserInfo): Promise<number>;
|
|
64
|
+
}
|
package/modules/auth.module.d.ts
CHANGED
|
@@ -5,6 +5,5 @@ export declare class AuthModule {
|
|
|
5
5
|
static forRootAsync(asyncOptions: AuthModuleAsyncOptions): DynamicModule;
|
|
6
6
|
private static buildDynamicModule;
|
|
7
7
|
private static createAsyncProviders;
|
|
8
|
-
private static getRepositoryProviders;
|
|
9
8
|
static getEntities(enableCompanyFeature?: boolean): any[];
|
|
10
9
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-auth",
|
|
3
|
-
"version": "1.0.0-
|
|
3
|
+
"version": "1.0.0-rc",
|
|
4
4
|
"description": "Authentication and authorization infrastructure for NestJS applications",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -84,14 +84,14 @@
|
|
|
84
84
|
"bcrypt": "^5.0.0 || ^6.0.0",
|
|
85
85
|
"class-transformer": "^0.5.0",
|
|
86
86
|
"class-validator": "^0.14.0",
|
|
87
|
-
"nodemailer": "^
|
|
87
|
+
"nodemailer": "^7.0.0",
|
|
88
88
|
"passport": "^0.7.0",
|
|
89
89
|
"passport-jwt": "^4.0.0",
|
|
90
90
|
"passport-local": "^1.0.0",
|
|
91
91
|
"typeorm": "^0.3.0"
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@flusys/nestjs-core": "1.0.0-
|
|
95
|
-
"@flusys/nestjs-shared": "1.0.0-
|
|
94
|
+
"@flusys/nestjs-core": "1.0.0-rc",
|
|
95
|
+
"@flusys/nestjs-shared": "1.0.0-rc"
|
|
96
96
|
}
|
|
97
97
|
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { DatabaseMode, IDatabaseConfig, ITenantDatabaseConfig } from '@flusys/nestjs-core';
|
|
2
|
+
import { IModuleConfigService } from '@flusys/nestjs-shared/interfaces';
|
|
2
3
|
import { AuthModuleOptions, IAuthModuleConfig } from '../interfaces/auth-module-options.interface';
|
|
3
|
-
export declare class AuthConfigService {
|
|
4
|
+
export declare class AuthConfigService implements IModuleConfigService {
|
|
5
|
+
private readonly logger;
|
|
4
6
|
private readonly options;
|
|
5
7
|
private readonly config;
|
|
8
|
+
private refreshTokenSecretWarned;
|
|
6
9
|
constructor(injectedOptions?: AuthModuleOptions);
|
|
7
10
|
getDatabaseMode(): DatabaseMode;
|
|
8
11
|
isMultiTenant(): boolean;
|
|
@@ -12,6 +15,7 @@ export declare class AuthConfigService {
|
|
|
12
15
|
getRefreshTokenSecret(): string;
|
|
13
16
|
getRefreshTokenExpiration(): string;
|
|
14
17
|
getRefreshTokenCookieName(): string;
|
|
18
|
+
getFrontendUrl(): string | undefined;
|
|
15
19
|
getDefaultDatabaseConfig(): IDatabaseConfig | undefined;
|
|
16
20
|
getTenantDefaultDatabaseConfig(): IDatabaseConfig | undefined;
|
|
17
21
|
getTenants(): ITenantDatabaseConfig[];
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
2
|
+
import { PendingRegistration } from '../entities/pending-registration.entity';
|
|
3
|
+
import { IAuthEmailProvider } from '../interfaces';
|
|
4
|
+
export declare class AuthEmailService {
|
|
5
|
+
private readonly dataSourceProvider?;
|
|
6
|
+
private readonly emailProvider?;
|
|
7
|
+
private readonly logger;
|
|
8
|
+
private readonly PASSWORD_RESET_EXPIRY;
|
|
9
|
+
private readonly EMAIL_VERIFICATION_EXPIRY;
|
|
10
|
+
constructor(dataSourceProvider?: AuthDataSourceProvider, emailProvider?: IAuthEmailProvider);
|
|
11
|
+
private getUserRepository;
|
|
12
|
+
private getUserTokenRepository;
|
|
13
|
+
private getPendingRegistrationRepository;
|
|
14
|
+
isEmailEnabled(): boolean;
|
|
15
|
+
private generateToken;
|
|
16
|
+
private hashToken;
|
|
17
|
+
createPendingRegistration(data: {
|
|
18
|
+
email: string;
|
|
19
|
+
name?: string;
|
|
20
|
+
phone?: string;
|
|
21
|
+
password: string;
|
|
22
|
+
companyData?: PendingRegistration['companyData'];
|
|
23
|
+
verifyUrlBase: string;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
success: boolean;
|
|
26
|
+
message: string;
|
|
27
|
+
}>;
|
|
28
|
+
forgotPassword(email: string, resetUrlBase: string): Promise<{
|
|
29
|
+
success: boolean;
|
|
30
|
+
message: string;
|
|
31
|
+
}>;
|
|
32
|
+
resetPassword(token: string, newPassword: string): Promise<{
|
|
33
|
+
success: boolean;
|
|
34
|
+
message: string;
|
|
35
|
+
}>;
|
|
36
|
+
verifyEmail(token: string): Promise<{
|
|
37
|
+
type: 'pending';
|
|
38
|
+
pending: PendingRegistration;
|
|
39
|
+
} | {
|
|
40
|
+
type: 'verified';
|
|
41
|
+
message: string;
|
|
42
|
+
}>;
|
|
43
|
+
resendVerificationEmail(email: string, verifyUrlBase: string): Promise<{
|
|
44
|
+
success: boolean;
|
|
45
|
+
message: string;
|
|
46
|
+
}>;
|
|
47
|
+
cleanupExpired(): Promise<{
|
|
48
|
+
deletedTokens: number;
|
|
49
|
+
deletedPending: number;
|
|
50
|
+
}>;
|
|
51
|
+
}
|