@flusys/nestjs-form-builder 3.0.0 → 4.0.0-rc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/cjs/config/index.js +1 -0
- package/cjs/config/message-keys.js +62 -0
- package/cjs/controllers/form-result.controller.js +4 -0
- package/cjs/controllers/form.controller.js +1 -0
- package/cjs/services/form-builder-datasource.provider.js +6 -2
- package/cjs/services/form-result.service.js +56 -8
- package/cjs/services/form.service.js +32 -11
- package/cjs/utils/permission.utils.js +12 -20
- package/config/index.d.ts +1 -0
- package/config/message-keys.d.ts +74 -0
- package/fesm/config/index.js +1 -0
- package/fesm/config/message-keys.js +42 -0
- package/fesm/controllers/form-result.controller.js +4 -0
- package/fesm/controllers/form.controller.js +1 -0
- package/fesm/services/form-builder-datasource.provider.js +7 -3
- package/fesm/services/form-result.service.js +56 -8
- package/fesm/services/form.service.js +32 -11
- package/fesm/utils/permission.utils.js +12 -24
- package/package.json +3 -3
- package/services/form-builder-datasource.provider.d.ts +0 -2
- package/utils/permission.utils.d.ts +1 -2
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Form Builder Package Guide
|
|
2
2
|
|
|
3
3
|
> **Package:** `@flusys/nestjs-form-builder`
|
|
4
|
-
> **Version:** 3.0.
|
|
4
|
+
> **Version:** 3.0.1
|
|
5
5
|
> **Type:** Dynamic form management with schema versioning and access control
|
|
6
6
|
|
|
7
7
|
This guide covers the NestJS form builder package - dynamic form creation, submission storage, and multi-tenant support.
|
package/cjs/config/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
3
3
|
value: true
|
|
4
4
|
});
|
|
5
5
|
_export_star(require("./form-builder.constants"), exports);
|
|
6
|
+
_export_star(require("./message-keys"), exports);
|
|
6
7
|
function _export_star(from, to) {
|
|
7
8
|
Object.keys(from).forEach(function(k) {
|
|
8
9
|
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// ==================== FORM BUILDER MODULE MESSAGE KEYS ====================
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
function _export(target, all) {
|
|
7
|
+
for(var name in all)Object.defineProperty(target, name, {
|
|
8
|
+
enumerable: true,
|
|
9
|
+
get: Object.getOwnPropertyDescriptor(all, name).get
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
_export(exports, {
|
|
13
|
+
get FORM_BUILDER_MODULE_MESSAGES () {
|
|
14
|
+
return FORM_BUILDER_MODULE_MESSAGES;
|
|
15
|
+
},
|
|
16
|
+
get FORM_MESSAGES () {
|
|
17
|
+
return FORM_MESSAGES;
|
|
18
|
+
},
|
|
19
|
+
get FORM_RESULT_MESSAGES () {
|
|
20
|
+
return FORM_RESULT_MESSAGES;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
const FORM_MESSAGES = {
|
|
24
|
+
CREATE_SUCCESS: 'form.create.success',
|
|
25
|
+
CREATE_MANY_SUCCESS: 'form.create.many.success',
|
|
26
|
+
GET_SUCCESS: 'form.get.success',
|
|
27
|
+
GET_ALL_SUCCESS: 'form.get.all.success',
|
|
28
|
+
UPDATE_SUCCESS: 'form.update.success',
|
|
29
|
+
UPDATE_MANY_SUCCESS: 'form.update.many.success',
|
|
30
|
+
DELETE_SUCCESS: 'form.delete.success',
|
|
31
|
+
RESTORE_SUCCESS: 'form.restore.success',
|
|
32
|
+
NOT_FOUND: 'form.not.found',
|
|
33
|
+
SCHEMA_SUCCESS: 'form.schema.success',
|
|
34
|
+
PUBLISH_SUCCESS: 'form.publish.success',
|
|
35
|
+
UNPUBLISH_SUCCESS: 'form.unpublish.success',
|
|
36
|
+
DUPLICATE_SUCCESS: 'form.duplicate.success',
|
|
37
|
+
NOT_PUBLIC: 'form.not.public',
|
|
38
|
+
AUTH_REQUIRED: 'form.auth.required',
|
|
39
|
+
ACCESS_DENIED: 'form.access.denied',
|
|
40
|
+
INVALID_ACCESS_TYPE: 'form.invalid.access.type',
|
|
41
|
+
PERMISSION_CHECK_FAILED: 'form.permission.check.failed'
|
|
42
|
+
};
|
|
43
|
+
const FORM_RESULT_MESSAGES = {
|
|
44
|
+
CREATE_SUCCESS: 'form.result.create.success',
|
|
45
|
+
CREATE_MANY_SUCCESS: 'form.result.create.many.success',
|
|
46
|
+
GET_SUCCESS: 'form.result.get.success',
|
|
47
|
+
GET_ALL_SUCCESS: 'form.result.get.all.success',
|
|
48
|
+
UPDATE_SUCCESS: 'form.result.update.success',
|
|
49
|
+
UPDATE_MANY_SUCCESS: 'form.result.update.many.success',
|
|
50
|
+
DELETE_SUCCESS: 'form.result.delete.success',
|
|
51
|
+
RESTORE_SUCCESS: 'form.result.restore.success',
|
|
52
|
+
NOT_FOUND: 'form.result.not.found',
|
|
53
|
+
SUBMIT_SUCCESS: 'form.result.submit.success',
|
|
54
|
+
EXPORT_SUCCESS: 'form.result.export.success',
|
|
55
|
+
STATS_SUCCESS: 'form.result.stats.success',
|
|
56
|
+
HAS_SUBMITTED_SUCCESS: 'form.result.has.submitted.success',
|
|
57
|
+
HAS_NOT_SUBMITTED_SUCCESS: 'form.result.has.not.submitted.success'
|
|
58
|
+
};
|
|
59
|
+
const FORM_BUILDER_MODULE_MESSAGES = {
|
|
60
|
+
FORM: FORM_MESSAGES,
|
|
61
|
+
FORM_RESULT: FORM_RESULT_MESSAGES
|
|
62
|
+
};
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "FormResultController", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
|
+
const _config = require("../config");
|
|
12
13
|
const _decorators = require("@flusys/nestjs-shared/decorators");
|
|
13
14
|
const _guards = require("@flusys/nestjs-shared/guards");
|
|
14
15
|
const _interfaces = require("@flusys/nestjs-shared/interfaces");
|
|
@@ -44,6 +45,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
44
45
|
};
|
|
45
46
|
}
|
|
46
47
|
let FormResultController = class FormResultController extends (0, _classes.createApiController)(_dtos.CreateFormResultDto, _dtos.UpdateFormResultDto, _dtos.FormResultResponseDto, {
|
|
48
|
+
entityName: 'formResult',
|
|
47
49
|
security: {
|
|
48
50
|
insert: {
|
|
49
51
|
level: 'permission',
|
|
@@ -125,6 +127,7 @@ let FormResultController = class FormResultController extends (0, _classes.creat
|
|
|
125
127
|
return {
|
|
126
128
|
success: true,
|
|
127
129
|
message: 'Form results retrieved successfully',
|
|
130
|
+
messageKey: _config.FORM_RESULT_MESSAGES.GET_ALL_SUCCESS,
|
|
128
131
|
data: result.data,
|
|
129
132
|
meta: {
|
|
130
133
|
total: result.total,
|
|
@@ -142,6 +145,7 @@ let FormResultController = class FormResultController extends (0, _classes.creat
|
|
|
142
145
|
return {
|
|
143
146
|
success: true,
|
|
144
147
|
message: hasSubmitted ? 'User has submitted this form' : 'User has not submitted this form',
|
|
148
|
+
messageKey: hasSubmitted ? _config.FORM_RESULT_MESSAGES.HAS_SUBMITTED_SUCCESS : _config.FORM_RESULT_MESSAGES.HAS_NOT_SUBMITTED_SUCCESS,
|
|
145
149
|
data: hasSubmitted
|
|
146
150
|
};
|
|
147
151
|
}
|
|
@@ -44,6 +44,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
44
44
|
};
|
|
45
45
|
}
|
|
46
46
|
let FormController = class FormController extends (0, _classes.createApiController)(_dtos.CreateFormDto, _dtos.UpdateFormDto, _dtos.FormResponseDto, {
|
|
47
|
+
entityName: 'form',
|
|
47
48
|
security: {
|
|
48
49
|
insert: {
|
|
49
50
|
level: 'permission',
|
|
@@ -10,6 +10,7 @@ Object.defineProperty(exports, "FormBuilderDataSourceProvider", {
|
|
|
10
10
|
});
|
|
11
11
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
12
12
|
const _common = require("@nestjs/common");
|
|
13
|
+
const _constants = require("@flusys/nestjs-shared/constants");
|
|
13
14
|
const _core = require("@nestjs/core");
|
|
14
15
|
const _express = require("express");
|
|
15
16
|
const _formbuilderconfigservice = require("./form-builder-config.service");
|
|
@@ -109,7 +110,10 @@ let FormBuilderDataSourceProvider = class FormBuilderDataSourceProvider extends
|
|
|
109
110
|
}
|
|
110
111
|
const config = this.getDefaultDatabaseConfig();
|
|
111
112
|
if (!config) {
|
|
112
|
-
throw new
|
|
113
|
+
throw new _common.InternalServerErrorException({
|
|
114
|
+
message: 'No database config available. Provide defaultDatabaseConfig in FormBuilderModule options.',
|
|
115
|
+
messageKey: _constants.SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
116
|
+
});
|
|
113
117
|
}
|
|
114
118
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
115
119
|
FormBuilderDataSourceProvider.singleConnectionLock = connectionPromise;
|
|
@@ -142,7 +146,7 @@ let FormBuilderDataSourceProvider = class FormBuilderDataSourceProvider extends
|
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
constructor(configService, request){
|
|
145
|
-
super(FormBuilderDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0),
|
|
149
|
+
super(FormBuilderDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), this.configService = configService;
|
|
146
150
|
}
|
|
147
151
|
};
|
|
148
152
|
_define_property(FormBuilderDataSourceProvider, "tenantConnections", new Map());
|
|
@@ -9,10 +9,14 @@ Object.defineProperty(exports, "FormResultService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
|
+
const _config = require("../config");
|
|
13
|
+
const _decorators = require("@flusys/nestjs-shared/decorators");
|
|
14
|
+
const _interfaces = require("@flusys/nestjs-shared/interfaces");
|
|
12
15
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
16
|
const _common = require("@nestjs/common");
|
|
14
17
|
const _typeorm = require("typeorm");
|
|
15
18
|
const _formbuilderconfigservice = require("./form-builder-config.service");
|
|
19
|
+
const _formresultdto = require("../dtos/form-result.dto");
|
|
16
20
|
const _entities = require("../entities");
|
|
17
21
|
const _formaccesstypeenum = require("../enums/form-access-type.enum");
|
|
18
22
|
const _formbuilderdatasourceprovider = require("./form-builder-datasource.provider");
|
|
@@ -69,7 +73,10 @@ let FormResultService = class FormResultService extends _classes.RequestScopedAp
|
|
|
69
73
|
}
|
|
70
74
|
});
|
|
71
75
|
if (!form) {
|
|
72
|
-
throw new _common.NotFoundException(
|
|
76
|
+
throw new _common.NotFoundException({
|
|
77
|
+
message: 'Form not found or inactive',
|
|
78
|
+
messageKey: _config.FORM_MESSAGES.NOT_FOUND
|
|
79
|
+
});
|
|
73
80
|
}
|
|
74
81
|
return form;
|
|
75
82
|
}
|
|
@@ -83,21 +90,33 @@ let FormResultService = class FormResultService extends _classes.RequestScopedAp
|
|
|
83
90
|
async validateSubmissionAccess(form, user, isPublic) {
|
|
84
91
|
if (isPublic) {
|
|
85
92
|
if (form.accessType !== _formaccesstypeenum.FormAccessType.PUBLIC) {
|
|
86
|
-
throw new _common.UnauthorizedException(
|
|
93
|
+
throw new _common.UnauthorizedException({
|
|
94
|
+
message: 'This form is not available for public submission',
|
|
95
|
+
messageKey: _config.FORM_MESSAGES.NOT_PUBLIC
|
|
96
|
+
});
|
|
87
97
|
}
|
|
88
98
|
return;
|
|
89
99
|
}
|
|
90
100
|
if (form.accessType === _formaccesstypeenum.FormAccessType.PUBLIC) return;
|
|
91
101
|
if (!user) {
|
|
92
|
-
throw new _common.UnauthorizedException(
|
|
102
|
+
throw new _common.UnauthorizedException({
|
|
103
|
+
message: 'Authentication required to submit this form',
|
|
104
|
+
messageKey: _config.FORM_MESSAGES.AUTH_REQUIRED
|
|
105
|
+
});
|
|
93
106
|
}
|
|
94
107
|
if (form.accessType === _formaccesstypeenum.FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
|
|
95
|
-
const hasPermission = await (0, _permissionutils.validateUserPermissions)(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled()
|
|
108
|
+
const hasPermission = await (0, _permissionutils.validateUserPermissions)(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled());
|
|
96
109
|
if (!hasPermission) {
|
|
97
|
-
throw new _common.ForbiddenException(
|
|
110
|
+
throw new _common.ForbiddenException({
|
|
111
|
+
message: 'You do not have permission to submit this form',
|
|
112
|
+
messageKey: _config.FORM_MESSAGES.ACCESS_DENIED
|
|
113
|
+
});
|
|
98
114
|
}
|
|
99
115
|
} else if (form.accessType !== _formaccesstypeenum.FormAccessType.AUTHENTICATED) {
|
|
100
|
-
throw new _common.BadRequestException(
|
|
116
|
+
throw new _common.BadRequestException({
|
|
117
|
+
message: 'Invalid form access type',
|
|
118
|
+
messageKey: _config.FORM_MESSAGES.INVALID_ACCESS_TYPE
|
|
119
|
+
});
|
|
101
120
|
}
|
|
102
121
|
}
|
|
103
122
|
buildUserDraftWhere(formId, userId) {
|
|
@@ -230,7 +249,10 @@ let FormResultService = class FormResultService extends _classes.RequestScopedAp
|
|
|
230
249
|
}
|
|
231
250
|
});
|
|
232
251
|
if (!draft) {
|
|
233
|
-
throw new _common.NotFoundException(
|
|
252
|
+
throw new _common.NotFoundException({
|
|
253
|
+
message: 'Draft not found',
|
|
254
|
+
messageKey: _config.FORM_RESULT_MESSAGES.NOT_FOUND
|
|
255
|
+
});
|
|
234
256
|
}
|
|
235
257
|
const form = await this.getActiveForm(dto.formId);
|
|
236
258
|
const isDraft = dto.isDraft ?? false;
|
|
@@ -273,9 +295,35 @@ let FormResultService = class FormResultService extends _classes.RequestScopedAp
|
|
|
273
295
|
return count > 0;
|
|
274
296
|
}
|
|
275
297
|
constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider){
|
|
276
|
-
super('form_result', null, cacheManager, utilsService, FormResultService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "formBuilderConfig", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "formRepository", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider, this.formRepository = null;
|
|
298
|
+
super('form_result', null, cacheManager, utilsService, FormResultService.name, true, 'form-builder'), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "formBuilderConfig", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "formRepository", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider, this.formRepository = null;
|
|
277
299
|
}
|
|
278
300
|
};
|
|
301
|
+
_ts_decorate([
|
|
302
|
+
(0, _decorators.LogAction)({
|
|
303
|
+
action: 'formResult.submit',
|
|
304
|
+
module: 'form-builder'
|
|
305
|
+
}),
|
|
306
|
+
_ts_metadata("design:type", Function),
|
|
307
|
+
_ts_metadata("design:paramtypes", [
|
|
308
|
+
typeof _formresultdto.SubmitFormDto === "undefined" ? Object : _formresultdto.SubmitFormDto,
|
|
309
|
+
Object,
|
|
310
|
+
void 0
|
|
311
|
+
]),
|
|
312
|
+
_ts_metadata("design:returntype", Promise)
|
|
313
|
+
], FormResultService.prototype, "submitForm", null);
|
|
314
|
+
_ts_decorate([
|
|
315
|
+
(0, _decorators.LogAction)({
|
|
316
|
+
action: 'formResult.updateDraft',
|
|
317
|
+
module: 'form-builder'
|
|
318
|
+
}),
|
|
319
|
+
_ts_metadata("design:type", Function),
|
|
320
|
+
_ts_metadata("design:paramtypes", [
|
|
321
|
+
String,
|
|
322
|
+
typeof _formresultdto.SubmitFormDto === "undefined" ? Object : _formresultdto.SubmitFormDto,
|
|
323
|
+
typeof _interfaces.ILoggedUserInfo === "undefined" ? Object : _interfaces.ILoggedUserInfo
|
|
324
|
+
]),
|
|
325
|
+
_ts_metadata("design:returntype", Promise)
|
|
326
|
+
], FormResultService.prototype, "updateDraft", null);
|
|
279
327
|
FormResultService = _ts_decorate([
|
|
280
328
|
(0, _common.Injectable)({
|
|
281
329
|
scope: _common.Scope.REQUEST
|
|
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "FormService", {
|
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
11
|
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
|
+
const _config = require("../config");
|
|
12
13
|
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
14
|
const _utils = require("@flusys/nestjs-shared/utils");
|
|
14
15
|
const _common = require("@nestjs/common");
|
|
@@ -65,14 +66,16 @@ let FormService = class FormService extends _classes.RequestScopedApiService {
|
|
|
65
66
|
}
|
|
66
67
|
});
|
|
67
68
|
if (!dbData) {
|
|
68
|
-
throw new _common.NotFoundException(
|
|
69
|
+
throw new _common.NotFoundException({
|
|
70
|
+
message: 'Form not found',
|
|
71
|
+
messageKey: _config.FORM_MESSAGES.NOT_FOUND
|
|
72
|
+
});
|
|
69
73
|
}
|
|
70
74
|
form = dbData;
|
|
71
75
|
isUpdate = true;
|
|
72
76
|
// Auto-increment schema version if schema changed
|
|
73
77
|
if (dto.schema && JSON.stringify(dto.schema) !== JSON.stringify(form.schema)) {
|
|
74
78
|
dto.schemaVersion = form.schemaVersion + 1;
|
|
75
|
-
this.logger.log(`Schema changed, incrementing version from ${form.schemaVersion} to ${dto.schemaVersion}`);
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
// Merge DTO into form
|
|
@@ -170,7 +173,7 @@ let FormService = class FormService extends _classes.RequestScopedApiService {
|
|
|
170
173
|
}
|
|
171
174
|
async findPublicActiveForm(where) {
|
|
172
175
|
await this.ensureRepositoryInitialized();
|
|
173
|
-
return this.repository.findOne({
|
|
176
|
+
return await this.repository.findOne({
|
|
174
177
|
where: {
|
|
175
178
|
...where,
|
|
176
179
|
accessType: _formaccesstypeenum.FormAccessType.PUBLIC,
|
|
@@ -185,7 +188,10 @@ let FormService = class FormService extends _classes.RequestScopedApiService {
|
|
|
185
188
|
id: formId
|
|
186
189
|
});
|
|
187
190
|
if (!form) {
|
|
188
|
-
throw new _common.NotFoundException(
|
|
191
|
+
throw new _common.NotFoundException({
|
|
192
|
+
message: 'Form not found or not available for public access',
|
|
193
|
+
messageKey: _config.FORM_MESSAGES.NOT_PUBLIC
|
|
194
|
+
});
|
|
189
195
|
}
|
|
190
196
|
return this.toPublicForm(form);
|
|
191
197
|
}
|
|
@@ -199,7 +205,10 @@ let FormService = class FormService extends _classes.RequestScopedApiService {
|
|
|
199
205
|
}
|
|
200
206
|
});
|
|
201
207
|
if (!form) {
|
|
202
|
-
throw new _common.NotFoundException(
|
|
208
|
+
throw new _common.NotFoundException({
|
|
209
|
+
message: 'Form not found or inactive',
|
|
210
|
+
messageKey: _config.FORM_MESSAGES.NOT_FOUND
|
|
211
|
+
});
|
|
203
212
|
}
|
|
204
213
|
// Access validation based on accessType
|
|
205
214
|
if (form.accessType === _formaccesstypeenum.FormAccessType.PUBLIC) {
|
|
@@ -207,12 +216,18 @@ let FormService = class FormService extends _classes.RequestScopedApiService {
|
|
|
207
216
|
}
|
|
208
217
|
// All non-public forms require authentication
|
|
209
218
|
if (!user) {
|
|
210
|
-
throw new _common.UnauthorizedException(
|
|
219
|
+
throw new _common.UnauthorizedException({
|
|
220
|
+
message: 'Authentication required to submit this form',
|
|
221
|
+
messageKey: _config.FORM_MESSAGES.AUTH_REQUIRED
|
|
222
|
+
});
|
|
211
223
|
}
|
|
212
224
|
if (form.accessType === _formaccesstypeenum.FormAccessType.AUTHENTICATED || form.accessType === _formaccesstypeenum.FormAccessType.ACTION_GROUP) {
|
|
213
225
|
return form; // Permission check for ACTION_GROUP is handled by the controller/guard
|
|
214
226
|
}
|
|
215
|
-
throw new _common.BadRequestException(
|
|
227
|
+
throw new _common.BadRequestException({
|
|
228
|
+
message: 'Invalid access type',
|
|
229
|
+
messageKey: _config.FORM_MESSAGES.INVALID_ACCESS_TYPE
|
|
230
|
+
});
|
|
216
231
|
}
|
|
217
232
|
async getBySlug(slug) {
|
|
218
233
|
await this.ensureRepositoryInitialized();
|
|
@@ -250,7 +265,10 @@ let FormService = class FormService extends _classes.RequestScopedApiService {
|
|
|
250
265
|
]
|
|
251
266
|
});
|
|
252
267
|
if (!form) {
|
|
253
|
-
throw new _common.NotFoundException(
|
|
268
|
+
throw new _common.NotFoundException({
|
|
269
|
+
message: 'Form not found',
|
|
270
|
+
messageKey: _config.FORM_MESSAGES.NOT_FOUND
|
|
271
|
+
});
|
|
254
272
|
}
|
|
255
273
|
return {
|
|
256
274
|
id: form.id,
|
|
@@ -265,15 +283,18 @@ let FormService = class FormService extends _classes.RequestScopedApiService {
|
|
|
265
283
|
const form = await this.getFormForSubmission(formId, user);
|
|
266
284
|
// For action_group access, check permissions from cache
|
|
267
285
|
if (form.accessType === _formaccesstypeenum.FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
|
|
268
|
-
const hasPermission = await (0, _permissionutils.validateUserPermissions)(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled()
|
|
286
|
+
const hasPermission = await (0, _permissionutils.validateUserPermissions)(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled());
|
|
269
287
|
if (!hasPermission) {
|
|
270
|
-
throw new _common.ForbiddenException(
|
|
288
|
+
throw new _common.ForbiddenException({
|
|
289
|
+
message: 'You do not have permission to access this form',
|
|
290
|
+
messageKey: _config.FORM_MESSAGES.ACCESS_DENIED
|
|
291
|
+
});
|
|
271
292
|
}
|
|
272
293
|
}
|
|
273
294
|
return this.toPublicForm(form);
|
|
274
295
|
}
|
|
275
296
|
constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider){
|
|
276
|
-
super('form', null, cacheManager, utilsService, FormService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "formBuilderConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider;
|
|
297
|
+
super('form', null, cacheManager, utilsService, FormService.name, true, 'form-builder'), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "formBuilderConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider;
|
|
277
298
|
}
|
|
278
299
|
};
|
|
279
300
|
FormService = _ts_decorate([
|
|
@@ -8,6 +8,7 @@ Object.defineProperty(exports, "validateUserPermissions", {
|
|
|
8
8
|
return validateUserPermissions;
|
|
9
9
|
}
|
|
10
10
|
});
|
|
11
|
+
const _config = require("../config");
|
|
11
12
|
const _common = require("@nestjs/common");
|
|
12
13
|
const CACHE_PREFIX = 'permissions';
|
|
13
14
|
/**
|
|
@@ -15,38 +16,29 @@ const CACHE_PREFIX = 'permissions';
|
|
|
15
16
|
* Uses same cache key format as PermissionGuard in nestjs-shared
|
|
16
17
|
*/ function buildPermissionCacheKey(user, enableCompanyFeature) {
|
|
17
18
|
if (enableCompanyFeature && user.companyId) {
|
|
18
|
-
// Company-based permissions
|
|
19
19
|
return `${CACHE_PREFIX}:company:${user.companyId}:branch:${user.branchId || 'null'}:user:${user.id}`;
|
|
20
20
|
}
|
|
21
|
-
// User-based permissions
|
|
22
21
|
return `${CACHE_PREFIX}:user:${user.id}`;
|
|
23
22
|
}
|
|
24
23
|
/**
|
|
25
24
|
* Get user permissions from cache
|
|
26
25
|
* Fail-closed: if cache fails, deny access rather than allowing potentially unauthorized access
|
|
27
|
-
*/ async function getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature
|
|
26
|
+
*/ async function getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature) {
|
|
28
27
|
const cacheKey = buildPermissionCacheKey(user, enableCompanyFeature);
|
|
29
28
|
try {
|
|
30
29
|
const permissions = await cacheManager.get(cacheKey);
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Fail-closed: if cache fails, deny access rather than allowing potentially unauthorized access
|
|
38
|
-
logger.error(`Failed to get permissions from cache for user ${user.id}: ${error}`);
|
|
39
|
-
throw new _common.ForbiddenException('Unable to verify permissions. Please try again.');
|
|
30
|
+
return permissions ?? [];
|
|
31
|
+
} catch {
|
|
32
|
+
throw new _common.ForbiddenException({
|
|
33
|
+
message: 'Unable to verify permissions. Please try again.',
|
|
34
|
+
messageKey: _config.FORM_MESSAGES.PERMISSION_CHECK_FAILED
|
|
35
|
+
});
|
|
40
36
|
}
|
|
41
37
|
}
|
|
42
|
-
async function validateUserPermissions(user, requiredPermissions, cacheManager, enableCompanyFeature
|
|
38
|
+
async function validateUserPermissions(user, requiredPermissions, cacheManager, enableCompanyFeature) {
|
|
43
39
|
if (!requiredPermissions.length) {
|
|
44
|
-
return true;
|
|
40
|
+
return true;
|
|
45
41
|
}
|
|
46
|
-
const userPermissions = await getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature
|
|
47
|
-
|
|
48
|
-
if (!hasPermission) {
|
|
49
|
-
logger.warn(`Permission denied for user ${user.id} ${context} ${resourceId}. ` + `Required: [${requiredPermissions.join(', ')}], User has: [${userPermissions.join(', ')}]`);
|
|
50
|
-
}
|
|
51
|
-
return hasPermission;
|
|
42
|
+
const userPermissions = await getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature);
|
|
43
|
+
return requiredPermissions.some((perm)=>userPermissions.includes(perm));
|
|
52
44
|
}
|
package/config/index.d.ts
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export declare const FORM_MESSAGES: {
|
|
2
|
+
readonly CREATE_SUCCESS: "form.create.success";
|
|
3
|
+
readonly CREATE_MANY_SUCCESS: "form.create.many.success";
|
|
4
|
+
readonly GET_SUCCESS: "form.get.success";
|
|
5
|
+
readonly GET_ALL_SUCCESS: "form.get.all.success";
|
|
6
|
+
readonly UPDATE_SUCCESS: "form.update.success";
|
|
7
|
+
readonly UPDATE_MANY_SUCCESS: "form.update.many.success";
|
|
8
|
+
readonly DELETE_SUCCESS: "form.delete.success";
|
|
9
|
+
readonly RESTORE_SUCCESS: "form.restore.success";
|
|
10
|
+
readonly NOT_FOUND: "form.not.found";
|
|
11
|
+
readonly SCHEMA_SUCCESS: "form.schema.success";
|
|
12
|
+
readonly PUBLISH_SUCCESS: "form.publish.success";
|
|
13
|
+
readonly UNPUBLISH_SUCCESS: "form.unpublish.success";
|
|
14
|
+
readonly DUPLICATE_SUCCESS: "form.duplicate.success";
|
|
15
|
+
readonly NOT_PUBLIC: "form.not.public";
|
|
16
|
+
readonly AUTH_REQUIRED: "form.auth.required";
|
|
17
|
+
readonly ACCESS_DENIED: "form.access.denied";
|
|
18
|
+
readonly INVALID_ACCESS_TYPE: "form.invalid.access.type";
|
|
19
|
+
readonly PERMISSION_CHECK_FAILED: "form.permission.check.failed";
|
|
20
|
+
};
|
|
21
|
+
export declare const FORM_RESULT_MESSAGES: {
|
|
22
|
+
readonly CREATE_SUCCESS: "form.result.create.success";
|
|
23
|
+
readonly CREATE_MANY_SUCCESS: "form.result.create.many.success";
|
|
24
|
+
readonly GET_SUCCESS: "form.result.get.success";
|
|
25
|
+
readonly GET_ALL_SUCCESS: "form.result.get.all.success";
|
|
26
|
+
readonly UPDATE_SUCCESS: "form.result.update.success";
|
|
27
|
+
readonly UPDATE_MANY_SUCCESS: "form.result.update.many.success";
|
|
28
|
+
readonly DELETE_SUCCESS: "form.result.delete.success";
|
|
29
|
+
readonly RESTORE_SUCCESS: "form.result.restore.success";
|
|
30
|
+
readonly NOT_FOUND: "form.result.not.found";
|
|
31
|
+
readonly SUBMIT_SUCCESS: "form.result.submit.success";
|
|
32
|
+
readonly EXPORT_SUCCESS: "form.result.export.success";
|
|
33
|
+
readonly STATS_SUCCESS: "form.result.stats.success";
|
|
34
|
+
readonly HAS_SUBMITTED_SUCCESS: "form.result.has.submitted.success";
|
|
35
|
+
readonly HAS_NOT_SUBMITTED_SUCCESS: "form.result.has.not.submitted.success";
|
|
36
|
+
};
|
|
37
|
+
export declare const FORM_BUILDER_MODULE_MESSAGES: {
|
|
38
|
+
readonly FORM: {
|
|
39
|
+
readonly CREATE_SUCCESS: "form.create.success";
|
|
40
|
+
readonly CREATE_MANY_SUCCESS: "form.create.many.success";
|
|
41
|
+
readonly GET_SUCCESS: "form.get.success";
|
|
42
|
+
readonly GET_ALL_SUCCESS: "form.get.all.success";
|
|
43
|
+
readonly UPDATE_SUCCESS: "form.update.success";
|
|
44
|
+
readonly UPDATE_MANY_SUCCESS: "form.update.many.success";
|
|
45
|
+
readonly DELETE_SUCCESS: "form.delete.success";
|
|
46
|
+
readonly RESTORE_SUCCESS: "form.restore.success";
|
|
47
|
+
readonly NOT_FOUND: "form.not.found";
|
|
48
|
+
readonly SCHEMA_SUCCESS: "form.schema.success";
|
|
49
|
+
readonly PUBLISH_SUCCESS: "form.publish.success";
|
|
50
|
+
readonly UNPUBLISH_SUCCESS: "form.unpublish.success";
|
|
51
|
+
readonly DUPLICATE_SUCCESS: "form.duplicate.success";
|
|
52
|
+
readonly NOT_PUBLIC: "form.not.public";
|
|
53
|
+
readonly AUTH_REQUIRED: "form.auth.required";
|
|
54
|
+
readonly ACCESS_DENIED: "form.access.denied";
|
|
55
|
+
readonly INVALID_ACCESS_TYPE: "form.invalid.access.type";
|
|
56
|
+
readonly PERMISSION_CHECK_FAILED: "form.permission.check.failed";
|
|
57
|
+
};
|
|
58
|
+
readonly FORM_RESULT: {
|
|
59
|
+
readonly CREATE_SUCCESS: "form.result.create.success";
|
|
60
|
+
readonly CREATE_MANY_SUCCESS: "form.result.create.many.success";
|
|
61
|
+
readonly GET_SUCCESS: "form.result.get.success";
|
|
62
|
+
readonly GET_ALL_SUCCESS: "form.result.get.all.success";
|
|
63
|
+
readonly UPDATE_SUCCESS: "form.result.update.success";
|
|
64
|
+
readonly UPDATE_MANY_SUCCESS: "form.result.update.many.success";
|
|
65
|
+
readonly DELETE_SUCCESS: "form.result.delete.success";
|
|
66
|
+
readonly RESTORE_SUCCESS: "form.result.restore.success";
|
|
67
|
+
readonly NOT_FOUND: "form.result.not.found";
|
|
68
|
+
readonly SUBMIT_SUCCESS: "form.result.submit.success";
|
|
69
|
+
readonly EXPORT_SUCCESS: "form.result.export.success";
|
|
70
|
+
readonly STATS_SUCCESS: "form.result.stats.success";
|
|
71
|
+
readonly HAS_SUBMITTED_SUCCESS: "form.result.has.submitted.success";
|
|
72
|
+
readonly HAS_NOT_SUBMITTED_SUCCESS: "form.result.has.not.submitted.success";
|
|
73
|
+
};
|
|
74
|
+
};
|
package/fesm/config/index.js
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// ==================== FORM BUILDER MODULE MESSAGE KEYS ====================
|
|
2
|
+
export const FORM_MESSAGES = {
|
|
3
|
+
CREATE_SUCCESS: 'form.create.success',
|
|
4
|
+
CREATE_MANY_SUCCESS: 'form.create.many.success',
|
|
5
|
+
GET_SUCCESS: 'form.get.success',
|
|
6
|
+
GET_ALL_SUCCESS: 'form.get.all.success',
|
|
7
|
+
UPDATE_SUCCESS: 'form.update.success',
|
|
8
|
+
UPDATE_MANY_SUCCESS: 'form.update.many.success',
|
|
9
|
+
DELETE_SUCCESS: 'form.delete.success',
|
|
10
|
+
RESTORE_SUCCESS: 'form.restore.success',
|
|
11
|
+
NOT_FOUND: 'form.not.found',
|
|
12
|
+
SCHEMA_SUCCESS: 'form.schema.success',
|
|
13
|
+
PUBLISH_SUCCESS: 'form.publish.success',
|
|
14
|
+
UNPUBLISH_SUCCESS: 'form.unpublish.success',
|
|
15
|
+
DUPLICATE_SUCCESS: 'form.duplicate.success',
|
|
16
|
+
NOT_PUBLIC: 'form.not.public',
|
|
17
|
+
AUTH_REQUIRED: 'form.auth.required',
|
|
18
|
+
ACCESS_DENIED: 'form.access.denied',
|
|
19
|
+
INVALID_ACCESS_TYPE: 'form.invalid.access.type',
|
|
20
|
+
PERMISSION_CHECK_FAILED: 'form.permission.check.failed'
|
|
21
|
+
};
|
|
22
|
+
export const FORM_RESULT_MESSAGES = {
|
|
23
|
+
CREATE_SUCCESS: 'form.result.create.success',
|
|
24
|
+
CREATE_MANY_SUCCESS: 'form.result.create.many.success',
|
|
25
|
+
GET_SUCCESS: 'form.result.get.success',
|
|
26
|
+
GET_ALL_SUCCESS: 'form.result.get.all.success',
|
|
27
|
+
UPDATE_SUCCESS: 'form.result.update.success',
|
|
28
|
+
UPDATE_MANY_SUCCESS: 'form.result.update.many.success',
|
|
29
|
+
DELETE_SUCCESS: 'form.result.delete.success',
|
|
30
|
+
RESTORE_SUCCESS: 'form.result.restore.success',
|
|
31
|
+
NOT_FOUND: 'form.result.not.found',
|
|
32
|
+
SUBMIT_SUCCESS: 'form.result.submit.success',
|
|
33
|
+
EXPORT_SUCCESS: 'form.result.export.success',
|
|
34
|
+
STATS_SUCCESS: 'form.result.stats.success',
|
|
35
|
+
HAS_SUBMITTED_SUCCESS: 'form.result.has.submitted.success',
|
|
36
|
+
HAS_NOT_SUBMITTED_SUCCESS: 'form.result.has.not.submitted.success'
|
|
37
|
+
};
|
|
38
|
+
// Aggregated export for backward compatibility
|
|
39
|
+
export const FORM_BUILDER_MODULE_MESSAGES = {
|
|
40
|
+
FORM: FORM_MESSAGES,
|
|
41
|
+
FORM_RESULT: FORM_RESULT_MESSAGES
|
|
42
|
+
};
|
|
@@ -26,6 +26,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { createApiController, FORM_RESULT_PERMISSIONS } from '@flusys/nestjs-shared/classes';
|
|
29
|
+
import { FORM_RESULT_MESSAGES } from '../config';
|
|
29
30
|
import { CurrentUser, Public, RequirePermission } from '@flusys/nestjs-shared/decorators';
|
|
30
31
|
import { JwtAuthGuard } from '@flusys/nestjs-shared/guards';
|
|
31
32
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
@@ -34,6 +35,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
|
34
35
|
import { CreateFormResultDto, FormResultResponseDto, GetMyDraftDto, GetResultsByFormDto, SubmitFormDto, UpdateDraftDto, UpdateFormResultDto } from '../dtos';
|
|
35
36
|
import { FormResultService } from '../services/form-result.service';
|
|
36
37
|
export class FormResultController extends createApiController(CreateFormResultDto, UpdateFormResultDto, FormResultResponseDto, {
|
|
38
|
+
entityName: 'formResult',
|
|
37
39
|
security: {
|
|
38
40
|
insert: {
|
|
39
41
|
level: 'permission',
|
|
@@ -115,6 +117,7 @@ export class FormResultController extends createApiController(CreateFormResultDt
|
|
|
115
117
|
return {
|
|
116
118
|
success: true,
|
|
117
119
|
message: 'Form results retrieved successfully',
|
|
120
|
+
messageKey: FORM_RESULT_MESSAGES.GET_ALL_SUCCESS,
|
|
118
121
|
data: result.data,
|
|
119
122
|
meta: {
|
|
120
123
|
total: result.total,
|
|
@@ -132,6 +135,7 @@ export class FormResultController extends createApiController(CreateFormResultDt
|
|
|
132
135
|
return {
|
|
133
136
|
success: true,
|
|
134
137
|
message: hasSubmitted ? 'User has submitted this form' : 'User has not submitted this form',
|
|
138
|
+
messageKey: hasSubmitted ? FORM_RESULT_MESSAGES.HAS_SUBMITTED_SUCCESS : FORM_RESULT_MESSAGES.HAS_NOT_SUBMITTED_SUCCESS,
|
|
135
139
|
data: hasSubmitted
|
|
136
140
|
};
|
|
137
141
|
}
|
|
@@ -34,6 +34,7 @@ import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
|
|
|
34
34
|
import { CreateFormDto, FormResponseDto, UpdateFormDto } from '../dtos';
|
|
35
35
|
import { FormService } from '../services/form.service';
|
|
36
36
|
export class FormController extends createApiController(CreateFormDto, UpdateFormDto, FormResponseDto, {
|
|
37
|
+
entityName: 'form',
|
|
37
38
|
security: {
|
|
38
39
|
insert: {
|
|
39
40
|
level: 'permission',
|
|
@@ -26,7 +26,8 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
|
|
29
|
-
import { Inject, Injectable,
|
|
29
|
+
import { Inject, Injectable, InternalServerErrorException, Optional, Scope } from '@nestjs/common';
|
|
30
|
+
import { SYSTEM_MESSAGES } from '@flusys/nestjs-shared/constants';
|
|
30
31
|
import { REQUEST } from '@nestjs/core';
|
|
31
32
|
import { Request } from 'express';
|
|
32
33
|
import { FormBuilderConfigService } from './form-builder-config.service';
|
|
@@ -58,7 +59,10 @@ export class FormBuilderDataSourceProvider extends MultiTenantDataSourceService
|
|
|
58
59
|
}
|
|
59
60
|
const config = this.getDefaultDatabaseConfig();
|
|
60
61
|
if (!config) {
|
|
61
|
-
throw new
|
|
62
|
+
throw new InternalServerErrorException({
|
|
63
|
+
message: 'No database config available. Provide defaultDatabaseConfig in FormBuilderModule options.',
|
|
64
|
+
messageKey: SYSTEM_MESSAGES.DATABASE_CONFIG_NOT_AVAILABLE
|
|
65
|
+
});
|
|
62
66
|
}
|
|
63
67
|
const connectionPromise = this.createDataSourceFromConfig(config);
|
|
64
68
|
FormBuilderDataSourceProvider.singleConnectionLock = connectionPromise;
|
|
@@ -91,7 +95,7 @@ export class FormBuilderDataSourceProvider extends MultiTenantDataSourceService
|
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
constructor(configService, request){
|
|
94
|
-
super(FormBuilderDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0),
|
|
98
|
+
super(FormBuilderDataSourceProvider.buildParentOptions(configService.getOptions()), request), _define_property(this, "configService", void 0), this.configService = configService;
|
|
95
99
|
}
|
|
96
100
|
}
|
|
97
101
|
_define_property(FormBuilderDataSourceProvider, "tenantConnections", new Map());
|
|
@@ -26,10 +26,14 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
|
+
import { FORM_MESSAGES, FORM_RESULT_MESSAGES } from '../config';
|
|
30
|
+
import { LogAction } from '@flusys/nestjs-shared/decorators';
|
|
31
|
+
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
29
32
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
33
|
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException, Scope, UnauthorizedException } from '@nestjs/common';
|
|
31
34
|
import { IsNull } from 'typeorm';
|
|
32
35
|
import { FormBuilderConfigService } from './form-builder-config.service';
|
|
36
|
+
import { SubmitFormDto } from '../dtos/form-result.dto';
|
|
33
37
|
import { Form, FormResult, FormWithCompany } from '../entities';
|
|
34
38
|
import { FormAccessType } from '../enums/form-access-type.enum';
|
|
35
39
|
import { FormBuilderDataSourceProvider } from './form-builder-datasource.provider';
|
|
@@ -59,7 +63,10 @@ export class FormResultService extends RequestScopedApiService {
|
|
|
59
63
|
}
|
|
60
64
|
});
|
|
61
65
|
if (!form) {
|
|
62
|
-
throw new NotFoundException(
|
|
66
|
+
throw new NotFoundException({
|
|
67
|
+
message: 'Form not found or inactive',
|
|
68
|
+
messageKey: FORM_MESSAGES.NOT_FOUND
|
|
69
|
+
});
|
|
63
70
|
}
|
|
64
71
|
return form;
|
|
65
72
|
}
|
|
@@ -73,21 +80,33 @@ export class FormResultService extends RequestScopedApiService {
|
|
|
73
80
|
async validateSubmissionAccess(form, user, isPublic) {
|
|
74
81
|
if (isPublic) {
|
|
75
82
|
if (form.accessType !== FormAccessType.PUBLIC) {
|
|
76
|
-
throw new UnauthorizedException(
|
|
83
|
+
throw new UnauthorizedException({
|
|
84
|
+
message: 'This form is not available for public submission',
|
|
85
|
+
messageKey: FORM_MESSAGES.NOT_PUBLIC
|
|
86
|
+
});
|
|
77
87
|
}
|
|
78
88
|
return;
|
|
79
89
|
}
|
|
80
90
|
if (form.accessType === FormAccessType.PUBLIC) return;
|
|
81
91
|
if (!user) {
|
|
82
|
-
throw new UnauthorizedException(
|
|
92
|
+
throw new UnauthorizedException({
|
|
93
|
+
message: 'Authentication required to submit this form',
|
|
94
|
+
messageKey: FORM_MESSAGES.AUTH_REQUIRED
|
|
95
|
+
});
|
|
83
96
|
}
|
|
84
97
|
if (form.accessType === FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
|
|
85
|
-
const hasPermission = await validateUserPermissions(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled()
|
|
98
|
+
const hasPermission = await validateUserPermissions(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled());
|
|
86
99
|
if (!hasPermission) {
|
|
87
|
-
throw new ForbiddenException(
|
|
100
|
+
throw new ForbiddenException({
|
|
101
|
+
message: 'You do not have permission to submit this form',
|
|
102
|
+
messageKey: FORM_MESSAGES.ACCESS_DENIED
|
|
103
|
+
});
|
|
88
104
|
}
|
|
89
105
|
} else if (form.accessType !== FormAccessType.AUTHENTICATED) {
|
|
90
|
-
throw new BadRequestException(
|
|
106
|
+
throw new BadRequestException({
|
|
107
|
+
message: 'Invalid form access type',
|
|
108
|
+
messageKey: FORM_MESSAGES.INVALID_ACCESS_TYPE
|
|
109
|
+
});
|
|
91
110
|
}
|
|
92
111
|
}
|
|
93
112
|
buildUserDraftWhere(formId, userId) {
|
|
@@ -220,7 +239,10 @@ export class FormResultService extends RequestScopedApiService {
|
|
|
220
239
|
}
|
|
221
240
|
});
|
|
222
241
|
if (!draft) {
|
|
223
|
-
throw new NotFoundException(
|
|
242
|
+
throw new NotFoundException({
|
|
243
|
+
message: 'Draft not found',
|
|
244
|
+
messageKey: FORM_RESULT_MESSAGES.NOT_FOUND
|
|
245
|
+
});
|
|
224
246
|
}
|
|
225
247
|
const form = await this.getActiveForm(dto.formId);
|
|
226
248
|
const isDraft = dto.isDraft ?? false;
|
|
@@ -263,9 +285,35 @@ export class FormResultService extends RequestScopedApiService {
|
|
|
263
285
|
return count > 0;
|
|
264
286
|
}
|
|
265
287
|
constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider){
|
|
266
|
-
super('form_result', null, cacheManager, utilsService, FormResultService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "formBuilderConfig", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "formRepository", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider, this.formRepository = null;
|
|
288
|
+
super('form_result', null, cacheManager, utilsService, FormResultService.name, true, 'form-builder'), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "formBuilderConfig", void 0), _define_property(this, "dataSourceProvider", void 0), _define_property(this, "formRepository", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider, this.formRepository = null;
|
|
267
289
|
}
|
|
268
290
|
}
|
|
291
|
+
_ts_decorate([
|
|
292
|
+
LogAction({
|
|
293
|
+
action: 'formResult.submit',
|
|
294
|
+
module: 'form-builder'
|
|
295
|
+
}),
|
|
296
|
+
_ts_metadata("design:type", Function),
|
|
297
|
+
_ts_metadata("design:paramtypes", [
|
|
298
|
+
typeof SubmitFormDto === "undefined" ? Object : SubmitFormDto,
|
|
299
|
+
Object,
|
|
300
|
+
void 0
|
|
301
|
+
]),
|
|
302
|
+
_ts_metadata("design:returntype", Promise)
|
|
303
|
+
], FormResultService.prototype, "submitForm", null);
|
|
304
|
+
_ts_decorate([
|
|
305
|
+
LogAction({
|
|
306
|
+
action: 'formResult.updateDraft',
|
|
307
|
+
module: 'form-builder'
|
|
308
|
+
}),
|
|
309
|
+
_ts_metadata("design:type", Function),
|
|
310
|
+
_ts_metadata("design:paramtypes", [
|
|
311
|
+
String,
|
|
312
|
+
typeof SubmitFormDto === "undefined" ? Object : SubmitFormDto,
|
|
313
|
+
typeof ILoggedUserInfo === "undefined" ? Object : ILoggedUserInfo
|
|
314
|
+
]),
|
|
315
|
+
_ts_metadata("design:returntype", Promise)
|
|
316
|
+
], FormResultService.prototype, "updateDraft", null);
|
|
269
317
|
FormResultService = _ts_decorate([
|
|
270
318
|
Injectable({
|
|
271
319
|
scope: Scope.REQUEST
|
|
@@ -26,6 +26,7 @@ function _ts_param(paramIndex, decorator) {
|
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
import { RequestScopedApiService, HybridCache } from '@flusys/nestjs-shared/classes';
|
|
29
|
+
import { FORM_MESSAGES } from '../config';
|
|
29
30
|
import { UtilsService } from '@flusys/nestjs-shared/modules';
|
|
30
31
|
import { applyCompanyFilter } from '@flusys/nestjs-shared/utils';
|
|
31
32
|
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException, Scope, UnauthorizedException } from '@nestjs/common';
|
|
@@ -55,14 +56,16 @@ export class FormService extends RequestScopedApiService {
|
|
|
55
56
|
}
|
|
56
57
|
});
|
|
57
58
|
if (!dbData) {
|
|
58
|
-
throw new NotFoundException(
|
|
59
|
+
throw new NotFoundException({
|
|
60
|
+
message: 'Form not found',
|
|
61
|
+
messageKey: FORM_MESSAGES.NOT_FOUND
|
|
62
|
+
});
|
|
59
63
|
}
|
|
60
64
|
form = dbData;
|
|
61
65
|
isUpdate = true;
|
|
62
66
|
// Auto-increment schema version if schema changed
|
|
63
67
|
if (dto.schema && JSON.stringify(dto.schema) !== JSON.stringify(form.schema)) {
|
|
64
68
|
dto.schemaVersion = form.schemaVersion + 1;
|
|
65
|
-
this.logger.log(`Schema changed, incrementing version from ${form.schemaVersion} to ${dto.schemaVersion}`);
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
// Merge DTO into form
|
|
@@ -160,7 +163,7 @@ export class FormService extends RequestScopedApiService {
|
|
|
160
163
|
}
|
|
161
164
|
async findPublicActiveForm(where) {
|
|
162
165
|
await this.ensureRepositoryInitialized();
|
|
163
|
-
return this.repository.findOne({
|
|
166
|
+
return await this.repository.findOne({
|
|
164
167
|
where: {
|
|
165
168
|
...where,
|
|
166
169
|
accessType: FormAccessType.PUBLIC,
|
|
@@ -175,7 +178,10 @@ export class FormService extends RequestScopedApiService {
|
|
|
175
178
|
id: formId
|
|
176
179
|
});
|
|
177
180
|
if (!form) {
|
|
178
|
-
throw new NotFoundException(
|
|
181
|
+
throw new NotFoundException({
|
|
182
|
+
message: 'Form not found or not available for public access',
|
|
183
|
+
messageKey: FORM_MESSAGES.NOT_PUBLIC
|
|
184
|
+
});
|
|
179
185
|
}
|
|
180
186
|
return this.toPublicForm(form);
|
|
181
187
|
}
|
|
@@ -189,7 +195,10 @@ export class FormService extends RequestScopedApiService {
|
|
|
189
195
|
}
|
|
190
196
|
});
|
|
191
197
|
if (!form) {
|
|
192
|
-
throw new NotFoundException(
|
|
198
|
+
throw new NotFoundException({
|
|
199
|
+
message: 'Form not found or inactive',
|
|
200
|
+
messageKey: FORM_MESSAGES.NOT_FOUND
|
|
201
|
+
});
|
|
193
202
|
}
|
|
194
203
|
// Access validation based on accessType
|
|
195
204
|
if (form.accessType === FormAccessType.PUBLIC) {
|
|
@@ -197,12 +206,18 @@ export class FormService extends RequestScopedApiService {
|
|
|
197
206
|
}
|
|
198
207
|
// All non-public forms require authentication
|
|
199
208
|
if (!user) {
|
|
200
|
-
throw new UnauthorizedException(
|
|
209
|
+
throw new UnauthorizedException({
|
|
210
|
+
message: 'Authentication required to submit this form',
|
|
211
|
+
messageKey: FORM_MESSAGES.AUTH_REQUIRED
|
|
212
|
+
});
|
|
201
213
|
}
|
|
202
214
|
if (form.accessType === FormAccessType.AUTHENTICATED || form.accessType === FormAccessType.ACTION_GROUP) {
|
|
203
215
|
return form; // Permission check for ACTION_GROUP is handled by the controller/guard
|
|
204
216
|
}
|
|
205
|
-
throw new BadRequestException(
|
|
217
|
+
throw new BadRequestException({
|
|
218
|
+
message: 'Invalid access type',
|
|
219
|
+
messageKey: FORM_MESSAGES.INVALID_ACCESS_TYPE
|
|
220
|
+
});
|
|
206
221
|
}
|
|
207
222
|
async getBySlug(slug) {
|
|
208
223
|
await this.ensureRepositoryInitialized();
|
|
@@ -240,7 +255,10 @@ export class FormService extends RequestScopedApiService {
|
|
|
240
255
|
]
|
|
241
256
|
});
|
|
242
257
|
if (!form) {
|
|
243
|
-
throw new NotFoundException(
|
|
258
|
+
throw new NotFoundException({
|
|
259
|
+
message: 'Form not found',
|
|
260
|
+
messageKey: FORM_MESSAGES.NOT_FOUND
|
|
261
|
+
});
|
|
244
262
|
}
|
|
245
263
|
return {
|
|
246
264
|
id: form.id,
|
|
@@ -255,15 +273,18 @@ export class FormService extends RequestScopedApiService {
|
|
|
255
273
|
const form = await this.getFormForSubmission(formId, user);
|
|
256
274
|
// For action_group access, check permissions from cache
|
|
257
275
|
if (form.accessType === FormAccessType.ACTION_GROUP && form.actionGroups?.length) {
|
|
258
|
-
const hasPermission = await validateUserPermissions(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled()
|
|
276
|
+
const hasPermission = await validateUserPermissions(user, form.actionGroups, this.cacheManager, this.formBuilderConfig.isCompanyFeatureEnabled());
|
|
259
277
|
if (!hasPermission) {
|
|
260
|
-
throw new ForbiddenException(
|
|
278
|
+
throw new ForbiddenException({
|
|
279
|
+
message: 'You do not have permission to access this form',
|
|
280
|
+
messageKey: FORM_MESSAGES.ACCESS_DENIED
|
|
281
|
+
});
|
|
261
282
|
}
|
|
262
283
|
}
|
|
263
284
|
return this.toPublicForm(form);
|
|
264
285
|
}
|
|
265
286
|
constructor(cacheManager, utilsService, formBuilderConfig, dataSourceProvider){
|
|
266
|
-
super('form', null, cacheManager, utilsService, FormService.name, true), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "formBuilderConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider;
|
|
287
|
+
super('form', null, cacheManager, utilsService, FormService.name, true, 'form-builder'), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), _define_property(this, "formBuilderConfig", void 0), _define_property(this, "dataSourceProvider", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService, this.formBuilderConfig = formBuilderConfig, this.dataSourceProvider = dataSourceProvider;
|
|
267
288
|
}
|
|
268
289
|
}
|
|
269
290
|
FormService = _ts_decorate([
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { FORM_MESSAGES } from '../config';
|
|
1
2
|
import { ForbiddenException } from '@nestjs/common';
|
|
2
3
|
const CACHE_PREFIX = 'permissions';
|
|
3
4
|
/**
|
|
@@ -5,50 +6,37 @@ const CACHE_PREFIX = 'permissions';
|
|
|
5
6
|
* Uses same cache key format as PermissionGuard in nestjs-shared
|
|
6
7
|
*/ function buildPermissionCacheKey(user, enableCompanyFeature) {
|
|
7
8
|
if (enableCompanyFeature && user.companyId) {
|
|
8
|
-
// Company-based permissions
|
|
9
9
|
return `${CACHE_PREFIX}:company:${user.companyId}:branch:${user.branchId || 'null'}:user:${user.id}`;
|
|
10
10
|
}
|
|
11
|
-
// User-based permissions
|
|
12
11
|
return `${CACHE_PREFIX}:user:${user.id}`;
|
|
13
12
|
}
|
|
14
13
|
/**
|
|
15
14
|
* Get user permissions from cache
|
|
16
15
|
* Fail-closed: if cache fails, deny access rather than allowing potentially unauthorized access
|
|
17
|
-
*/ async function getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature
|
|
16
|
+
*/ async function getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature) {
|
|
18
17
|
const cacheKey = buildPermissionCacheKey(user, enableCompanyFeature);
|
|
19
18
|
try {
|
|
20
19
|
const permissions = await cacheManager.get(cacheKey);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// Fail-closed: if cache fails, deny access rather than allowing potentially unauthorized access
|
|
28
|
-
logger.error(`Failed to get permissions from cache for user ${user.id}: ${error}`);
|
|
29
|
-
throw new ForbiddenException('Unable to verify permissions. Please try again.');
|
|
20
|
+
return permissions ?? [];
|
|
21
|
+
} catch {
|
|
22
|
+
throw new ForbiddenException({
|
|
23
|
+
message: 'Unable to verify permissions. Please try again.',
|
|
24
|
+
messageKey: FORM_MESSAGES.PERMISSION_CHECK_FAILED
|
|
25
|
+
});
|
|
30
26
|
}
|
|
31
27
|
}
|
|
32
28
|
/**
|
|
33
29
|
* Validate user has at least one of the required permissions
|
|
34
|
-
* Logs audit warning on permission denial
|
|
35
30
|
*
|
|
36
31
|
* @param user - The logged-in user
|
|
37
32
|
* @param requiredPermissions - Array of required permission strings (user needs at least one)
|
|
38
33
|
* @param cacheManager - Cache manager instance
|
|
39
34
|
* @param enableCompanyFeature - Whether company feature is enabled
|
|
40
|
-
* @param logger - Logger instance for audit logging
|
|
41
|
-
* @param context - Context string for audit logging (e.g., "accessing form", "submitting form")
|
|
42
|
-
* @param resourceId - Resource ID for audit logging
|
|
43
35
|
* @returns true if user has permission, false otherwise
|
|
44
|
-
*/ export async function validateUserPermissions(user, requiredPermissions, cacheManager, enableCompanyFeature
|
|
36
|
+
*/ export async function validateUserPermissions(user, requiredPermissions, cacheManager, enableCompanyFeature) {
|
|
45
37
|
if (!requiredPermissions.length) {
|
|
46
|
-
return true;
|
|
38
|
+
return true;
|
|
47
39
|
}
|
|
48
|
-
const userPermissions = await getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature
|
|
49
|
-
|
|
50
|
-
if (!hasPermission) {
|
|
51
|
-
logger.warn(`Permission denied for user ${user.id} ${context} ${resourceId}. ` + `Required: [${requiredPermissions.join(', ')}], User has: [${userPermissions.join(', ')}]`);
|
|
52
|
-
}
|
|
53
|
-
return hasPermission;
|
|
40
|
+
const userPermissions = await getUserPermissionsFromCache(user, cacheManager, enableCompanyFeature);
|
|
41
|
+
return requiredPermissions.some((perm)=>userPermissions.includes(perm));
|
|
54
42
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flusys/nestjs-form-builder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0-rc",
|
|
4
4
|
"description": "Dynamic form builder module with schema versioning and access control",
|
|
5
5
|
"main": "cjs/index.js",
|
|
6
6
|
"module": "fesm/index.js",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"typeorm": "^0.3.0"
|
|
84
84
|
},
|
|
85
85
|
"dependencies": {
|
|
86
|
-
"@flusys/nestjs-core": "
|
|
87
|
-
"@flusys/nestjs-shared": "
|
|
86
|
+
"@flusys/nestjs-core": "4.0.0-rc",
|
|
87
|
+
"@flusys/nestjs-shared": "4.0.0-rc"
|
|
88
88
|
}
|
|
89
89
|
}
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { MultiTenantDataSourceService } from '@flusys/nestjs-shared/modules';
|
|
2
2
|
import { IDatabaseConfig, ITenantDatabaseConfig } from '@flusys/nestjs-core';
|
|
3
|
-
import { Logger } from '@nestjs/common';
|
|
4
3
|
import { Request } from 'express';
|
|
5
4
|
import { DataSource } from 'typeorm';
|
|
6
5
|
import { FormBuilderConfigService } from './form-builder-config.service';
|
|
7
6
|
export declare class FormBuilderDataSourceProvider extends MultiTenantDataSourceService {
|
|
8
7
|
private readonly configService;
|
|
9
|
-
protected readonly logger: Logger;
|
|
10
8
|
protected static readonly tenantConnections: Map<string, DataSource>;
|
|
11
9
|
protected static singleDataSource: DataSource | null;
|
|
12
10
|
protected static readonly tenantsRegistry: Map<string, ITenantDatabaseConfig>;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
import { HybridCache } from '@flusys/nestjs-shared/classes';
|
|
2
2
|
import { ILoggedUserInfo } from '@flusys/nestjs-shared/interfaces';
|
|
3
|
-
|
|
4
|
-
export declare function validateUserPermissions(user: ILoggedUserInfo, requiredPermissions: string[], cacheManager: HybridCache, enableCompanyFeature: boolean, logger: Logger, context: string, resourceId: string): Promise<boolean>;
|
|
3
|
+
export declare function validateUserPermissions(user: ILoggedUserInfo, requiredPermissions: string[], cacheManager: HybridCache, enableCompanyFeature: boolean): Promise<boolean>;
|