@flusys/nestjs-auth 1.0.0-beta → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +590 -473
- package/cjs/config/auth.constants.js +12 -0
- package/cjs/controllers/authentication.controller.js +365 -24
- package/cjs/controllers/branch.controller.js +44 -1
- package/cjs/controllers/company-selection.controller.js +27 -20
- package/cjs/controllers/company.controller.js +44 -1
- package/cjs/controllers/user-permission.controller.js +82 -154
- package/cjs/controllers/user.controller.js +211 -7
- package/cjs/docs/auth-swagger.config.js +14 -7
- package/cjs/dtos/authentication.dto.js +114 -38
- package/cjs/dtos/index.js +0 -1
- package/cjs/dtos/user-permission.dto.js +28 -0
- package/cjs/dtos/user.dto.js +19 -4
- package/cjs/entities/index.js +5 -1
- package/cjs/entities/pending-registration.entity.js +125 -0
- package/cjs/entities/user-company-permission.entity.js +3 -9
- package/cjs/entities/user-token.entity.js +129 -0
- package/cjs/interceptors/clear-token.interceptor.js +10 -23
- package/cjs/interceptors/set-token.interceptor.js +15 -55
- package/cjs/interfaces/auth-email-provider.interface.js +45 -0
- package/cjs/interfaces/index.js +2 -0
- package/cjs/interfaces/user-enricher.interface.js +4 -0
- package/cjs/modules/auth.module.js +65 -143
- package/cjs/services/auth-config.service.js +25 -27
- package/cjs/services/auth-datasource.provider.js +13 -23
- package/cjs/services/auth-email.service.js +456 -0
- package/cjs/services/authentication.service.js +297 -349
- package/cjs/services/branch.service.js +9 -4
- package/cjs/services/company-selection-session.service.js +1 -1
- package/cjs/services/index.js +1 -0
- package/cjs/services/user-permission.service.js +43 -73
- package/cjs/services/user.service.js +184 -34
- package/cjs/strategies/jwt.strategy.js +44 -44
- package/cjs/validators/password.validator.js +45 -0
- package/config/auth.constants.d.ts +3 -0
- package/controllers/authentication.controller.d.ts +16 -10
- package/controllers/company-selection.controller.d.ts +6 -2
- package/controllers/user-permission.controller.d.ts +5 -4
- package/controllers/user.controller.d.ts +33 -0
- package/dtos/authentication.dto.d.ts +20 -6
- package/dtos/company-branch.dto.d.ts +0 -1
- package/dtos/company.dto.d.ts +0 -1
- package/dtos/index.d.ts +0 -1
- package/dtos/user-permission.dto.d.ts +4 -0
- package/dtos/user.dto.d.ts +1 -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 +1 -3
- package/entities/user-token.entity.d.ts +15 -0
- package/fesm/config/auth.constants.js +6 -2
- package/fesm/controllers/authentication.controller.js +368 -27
- package/fesm/controllers/branch.controller.js +45 -2
- package/fesm/controllers/company-selection.controller.js +28 -21
- package/fesm/controllers/company.controller.js +45 -2
- package/fesm/controllers/user-permission.controller.js +84 -156
- package/fesm/controllers/user.controller.js +214 -10
- package/fesm/docs/auth-swagger.config.js +14 -7
- package/fesm/dtos/authentication.dto.js +101 -39
- package/fesm/dtos/index.js +0 -1
- package/fesm/dtos/user-permission.dto.js +28 -0
- package/fesm/dtos/user.dto.js +20 -5
- package/fesm/entities/index.js +7 -1
- package/fesm/entities/pending-registration.entity.js +115 -0
- package/fesm/entities/user-company-permission.entity.js +3 -9
- package/fesm/entities/user-token.entity.js +113 -0
- package/fesm/interceptors/clear-token.interceptor.js +9 -22
- package/fesm/interceptors/set-token.interceptor.js +14 -54
- package/fesm/interfaces/auth-email-provider.interface.js +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 +67 -145
- package/fesm/services/auth-config.service.js +26 -28
- package/fesm/services/auth-datasource.provider.js +14 -24
- package/fesm/services/auth-email.service.js +405 -0
- package/fesm/services/authentication.service.js +297 -349
- package/fesm/services/branch.service.js +9 -4
- package/fesm/services/company-selection-session.service.js +1 -1
- package/fesm/services/index.js +1 -0
- package/fesm/services/user-permission.service.js +44 -74
- package/fesm/services/user.service.js +186 -36
- package/fesm/strategies/jwt.strategy.js +44 -44
- package/fesm/validators/password.validator.js +48 -0
- package/interceptors/clear-token.interceptor.d.ts +0 -1
- package/interceptors/set-token.interceptor.d.ts +1 -1
- package/interfaces/auth-email-provider.interface.d.ts +6 -0
- package/interfaces/auth-module-options.interface.d.ts +4 -1
- package/interfaces/authentication.interface.d.ts +13 -3
- 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 +6 -3
- package/package.json +4 -4
- package/services/auth-config.service.d.ts +7 -8
- package/services/auth-datasource.provider.d.ts +2 -4
- package/services/auth-email.service.d.ts +54 -0
- package/services/authentication.service.d.ts +27 -16
- package/services/company-selection-session.service.d.ts +1 -1
- package/services/index.d.ts +1 -0
- package/services/user-permission.service.d.ts +6 -9
- package/services/user.service.d.ts +19 -3
- package/strategies/jwt.strategy.d.ts +4 -1
- package/validators/password.validator.d.ts +10 -0
- package/cjs/dtos/user-company-permission.dto.js +0 -110
- package/dtos/user-company-permission.dto.d.ts +0 -10
- package/fesm/dtos/user-company-permission.dto.js +0 -98
|
@@ -27,6 +27,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
|
+
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
30
31
|
import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
32
|
import { CompanyBranch } from '../entities/company-branch.entity';
|
|
32
33
|
import { AuthConfigService } from './auth-config.service';
|
|
@@ -66,10 +67,14 @@ export class BranchService extends RequestScopedApiService {
|
|
|
66
67
|
}
|
|
67
68
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
68
69
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
// Only apply company filter if companyId is NOT explicitly provided in filter
|
|
71
|
+
// This prevents dual filtering when user selects a specific company
|
|
72
|
+
if (!filterDto?.filter?.companyId) {
|
|
73
|
+
applyCompanyFilter(query, {
|
|
74
|
+
isCompanyFeatureEnabled: this.authConfig.isCompanyFeatureEnabled(),
|
|
75
|
+
entityAlias: 'company_branch',
|
|
76
|
+
columnName: 'company_id'
|
|
77
|
+
}, user);
|
|
73
78
|
}
|
|
74
79
|
return result;
|
|
75
80
|
}
|
|
@@ -33,7 +33,7 @@ const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
33
33
|
export class CompanySelectionSessionService {
|
|
34
34
|
/**
|
|
35
35
|
* Create a new session for company/branch selection
|
|
36
|
-
*/ async createSession(userId
|
|
36
|
+
*/ async createSession(userId) {
|
|
37
37
|
const sessionId = randomUUID();
|
|
38
38
|
const now = new Date();
|
|
39
39
|
const expiresAt = new Date(now.getTime() + SESSION_TTL_MS);
|
package/fesm/services/index.js
CHANGED
|
@@ -25,126 +25,96 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
28
|
+
import { ConflictException, Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
|
|
29
29
|
import { PermissionType, UserCompanyPermission } from '../entities/user-company-permission.entity';
|
|
30
30
|
import { AuthDataSourceProvider } from './auth-datasource.provider';
|
|
31
31
|
export class UserPermissionService {
|
|
32
|
-
|
|
32
|
+
async getRepository() {
|
|
33
33
|
if (!this._repository) {
|
|
34
34
|
this._repository = await this.dataSourceProvider.getRepository(UserCompanyPermission);
|
|
35
35
|
}
|
|
36
36
|
return this._repository;
|
|
37
37
|
}
|
|
38
|
-
//
|
|
39
|
-
/** Find all
|
|
38
|
+
// Generic Permission Operations
|
|
39
|
+
/** Find all permissions of a specific type for a user */ async findUserPermissions(userId, type) {
|
|
40
40
|
const repository = await this.getRepository();
|
|
41
41
|
return repository.find({
|
|
42
42
|
where: {
|
|
43
43
|
userId,
|
|
44
|
-
permissionType:
|
|
44
|
+
permissionType: type
|
|
45
45
|
},
|
|
46
46
|
relations: [
|
|
47
|
-
'company'
|
|
47
|
+
type === PermissionType.COMPANY ? 'company' : 'branch'
|
|
48
48
|
],
|
|
49
49
|
order: {
|
|
50
50
|
createdAt: 'DESC'
|
|
51
51
|
}
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
|
-
/** Assign
|
|
54
|
+
/** Assign permission to user */ async assignPermission(userId, targetId, type, metadata) {
|
|
55
55
|
const repository = await this.getRepository();
|
|
56
|
-
|
|
56
|
+
const typeName = type === PermissionType.COMPANY ? 'company' : 'branch';
|
|
57
57
|
const existing = await repository.findOne({
|
|
58
58
|
where: {
|
|
59
|
-
userId
|
|
60
|
-
targetId
|
|
61
|
-
permissionType:
|
|
59
|
+
userId,
|
|
60
|
+
targetId,
|
|
61
|
+
permissionType: type
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
64
|
if (existing) {
|
|
65
|
-
throw new
|
|
65
|
+
throw new ConflictException(`User already assigned to this ${typeName}`);
|
|
66
66
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
permissionType: PermissionType.COMPANY,
|
|
70
|
-
targetId: dto.companyId,
|
|
71
|
-
metadata: dto.metadata
|
|
72
|
-
});
|
|
73
|
-
await repository.save(permission);
|
|
74
|
-
}
|
|
75
|
-
/** Revoke user access to a company */ async revokeCompanyAccess(userId, companyId) {
|
|
76
|
-
const repository = await this.getRepository();
|
|
77
|
-
const permission = await repository.findOne({
|
|
78
|
-
where: {
|
|
67
|
+
try {
|
|
68
|
+
await repository.save(repository.create({
|
|
79
69
|
userId,
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
permissionType: type,
|
|
71
|
+
targetId,
|
|
72
|
+
metadata
|
|
73
|
+
}));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
|
|
76
|
+
throw new ConflictException(`User already assigned to this ${typeName}. Please refresh and try again.`);
|
|
82
77
|
}
|
|
83
|
-
|
|
84
|
-
if (!permission) {
|
|
85
|
-
throw new NotFoundException('Company permission not found');
|
|
78
|
+
throw error;
|
|
86
79
|
}
|
|
87
|
-
await repository.remove(permission);
|
|
88
80
|
}
|
|
89
|
-
|
|
90
|
-
/** Find all branches a user has access to */ async findUserBranches(userId) {
|
|
91
|
-
const repository = await this.getRepository();
|
|
92
|
-
return repository.find({
|
|
93
|
-
where: {
|
|
94
|
-
userId,
|
|
95
|
-
permissionType: PermissionType.BRANCH
|
|
96
|
-
},
|
|
97
|
-
relations: [
|
|
98
|
-
'branch'
|
|
99
|
-
],
|
|
100
|
-
order: {
|
|
101
|
-
createdAt: 'DESC'
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
/** Assign user to a branch */ async assignUserToBranch(dto) {
|
|
106
|
-
const repository = await this.getRepository();
|
|
107
|
-
// Check if already exists
|
|
108
|
-
const existing = await repository.findOne({
|
|
109
|
-
where: {
|
|
110
|
-
userId: dto.userId,
|
|
111
|
-
targetId: dto.branchId,
|
|
112
|
-
permissionType: PermissionType.BRANCH
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
if (existing) {
|
|
116
|
-
throw new BadRequestException('User already assigned to this branch');
|
|
117
|
-
}
|
|
118
|
-
const permission = repository.create({
|
|
119
|
-
userId: dto.userId,
|
|
120
|
-
permissionType: PermissionType.BRANCH,
|
|
121
|
-
targetId: dto.branchId,
|
|
122
|
-
metadata: dto.metadata
|
|
123
|
-
});
|
|
124
|
-
await repository.save(permission);
|
|
125
|
-
}
|
|
126
|
-
/** Revoke user access to a branch */ async revokeBranchAccess(userId, branchId) {
|
|
81
|
+
/** Revoke permission from user */ async revokePermission(userId, targetId, type) {
|
|
127
82
|
const repository = await this.getRepository();
|
|
83
|
+
const typeName = type === PermissionType.COMPANY ? 'company' : 'branch';
|
|
128
84
|
const permission = await repository.findOne({
|
|
129
85
|
where: {
|
|
130
86
|
userId,
|
|
131
|
-
targetId
|
|
132
|
-
permissionType:
|
|
87
|
+
targetId,
|
|
88
|
+
permissionType: type
|
|
133
89
|
}
|
|
134
90
|
});
|
|
135
91
|
if (!permission) {
|
|
136
|
-
throw new NotFoundException(
|
|
92
|
+
throw new NotFoundException(`${typeName.charAt(0).toUpperCase() + typeName.slice(1)} permission not found`);
|
|
137
93
|
}
|
|
138
94
|
await repository.remove(permission);
|
|
139
95
|
}
|
|
140
|
-
|
|
96
|
+
/** Find user branch permissions with optional company filter */ async findUserBranches(userId, companyId) {
|
|
97
|
+
const repository = await this.getRepository();
|
|
98
|
+
const queryBuilder = repository.createQueryBuilder('permission').leftJoinAndSelect('permission.branch', 'branch').where('permission.userId = :userId', {
|
|
99
|
+
userId
|
|
100
|
+
}).andWhere('permission.permissionType = :type', {
|
|
101
|
+
type: PermissionType.BRANCH
|
|
102
|
+
}).orderBy('permission.createdAt', 'DESC');
|
|
103
|
+
if (companyId) {
|
|
104
|
+
queryBuilder.andWhere('branch.companyId = :companyId', {
|
|
105
|
+
companyId
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return queryBuilder.getMany();
|
|
109
|
+
}
|
|
141
110
|
constructor(dataSourceProvider){
|
|
142
111
|
_define_property(this, "dataSourceProvider", void 0);
|
|
143
|
-
_define_property(this, "logger", void 0);
|
|
144
112
|
_define_property(this, "_repository", void 0);
|
|
113
|
+
// Convenience Methods
|
|
114
|
+
_define_property(this, "findUserCompanies", void 0);
|
|
145
115
|
this.dataSourceProvider = dataSourceProvider;
|
|
146
|
-
this.logger = new Logger(UserPermissionService.name);
|
|
147
116
|
this._repository = null;
|
|
117
|
+
this.findUserCompanies = (userId)=>this.findUserPermissions(userId, PermissionType.COMPANY);
|
|
148
118
|
}
|
|
149
119
|
}
|
|
150
120
|
UserPermissionService = _ts_decorate([
|
|
@@ -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,24 @@ export class UserService extends RequestScopedApiService {
|
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
// Custom Methods
|
|
62
|
+
/**
|
|
63
|
+
* Validate profile ownership, ensure repository initialized, and fetch user.
|
|
64
|
+
* Throws ForbiddenException if not owner, NotFoundException if user not found.
|
|
65
|
+
*/ async getUserForProfile(userId, user) {
|
|
66
|
+
if (userId !== user.id) {
|
|
67
|
+
throw new ForbiddenException('Cannot access another user\'s profile');
|
|
68
|
+
}
|
|
69
|
+
await this.ensureRepositoryInitialized();
|
|
70
|
+
const userEntity = await this.repository.findOne({
|
|
71
|
+
where: {
|
|
72
|
+
id: userId
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
if (!userEntity) {
|
|
76
|
+
throw new NotFoundException('User not found');
|
|
77
|
+
}
|
|
78
|
+
return userEntity;
|
|
79
|
+
}
|
|
61
80
|
/** Verify email */ async verifyEmail(id) {
|
|
62
81
|
// Ensure repository is initialized for custom method
|
|
63
82
|
await this.ensureRepositoryInitialized();
|
|
@@ -119,33 +138,58 @@ export class UserService extends RequestScopedApiService {
|
|
|
119
138
|
/** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
|
|
120
139
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
121
140
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
122
|
-
// Hash password if provided
|
|
123
|
-
if (dto.password) {
|
|
141
|
+
// Hash password if provided (CreateUserDto has password field)
|
|
142
|
+
if ('password' in dto && dto.password) {
|
|
124
143
|
entity.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
125
144
|
}
|
|
126
145
|
return entity;
|
|
127
146
|
}
|
|
128
147
|
/** Convert entity to response DTO (removes password). In raw mode with company feature, isActive comes from permission table. */ convertEntityToResponseDto(entity, isRaw) {
|
|
148
|
+
if (isRaw) {
|
|
149
|
+
return this.convertRawToResponseDto(entity);
|
|
150
|
+
}
|
|
151
|
+
return this.convertEntityToDto(entity);
|
|
152
|
+
}
|
|
153
|
+
/** Convert raw query result to response DTO */ convertRawToResponseDto(raw) {
|
|
129
154
|
// 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;
|
|
155
|
+
const isActive = raw.user_companyIsActive !== undefined ? Boolean(raw.user_companyIsActive) : raw.user_is_active;
|
|
132
156
|
return {
|
|
133
|
-
id:
|
|
134
|
-
name:
|
|
135
|
-
phone:
|
|
136
|
-
email:
|
|
137
|
-
profilePictureId:
|
|
157
|
+
id: raw.user_id,
|
|
158
|
+
name: raw.user_name,
|
|
159
|
+
phone: raw.user_phone,
|
|
160
|
+
email: raw.user_email,
|
|
161
|
+
profilePictureId: raw.user_profile_picture_id,
|
|
138
162
|
isActive,
|
|
139
|
-
emailVerified:
|
|
140
|
-
phoneVerified:
|
|
141
|
-
lastLoginAt:
|
|
142
|
-
additionalFields:
|
|
143
|
-
createdAt:
|
|
144
|
-
updatedAt:
|
|
145
|
-
deletedAt:
|
|
146
|
-
createdById:
|
|
147
|
-
updatedById:
|
|
148
|
-
deletedById:
|
|
163
|
+
emailVerified: raw.user_email_verified,
|
|
164
|
+
phoneVerified: raw.user_phone_verified,
|
|
165
|
+
lastLoginAt: raw.user_last_login_at,
|
|
166
|
+
additionalFields: raw.user_additional_fields,
|
|
167
|
+
createdAt: raw.user_created_at,
|
|
168
|
+
updatedAt: raw.user_updated_at,
|
|
169
|
+
deletedAt: raw.user_deleted_at,
|
|
170
|
+
createdById: raw.user_created_by_id,
|
|
171
|
+
updatedById: raw.user_updated_by_id,
|
|
172
|
+
deletedById: raw.user_deleted_by_id
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Convert entity to response DTO */ convertEntityToDto(entity) {
|
|
176
|
+
return {
|
|
177
|
+
id: entity.id,
|
|
178
|
+
name: entity.name,
|
|
179
|
+
phone: entity.phone,
|
|
180
|
+
email: entity.email,
|
|
181
|
+
profilePictureId: entity.profilePictureId,
|
|
182
|
+
isActive: entity.isActive,
|
|
183
|
+
emailVerified: entity.emailVerified,
|
|
184
|
+
phoneVerified: entity.phoneVerified,
|
|
185
|
+
lastLoginAt: entity.lastLoginAt,
|
|
186
|
+
additionalFields: entity.additionalFields,
|
|
187
|
+
createdAt: entity.createdAt,
|
|
188
|
+
updatedAt: entity.updatedAt,
|
|
189
|
+
deletedAt: entity.deletedAt,
|
|
190
|
+
createdById: entity.createdById,
|
|
191
|
+
updatedById: entity.updatedById,
|
|
192
|
+
deletedById: entity.deletedById
|
|
149
193
|
};
|
|
150
194
|
}
|
|
151
195
|
/** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
|
|
@@ -162,7 +206,11 @@ export class UserService extends RequestScopedApiService {
|
|
|
162
206
|
}
|
|
163
207
|
/** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
|
|
164
208
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
165
|
-
|
|
209
|
+
let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
|
|
210
|
+
// Apply user enricher query modifications if available
|
|
211
|
+
if (this.userEnricher?.enrichListQuery) {
|
|
212
|
+
await this.userEnricher.enrichListQuery(query, user);
|
|
213
|
+
}
|
|
166
214
|
// Only apply company filtering if:
|
|
167
215
|
// 1. Company feature is enabled at bootstrap
|
|
168
216
|
// 2. User has company context
|
|
@@ -282,16 +330,105 @@ export class UserService extends RequestScopedApiService {
|
|
|
282
330
|
await queryRunner.release();
|
|
283
331
|
}
|
|
284
332
|
}
|
|
285
|
-
/**
|
|
286
|
-
|
|
333
|
+
/**
|
|
334
|
+
* Get user profile with extras from enricher
|
|
335
|
+
* Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
|
|
336
|
+
*/ async getProfileWithExtras(userId, user) {
|
|
337
|
+
const userEntity = await this.getUserForProfile(userId, user);
|
|
338
|
+
const userData = this.convertEntityToResponseDto(userEntity, false);
|
|
339
|
+
// Get extras from enricher if available
|
|
340
|
+
let extras;
|
|
341
|
+
if (this.userEnricher?.getProfileExtras) {
|
|
342
|
+
extras = await this.userEnricher.getProfileExtras(userId, user);
|
|
343
|
+
}
|
|
344
|
+
return {
|
|
345
|
+
user: userData,
|
|
346
|
+
extras
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get enriched user list
|
|
351
|
+
* Applies enricher transformations after query execution
|
|
352
|
+
*/ async getEnrichedList(users, user) {
|
|
353
|
+
if (this.userEnricher?.enrichListItems) {
|
|
354
|
+
return this.userEnricher.enrichListItems(users, user);
|
|
355
|
+
}
|
|
356
|
+
return users;
|
|
357
|
+
}
|
|
358
|
+
// ============================================
|
|
359
|
+
// Multi-Step Profile Methods (via Enricher)
|
|
360
|
+
// ============================================
|
|
361
|
+
/**
|
|
362
|
+
* Get profile section definitions from enricher
|
|
363
|
+
*/ async getProfileSections() {
|
|
364
|
+
if (this.userEnricher?.getProfileSections) {
|
|
365
|
+
return this.userEnricher.getProfileSections();
|
|
366
|
+
}
|
|
367
|
+
return [];
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Get specific profile section data
|
|
371
|
+
*/ async getProfileSectionData(userId, sectionId, user) {
|
|
372
|
+
await this.getUserForProfile(userId, user);
|
|
373
|
+
if (this.userEnricher?.getProfileSectionData) {
|
|
374
|
+
return this.userEnricher.getProfileSectionData(userId, sectionId, user);
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Update specific profile section
|
|
380
|
+
*/ async updateProfileSection(userId, sectionId, data, user) {
|
|
381
|
+
const userEntity = await this.getUserForProfile(userId, user);
|
|
287
382
|
if (!this.dataSource) {
|
|
288
383
|
throw new BadRequestException('DataSource not available');
|
|
289
384
|
}
|
|
385
|
+
if (!this.userEnricher?.updateProfileSection) {
|
|
386
|
+
throw new BadRequestException('Profile sections not supported');
|
|
387
|
+
}
|
|
388
|
+
const queryRunner = this.dataSource.createQueryRunner();
|
|
389
|
+
await queryRunner.connect();
|
|
390
|
+
await queryRunner.startTransaction();
|
|
391
|
+
try {
|
|
392
|
+
await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
|
|
393
|
+
await queryRunner.commitTransaction();
|
|
394
|
+
await this.clearCacheForId([
|
|
395
|
+
userEntity
|
|
396
|
+
]);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
await queryRunner.rollbackTransaction();
|
|
399
|
+
ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
|
|
400
|
+
entity: this.entityName,
|
|
401
|
+
sectionId
|
|
402
|
+
});
|
|
403
|
+
ErrorHandler.rethrowError(error);
|
|
404
|
+
} finally{
|
|
405
|
+
await queryRunner.release();
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Get profile completion percentage
|
|
410
|
+
*/ async getProfileCompletion(userId, user) {
|
|
411
|
+
await this.getUserForProfile(userId, user);
|
|
412
|
+
if (this.userEnricher?.calculateProfileCompletion) {
|
|
413
|
+
return this.userEnricher.calculateProfileCompletion(userId, user);
|
|
414
|
+
}
|
|
415
|
+
return 100; // Default to 100% if no enricher
|
|
416
|
+
}
|
|
417
|
+
/** Update user profile with password handling and extras */ async profile(dto, user) {
|
|
418
|
+
// Ensure repository and dataSource are initialized
|
|
419
|
+
await this.ensureRepositoryInitialized();
|
|
420
|
+
if (!this.dataSource) {
|
|
421
|
+
throw new BadRequestException('DataSource not available');
|
|
422
|
+
}
|
|
423
|
+
const userId = dto.id ?? user.id;
|
|
424
|
+
// Validate extras if enricher provides validation
|
|
425
|
+
if (dto.extras && this.userEnricher?.validateProfileExtras) {
|
|
426
|
+
await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
|
|
427
|
+
}
|
|
290
428
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
291
429
|
await queryRunner.connect();
|
|
292
430
|
await queryRunner.startTransaction();
|
|
293
431
|
try {
|
|
294
|
-
const userId = dto.id ?? user.id;
|
|
295
432
|
const findUser = await queryRunner.manager.findOne(this.repository.target, {
|
|
296
433
|
where: {
|
|
297
434
|
id: userId
|
|
@@ -302,18 +439,28 @@ export class UserService extends RequestScopedApiService {
|
|
|
302
439
|
}
|
|
303
440
|
// Handle password update
|
|
304
441
|
if (dto.password) {
|
|
305
|
-
// If updating own profile, require old password
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
442
|
+
// If updating own profile, require old password verification
|
|
443
|
+
const isOwnProfile = !dto.id || dto.id === user.id;
|
|
444
|
+
if (isOwnProfile) {
|
|
445
|
+
if (!dto.oldPassword) {
|
|
446
|
+
throw new BadRequestException('Current password is required to change your password');
|
|
447
|
+
}
|
|
448
|
+
const isPasswordValid = await bcrypt.compare(dto.oldPassword, findUser.password);
|
|
449
|
+
if (!isPasswordValid) {
|
|
450
|
+
throw new BadRequestException('Current password is incorrect');
|
|
451
|
+
}
|
|
309
452
|
}
|
|
310
|
-
|
|
311
|
-
findUser.password = await bcrypt.hash(dto.password, salt);
|
|
453
|
+
findUser.password = await bcrypt.hash(dto.password, BCRYPT_SALT_ROUNDS);
|
|
312
454
|
}
|
|
313
|
-
// Merge other fields (excluding password)
|
|
314
|
-
|
|
455
|
+
// Merge other fields (excluding password, oldPassword, extras, and sensitive flags)
|
|
456
|
+
// Users cannot self-modify emailVerified, phoneVerified, or isActive via profile update
|
|
457
|
+
const { password, oldPassword, extras, emailVerified, phoneVerified, isActive, ...restDto } = dto;
|
|
315
458
|
Object.assign(findUser, restDto);
|
|
316
459
|
await queryRunner.manager.save(this.repository.target, findUser);
|
|
460
|
+
// Handle extras update via enricher
|
|
461
|
+
if (extras && this.userEnricher?.updateProfileExtras) {
|
|
462
|
+
await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
|
|
463
|
+
}
|
|
317
464
|
await queryRunner.commitTransaction();
|
|
318
465
|
await this.clearCacheForId([
|
|
319
466
|
findUser
|
|
@@ -329,8 +476,8 @@ export class UserService extends RequestScopedApiService {
|
|
|
329
476
|
await queryRunner.release();
|
|
330
477
|
}
|
|
331
478
|
}
|
|
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;
|
|
479
|
+
constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
|
|
480
|
+
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
481
|
}
|
|
335
482
|
}
|
|
336
483
|
UserService = _ts_decorate([
|
|
@@ -341,11 +488,14 @@ UserService = _ts_decorate([
|
|
|
341
488
|
_ts_param(1, Inject(UtilsService)),
|
|
342
489
|
_ts_param(2, Inject(AuthDataSourceProvider)),
|
|
343
490
|
_ts_param(3, Inject(AuthConfigService)),
|
|
491
|
+
_ts_param(4, Optional()),
|
|
492
|
+
_ts_param(4, Inject(USER_ENRICHER)),
|
|
344
493
|
_ts_metadata("design:type", Function),
|
|
345
494
|
_ts_metadata("design:paramtypes", [
|
|
346
495
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
347
496
|
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
348
497
|
typeof AuthDataSourceProvider === "undefined" ? Object : AuthDataSourceProvider,
|
|
349
|
-
typeof AuthConfigService === "undefined" ? Object : AuthConfigService
|
|
498
|
+
typeof AuthConfigService === "undefined" ? Object : AuthConfigService,
|
|
499
|
+
typeof IUserEnricher === "undefined" ? Object : IUserEnricher
|
|
350
500
|
])
|
|
351
501
|
], UserService);
|
|
@@ -24,75 +24,75 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
|
|
|
24
24
|
import { ModuleRef } from '@nestjs/core';
|
|
25
25
|
import { PassportStrategy } from '@nestjs/passport';
|
|
26
26
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
|
27
|
+
import { User } from '../entities/user.entity';
|
|
27
28
|
import { AuthConfigService } from '../services/auth-config.service';
|
|
28
29
|
import { AuthDataSourceProvider } from '../services/auth-datasource.provider';
|
|
29
30
|
import { AuthenticationService } from '../services/authentication.service';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Type guard to check if payload includes company context
|
|
33
|
-
*/ function isTokenPayloadWithCompany(payload) {
|
|
31
|
+
function hasCompanyContext(payload) {
|
|
34
32
|
return 'companyId' in payload && 'branchId' in payload;
|
|
35
33
|
}
|
|
36
34
|
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|
37
35
|
async validate(_req, payload) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
const userRepo = await this.resolveUserRepository();
|
|
37
|
+
await this.checkTokenRevocation(payload);
|
|
38
|
+
const user = await userRepo.findOne({
|
|
39
|
+
where: {
|
|
40
|
+
id: payload.id,
|
|
41
|
+
isActive: true
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
if (!user) {
|
|
45
|
+
throw new UnauthorizedException('User not found or inactive');
|
|
46
|
+
}
|
|
47
|
+
return this.buildUserInfo(payload);
|
|
48
|
+
}
|
|
49
|
+
async resolveUserRepository() {
|
|
41
50
|
try {
|
|
42
|
-
const
|
|
51
|
+
const provider = await this.moduleRef.resolve(AuthDataSourceProvider, undefined, {
|
|
43
52
|
strict: false
|
|
44
53
|
});
|
|
45
|
-
if (
|
|
46
|
-
|
|
54
|
+
if (provider) {
|
|
55
|
+
return provider.getRepository(User);
|
|
47
56
|
}
|
|
48
|
-
|
|
49
|
-
authService = await this.moduleRef.resolve(AuthenticationService, undefined, {
|
|
50
|
-
strict: false
|
|
51
|
-
});
|
|
52
|
-
} catch (error) {
|
|
57
|
+
} catch {
|
|
53
58
|
throw new UnauthorizedException('Unable to access user data');
|
|
54
59
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
60
|
+
throw new UnauthorizedException('User repository not available');
|
|
61
|
+
}
|
|
62
|
+
async checkTokenRevocation(payload) {
|
|
63
|
+
if (!payload.iat) return;
|
|
64
|
+
try {
|
|
65
|
+
const authService = await this.moduleRef.resolve(AuthenticationService, undefined, {
|
|
66
|
+
strict: false
|
|
67
|
+
});
|
|
68
|
+
if (authService && await authService.isTokenRevoked(payload.id, payload.iat)) {
|
|
62
69
|
throw new UnauthorizedException('Token has been revoked');
|
|
63
70
|
}
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (err instanceof UnauthorizedException) throw err;
|
|
64
73
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
id: payload.id,
|
|
69
|
-
isActive: true
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
if (!user) {
|
|
73
|
-
throw new UnauthorizedException('User not found or inactive');
|
|
74
|
-
}
|
|
75
|
-
const result = {
|
|
74
|
+
}
|
|
75
|
+
buildUserInfo(payload) {
|
|
76
|
+
const base = {
|
|
76
77
|
id: payload.id,
|
|
77
78
|
email: payload.email
|
|
78
79
|
};
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
result.companyId = payload.companyId;
|
|
82
|
-
// Only set branchId if it's not empty string
|
|
83
|
-
if (payload.branchId) {
|
|
84
|
-
result.branchId = payload.branchId;
|
|
85
|
-
}
|
|
80
|
+
if (!hasCompanyContext(payload)) {
|
|
81
|
+
return base;
|
|
86
82
|
}
|
|
87
|
-
return
|
|
83
|
+
return {
|
|
84
|
+
...base,
|
|
85
|
+
companyId: payload.companyId,
|
|
86
|
+
...payload.branchId && {
|
|
87
|
+
branchId: payload.branchId
|
|
88
|
+
}
|
|
89
|
+
};
|
|
88
90
|
}
|
|
89
91
|
constructor(authConfig, moduleRef){
|
|
90
92
|
super({
|
|
91
93
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
92
94
|
ignoreExpiration: false,
|
|
93
|
-
|
|
94
|
-
// This ensures AuthConfigService is fully initialized when using forRootAsync
|
|
95
|
-
secretOrKeyProvider: (_request, _rawJwtToken, done)=>{
|
|
95
|
+
secretOrKeyProvider: (_req, _token, done)=>{
|
|
96
96
|
done(null, this.authConfig.getJwtSecret());
|
|
97
97
|
},
|
|
98
98
|
passReqToCallback: true
|