@flusys/nestjs-iam 1.0.0-rc → 2.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 +219 -118
- package/cjs/controllers/company-action-permission.controller.js +2 -17
- package/cjs/controllers/my-permission.controller.js +1 -2
- package/cjs/controllers/role-permission.controller.js +3 -9
- package/cjs/controllers/user-action-permission.controller.js +3 -9
- package/cjs/dtos/action.dto.js +0 -27
- package/cjs/dtos/permission.dto.js +81 -27
- package/cjs/dtos/role.dto.js +0 -27
- package/cjs/helpers/company-access.helper.js +19 -0
- package/cjs/helpers/index.js +1 -1
- package/cjs/interfaces/iam-module-options.interface.js +0 -14
- package/cjs/interfaces/index.js +0 -1
- package/cjs/modules/iam.module.js +38 -106
- package/cjs/services/action.service.js +30 -41
- package/cjs/services/iam-config.service.js +2 -5
- package/cjs/services/{iam-datasource.provider.js → iam-datasource.service.js} +33 -36
- package/cjs/services/index.js +1 -1
- package/cjs/services/permission-cache.service.js +6 -46
- package/cjs/services/permission.service.js +52 -41
- package/cjs/services/role.service.js +3 -3
- package/controllers/company-action-permission.controller.d.ts +2 -5
- package/controllers/role-permission.controller.d.ts +0 -1
- package/controllers/user-action-permission.controller.d.ts +0 -1
- package/dtos/action.dto.d.ts +0 -4
- package/dtos/role.dto.d.ts +0 -4
- package/fesm/controllers/company-action-permission.controller.js +4 -19
- package/fesm/controllers/my-permission.controller.js +1 -2
- package/fesm/controllers/role-permission.controller.js +4 -10
- package/fesm/controllers/user-action-permission.controller.js +4 -10
- package/fesm/dtos/action.dto.js +0 -24
- package/fesm/dtos/permission.dto.js +81 -27
- package/fesm/dtos/role.dto.js +0 -24
- package/fesm/helpers/company-access.helper.js +14 -0
- package/fesm/helpers/index.js +1 -1
- package/fesm/interfaces/iam-module-options.interface.js +3 -1
- package/fesm/interfaces/index.js +0 -1
- package/fesm/modules/iam.module.js +40 -108
- package/fesm/services/action.service.js +31 -42
- package/fesm/services/iam-config.service.js +2 -5
- package/fesm/services/{iam-datasource.provider.js → iam-datasource.service.js} +31 -34
- package/fesm/services/index.js +1 -1
- package/fesm/services/permission-cache.service.js +6 -46
- package/fesm/services/permission.service.js +53 -42
- package/fesm/services/role.service.js +3 -3
- package/helpers/company-access.helper.d.ts +3 -0
- package/helpers/index.d.ts +1 -1
- package/interfaces/iam-module-options.interface.d.ts +9 -1
- package/interfaces/index.d.ts +0 -1
- package/modules/iam.module.d.ts +1 -2
- package/package.json +3 -3
- package/services/action.service.d.ts +6 -4
- package/services/iam-config.service.d.ts +0 -1
- package/services/{iam-datasource.provider.d.ts → iam-datasource.service.d.ts} +4 -5
- package/services/index.d.ts +1 -1
- package/services/permission-cache.service.d.ts +1 -4
- package/services/permission.service.d.ts +4 -2
- package/services/role.service.d.ts +3 -3
- package/cjs/helpers/permission-evaluator.helper.js +0 -175
- package/cjs/interfaces/iam-module-async-options.interface.js +0 -4
- package/fesm/helpers/permission-evaluator.helper.js +0 -165
- package/fesm/interfaces/iam-module-async-options.interface.js +0 -3
- package/helpers/permission-evaluator.helper.d.ts +0 -26
- package/interfaces/iam-module-async-options.interface.d.ts +0 -11
|
@@ -25,13 +25,13 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import {
|
|
28
|
+
import { HybridCache, RequestScopedApiService } from '@flusys/nestjs-shared/classes';
|
|
29
29
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
30
|
import { BadRequestException, Inject, Injectable, Scope } from '@nestjs/common';
|
|
31
31
|
import { In } from 'typeorm';
|
|
32
32
|
import { Action } from '../entities/action.entity';
|
|
33
33
|
import { IAMConfigService } from './iam-config.service';
|
|
34
|
-
import {
|
|
34
|
+
import { IAMDataSourceService } from './iam-datasource.service';
|
|
35
35
|
import { PermissionService } from './permission.service';
|
|
36
36
|
export class ActionService extends RequestScopedApiService {
|
|
37
37
|
resolveEntity() {
|
|
@@ -93,55 +93,33 @@ export class ActionService extends RequestScopedApiService {
|
|
|
93
93
|
deletedById: entity.deletedById
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
|
-
|
|
97
|
-
/** Get actions available for permission assignment (filtered by company whitelist) */ async getActionsForPermission(user) {
|
|
98
|
-
await this.ensureRepositoryInitialized();
|
|
96
|
+
requireUser(user, methodName) {
|
|
99
97
|
if (!user) {
|
|
100
|
-
throw new BadRequestException(
|
|
98
|
+
throw new BadRequestException(`User is required for ${methodName}`);
|
|
101
99
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
'permissionLogic',
|
|
109
|
-
'isActive',
|
|
110
|
-
'parentId',
|
|
111
|
-
'serial'
|
|
112
|
-
];
|
|
113
|
-
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
114
|
-
if (enableCompanyFeature && user.companyId) {
|
|
100
|
+
}
|
|
101
|
+
/** Get actions available for permission assignment (filtered by company whitelist) */ async getActionsForPermission(user) {
|
|
102
|
+
await this.ensureRepositoryInitialized();
|
|
103
|
+
this.requireUser(user, 'getActionsForPermission');
|
|
104
|
+
let whereClause = {};
|
|
105
|
+
if (this.iamConfigService.isCompanyFeatureEnabled() && user.companyId) {
|
|
115
106
|
const companyActionIds = await this.permissionService.getCompanyActionIds(user.companyId);
|
|
116
107
|
if (companyActionIds.length === 0) {
|
|
117
108
|
return [];
|
|
118
109
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
},
|
|
123
|
-
select: selectFields
|
|
124
|
-
});
|
|
125
|
-
return actions.map((action)=>this.convertEntityToResponseDto(action, false));
|
|
110
|
+
whereClause = {
|
|
111
|
+
id: In(companyActionIds)
|
|
112
|
+
};
|
|
126
113
|
}
|
|
127
114
|
const actions = await this.repository.find({
|
|
128
|
-
|
|
115
|
+
where: whereClause,
|
|
116
|
+
select: this.actionSelectFields
|
|
129
117
|
});
|
|
130
118
|
return actions.map((action)=>this.convertEntityToResponseDto(action, false));
|
|
131
119
|
}
|
|
132
|
-
/**
|
|
133
|
-
* Get actions in hierarchical tree structure
|
|
134
|
-
*
|
|
135
|
-
* @param user - Logged in user info for company filtering
|
|
136
|
-
* @param search - Optional search term (name or code)
|
|
137
|
-
* @param isActive - Optional filter by active status
|
|
138
|
-
* @param withDeleted - Include deleted actions (default: false)
|
|
139
|
-
* @returns Array of root actions with nested children
|
|
140
|
-
*/ async getActionTree(user, search, isActive, withDeleted = false) {
|
|
120
|
+
/** Get actions in hierarchical tree structure */ async getActionTree(user, search, isActive, withDeleted = false) {
|
|
141
121
|
await this.ensureRepositoryInitialized();
|
|
142
|
-
|
|
143
|
-
throw new BadRequestException('User is required for getActionTree');
|
|
144
|
-
}
|
|
122
|
+
this.requireUser(user, 'getActionTree');
|
|
145
123
|
const query = this.repository.createQueryBuilder('action');
|
|
146
124
|
if (!withDeleted) {
|
|
147
125
|
query.andWhere('action.deletedAt IS NULL');
|
|
@@ -189,7 +167,18 @@ export class ActionService extends RequestScopedApiService {
|
|
|
189
167
|
return rootNodes;
|
|
190
168
|
}
|
|
191
169
|
constructor(cacheManager, utilsService, iamConfigService, dataSourceProvider, permissionService){
|
|
192
|
-
super('action', null, cacheManager, utilsService, ActionService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "permissionService", void 0),
|
|
170
|
+
super('action', null, cacheManager, utilsService, ActionService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "iamConfigService", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "permissionService", void 0), // Custom Methods
|
|
171
|
+
_define_property(this, "actionSelectFields", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.iamConfigService = iamConfigService, this.dataSourceProvider = dataSourceProvider, this.permissionService = permissionService, this.actionSelectFields = [
|
|
172
|
+
'id',
|
|
173
|
+
'code',
|
|
174
|
+
'name',
|
|
175
|
+
'description',
|
|
176
|
+
'actionType',
|
|
177
|
+
'permissionLogic',
|
|
178
|
+
'isActive',
|
|
179
|
+
'parentId',
|
|
180
|
+
'serial'
|
|
181
|
+
];
|
|
193
182
|
}
|
|
194
183
|
}
|
|
195
184
|
ActionService = _ts_decorate([
|
|
@@ -199,14 +188,14 @@ ActionService = _ts_decorate([
|
|
|
199
188
|
_ts_param(0, Inject('CACHE_INSTANCE')),
|
|
200
189
|
_ts_param(1, Inject(UtilsService)),
|
|
201
190
|
_ts_param(2, Inject(IAMConfigService)),
|
|
202
|
-
_ts_param(3, Inject(
|
|
191
|
+
_ts_param(3, Inject(IAMDataSourceService)),
|
|
203
192
|
_ts_param(4, Inject(PermissionService)),
|
|
204
193
|
_ts_metadata("design:type", Function),
|
|
205
194
|
_ts_metadata("design:paramtypes", [
|
|
206
195
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
207
196
|
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
208
197
|
typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
|
|
209
|
-
typeof
|
|
198
|
+
typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService,
|
|
210
199
|
typeof PermissionService === "undefined" ? Object : PermissionService
|
|
211
200
|
])
|
|
212
201
|
], ActionService);
|
|
@@ -38,12 +38,9 @@ export class IAMConfigService {
|
|
|
38
38
|
isMultiTenant() {
|
|
39
39
|
return this.getDatabaseMode() === 'multi-tenant';
|
|
40
40
|
}
|
|
41
|
-
//
|
|
42
|
-
getEnableCompanyFeature() {
|
|
43
|
-
return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
44
|
-
}
|
|
41
|
+
// Feature Flags
|
|
45
42
|
isCompanyFeatureEnabled() {
|
|
46
|
-
return this.
|
|
43
|
+
return this.options.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
47
44
|
}
|
|
48
45
|
// Permission Mode
|
|
49
46
|
getPermissionMode() {
|
|
@@ -29,9 +29,9 @@ import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
|
|
|
29
29
|
import { Inject, Injectable, Logger, Optional, Scope } from '@nestjs/common';
|
|
30
30
|
import { REQUEST } from '@nestjs/core';
|
|
31
31
|
import { Request } from 'express';
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
export class
|
|
32
|
+
import { PermissionModeHelper } from '../helpers';
|
|
33
|
+
import { IAMConfigService } from './iam-config.service';
|
|
34
|
+
export class IAMDataSourceService extends MultiTenantDataSourceService {
|
|
35
35
|
// Factory Methods
|
|
36
36
|
static buildParentOptions(options) {
|
|
37
37
|
return {
|
|
@@ -42,20 +42,17 @@ export class IAMDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
// Feature Flags
|
|
45
|
-
getEnableCompanyFeature() {
|
|
46
|
-
return this.iamOptions.bootstrapAppConfig?.enableCompanyFeature ?? false;
|
|
47
|
-
}
|
|
48
45
|
getEnableCompanyFeatureForTenant(tenant) {
|
|
49
|
-
return tenant?.enableCompanyFeature ?? this.
|
|
46
|
+
return tenant?.enableCompanyFeature ?? this.configService.isCompanyFeatureEnabled();
|
|
50
47
|
}
|
|
51
48
|
getEnableCompanyFeatureForCurrentTenant() {
|
|
52
49
|
return this.getEnableCompanyFeatureForTenant(this.getCurrentTenant() ?? undefined);
|
|
53
50
|
}
|
|
54
51
|
// Entity Management
|
|
55
52
|
async getIAMEntities() {
|
|
56
|
-
const {
|
|
53
|
+
const { getIAMEntitiesByConfig } = await import('../entities');
|
|
57
54
|
const enableCompanyFeature = this.getEnableCompanyFeatureForCurrentTenant();
|
|
58
|
-
const permissionMode = this.
|
|
55
|
+
const permissionMode = PermissionModeHelper.toString(this.configService.getPermissionMode());
|
|
59
56
|
return getIAMEntitiesByConfig(enableCompanyFeature, permissionMode);
|
|
60
57
|
}
|
|
61
58
|
// Overrides
|
|
@@ -64,9 +61,9 @@ export class IAMDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
64
61
|
return super.createDataSourceFromConfig(config, entities);
|
|
65
62
|
}
|
|
66
63
|
async getSingleDataSource() {
|
|
67
|
-
if (!
|
|
68
|
-
if (
|
|
69
|
-
return
|
|
64
|
+
if (!IAMDataSourceService.singleDataSource) {
|
|
65
|
+
if (IAMDataSourceService.singleConnectionLock) {
|
|
66
|
+
return IAMDataSourceService.singleConnectionLock;
|
|
70
67
|
}
|
|
71
68
|
const lockPromise = (async ()=>{
|
|
72
69
|
const config = this.getDefaultDatabaseConfig();
|
|
@@ -74,63 +71,63 @@ export class IAMDataSourceProvider extends MultiTenantDataSourceService {
|
|
|
74
71
|
throw new Error('Default database config is not available');
|
|
75
72
|
}
|
|
76
73
|
const ds = await this.createDataSourceFromConfig(config);
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
IAMDataSourceService.singleDataSource = ds;
|
|
75
|
+
IAMDataSourceService.initialized = true;
|
|
79
76
|
return ds;
|
|
80
77
|
})();
|
|
81
|
-
|
|
78
|
+
IAMDataSourceService.singleConnectionLock = lockPromise;
|
|
82
79
|
try {
|
|
83
80
|
return await lockPromise;
|
|
84
81
|
} finally{
|
|
85
|
-
|
|
82
|
+
IAMDataSourceService.singleConnectionLock = null;
|
|
86
83
|
}
|
|
87
84
|
}
|
|
88
|
-
return
|
|
85
|
+
return IAMDataSourceService.singleDataSource;
|
|
89
86
|
}
|
|
90
87
|
async getOrCreateTenantConnection(tenant) {
|
|
91
88
|
// Return existing initialized connection from IAM-specific cache
|
|
92
|
-
const existing =
|
|
89
|
+
const existing = IAMDataSourceService.tenantConnections.get(tenant.id);
|
|
93
90
|
if (existing?.isInitialized) {
|
|
94
91
|
return existing;
|
|
95
92
|
}
|
|
96
93
|
// If another request is creating this tenant's connection, wait for it
|
|
97
|
-
const pendingConnection =
|
|
94
|
+
const pendingConnection = IAMDataSourceService.connectionLocks.get(tenant.id);
|
|
98
95
|
if (pendingConnection) {
|
|
99
96
|
return pendingConnection;
|
|
100
97
|
}
|
|
101
98
|
// Create connection with lock to prevent race conditions
|
|
102
99
|
const config = this.buildTenantDatabaseConfig(tenant);
|
|
103
100
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
104
|
-
|
|
101
|
+
IAMDataSourceService.connectionLocks.set(tenant.id, connectionPromise);
|
|
105
102
|
try {
|
|
106
103
|
const dataSource = await connectionPromise;
|
|
107
|
-
|
|
104
|
+
IAMDataSourceService.tenantConnections.set(tenant.id, dataSource);
|
|
108
105
|
return dataSource;
|
|
109
106
|
} finally{
|
|
110
|
-
|
|
107
|
+
IAMDataSourceService.connectionLocks.delete(tenant.id);
|
|
111
108
|
}
|
|
112
109
|
}
|
|
113
|
-
constructor(
|
|
114
|
-
super(
|
|
110
|
+
constructor(configService, request){
|
|
111
|
+
super(IAMDataSourceService.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), _define_property(this, "logger", void 0), this.configService = configService, this.logger = new Logger(IAMDataSourceService.name);
|
|
115
112
|
}
|
|
116
113
|
}
|
|
117
114
|
// Override parent's static properties to have IAM-specific cache
|
|
118
|
-
_define_property(
|
|
119
|
-
_define_property(
|
|
120
|
-
_define_property(
|
|
121
|
-
_define_property(
|
|
122
|
-
_define_property(
|
|
123
|
-
_define_property(
|
|
124
|
-
|
|
115
|
+
_define_property(IAMDataSourceService, "tenantConnections", new Map());
|
|
116
|
+
_define_property(IAMDataSourceService, "singleDataSource", null);
|
|
117
|
+
_define_property(IAMDataSourceService, "tenantsRegistry", new Map());
|
|
118
|
+
_define_property(IAMDataSourceService, "initialized", false);
|
|
119
|
+
_define_property(IAMDataSourceService, "connectionLocks", new Map());
|
|
120
|
+
_define_property(IAMDataSourceService, "singleConnectionLock", null);
|
|
121
|
+
IAMDataSourceService = _ts_decorate([
|
|
125
122
|
Injectable({
|
|
126
123
|
scope: Scope.REQUEST
|
|
127
124
|
}),
|
|
128
|
-
_ts_param(0, Inject(
|
|
125
|
+
_ts_param(0, Inject(IAMConfigService)),
|
|
129
126
|
_ts_param(1, Optional()),
|
|
130
127
|
_ts_param(1, Inject(REQUEST)),
|
|
131
128
|
_ts_metadata("design:type", Function),
|
|
132
129
|
_ts_metadata("design:paramtypes", [
|
|
133
|
-
typeof
|
|
130
|
+
typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
|
|
134
131
|
typeof Request === "undefined" ? Object : Request
|
|
135
132
|
])
|
|
136
|
-
],
|
|
133
|
+
], IAMDataSourceService);
|
package/fesm/services/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from './action.service';
|
|
2
2
|
export * from './iam-config.service';
|
|
3
|
-
export * from './iam-datasource.
|
|
3
|
+
export * from './iam-datasource.service';
|
|
4
4
|
export * from './permission-cache.service';
|
|
5
5
|
export * from './permission.service';
|
|
6
6
|
export * from './role.service';
|
|
@@ -31,18 +31,17 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
|
31
31
|
export class PermissionCacheService {
|
|
32
32
|
// Cache Key Generation
|
|
33
33
|
generateCacheKey(options) {
|
|
34
|
-
|
|
35
|
-
if (enableCompanyFeature && companyId) {
|
|
36
|
-
return `${this.CACHE_PREFIX}:company:${companyId}:branch:${branchId || 'null'}:user:${userId}`;
|
|
37
|
-
}
|
|
38
|
-
return `${this.CACHE_PREFIX}:user:${userId}`;
|
|
34
|
+
return this.buildCacheKey(this.CACHE_PREFIX, options);
|
|
39
35
|
}
|
|
40
36
|
generateMyPermissionsCacheKey(options) {
|
|
37
|
+
return this.buildCacheKey(this.MY_PERMISSIONS_PREFIX, options);
|
|
38
|
+
}
|
|
39
|
+
buildCacheKey(prefix, options) {
|
|
41
40
|
const { userId, companyId, branchId, enableCompanyFeature } = options;
|
|
42
41
|
if (enableCompanyFeature && companyId) {
|
|
43
|
-
return `${
|
|
42
|
+
return `${prefix}:company:${companyId}:branch:${branchId || 'null'}:user:${userId}`;
|
|
44
43
|
}
|
|
45
|
-
return `${
|
|
44
|
+
return `${prefix}:user:${userId}`;
|
|
46
45
|
}
|
|
47
46
|
// Cache Operations
|
|
48
47
|
async setPermissions(options, permissions) {
|
|
@@ -56,17 +55,6 @@ export class PermissionCacheService {
|
|
|
56
55
|
// Don't throw - cache failure shouldn't break the operation
|
|
57
56
|
}
|
|
58
57
|
}
|
|
59
|
-
async getPermissions(options) {
|
|
60
|
-
try {
|
|
61
|
-
const key = this.generateCacheKey(options);
|
|
62
|
-
const result = await this.cacheManager.get(key);
|
|
63
|
-
return result || null;
|
|
64
|
-
} catch (error) {
|
|
65
|
-
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
66
|
-
this.logger.error(`Failed to get permissions from cache: ${errorMessage}`);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
58
|
// My-Permissions Cache Operations
|
|
71
59
|
async setMyPermissions(options, data) {
|
|
72
60
|
try {
|
|
@@ -129,16 +117,6 @@ export class PermissionCacheService {
|
|
|
129
117
|
return null;
|
|
130
118
|
}
|
|
131
119
|
}
|
|
132
|
-
async invalidateActionCodeCache(tenantId) {
|
|
133
|
-
try {
|
|
134
|
-
const key = this.generateActionCodeCacheKey(tenantId);
|
|
135
|
-
await this.cacheManager.del(key);
|
|
136
|
-
this.logger.debug(`Invalidated action code cache${tenantId ? ` for tenant ${tenantId}` : ''}`);
|
|
137
|
-
} catch (error) {
|
|
138
|
-
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
139
|
-
this.logger.warn(`Failed to invalidate action code cache: ${errorMessage}`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
120
|
// Cache Invalidation
|
|
143
121
|
async invalidateUser(userId, companyId, branchIds) {
|
|
144
122
|
try {
|
|
@@ -178,13 +156,6 @@ export class PermissionCacheService {
|
|
|
178
156
|
}
|
|
179
157
|
return successCount;
|
|
180
158
|
}
|
|
181
|
-
/** Invalidate cache for all users in company (requires userIds via invalidateUsers) */ async invalidateCompany(companyId) {
|
|
182
|
-
// Note: HybridCache doesn't support pattern matching (keys() method)
|
|
183
|
-
// Company-wide invalidation requires passing individual user IDs
|
|
184
|
-
// This is a placeholder that logs a warning
|
|
185
|
-
this.logger.warn(`invalidateCompany called for ${companyId}, but pattern matching is not supported. ` + `Use invalidateUsers() with specific user IDs instead.`);
|
|
186
|
-
return 0;
|
|
187
|
-
}
|
|
188
159
|
async invalidateRole(roleId, userIds, companyId, branchIds) {
|
|
189
160
|
if (userIds.length === 0) {
|
|
190
161
|
this.logger.debug(`No users found for role ${roleId}`);
|
|
@@ -196,17 +167,6 @@ export class PermissionCacheService {
|
|
|
196
167
|
}
|
|
197
168
|
return count;
|
|
198
169
|
}
|
|
199
|
-
// Administrative Operations
|
|
200
|
-
/** Clear all permission caches (memory and redis) */ async clearAll() {
|
|
201
|
-
try {
|
|
202
|
-
await this.cacheManager.reset(); // Clear memory cache
|
|
203
|
-
await this.cacheManager.resetL2(); // Clear redis cache
|
|
204
|
-
this.logger.warn('Cleared all cache entries (memory and redis)');
|
|
205
|
-
} catch (error) {
|
|
206
|
-
const errorMessage = ErrorHandler.getErrorMessage(error);
|
|
207
|
-
this.logger.error(`Failed to clear all caches: ${errorMessage}`);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
170
|
constructor(cacheManager){
|
|
211
171
|
_define_property(this, "cacheManager", void 0);
|
|
212
172
|
_define_property(this, "logger", void 0);
|
|
@@ -25,7 +25,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
25
25
|
decorator(target, key, paramIndex);
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
import { BadRequestException, Inject, Injectable, Logger, Scope } from '@nestjs/common';
|
|
28
|
+
import { BadRequestException, ConflictException, Inject, Injectable, Logger, Scope } from '@nestjs/common';
|
|
29
29
|
import { In, IsNull } from 'typeorm';
|
|
30
30
|
import { PermissionAction } from '../dtos/permission.dto';
|
|
31
31
|
import { Action } from '../entities/action.entity';
|
|
@@ -36,7 +36,7 @@ import { IamEntityType, IamPermissionType, UserIamPermission } from '../entities
|
|
|
36
36
|
import { ActionType } from '../enums/action-type.enum';
|
|
37
37
|
import { IAMPermissionMode } from '../enums/permission-type.enum';
|
|
38
38
|
import { IAMConfigService } from './iam-config.service';
|
|
39
|
-
import {
|
|
39
|
+
import { IAMDataSourceService } from './iam-datasource.service';
|
|
40
40
|
import { PermissionCacheService } from './permission-cache.service';
|
|
41
41
|
export class PermissionService {
|
|
42
42
|
// Repository Getters
|
|
@@ -62,8 +62,7 @@ export class PermissionService {
|
|
|
62
62
|
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
63
63
|
const branchId = dto.branchId ?? null;
|
|
64
64
|
const companyId = dto.companyId ?? null;
|
|
65
|
-
const itemsToAdd = dto.items
|
|
66
|
-
const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
|
|
65
|
+
const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
|
|
67
66
|
let added = 0;
|
|
68
67
|
let removed = 0;
|
|
69
68
|
if (itemsToAdd.length > 0) {
|
|
@@ -97,8 +96,15 @@ export class PermissionService {
|
|
|
97
96
|
branchId: enableCompanyFeature ? branchId : null
|
|
98
97
|
}));
|
|
99
98
|
if (newPermissions.length > 0) {
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
try {
|
|
100
|
+
await permissionRepo.save(newPermissions);
|
|
101
|
+
added = newPermissions.length;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
|
|
104
|
+
throw new ConflictException('Some permissions already exist for this user. Please refresh and try again.');
|
|
105
|
+
}
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
102
108
|
}
|
|
103
109
|
}
|
|
104
110
|
if (itemsToRemove.length > 0) {
|
|
@@ -118,12 +124,7 @@ export class PermissionService {
|
|
|
118
124
|
removed = result.affected || 0;
|
|
119
125
|
}
|
|
120
126
|
await this.invalidateUserPermissionCache(dto.userId, branchId, companyId);
|
|
121
|
-
return
|
|
122
|
-
success: true,
|
|
123
|
-
added,
|
|
124
|
-
removed,
|
|
125
|
-
message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed`
|
|
126
|
-
};
|
|
127
|
+
return this.buildOperationResult(dto.items.length, added, removed);
|
|
127
128
|
}
|
|
128
129
|
async getUserActions(userId, branchId, companyId) {
|
|
129
130
|
const permissionRepo = await this.getPermissionRepository();
|
|
@@ -189,8 +190,7 @@ export class PermissionService {
|
|
|
189
190
|
});
|
|
190
191
|
roleCompanyId = role?.companyId ?? null;
|
|
191
192
|
}
|
|
192
|
-
const itemsToAdd = dto.items
|
|
193
|
-
const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
|
|
193
|
+
const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
|
|
194
194
|
let added = 0;
|
|
195
195
|
let removed = 0;
|
|
196
196
|
if (itemsToAdd.length > 0) {
|
|
@@ -219,8 +219,15 @@ export class PermissionService {
|
|
|
219
219
|
branchId: null
|
|
220
220
|
}));
|
|
221
221
|
if (newPermissions.length > 0) {
|
|
222
|
-
|
|
223
|
-
|
|
222
|
+
try {
|
|
223
|
+
await permissionRepo.save(newPermissions);
|
|
224
|
+
added = newPermissions.length;
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
|
|
227
|
+
throw new ConflictException('Some role-action permissions already exist. Please refresh and try again.');
|
|
228
|
+
}
|
|
229
|
+
throw error;
|
|
230
|
+
}
|
|
224
231
|
}
|
|
225
232
|
}
|
|
226
233
|
if (itemsToRemove.length > 0) {
|
|
@@ -235,12 +242,7 @@ export class PermissionService {
|
|
|
235
242
|
removed = result.affected || 0;
|
|
236
243
|
}
|
|
237
244
|
const affectedUsers = await this.invalidateRoleMembersCache(dto.roleId);
|
|
238
|
-
return {
|
|
239
|
-
success: true,
|
|
240
|
-
added,
|
|
241
|
-
removed,
|
|
242
|
-
message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed. Invalidated cache for ${affectedUsers} users.`
|
|
243
|
-
};
|
|
245
|
+
return this.buildOperationResult(dto.items.length, added, removed, `. Invalidated cache for ${affectedUsers} users.`);
|
|
244
246
|
}
|
|
245
247
|
async getRoleActions(roleId) {
|
|
246
248
|
const permissionRepo = await this.getPermissionRepository();
|
|
@@ -282,8 +284,7 @@ export class PermissionService {
|
|
|
282
284
|
/** Assign or remove actions to/from a company (whitelist) */ async assignCompanyActions(dto) {
|
|
283
285
|
const permissionRepo = await this.getPermissionRepository();
|
|
284
286
|
const dataSource = permissionRepo.manager.connection;
|
|
285
|
-
const itemsToAdd = dto.items
|
|
286
|
-
const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
|
|
287
|
+
const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
|
|
287
288
|
let added = 0;
|
|
288
289
|
let removed = 0;
|
|
289
290
|
let removedRoleActions = 0;
|
|
@@ -303,12 +304,7 @@ export class PermissionService {
|
|
|
303
304
|
});
|
|
304
305
|
const affectedCacheEntries = await this.invalidateCompanyMembersCache(dto.companyId);
|
|
305
306
|
const cascadeInfo = removedRoleActions > 0 || removedUserActions > 0 ? ` Cascaded removal: ${removedRoleActions} role permissions, ${removedUserActions} user permissions.` : '';
|
|
306
|
-
return {
|
|
307
|
-
success: true,
|
|
308
|
-
added,
|
|
309
|
-
removed,
|
|
310
|
-
message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed.${cascadeInfo} Invalidated ${affectedCacheEntries} cache entries.`
|
|
311
|
-
};
|
|
307
|
+
return this.buildOperationResult(dto.items.length, added, removed, `.${cascadeInfo} Invalidated ${affectedCacheEntries} cache entries.`);
|
|
312
308
|
}
|
|
313
309
|
async addCompanyActions(permissionRepo, companyId, actionIds) {
|
|
314
310
|
const existingPermissions = await permissionRepo.find({
|
|
@@ -449,8 +445,7 @@ export class PermissionService {
|
|
|
449
445
|
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
450
446
|
const branchId = dto.branchId ?? null;
|
|
451
447
|
const companyId = dto.companyId ?? null;
|
|
452
|
-
const itemsToAdd = dto.items
|
|
453
|
-
const itemsToRemove = dto.items.filter((item)=>item.action === PermissionAction.REMOVE);
|
|
448
|
+
const { toAdd: itemsToAdd, toRemove: itemsToRemove } = this.splitItemsByAction(dto.items);
|
|
454
449
|
let added = 0;
|
|
455
450
|
let removed = 0;
|
|
456
451
|
if (itemsToAdd.length > 0) {
|
|
@@ -484,8 +479,15 @@ export class PermissionService {
|
|
|
484
479
|
branchId: enableCompanyFeature ? branchId : null
|
|
485
480
|
}));
|
|
486
481
|
if (newPermissions.length > 0) {
|
|
487
|
-
|
|
488
|
-
|
|
482
|
+
try {
|
|
483
|
+
await permissionRepo.save(newPermissions);
|
|
484
|
+
added = newPermissions.length;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
if (error?.code === 'ER_DUP_ENTRY' || error?.message?.includes('Duplicate entry')) {
|
|
487
|
+
throw new ConflictException('Some user-role permissions already exist. Please refresh and try again.');
|
|
488
|
+
}
|
|
489
|
+
throw error;
|
|
490
|
+
}
|
|
489
491
|
}
|
|
490
492
|
}
|
|
491
493
|
if (itemsToRemove.length > 0) {
|
|
@@ -505,12 +507,7 @@ export class PermissionService {
|
|
|
505
507
|
removed = result.affected || 0;
|
|
506
508
|
}
|
|
507
509
|
await this.invalidateUserPermissionCache(dto.userId, branchId, companyId);
|
|
508
|
-
return
|
|
509
|
-
success: true,
|
|
510
|
-
added,
|
|
511
|
-
removed,
|
|
512
|
-
message: `Successfully processed ${dto.items.length} items: ${added} added, ${removed} removed`
|
|
513
|
-
};
|
|
510
|
+
return this.buildOperationResult(dto.items.length, added, removed);
|
|
514
511
|
}
|
|
515
512
|
/** Get user's roles (branch-scoped, filtered by companyId and branchId if provided) */ async getUserRoles(userId, branchId, companyId) {
|
|
516
513
|
const permissionRepo = await this.getPermissionRepository();
|
|
@@ -702,6 +699,20 @@ export class PermissionService {
|
|
|
702
699
|
return cacheData;
|
|
703
700
|
}
|
|
704
701
|
// Helper Methods
|
|
702
|
+
/** Split permission items into add and remove arrays */ splitItemsByAction(items) {
|
|
703
|
+
return {
|
|
704
|
+
toAdd: items.filter((item)=>item.action === PermissionAction.ADD),
|
|
705
|
+
toRemove: items.filter((item)=>item.action === PermissionAction.REMOVE)
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
/** Build standard operation result DTO */ buildOperationResult(totalItems, added, removed, additionalMessage = '') {
|
|
709
|
+
return {
|
|
710
|
+
success: true,
|
|
711
|
+
added,
|
|
712
|
+
removed,
|
|
713
|
+
message: `Successfully processed ${totalItems} items: ${added} added, ${removed} removed${additionalMessage}`
|
|
714
|
+
};
|
|
715
|
+
}
|
|
705
716
|
/** Get role IDs assigned to a user (merges company-wide + branch-specific roles) */ async getUserRoleIds(userId, branchId, companyId) {
|
|
706
717
|
const permissionRepo = await this.getPermissionRepository();
|
|
707
718
|
const enableCompanyFeature = this.iamConfigService.isCompanyFeatureEnabled();
|
|
@@ -889,11 +900,11 @@ PermissionService = _ts_decorate([
|
|
|
889
900
|
}),
|
|
890
901
|
_ts_param(0, Inject(PermissionCacheService)),
|
|
891
902
|
_ts_param(1, Inject(IAMConfigService)),
|
|
892
|
-
_ts_param(2, Inject(
|
|
903
|
+
_ts_param(2, Inject(IAMDataSourceService)),
|
|
893
904
|
_ts_metadata("design:type", Function),
|
|
894
905
|
_ts_metadata("design:paramtypes", [
|
|
895
906
|
typeof PermissionCacheService === "undefined" ? Object : PermissionCacheService,
|
|
896
907
|
typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
|
|
897
|
-
typeof
|
|
908
|
+
typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService
|
|
898
909
|
])
|
|
899
910
|
], PermissionService);
|
|
@@ -32,7 +32,7 @@ import { Inject, Injectable, Scope } from '@nestjs/common';
|
|
|
32
32
|
import { RoleWithCompany } from '../entities/role-with-company.entity';
|
|
33
33
|
import { Role } from '../entities/role.entity';
|
|
34
34
|
import { IAMConfigService } from './iam-config.service';
|
|
35
|
-
import {
|
|
35
|
+
import { IAMDataSourceService } from './iam-datasource.service';
|
|
36
36
|
export class RoleService extends RequestScopedApiService {
|
|
37
37
|
resolveEntity() {
|
|
38
38
|
return this.iamConfigService.isCompanyFeatureEnabled() ? RoleWithCompany : Role;
|
|
@@ -122,12 +122,12 @@ RoleService = _ts_decorate([
|
|
|
122
122
|
_ts_param(0, Inject('CACHE_INSTANCE')),
|
|
123
123
|
_ts_param(1, Inject(UtilsService)),
|
|
124
124
|
_ts_param(2, Inject(IAMConfigService)),
|
|
125
|
-
_ts_param(3, Inject(
|
|
125
|
+
_ts_param(3, Inject(IAMDataSourceService)),
|
|
126
126
|
_ts_metadata("design:type", Function),
|
|
127
127
|
_ts_metadata("design:paramtypes", [
|
|
128
128
|
typeof HybridCache === "undefined" ? Object : HybridCache,
|
|
129
129
|
typeof UtilsService === "undefined" ? Object : UtilsService,
|
|
130
130
|
typeof IAMConfigService === "undefined" ? Object : IAMConfigService,
|
|
131
|
-
typeof
|
|
131
|
+
typeof IAMDataSourceService === "undefined" ? Object : IAMDataSourceService
|
|
132
132
|
])
|
|
133
133
|
], RoleService);
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { ILoggedUserInfo } from '@flusys/nestjs-shared';
|
|
2
|
+
import { IAMConfigService } from '../services/iam-config.service';
|
|
3
|
+
export declare function validateCompanyAccess(config: IAMConfigService, companyId: string | undefined, user: ILoggedUserInfo, errorMessage?: string): void;
|
package/helpers/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from './
|
|
1
|
+
export * from './company-access.helper';
|
|
2
2
|
export * from './permission-mode.helper';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IBootstrapAppConfig, IDataSourceServiceOptions, IDynamicModuleConfig, IModuleOptionsFactory } from '@flusys/nestjs-core';
|
|
2
|
+
import { ModuleMetadata, Type } from '@nestjs/common';
|
|
2
3
|
export interface IIAMModuleConfig extends IDataSourceServiceOptions {
|
|
3
4
|
}
|
|
4
5
|
export interface IAMModuleOptions extends IDynamicModuleConfig {
|
|
@@ -9,4 +10,11 @@ export interface IAMOptionsFactory extends IModuleOptionsFactory<IIAMModuleConfi
|
|
|
9
10
|
createIAMOptions(): Promise<IIAMModuleConfig> | IIAMModuleConfig;
|
|
10
11
|
createOptions(): Promise<IIAMModuleConfig> | IIAMModuleConfig;
|
|
11
12
|
}
|
|
12
|
-
export
|
|
13
|
+
export interface IAMModuleAsyncOptions extends Pick<ModuleMetadata, 'imports'>, IDynamicModuleConfig {
|
|
14
|
+
bootstrapAppConfig?: IBootstrapAppConfig;
|
|
15
|
+
config?: IIAMModuleConfig;
|
|
16
|
+
useFactory?: (...args: any[]) => Promise<IIAMModuleConfig> | IIAMModuleConfig;
|
|
17
|
+
inject?: any[];
|
|
18
|
+
useClass?: Type<IAMOptionsFactory>;
|
|
19
|
+
useExisting?: Type<IAMOptionsFactory>;
|
|
20
|
+
}
|
package/interfaces/index.d.ts
CHANGED
package/modules/iam.module.d.ts
CHANGED
|
@@ -2,10 +2,9 @@ import { DynamicModule } from '@nestjs/common';
|
|
|
2
2
|
import { IAMModuleAsyncOptions, IAMModuleOptions } from '../interfaces/iam-module-options.interface';
|
|
3
3
|
export declare class IAMModule {
|
|
4
4
|
private static getControllers;
|
|
5
|
-
private static getEntities;
|
|
6
5
|
private static getServices;
|
|
7
6
|
private static getPermissionGuardConfigProvider;
|
|
8
|
-
private static
|
|
7
|
+
private static getExports;
|
|
9
8
|
static forRoot(options?: IAMModuleOptions): DynamicModule;
|
|
10
9
|
static forRootAsync(asyncOptions: IAMModuleAsyncOptions): DynamicModule;
|
|
11
10
|
private static createAsyncProviders;
|