@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
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "BranchService", {
|
|
|
10
10
|
});
|
|
11
11
|
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
12
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
|
+
const _utils = require("@flusys/nestjs-shared/utils");
|
|
13
14
|
const _common = require("@nestjs/common");
|
|
14
15
|
const _companybranchentity = require("../entities/company-branch.entity");
|
|
15
16
|
const _authconfigservice = require("./auth-config.service");
|
|
@@ -76,10 +77,14 @@ let BranchService = class BranchService extends _classes.RequestScopedApiService
|
|
|
76
77
|
}
|
|
77
78
|
async getExtraManipulateQuery(query, filterDto, user) {
|
|
78
79
|
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
80
|
+
// Only apply company filter if companyId is NOT explicitly provided in filter
|
|
81
|
+
// This prevents dual filtering when user selects a specific company
|
|
82
|
+
if (!filterDto?.filter?.companyId) {
|
|
83
|
+
(0, _utils.applyCompanyFilter)(query, {
|
|
84
|
+
isCompanyFeatureEnabled: this.authConfig.isCompanyFeatureEnabled(),
|
|
85
|
+
entityAlias: 'company_branch',
|
|
86
|
+
columnName: 'company_id'
|
|
87
|
+
}, user);
|
|
83
88
|
}
|
|
84
89
|
return result;
|
|
85
90
|
}
|
|
@@ -43,7 +43,7 @@ const SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
|
43
43
|
let CompanySelectionSessionService = class CompanySelectionSessionService {
|
|
44
44
|
/**
|
|
45
45
|
* Create a new session for company/branch selection
|
|
46
|
-
*/ async createSession(userId
|
|
46
|
+
*/ async createSession(userId) {
|
|
47
47
|
const sessionId = (0, _crypto.randomUUID)();
|
|
48
48
|
const now = new Date();
|
|
49
49
|
const expiresAt = new Date(now.getTime() + SESSION_TTL_MS);
|
package/cjs/services/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./authentication.service"), exports);
|
|
6
|
+
_export_star(require("./auth-email.service"), exports);
|
|
6
7
|
_export_star(require("./company-selection-session.service"), exports);
|
|
7
8
|
_export_star(require("./user.service"), exports);
|
|
8
9
|
_export_star(require("./company.service"), exports);
|
|
@@ -39,122 +39,92 @@ function _ts_param(paramIndex, decorator) {
|
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
let UserPermissionService = class UserPermissionService {
|
|
42
|
-
|
|
42
|
+
async getRepository() {
|
|
43
43
|
if (!this._repository) {
|
|
44
44
|
this._repository = await this.dataSourceProvider.getRepository(_usercompanypermissionentity.UserCompanyPermission);
|
|
45
45
|
}
|
|
46
46
|
return this._repository;
|
|
47
47
|
}
|
|
48
|
-
//
|
|
49
|
-
/** Find all
|
|
48
|
+
// Generic Permission Operations
|
|
49
|
+
/** Find all permissions of a specific type for a user */ async findUserPermissions(userId, type) {
|
|
50
50
|
const repository = await this.getRepository();
|
|
51
51
|
return repository.find({
|
|
52
52
|
where: {
|
|
53
53
|
userId,
|
|
54
|
-
permissionType:
|
|
54
|
+
permissionType: type
|
|
55
55
|
},
|
|
56
56
|
relations: [
|
|
57
|
-
'company'
|
|
57
|
+
type === _usercompanypermissionentity.PermissionType.COMPANY ? 'company' : 'branch'
|
|
58
58
|
],
|
|
59
59
|
order: {
|
|
60
60
|
createdAt: 'DESC'
|
|
61
61
|
}
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
|
-
/** Assign
|
|
64
|
+
/** Assign permission to user */ async assignPermission(userId, targetId, type, metadata) {
|
|
65
65
|
const repository = await this.getRepository();
|
|
66
|
-
|
|
66
|
+
const typeName = type === _usercompanypermissionentity.PermissionType.COMPANY ? 'company' : 'branch';
|
|
67
67
|
const existing = await repository.findOne({
|
|
68
68
|
where: {
|
|
69
|
-
userId
|
|
70
|
-
targetId
|
|
71
|
-
permissionType:
|
|
69
|
+
userId,
|
|
70
|
+
targetId,
|
|
71
|
+
permissionType: type
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
if (existing) {
|
|
75
|
-
throw new _common.
|
|
75
|
+
throw new _common.ConflictException(`User already assigned to this ${typeName}`);
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
permissionType: _usercompanypermissionentity.PermissionType.COMPANY,
|
|
80
|
-
targetId: dto.companyId,
|
|
81
|
-
metadata: dto.metadata
|
|
82
|
-
});
|
|
83
|
-
await repository.save(permission);
|
|
84
|
-
}
|
|
85
|
-
/** Revoke user access to a company */ async revokeCompanyAccess(userId, companyId) {
|
|
86
|
-
const repository = await this.getRepository();
|
|
87
|
-
const permission = await repository.findOne({
|
|
88
|
-
where: {
|
|
77
|
+
try {
|
|
78
|
+
await repository.save(repository.create({
|
|
89
79
|
userId,
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
permissionType: type,
|
|
81
|
+
targetId,
|
|
82
|
+
metadata
|
|
83
|
+
}));
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
|
|
86
|
+
throw new _common.ConflictException(`User already assigned to this ${typeName}. Please refresh and try again.`);
|
|
92
87
|
}
|
|
93
|
-
|
|
94
|
-
if (!permission) {
|
|
95
|
-
throw new _common.NotFoundException('Company permission not found');
|
|
88
|
+
throw error;
|
|
96
89
|
}
|
|
97
|
-
await repository.remove(permission);
|
|
98
90
|
}
|
|
99
|
-
|
|
100
|
-
/** Find all branches a user has access to */ async findUserBranches(userId) {
|
|
101
|
-
const repository = await this.getRepository();
|
|
102
|
-
return repository.find({
|
|
103
|
-
where: {
|
|
104
|
-
userId,
|
|
105
|
-
permissionType: _usercompanypermissionentity.PermissionType.BRANCH
|
|
106
|
-
},
|
|
107
|
-
relations: [
|
|
108
|
-
'branch'
|
|
109
|
-
],
|
|
110
|
-
order: {
|
|
111
|
-
createdAt: 'DESC'
|
|
112
|
-
}
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
/** Assign user to a branch */ async assignUserToBranch(dto) {
|
|
116
|
-
const repository = await this.getRepository();
|
|
117
|
-
// Check if already exists
|
|
118
|
-
const existing = await repository.findOne({
|
|
119
|
-
where: {
|
|
120
|
-
userId: dto.userId,
|
|
121
|
-
targetId: dto.branchId,
|
|
122
|
-
permissionType: _usercompanypermissionentity.PermissionType.BRANCH
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
if (existing) {
|
|
126
|
-
throw new _common.BadRequestException('User already assigned to this branch');
|
|
127
|
-
}
|
|
128
|
-
const permission = repository.create({
|
|
129
|
-
userId: dto.userId,
|
|
130
|
-
permissionType: _usercompanypermissionentity.PermissionType.BRANCH,
|
|
131
|
-
targetId: dto.branchId,
|
|
132
|
-
metadata: dto.metadata
|
|
133
|
-
});
|
|
134
|
-
await repository.save(permission);
|
|
135
|
-
}
|
|
136
|
-
/** Revoke user access to a branch */ async revokeBranchAccess(userId, branchId) {
|
|
91
|
+
/** Revoke permission from user */ async revokePermission(userId, targetId, type) {
|
|
137
92
|
const repository = await this.getRepository();
|
|
93
|
+
const typeName = type === _usercompanypermissionentity.PermissionType.COMPANY ? 'company' : 'branch';
|
|
138
94
|
const permission = await repository.findOne({
|
|
139
95
|
where: {
|
|
140
96
|
userId,
|
|
141
|
-
targetId
|
|
142
|
-
permissionType:
|
|
97
|
+
targetId,
|
|
98
|
+
permissionType: type
|
|
143
99
|
}
|
|
144
100
|
});
|
|
145
101
|
if (!permission) {
|
|
146
|
-
throw new _common.NotFoundException(
|
|
102
|
+
throw new _common.NotFoundException(`${typeName.charAt(0).toUpperCase() + typeName.slice(1)} permission not found`);
|
|
147
103
|
}
|
|
148
104
|
await repository.remove(permission);
|
|
149
105
|
}
|
|
150
|
-
|
|
106
|
+
/** Find user branch permissions with optional company filter */ async findUserBranches(userId, companyId) {
|
|
107
|
+
const repository = await this.getRepository();
|
|
108
|
+
const queryBuilder = repository.createQueryBuilder('permission').leftJoinAndSelect('permission.branch', 'branch').where('permission.userId = :userId', {
|
|
109
|
+
userId
|
|
110
|
+
}).andWhere('permission.permissionType = :type', {
|
|
111
|
+
type: _usercompanypermissionentity.PermissionType.BRANCH
|
|
112
|
+
}).orderBy('permission.createdAt', 'DESC');
|
|
113
|
+
if (companyId) {
|
|
114
|
+
queryBuilder.andWhere('branch.companyId = :companyId', {
|
|
115
|
+
companyId
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
return queryBuilder.getMany();
|
|
119
|
+
}
|
|
151
120
|
constructor(dataSourceProvider){
|
|
152
121
|
_define_property(this, "dataSourceProvider", void 0);
|
|
153
|
-
_define_property(this, "logger", void 0);
|
|
154
122
|
_define_property(this, "_repository", void 0);
|
|
123
|
+
// Convenience Methods
|
|
124
|
+
_define_property(this, "findUserCompanies", void 0);
|
|
155
125
|
this.dataSourceProvider = dataSourceProvider;
|
|
156
|
-
this.logger = new _common.Logger(UserPermissionService.name);
|
|
157
126
|
this._repository = null;
|
|
127
|
+
this.findUserCompanies = (userId)=>this.findUserPermissions(userId, _usercompanypermissionentity.PermissionType.COMPANY);
|
|
158
128
|
}
|
|
159
129
|
};
|
|
160
130
|
UserPermissionService = _ts_decorate([
|
|
@@ -17,6 +17,7 @@ const _authconfigservice = require("./auth-config.service");
|
|
|
17
17
|
const _authdatasourceprovider = require("./auth-datasource.provider");
|
|
18
18
|
const _config = require("../config");
|
|
19
19
|
const _enums = require("../enums");
|
|
20
|
+
const _userenricherinterface = require("../interfaces/user-enricher.interface");
|
|
20
21
|
const _usercompanypermissionentity = require("../entities/user-company-permission.entity");
|
|
21
22
|
const _userentity = require("../entities/user.entity");
|
|
22
23
|
function _define_property(obj, key, value) {
|
|
@@ -109,6 +110,24 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
109
110
|
}
|
|
110
111
|
}
|
|
111
112
|
// Custom Methods
|
|
113
|
+
/**
|
|
114
|
+
* Validate profile ownership, ensure repository initialized, and fetch user.
|
|
115
|
+
* Throws ForbiddenException if not owner, NotFoundException if user not found.
|
|
116
|
+
*/ async getUserForProfile(userId, user) {
|
|
117
|
+
if (userId !== user.id) {
|
|
118
|
+
throw new _common.ForbiddenException('Cannot access another user\'s profile');
|
|
119
|
+
}
|
|
120
|
+
await this.ensureRepositoryInitialized();
|
|
121
|
+
const userEntity = await this.repository.findOne({
|
|
122
|
+
where: {
|
|
123
|
+
id: userId
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
if (!userEntity) {
|
|
127
|
+
throw new _common.NotFoundException('User not found');
|
|
128
|
+
}
|
|
129
|
+
return userEntity;
|
|
130
|
+
}
|
|
112
131
|
/** Verify email */ async verifyEmail(id) {
|
|
113
132
|
// Ensure repository is initialized for custom method
|
|
114
133
|
await this.ensureRepositoryInitialized();
|
|
@@ -170,33 +189,58 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
170
189
|
/** Convert DTO to entity (hash password before saving) */ async convertSingleDtoToEntity(dto, user) {
|
|
171
190
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
172
191
|
const entity = await super.convertSingleDtoToEntity(dto, user);
|
|
173
|
-
// Hash password if provided
|
|
174
|
-
if (dto.password) {
|
|
192
|
+
// Hash password if provided (CreateUserDto has password field)
|
|
193
|
+
if ('password' in dto && dto.password) {
|
|
175
194
|
entity.password = await _bcrypt.hash(dto.password, _config.BCRYPT_SALT_ROUNDS);
|
|
176
195
|
}
|
|
177
196
|
return entity;
|
|
178
197
|
}
|
|
179
198
|
/** Convert entity to response DTO (removes password). In raw mode with company feature, isActive comes from permission table. */ convertEntityToResponseDto(entity, isRaw) {
|
|
199
|
+
if (isRaw) {
|
|
200
|
+
return this.convertRawToResponseDto(entity);
|
|
201
|
+
}
|
|
202
|
+
return this.convertEntityToDto(entity);
|
|
203
|
+
}
|
|
204
|
+
/** Convert raw query result to response DTO */ convertRawToResponseDto(raw) {
|
|
180
205
|
// When raw mode is used with company feature, isActive comes from computed field
|
|
181
|
-
|
|
182
|
-
const isActive = isRaw && entity.user_companyIsActive !== undefined ? Boolean(entity.user_companyIsActive) : isRaw ? entity.user_is_active : entity.isActive;
|
|
206
|
+
const isActive = raw.user_companyIsActive !== undefined ? Boolean(raw.user_companyIsActive) : raw.user_is_active;
|
|
183
207
|
return {
|
|
184
|
-
id:
|
|
185
|
-
name:
|
|
186
|
-
phone:
|
|
187
|
-
email:
|
|
188
|
-
profilePictureId:
|
|
208
|
+
id: raw.user_id,
|
|
209
|
+
name: raw.user_name,
|
|
210
|
+
phone: raw.user_phone,
|
|
211
|
+
email: raw.user_email,
|
|
212
|
+
profilePictureId: raw.user_profile_picture_id,
|
|
189
213
|
isActive,
|
|
190
|
-
emailVerified:
|
|
191
|
-
phoneVerified:
|
|
192
|
-
lastLoginAt:
|
|
193
|
-
additionalFields:
|
|
194
|
-
createdAt:
|
|
195
|
-
updatedAt:
|
|
196
|
-
deletedAt:
|
|
197
|
-
createdById:
|
|
198
|
-
updatedById:
|
|
199
|
-
deletedById:
|
|
214
|
+
emailVerified: raw.user_email_verified,
|
|
215
|
+
phoneVerified: raw.user_phone_verified,
|
|
216
|
+
lastLoginAt: raw.user_last_login_at,
|
|
217
|
+
additionalFields: raw.user_additional_fields,
|
|
218
|
+
createdAt: raw.user_created_at,
|
|
219
|
+
updatedAt: raw.user_updated_at,
|
|
220
|
+
deletedAt: raw.user_deleted_at,
|
|
221
|
+
createdById: raw.user_created_by_id,
|
|
222
|
+
updatedById: raw.user_updated_by_id,
|
|
223
|
+
deletedById: raw.user_deleted_by_id
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
/** Convert entity to response DTO */ convertEntityToDto(entity) {
|
|
227
|
+
return {
|
|
228
|
+
id: entity.id,
|
|
229
|
+
name: entity.name,
|
|
230
|
+
phone: entity.phone,
|
|
231
|
+
email: entity.email,
|
|
232
|
+
profilePictureId: entity.profilePictureId,
|
|
233
|
+
isActive: entity.isActive,
|
|
234
|
+
emailVerified: entity.emailVerified,
|
|
235
|
+
phoneVerified: entity.phoneVerified,
|
|
236
|
+
lastLoginAt: entity.lastLoginAt,
|
|
237
|
+
additionalFields: entity.additionalFields,
|
|
238
|
+
createdAt: entity.createdAt,
|
|
239
|
+
updatedAt: entity.updatedAt,
|
|
240
|
+
deletedAt: entity.deletedAt,
|
|
241
|
+
createdById: entity.createdById,
|
|
242
|
+
updatedById: entity.updatedById,
|
|
243
|
+
deletedById: entity.deletedById
|
|
200
244
|
};
|
|
201
245
|
}
|
|
202
246
|
/** Convert entity list to response DTO list */ convertEntityListToResponseListDto(entities, isRaw) {
|
|
@@ -213,7 +257,11 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
213
257
|
}
|
|
214
258
|
/** Add company permission-based isActive check when company feature enabled. Returns isRaw: true for computed fields. */ async getExtraManipulateQuery(query, filterAndPaginationDto, user) {
|
|
215
259
|
// Repository initialization is handled automatically by RequestScopedApiService
|
|
216
|
-
|
|
260
|
+
let result = await super.getExtraManipulateQuery(query, filterAndPaginationDto, user);
|
|
261
|
+
// Apply user enricher query modifications if available
|
|
262
|
+
if (this.userEnricher?.enrichListQuery) {
|
|
263
|
+
await this.userEnricher.enrichListQuery(query, user);
|
|
264
|
+
}
|
|
217
265
|
// Only apply company filtering if:
|
|
218
266
|
// 1. Company feature is enabled at bootstrap
|
|
219
267
|
// 2. User has company context
|
|
@@ -333,16 +381,105 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
333
381
|
await queryRunner.release();
|
|
334
382
|
}
|
|
335
383
|
}
|
|
336
|
-
/**
|
|
337
|
-
|
|
384
|
+
/**
|
|
385
|
+
* Get user profile with extras from enricher
|
|
386
|
+
* Returns base user data plus any extra data provided by enricher (roles, permissions, etc.)
|
|
387
|
+
*/ async getProfileWithExtras(userId, user) {
|
|
388
|
+
const userEntity = await this.getUserForProfile(userId, user);
|
|
389
|
+
const userData = this.convertEntityToResponseDto(userEntity, false);
|
|
390
|
+
// Get extras from enricher if available
|
|
391
|
+
let extras;
|
|
392
|
+
if (this.userEnricher?.getProfileExtras) {
|
|
393
|
+
extras = await this.userEnricher.getProfileExtras(userId, user);
|
|
394
|
+
}
|
|
395
|
+
return {
|
|
396
|
+
user: userData,
|
|
397
|
+
extras
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Get enriched user list
|
|
402
|
+
* Applies enricher transformations after query execution
|
|
403
|
+
*/ async getEnrichedList(users, user) {
|
|
404
|
+
if (this.userEnricher?.enrichListItems) {
|
|
405
|
+
return this.userEnricher.enrichListItems(users, user);
|
|
406
|
+
}
|
|
407
|
+
return users;
|
|
408
|
+
}
|
|
409
|
+
// ============================================
|
|
410
|
+
// Multi-Step Profile Methods (via Enricher)
|
|
411
|
+
// ============================================
|
|
412
|
+
/**
|
|
413
|
+
* Get profile section definitions from enricher
|
|
414
|
+
*/ async getProfileSections() {
|
|
415
|
+
if (this.userEnricher?.getProfileSections) {
|
|
416
|
+
return this.userEnricher.getProfileSections();
|
|
417
|
+
}
|
|
418
|
+
return [];
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Get specific profile section data
|
|
422
|
+
*/ async getProfileSectionData(userId, sectionId, user) {
|
|
423
|
+
await this.getUserForProfile(userId, user);
|
|
424
|
+
if (this.userEnricher?.getProfileSectionData) {
|
|
425
|
+
return this.userEnricher.getProfileSectionData(userId, sectionId, user);
|
|
426
|
+
}
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Update specific profile section
|
|
431
|
+
*/ async updateProfileSection(userId, sectionId, data, user) {
|
|
432
|
+
const userEntity = await this.getUserForProfile(userId, user);
|
|
338
433
|
if (!this.dataSource) {
|
|
339
434
|
throw new _common.BadRequestException('DataSource not available');
|
|
340
435
|
}
|
|
436
|
+
if (!this.userEnricher?.updateProfileSection) {
|
|
437
|
+
throw new _common.BadRequestException('Profile sections not supported');
|
|
438
|
+
}
|
|
439
|
+
const queryRunner = this.dataSource.createQueryRunner();
|
|
440
|
+
await queryRunner.connect();
|
|
441
|
+
await queryRunner.startTransaction();
|
|
442
|
+
try {
|
|
443
|
+
await this.userEnricher.updateProfileSection(userId, sectionId, data, queryRunner);
|
|
444
|
+
await queryRunner.commitTransaction();
|
|
445
|
+
await this.clearCacheForId([
|
|
446
|
+
userEntity
|
|
447
|
+
]);
|
|
448
|
+
} catch (error) {
|
|
449
|
+
await queryRunner.rollbackTransaction();
|
|
450
|
+
_utils.ErrorHandler.logError(this.logger, error, 'updateProfileSection', {
|
|
451
|
+
entity: this.entityName,
|
|
452
|
+
sectionId
|
|
453
|
+
});
|
|
454
|
+
_utils.ErrorHandler.rethrowError(error);
|
|
455
|
+
} finally{
|
|
456
|
+
await queryRunner.release();
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Get profile completion percentage
|
|
461
|
+
*/ async getProfileCompletion(userId, user) {
|
|
462
|
+
await this.getUserForProfile(userId, user);
|
|
463
|
+
if (this.userEnricher?.calculateProfileCompletion) {
|
|
464
|
+
return this.userEnricher.calculateProfileCompletion(userId, user);
|
|
465
|
+
}
|
|
466
|
+
return 100; // Default to 100% if no enricher
|
|
467
|
+
}
|
|
468
|
+
/** Update user profile with password handling and extras */ async profile(dto, user) {
|
|
469
|
+
// Ensure repository and dataSource are initialized
|
|
470
|
+
await this.ensureRepositoryInitialized();
|
|
471
|
+
if (!this.dataSource) {
|
|
472
|
+
throw new _common.BadRequestException('DataSource not available');
|
|
473
|
+
}
|
|
474
|
+
const userId = dto.id ?? user.id;
|
|
475
|
+
// Validate extras if enricher provides validation
|
|
476
|
+
if (dto.extras && this.userEnricher?.validateProfileExtras) {
|
|
477
|
+
await this.userEnricher.validateProfileExtras(userId, dto.extras, user);
|
|
478
|
+
}
|
|
341
479
|
const queryRunner = this.dataSource.createQueryRunner();
|
|
342
480
|
await queryRunner.connect();
|
|
343
481
|
await queryRunner.startTransaction();
|
|
344
482
|
try {
|
|
345
|
-
const userId = dto.id ?? user.id;
|
|
346
483
|
const findUser = await queryRunner.manager.findOne(this.repository.target, {
|
|
347
484
|
where: {
|
|
348
485
|
id: userId
|
|
@@ -353,18 +490,28 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
353
490
|
}
|
|
354
491
|
// Handle password update
|
|
355
492
|
if (dto.password) {
|
|
356
|
-
// If updating own profile, require old password
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
493
|
+
// If updating own profile, require old password verification
|
|
494
|
+
const isOwnProfile = !dto.id || dto.id === user.id;
|
|
495
|
+
if (isOwnProfile) {
|
|
496
|
+
if (!dto.oldPassword) {
|
|
497
|
+
throw new _common.BadRequestException('Current password is required to change your password');
|
|
498
|
+
}
|
|
499
|
+
const isPasswordValid = await _bcrypt.compare(dto.oldPassword, findUser.password);
|
|
500
|
+
if (!isPasswordValid) {
|
|
501
|
+
throw new _common.BadRequestException('Current password is incorrect');
|
|
502
|
+
}
|
|
360
503
|
}
|
|
361
|
-
|
|
362
|
-
findUser.password = await _bcrypt.hash(dto.password, salt);
|
|
504
|
+
findUser.password = await _bcrypt.hash(dto.password, _config.BCRYPT_SALT_ROUNDS);
|
|
363
505
|
}
|
|
364
|
-
// Merge other fields (excluding password)
|
|
365
|
-
|
|
506
|
+
// Merge other fields (excluding password, oldPassword, extras, and sensitive flags)
|
|
507
|
+
// Users cannot self-modify emailVerified, phoneVerified, or isActive via profile update
|
|
508
|
+
const { password, oldPassword, extras, emailVerified, phoneVerified, isActive, ...restDto } = dto;
|
|
366
509
|
Object.assign(findUser, restDto);
|
|
367
510
|
await queryRunner.manager.save(this.repository.target, findUser);
|
|
511
|
+
// Handle extras update via enricher
|
|
512
|
+
if (extras && this.userEnricher?.updateProfileExtras) {
|
|
513
|
+
await this.userEnricher.updateProfileExtras(userId, extras, queryRunner);
|
|
514
|
+
}
|
|
368
515
|
await queryRunner.commitTransaction();
|
|
369
516
|
await this.clearCacheForId([
|
|
370
517
|
findUser
|
|
@@ -380,8 +527,8 @@ let UserService = class UserService extends _classes.RequestScopedApiService {
|
|
|
380
527
|
await queryRunner.release();
|
|
381
528
|
}
|
|
382
529
|
}
|
|
383
|
-
constructor(cacheManager, utilsService, dataSourceProvider, authConfig){
|
|
384
|
-
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;
|
|
530
|
+
constructor(cacheManager, utilsService, dataSourceProvider, authConfig, userEnricher){
|
|
531
|
+
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;
|
|
385
532
|
}
|
|
386
533
|
};
|
|
387
534
|
UserService = _ts_decorate([
|
|
@@ -392,11 +539,14 @@ UserService = _ts_decorate([
|
|
|
392
539
|
_ts_param(1, (0, _common.Inject)(_modules.UtilsService)),
|
|
393
540
|
_ts_param(2, (0, _common.Inject)(_authdatasourceprovider.AuthDataSourceProvider)),
|
|
394
541
|
_ts_param(3, (0, _common.Inject)(_authconfigservice.AuthConfigService)),
|
|
542
|
+
_ts_param(4, (0, _common.Optional)()),
|
|
543
|
+
_ts_param(4, (0, _common.Inject)(_config.USER_ENRICHER)),
|
|
395
544
|
_ts_metadata("design:type", Function),
|
|
396
545
|
_ts_metadata("design:paramtypes", [
|
|
397
546
|
typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
|
|
398
547
|
typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
|
|
399
548
|
typeof _authdatasourceprovider.AuthDataSourceProvider === "undefined" ? Object : _authdatasourceprovider.AuthDataSourceProvider,
|
|
400
|
-
typeof _authconfigservice.AuthConfigService === "undefined" ? Object : _authconfigservice.AuthConfigService
|
|
549
|
+
typeof _authconfigservice.AuthConfigService === "undefined" ? Object : _authconfigservice.AuthConfigService,
|
|
550
|
+
typeof _userenricherinterface.IUserEnricher === "undefined" ? Object : _userenricherinterface.IUserEnricher
|
|
401
551
|
])
|
|
402
552
|
], UserService);
|
|
@@ -12,10 +12,10 @@ const _common = require("@nestjs/common");
|
|
|
12
12
|
const _core = require("@nestjs/core");
|
|
13
13
|
const _passport = require("@nestjs/passport");
|
|
14
14
|
const _passportjwt = require("passport-jwt");
|
|
15
|
+
const _userentity = require("../entities/user.entity");
|
|
15
16
|
const _authconfigservice = require("../services/auth-config.service");
|
|
16
17
|
const _authdatasourceprovider = require("../services/auth-datasource.provider");
|
|
17
18
|
const _authenticationservice = require("../services/authentication.service");
|
|
18
|
-
const _userentity = require("../entities/user.entity");
|
|
19
19
|
function _define_property(obj, key, value) {
|
|
20
20
|
if (key in obj) {
|
|
21
21
|
Object.defineProperty(obj, key, {
|
|
@@ -38,71 +38,71 @@ function _ts_decorate(decorators, target, key, desc) {
|
|
|
38
38
|
function _ts_metadata(k, v) {
|
|
39
39
|
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
* Type guard to check if payload includes company context
|
|
43
|
-
*/ function isTokenPayloadWithCompany(payload) {
|
|
41
|
+
function hasCompanyContext(payload) {
|
|
44
42
|
return 'companyId' in payload && 'branchId' in payload;
|
|
45
43
|
}
|
|
46
44
|
let JwtStrategy = class JwtStrategy extends (0, _passport.PassportStrategy)(_passportjwt.Strategy, 'jwt') {
|
|
47
45
|
async validate(_req, payload) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
const userRepo = await this.resolveUserRepository();
|
|
47
|
+
await this.checkTokenRevocation(payload);
|
|
48
|
+
const user = await userRepo.findOne({
|
|
49
|
+
where: {
|
|
50
|
+
id: payload.id,
|
|
51
|
+
isActive: true
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
if (!user) {
|
|
55
|
+
throw new _common.UnauthorizedException('User not found or inactive');
|
|
56
|
+
}
|
|
57
|
+
return this.buildUserInfo(payload);
|
|
58
|
+
}
|
|
59
|
+
async resolveUserRepository() {
|
|
51
60
|
try {
|
|
52
|
-
const
|
|
61
|
+
const provider = await this.moduleRef.resolve(_authdatasourceprovider.AuthDataSourceProvider, undefined, {
|
|
53
62
|
strict: false
|
|
54
63
|
});
|
|
55
|
-
if (
|
|
56
|
-
|
|
64
|
+
if (provider) {
|
|
65
|
+
return provider.getRepository(_userentity.User);
|
|
57
66
|
}
|
|
58
|
-
|
|
59
|
-
authService = await this.moduleRef.resolve(_authenticationservice.AuthenticationService, undefined, {
|
|
60
|
-
strict: false
|
|
61
|
-
});
|
|
62
|
-
} catch (error) {
|
|
67
|
+
} catch {
|
|
63
68
|
throw new _common.UnauthorizedException('Unable to access user data');
|
|
64
69
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
70
|
+
throw new _common.UnauthorizedException('User repository not available');
|
|
71
|
+
}
|
|
72
|
+
async checkTokenRevocation(payload) {
|
|
73
|
+
if (!payload.iat) return;
|
|
74
|
+
try {
|
|
75
|
+
const authService = await this.moduleRef.resolve(_authenticationservice.AuthenticationService, undefined, {
|
|
76
|
+
strict: false
|
|
77
|
+
});
|
|
78
|
+
if (authService && await authService.isTokenRevoked(payload.id, payload.iat)) {
|
|
72
79
|
throw new _common.UnauthorizedException('Token has been revoked');
|
|
73
80
|
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
if (err instanceof _common.UnauthorizedException) throw err;
|
|
74
83
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
id: payload.id,
|
|
79
|
-
isActive: true
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
if (!user) {
|
|
83
|
-
throw new _common.UnauthorizedException('User not found or inactive');
|
|
84
|
-
}
|
|
85
|
-
const result = {
|
|
84
|
+
}
|
|
85
|
+
buildUserInfo(payload) {
|
|
86
|
+
const base = {
|
|
86
87
|
id: payload.id,
|
|
87
88
|
email: payload.email
|
|
88
89
|
};
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
result.companyId = payload.companyId;
|
|
92
|
-
// Only set branchId if it's not empty string
|
|
93
|
-
if (payload.branchId) {
|
|
94
|
-
result.branchId = payload.branchId;
|
|
95
|
-
}
|
|
90
|
+
if (!hasCompanyContext(payload)) {
|
|
91
|
+
return base;
|
|
96
92
|
}
|
|
97
|
-
return
|
|
93
|
+
return {
|
|
94
|
+
...base,
|
|
95
|
+
companyId: payload.companyId,
|
|
96
|
+
...payload.branchId && {
|
|
97
|
+
branchId: payload.branchId
|
|
98
|
+
}
|
|
99
|
+
};
|
|
98
100
|
}
|
|
99
101
|
constructor(authConfig, moduleRef){
|
|
100
102
|
super({
|
|
101
103
|
jwtFromRequest: _passportjwt.ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
102
104
|
ignoreExpiration: false,
|
|
103
|
-
|
|
104
|
-
// This ensures AuthConfigService is fully initialized when using forRootAsync
|
|
105
|
-
secretOrKeyProvider: (_request, _rawJwtToken, done)=>{
|
|
105
|
+
secretOrKeyProvider: (_req, _token, done)=>{
|
|
106
106
|
done(null, this.authConfig.getJwtSecret());
|
|
107
107
|
},
|
|
108
108
|
passReqToCallback: true
|