@oondemand/oon-core-back 0.3.75 → 0.3.76
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/package.json +1 -1
- package/src/core/errors/GenericError.js +3 -2
- package/src/core/factories/crudController.js +96 -27
- package/src/core/factories/crudService.js +101 -22
- package/src/core/middlewares/auth.js +38 -3
- package/src/core/tenancy.js +200 -0
- package/src/index.js +11 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Erro de domínio reconhecido pelo error middleware do Core.
|
|
3
|
-
* Permite que services/triggers sinalizem statusCode e detalhes.
|
|
3
|
+
* Permite que services/triggers sinalizem statusCode, code e detalhes.
|
|
4
4
|
*/
|
|
5
5
|
class GenericError extends Error {
|
|
6
|
-
constructor(message, { statusCode = 400, details = null } = {}) {
|
|
6
|
+
constructor(message, { statusCode = 400, code = undefined, details = null } = {}) {
|
|
7
7
|
super(message);
|
|
8
8
|
this.name = "GenericError";
|
|
9
9
|
this.statusCode = statusCode;
|
|
10
|
+
this.code = code;
|
|
10
11
|
this.details = details;
|
|
11
12
|
}
|
|
12
13
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const Helpers = require("../utils/helpers");
|
|
2
2
|
const registry = require("../registry");
|
|
3
|
+
const { scopeMutation } = require("../tenancy");
|
|
3
4
|
const {
|
|
4
5
|
prepareProcessMutation,
|
|
5
6
|
assertAtomicInvariants,
|
|
@@ -38,13 +39,27 @@ function createCrudController(entry, service) {
|
|
|
38
39
|
if (validate) await validate(data, context);
|
|
39
40
|
};
|
|
40
41
|
|
|
41
|
-
const prepareCreate = async (
|
|
42
|
-
|
|
42
|
+
const prepareCreate = async (
|
|
43
|
+
data,
|
|
44
|
+
method = "post",
|
|
45
|
+
session = null,
|
|
46
|
+
accessContext = undefined,
|
|
47
|
+
) => {
|
|
48
|
+
const scopedData = scopeMutation(entry, data, accessContext);
|
|
49
|
+
const prepared = await prepareProcessMutation(entry, {}, scopedData, {
|
|
43
50
|
op: "create",
|
|
44
51
|
session,
|
|
52
|
+
accessContext,
|
|
45
53
|
});
|
|
46
54
|
|
|
47
|
-
|
|
55
|
+
// Reaplica o escopo depois das regras para impedir que fórmula/hook de
|
|
56
|
+
// domínio substitua tenantId/usuarioId confiável. O controller usa o
|
|
57
|
+
// contrato público de tenancy e não exige helpers extras em services
|
|
58
|
+
// customizados/legados.
|
|
59
|
+
prepared.changes = scopeMutation(entry, prepared.changes, accessContext);
|
|
60
|
+
prepared.consolidated = scopeMutation(entry, prepared.consolidated, accessContext);
|
|
61
|
+
|
|
62
|
+
await assertReferencePolicies(entry, prepared.consolidated, { session, accessContext });
|
|
48
63
|
await runCustomValidation(prepared.consolidated, {
|
|
49
64
|
op: "create",
|
|
50
65
|
method,
|
|
@@ -54,14 +69,24 @@ function createCrudController(entry, service) {
|
|
|
54
69
|
consolidated: prepared.consolidated,
|
|
55
70
|
changedFields: prepared.changedFields,
|
|
56
71
|
session,
|
|
72
|
+
accessContext,
|
|
57
73
|
});
|
|
58
74
|
return prepared;
|
|
59
75
|
};
|
|
60
76
|
|
|
61
|
-
const prepareUpdate = async (
|
|
77
|
+
const prepareUpdate = async (
|
|
78
|
+
id,
|
|
79
|
+
changes,
|
|
80
|
+
method,
|
|
81
|
+
session = null,
|
|
82
|
+
accessContext = undefined,
|
|
83
|
+
) => {
|
|
84
|
+
const scopedChanges = scopeMutation(entry, changes, accessContext);
|
|
62
85
|
const validate = registry.getValidation(entry.name);
|
|
63
86
|
if (!validate && !hasDomainRules(entry) && !hasProcessRules(entry)) {
|
|
64
|
-
const normalizedChanges =
|
|
87
|
+
const normalizedChanges = scopedChanges && typeof scopedChanges === "object"
|
|
88
|
+
? { ...scopedChanges }
|
|
89
|
+
: {};
|
|
65
90
|
return {
|
|
66
91
|
current: null,
|
|
67
92
|
requestedChanges: normalizedChanges,
|
|
@@ -71,14 +96,17 @@ function createCrudController(entry, service) {
|
|
|
71
96
|
};
|
|
72
97
|
}
|
|
73
98
|
|
|
74
|
-
const currentRecord = await service.getById(id, { session });
|
|
99
|
+
const currentRecord = await service.getById(id, { session, accessContext });
|
|
75
100
|
const current = toPlainRecord(currentRecord);
|
|
76
|
-
const prepared = await prepareProcessMutation(entry, current,
|
|
101
|
+
const prepared = await prepareProcessMutation(entry, current, scopedChanges, {
|
|
77
102
|
op: "update",
|
|
78
103
|
session,
|
|
104
|
+
accessContext,
|
|
79
105
|
});
|
|
106
|
+
prepared.changes = scopeMutation(entry, prepared.changes, accessContext);
|
|
107
|
+
prepared.consolidated = scopeMutation(entry, prepared.consolidated, accessContext);
|
|
80
108
|
|
|
81
|
-
await assertReferencePolicies(entry, prepared.consolidated, { session });
|
|
109
|
+
await assertReferencePolicies(entry, prepared.consolidated, { session, accessContext });
|
|
82
110
|
if (validate) {
|
|
83
111
|
await validate(prepared.consolidated, {
|
|
84
112
|
op: "update",
|
|
@@ -90,30 +118,33 @@ function createCrudController(entry, service) {
|
|
|
90
118
|
consolidated: prepared.consolidated,
|
|
91
119
|
changedFields: prepared.changedFields,
|
|
92
120
|
session,
|
|
121
|
+
accessContext,
|
|
93
122
|
});
|
|
94
123
|
}
|
|
95
124
|
|
|
96
125
|
return { current, ...prepared };
|
|
97
126
|
};
|
|
98
127
|
|
|
99
|
-
async function createOne(data, method, session) {
|
|
100
|
-
const prepared = await prepareCreate(data, method, session);
|
|
128
|
+
async function createOne(data, method, session, accessContext) {
|
|
129
|
+
const prepared = await prepareCreate(data, method, session, accessContext);
|
|
101
130
|
await assertAtomicInvariants(entry, prepared.consolidated, null, session);
|
|
102
|
-
const record = await service.create(prepared.changes, { session });
|
|
131
|
+
const record = await service.create(prepared.changes, { session, accessContext });
|
|
103
132
|
await recalculateDependents(entry, null, toPlainRecord(record), {
|
|
104
133
|
session,
|
|
105
134
|
changedFields: prepared.changedFields,
|
|
135
|
+
accessContext,
|
|
106
136
|
});
|
|
107
137
|
return record;
|
|
108
138
|
}
|
|
109
139
|
|
|
110
|
-
async function updateOne(id, changes, method, session) {
|
|
111
|
-
const prepared = await prepareUpdate(id, changes, method, session);
|
|
140
|
+
async function updateOne(id, changes, method, session, accessContext) {
|
|
141
|
+
const prepared = await prepareUpdate(id, changes, method, session, accessContext);
|
|
112
142
|
await assertAtomicInvariants(entry, prepared.consolidated, prepared.current, session);
|
|
113
|
-
const record = await service.update(id, prepared.changes, { session });
|
|
143
|
+
const record = await service.update(id, prepared.changes, { session, accessContext });
|
|
114
144
|
await recalculateDependents(entry, prepared.current, toPlainRecord(record), {
|
|
115
145
|
session,
|
|
116
146
|
changedFields: prepared.changedFields,
|
|
147
|
+
accessContext,
|
|
117
148
|
});
|
|
118
149
|
return record;
|
|
119
150
|
}
|
|
@@ -127,19 +158,27 @@ function createCrudController(entry, service) {
|
|
|
127
158
|
searchTerm,
|
|
128
159
|
sort,
|
|
129
160
|
filtros,
|
|
161
|
+
accessContext: req.accessContext,
|
|
130
162
|
});
|
|
131
163
|
Helpers.sendPaginatedResponse({ res, statusCode: 200, results, pagination });
|
|
132
164
|
},
|
|
133
165
|
|
|
134
166
|
async getById(req, res) {
|
|
135
|
-
const registro = await service.getById(req.params.id
|
|
167
|
+
const registro = await service.getById(req.params.id, {
|
|
168
|
+
accessContext: req.accessContext,
|
|
169
|
+
});
|
|
136
170
|
Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
|
|
137
171
|
},
|
|
138
172
|
|
|
139
173
|
async create(req, res) {
|
|
140
174
|
const registro = await withProcessTransaction(
|
|
141
175
|
entry,
|
|
142
|
-
(session) => createOne(
|
|
176
|
+
(session) => createOne(
|
|
177
|
+
req.body ?? {},
|
|
178
|
+
"post",
|
|
179
|
+
session,
|
|
180
|
+
req.accessContext,
|
|
181
|
+
),
|
|
143
182
|
);
|
|
144
183
|
Helpers.sendResponse({ res, statusCode: 201, [entry.singular]: registro });
|
|
145
184
|
},
|
|
@@ -147,7 +186,13 @@ function createCrudController(entry, service) {
|
|
|
147
186
|
async update(req, res) {
|
|
148
187
|
const registro = await withProcessTransaction(
|
|
149
188
|
entry,
|
|
150
|
-
(session) => updateOne(
|
|
189
|
+
(session) => updateOne(
|
|
190
|
+
req.params.id,
|
|
191
|
+
req.body ?? {},
|
|
192
|
+
"put",
|
|
193
|
+
session,
|
|
194
|
+
req.accessContext,
|
|
195
|
+
),
|
|
151
196
|
);
|
|
152
197
|
Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
|
|
153
198
|
},
|
|
@@ -155,20 +200,36 @@ function createCrudController(entry, service) {
|
|
|
155
200
|
async patch(req, res) {
|
|
156
201
|
const registro = await withProcessTransaction(
|
|
157
202
|
entry,
|
|
158
|
-
(session) => updateOne(
|
|
203
|
+
(session) => updateOne(
|
|
204
|
+
req.params.id,
|
|
205
|
+
req.body ?? {},
|
|
206
|
+
"patch",
|
|
207
|
+
session,
|
|
208
|
+
req.accessContext,
|
|
209
|
+
),
|
|
159
210
|
);
|
|
160
211
|
Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
|
|
161
212
|
},
|
|
162
213
|
|
|
163
214
|
async remove(req, res) {
|
|
164
215
|
const registro = await withProcessTransaction(entry, async (session) => {
|
|
165
|
-
const currentRecord = await service.getById(req.params.id, {
|
|
216
|
+
const currentRecord = await service.getById(req.params.id, {
|
|
217
|
+
session,
|
|
218
|
+
accessContext: req.accessContext,
|
|
219
|
+
});
|
|
166
220
|
const current = toPlainRecord(currentRecord);
|
|
167
|
-
await assertDeleteAllowed(entry, req.params.id, {
|
|
168
|
-
|
|
221
|
+
await assertDeleteAllowed(entry, req.params.id, {
|
|
222
|
+
session,
|
|
223
|
+
accessContext: req.accessContext,
|
|
224
|
+
});
|
|
225
|
+
const removed = await service.remove(req.params.id, {
|
|
226
|
+
session,
|
|
227
|
+
accessContext: req.accessContext,
|
|
228
|
+
});
|
|
169
229
|
await recalculateDependents(entry, current, null, {
|
|
170
230
|
session,
|
|
171
231
|
changedFields: Object.keys(current),
|
|
232
|
+
accessContext: req.accessContext,
|
|
172
233
|
});
|
|
173
234
|
return removed;
|
|
174
235
|
});
|
|
@@ -178,16 +239,22 @@ function createCrudController(entry, service) {
|
|
|
178
239
|
async importMany(req, res) {
|
|
179
240
|
const rows = req.body?.rows || req.body;
|
|
180
241
|
if (!Array.isArray(rows)) {
|
|
181
|
-
const result = await service.importMany(rows
|
|
242
|
+
const result = await service.importMany(rows, {
|
|
243
|
+
accessContext: req.accessContext,
|
|
244
|
+
});
|
|
182
245
|
Helpers.sendResponse({ res, statusCode: 201, ...result });
|
|
183
246
|
return;
|
|
184
247
|
}
|
|
185
248
|
|
|
186
249
|
if (!hasProcessRules(entry)) {
|
|
187
250
|
const preparedRows = await Promise.all(
|
|
188
|
-
rows.map(async (row) => (
|
|
251
|
+
rows.map(async (row) => (
|
|
252
|
+
await prepareCreate(row, "import", null, req.accessContext)
|
|
253
|
+
).changes),
|
|
189
254
|
);
|
|
190
|
-
const result = await service.importMany(preparedRows
|
|
255
|
+
const result = await service.importMany(preparedRows, {
|
|
256
|
+
accessContext: req.accessContext,
|
|
257
|
+
});
|
|
191
258
|
Helpers.sendResponse({ res, statusCode: 201, ...result });
|
|
192
259
|
return;
|
|
193
260
|
}
|
|
@@ -195,7 +262,7 @@ function createCrudController(entry, service) {
|
|
|
195
262
|
let inserted = 0;
|
|
196
263
|
for (const row of rows) {
|
|
197
264
|
await withProcessTransaction(entry, async (session) => {
|
|
198
|
-
await createOne(row, "import", session);
|
|
265
|
+
await createOne(row, "import", session, req.accessContext);
|
|
199
266
|
inserted += 1;
|
|
200
267
|
});
|
|
201
268
|
}
|
|
@@ -203,7 +270,9 @@ function createCrudController(entry, service) {
|
|
|
203
270
|
},
|
|
204
271
|
|
|
205
272
|
async exportAll(req, res) {
|
|
206
|
-
const results = await service.exportAll(req.query
|
|
273
|
+
const results = await service.exportAll(req.query, {
|
|
274
|
+
accessContext: req.accessContext,
|
|
275
|
+
});
|
|
207
276
|
Helpers.sendResponse({ res, statusCode: 200, results });
|
|
208
277
|
},
|
|
209
278
|
};
|
|
@@ -214,4 +283,4 @@ module.exports = {
|
|
|
214
283
|
toPlainRecord,
|
|
215
284
|
hasDomainRules,
|
|
216
285
|
hasProcessRules,
|
|
217
|
-
};
|
|
286
|
+
};
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
const GenericError = require("../errors/GenericError");
|
|
2
2
|
const registry = require("../registry");
|
|
3
|
+
const {
|
|
4
|
+
dataScope,
|
|
5
|
+
scopeFilter,
|
|
6
|
+
mergeScopedFilter,
|
|
7
|
+
scopeMutation,
|
|
8
|
+
scopedIdFilter,
|
|
9
|
+
} = require("../tenancy");
|
|
3
10
|
|
|
4
11
|
const COMMON_REFERENCE_LABEL_FIELDS = [
|
|
5
12
|
"nome",
|
|
@@ -15,7 +22,7 @@ const COMMON_REFERENCE_LABEL_FIELDS = [
|
|
|
15
22
|
"sigla",
|
|
16
23
|
];
|
|
17
24
|
|
|
18
|
-
function referencePopulateSpecs(entry) {
|
|
25
|
+
function referencePopulateSpecs(entry, accessContext) {
|
|
19
26
|
const setting = entry.definition?.crud?.populateRefs;
|
|
20
27
|
if (!setting) return [];
|
|
21
28
|
|
|
@@ -35,10 +42,15 @@ function referencePopulateSpecs(entry) {
|
|
|
35
42
|
...COMMON_REFERENCE_LABEL_FIELDS,
|
|
36
43
|
...(referencedEntry?.searchableFields || []),
|
|
37
44
|
];
|
|
45
|
+
let match;
|
|
46
|
+
if (referencedEntry && dataScope(referencedEntry) !== "legacy") {
|
|
47
|
+
match = scopeFilter(referencedEntry, accessContext);
|
|
48
|
+
}
|
|
38
49
|
|
|
39
50
|
return {
|
|
40
51
|
path: field.name,
|
|
41
52
|
select: [...new Set(select)].join(" "),
|
|
53
|
+
...(match && Object.keys(match).length ? { match } : {}),
|
|
42
54
|
};
|
|
43
55
|
});
|
|
44
56
|
}
|
|
@@ -62,21 +74,47 @@ function withSession(query, session) {
|
|
|
62
74
|
return session && query && typeof query.session === "function" ? query.session(session) : query;
|
|
63
75
|
}
|
|
64
76
|
|
|
77
|
+
function scopeDenied() {
|
|
78
|
+
return new GenericError("Registro fora do contexto de acesso atual.", {
|
|
79
|
+
statusCode: 403,
|
|
80
|
+
code: "TENANT_SCOPE_DENIED",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function notFound() {
|
|
85
|
+
return new GenericError("Registro não encontrado.", { statusCode: 404 });
|
|
86
|
+
}
|
|
87
|
+
|
|
65
88
|
function createCrudService(entry) {
|
|
66
89
|
const { mongooseModel: Model, searchableFields } = entry;
|
|
67
|
-
const populateSpecs = referencePopulateSpecs(entry);
|
|
68
90
|
|
|
69
|
-
async function
|
|
91
|
+
async function assertScopedMiss(id, accessContext, session) {
|
|
92
|
+
const scope = dataScope(entry);
|
|
93
|
+
if (scope === "legacy" || scope === "system") throw notFound();
|
|
94
|
+
const exists = await withSession(Model.exists({ _id: id }), session);
|
|
95
|
+
if (exists) throw scopeDenied();
|
|
96
|
+
throw notFound();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function listPaginated({
|
|
100
|
+
pageIndex = 0,
|
|
101
|
+
pageSize = 20,
|
|
102
|
+
searchTerm,
|
|
103
|
+
sort,
|
|
104
|
+
filtros = {},
|
|
105
|
+
accessContext,
|
|
106
|
+
} = {}) {
|
|
70
107
|
const page = Math.max(0, Number(pageIndex) || 0);
|
|
71
108
|
const limit = Math.min(200, Math.max(1, Number(pageSize) || 20));
|
|
72
109
|
|
|
73
|
-
const query =
|
|
110
|
+
const query = mergeScopedFilter(entry, cleanFilters(filtros), accessContext);
|
|
74
111
|
if (searchTerm && searchableFields.length) {
|
|
75
112
|
const rx = new RegExp(escapeRegex(searchTerm), "i");
|
|
76
113
|
query.$or = searchableFields.map((f) => ({ [f]: rx }));
|
|
77
114
|
}
|
|
78
115
|
|
|
79
116
|
const sortSpec = parseSort(sort);
|
|
117
|
+
const populateSpecs = referencePopulateSpecs(entry, accessContext);
|
|
80
118
|
const listQuery = populateQuery(
|
|
81
119
|
Model.find(query).sort(sortSpec).skip(page * limit).limit(limit),
|
|
82
120
|
populateSpecs
|
|
@@ -98,32 +136,48 @@ function createCrudService(entry) {
|
|
|
98
136
|
}
|
|
99
137
|
|
|
100
138
|
async function getById(id, options = {}) {
|
|
101
|
-
const
|
|
139
|
+
const populateSpecs = referencePopulateSpecs(entry, options.accessContext);
|
|
140
|
+
const query = withSession(
|
|
141
|
+
Model.findOne(scopedIdFilter(entry, id, options.accessContext)),
|
|
142
|
+
options.session,
|
|
143
|
+
);
|
|
102
144
|
const doc = await resolvePopulated(query, populateSpecs);
|
|
103
|
-
if (!doc)
|
|
145
|
+
if (!doc) await assertScopedMiss(id, options.accessContext, options.session);
|
|
104
146
|
return doc;
|
|
105
147
|
}
|
|
106
148
|
|
|
107
149
|
async function create(data, options = {}) {
|
|
108
|
-
const
|
|
150
|
+
const scopedData = scopeMutation(entry, data, options.accessContext);
|
|
151
|
+
const doc = new Model(scopedData);
|
|
109
152
|
await doc.save({ session: options.session || undefined });
|
|
110
|
-
return resolvePopulated(doc,
|
|
153
|
+
return resolvePopulated(doc, referencePopulateSpecs(entry, options.accessContext));
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
async function update(id, data, options = {}) {
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
157
|
+
const scopedData = scopeMutation(entry, data, options.accessContext);
|
|
158
|
+
const query = Model.findOneAndUpdate(
|
|
159
|
+
scopedIdFilter(entry, id, options.accessContext),
|
|
160
|
+
scopedData,
|
|
161
|
+
{
|
|
162
|
+
new: true,
|
|
163
|
+
runValidators: true,
|
|
164
|
+
session: options.session || undefined,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
const doc = await resolvePopulated(
|
|
168
|
+
query,
|
|
169
|
+
referencePopulateSpecs(entry, options.accessContext),
|
|
170
|
+
);
|
|
171
|
+
if (!doc) await assertScopedMiss(id, options.accessContext, options.session);
|
|
121
172
|
return doc;
|
|
122
173
|
}
|
|
123
174
|
|
|
124
175
|
async function remove(id, options = {}) {
|
|
125
|
-
const doc = await withSession(
|
|
126
|
-
|
|
176
|
+
const doc = await withSession(
|
|
177
|
+
Model.findOneAndDelete(scopedIdFilter(entry, id, options.accessContext)),
|
|
178
|
+
options.session,
|
|
179
|
+
);
|
|
180
|
+
if (!doc) await assertScopedMiss(id, options.accessContext, options.session);
|
|
127
181
|
return doc;
|
|
128
182
|
}
|
|
129
183
|
|
|
@@ -131,18 +185,38 @@ function createCrudService(entry) {
|
|
|
131
185
|
if (!Array.isArray(rows)) {
|
|
132
186
|
throw new GenericError("Esperado um array de registros.", { statusCode: 400 });
|
|
133
187
|
}
|
|
134
|
-
const
|
|
188
|
+
const scopedRows = rows.map((row) => scopeMutation(entry, row, options.accessContext));
|
|
189
|
+
const docs = await Model.insertMany(scopedRows, {
|
|
135
190
|
ordered: false,
|
|
136
191
|
session: options.session || undefined,
|
|
137
192
|
});
|
|
138
193
|
return { inserted: docs.length };
|
|
139
194
|
}
|
|
140
195
|
|
|
141
|
-
async function exportAll(filtros = {}) {
|
|
142
|
-
|
|
196
|
+
async function exportAll(filtros = {}, options = {}) {
|
|
197
|
+
const query = mergeScopedFilter(entry, cleanFilters(filtros), options.accessContext);
|
|
198
|
+
return Model.find(query).lean();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function scopeQuery(filter = {}, accessContext) {
|
|
202
|
+
return mergeScopedFilter(entry, filter, accessContext);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function scopedData(data = {}, accessContext) {
|
|
206
|
+
return scopeMutation(entry, data, accessContext);
|
|
143
207
|
}
|
|
144
208
|
|
|
145
|
-
return {
|
|
209
|
+
return {
|
|
210
|
+
listPaginated,
|
|
211
|
+
getById,
|
|
212
|
+
create,
|
|
213
|
+
update,
|
|
214
|
+
remove,
|
|
215
|
+
importMany,
|
|
216
|
+
exportAll,
|
|
217
|
+
scopeQuery,
|
|
218
|
+
scopedData,
|
|
219
|
+
};
|
|
146
220
|
}
|
|
147
221
|
|
|
148
222
|
function cleanFilters(filtros) {
|
|
@@ -163,4 +237,9 @@ function escapeRegex(str) {
|
|
|
163
237
|
return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
164
238
|
}
|
|
165
239
|
|
|
166
|
-
module.exports = {
|
|
240
|
+
module.exports = {
|
|
241
|
+
createCrudService,
|
|
242
|
+
referencePopulateSpecs,
|
|
243
|
+
resolvePopulated,
|
|
244
|
+
cleanFilters,
|
|
245
|
+
};
|
|
@@ -8,10 +8,16 @@ const {
|
|
|
8
8
|
getTimeout,
|
|
9
9
|
} = require("../utils/activationProvider");
|
|
10
10
|
const { resolveUserAccess } = require("../rbac/storage");
|
|
11
|
+
const {
|
|
12
|
+
TENANT_HEADER,
|
|
13
|
+
readRequestedTenantId,
|
|
14
|
+
createAccessContext,
|
|
15
|
+
} = require("../tenancy");
|
|
11
16
|
|
|
12
17
|
async function defaultVerifyToken(token, { req } = {}) {
|
|
13
18
|
const baseUrl = getBaseUrl(req);
|
|
14
19
|
const appCode = getAppCode();
|
|
20
|
+
const requestedTenantId = readRequestedTenantId(req);
|
|
15
21
|
|
|
16
22
|
let response;
|
|
17
23
|
try {
|
|
@@ -21,6 +27,7 @@ async function defaultVerifyToken(token, { req } = {}) {
|
|
|
21
27
|
headers: {
|
|
22
28
|
Authorization: `Bearer ${token}`,
|
|
23
29
|
"x-app-code": appCode,
|
|
30
|
+
...(requestedTenantId ? { [TENANT_HEADER]: requestedTenantId } : {}),
|
|
24
31
|
},
|
|
25
32
|
});
|
|
26
33
|
} catch (error) {
|
|
@@ -28,6 +35,7 @@ async function defaultVerifyToken(token, { req } = {}) {
|
|
|
28
35
|
error.response?.data?.message || error.message || "Token inválido ou erro na autenticação.",
|
|
29
36
|
);
|
|
30
37
|
err.statusCode = error.response?.status || error.statusCode || 401;
|
|
38
|
+
err.code = error.response?.data?.code || error.code;
|
|
31
39
|
throw err;
|
|
32
40
|
}
|
|
33
41
|
|
|
@@ -41,6 +49,12 @@ async function defaultVerifyToken(token, { req } = {}) {
|
|
|
41
49
|
const tipoAcesso = data.aplicativo?.tipoAcesso || data.tipoAcesso || data.tipo || data.perfil;
|
|
42
50
|
return {
|
|
43
51
|
...data,
|
|
52
|
+
acesso: response.data.acesso || data.acesso,
|
|
53
|
+
tenant: response.data.tenant || data.tenant,
|
|
54
|
+
tenancyModel:
|
|
55
|
+
response.data.acesso?.app?.tenancyModel ||
|
|
56
|
+
data.aplicativo?.tenancyModel ||
|
|
57
|
+
data.tenancyModel,
|
|
44
58
|
tipo: tipoAcesso === "master" ? "admin" : tipoAcesso,
|
|
45
59
|
perfil: data.perfil || tipoAcesso,
|
|
46
60
|
nome: data.nome,
|
|
@@ -48,18 +62,25 @@ async function defaultVerifyToken(token, { req } = {}) {
|
|
|
48
62
|
};
|
|
49
63
|
}
|
|
50
64
|
|
|
51
|
-
function localDevelopmentVerifyToken(token) {
|
|
65
|
+
function localDevelopmentVerifyToken(token, { req } = {}) {
|
|
52
66
|
const expected = String(process.env.DEV_TOKEN || "");
|
|
53
67
|
if (process.env.NODE_ENV !== "development" || !expected || token !== expected) {
|
|
54
68
|
const error = new Error("Token local de desenvolvimento inválido.");
|
|
55
69
|
error.statusCode = 401;
|
|
56
70
|
throw error;
|
|
57
71
|
}
|
|
72
|
+
const requestedTenantId = readRequestedTenantId(req);
|
|
58
73
|
return {
|
|
74
|
+
id: process.env.DEV_USER_ID || "dev-local",
|
|
75
|
+
usuarioId: process.env.DEV_USER_ID || "dev-local",
|
|
59
76
|
tipo: process.env.DEV_USER_ROLE || "admin",
|
|
60
77
|
perfil: process.env.DEV_USER_ROLE || "admin",
|
|
78
|
+
roleCode: process.env.DEV_USER_ROLE || "admin",
|
|
61
79
|
nome: process.env.DEV_USER_NAME || "Dev Local",
|
|
62
80
|
email: process.env.DEV_USER_EMAIL || "dev@local",
|
|
81
|
+
tenantId: requestedTenantId || process.env.DEV_TENANT_ID,
|
|
82
|
+
appCode: process.env.APP_CODE,
|
|
83
|
+
tenancyModel: process.env.DEV_TENANCY_MODEL || ctx.config.tenancyModel || "single_tenant",
|
|
63
84
|
capabilities: ctx.config.capabilities || [],
|
|
64
85
|
};
|
|
65
86
|
}
|
|
@@ -101,14 +122,28 @@ const authMiddleware = async (req, res, next) => {
|
|
|
101
122
|
}
|
|
102
123
|
|
|
103
124
|
try {
|
|
104
|
-
const
|
|
125
|
+
const requestedTenantId = readRequestedTenantId(req);
|
|
126
|
+
const providerUser = await resolveVerifyToken()(token, { req, requestedTenantId });
|
|
105
127
|
if (!providerUser) {
|
|
106
128
|
const error = new Error("Usuário não resolvido.");
|
|
107
129
|
error.statusCode = 401;
|
|
108
130
|
throw error;
|
|
109
131
|
}
|
|
110
132
|
const usuario = await applyEffectiveAccess(providerUser);
|
|
111
|
-
|
|
133
|
+
const accessContext = createAccessContext(usuario, {
|
|
134
|
+
requestedTenantId,
|
|
135
|
+
tenancyModel: ctx.config.tenancyModel || ctx.config.app?.tenancyModel,
|
|
136
|
+
appCode: getAppCode(),
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
req.usuario = {
|
|
140
|
+
...usuario,
|
|
141
|
+
tenantId: accessContext.tenantId,
|
|
142
|
+
appCode: accessContext.appCode || usuario.appCode,
|
|
143
|
+
};
|
|
144
|
+
// Contexto imutável e validado. Controllers e rotas customizadas devem
|
|
145
|
+
// consumir req.accessContext em vez de tenantId vindo de query/body.
|
|
146
|
+
req.accessContext = accessContext;
|
|
112
147
|
|
|
113
148
|
if (usuario.mustChangePassword && !isFirstAccessRoute(req)) {
|
|
114
149
|
return Helpers.sendErrorResponse({
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
const GenericError = require("./errors/GenericError");
|
|
2
|
+
|
|
3
|
+
const TENANT_HEADER = "x-oon-tenant-id";
|
|
4
|
+
const TENANCY_MODELS = Object.freeze(["none", "single_tenant", "multi_tenant"]);
|
|
5
|
+
const DATA_SCOPES = Object.freeze(["legacy", "system", "tenant", "user"]);
|
|
6
|
+
|
|
7
|
+
function stringId(value) {
|
|
8
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
9
|
+
if (typeof value === "object") {
|
|
10
|
+
if (value.id !== undefined) return stringId(value.id);
|
|
11
|
+
if (value._id !== undefined) return stringId(value._id);
|
|
12
|
+
}
|
|
13
|
+
return String(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function forbidden(code, message) {
|
|
17
|
+
return new GenericError(message, { statusCode: 403, code });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readRequestedTenantId(req) {
|
|
21
|
+
return stringId(req?.headers?.[TENANT_HEADER]);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function inferVerifiedTenantId(usuario) {
|
|
25
|
+
return stringId(
|
|
26
|
+
usuario?.tenantId ||
|
|
27
|
+
usuario?.tenant?.id ||
|
|
28
|
+
usuario?.tenant?._id ||
|
|
29
|
+
usuario?.acesso?.tenant?.id ||
|
|
30
|
+
usuario?.acesso?.tenant?._id ||
|
|
31
|
+
usuario?.licenca?.tenantId ||
|
|
32
|
+
usuario?.aplicativo?.tenantId,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function inferTenancyModel(usuario, fallback = "single_tenant") {
|
|
37
|
+
const value =
|
|
38
|
+
usuario?.tenancyModel ||
|
|
39
|
+
usuario?.app?.tenancyModel ||
|
|
40
|
+
usuario?.aplicativo?.tenancyModel ||
|
|
41
|
+
usuario?.acesso?.app?.tenancyModel ||
|
|
42
|
+
fallback;
|
|
43
|
+
return TENANCY_MODELS.includes(value) ? value : fallback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function inferPermissions(usuario) {
|
|
47
|
+
const raw = usuario?.permissions || usuario?.permissoes || [];
|
|
48
|
+
if (Array.isArray(raw)) return [...new Set(raw.map(String))];
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function createAccessContext(usuario, options = {}) {
|
|
53
|
+
const requestedTenantId = stringId(options.requestedTenantId);
|
|
54
|
+
const verifiedTenantId = inferVerifiedTenantId(usuario);
|
|
55
|
+
const tenancyModel = inferTenancyModel(usuario, options.tenancyModel || "single_tenant");
|
|
56
|
+
|
|
57
|
+
// O header seleciona um tenant, mas nunca é autoridade. A autoridade é o
|
|
58
|
+
// tenant devolvido pelo provedor de identidade/Control Plane após validar o
|
|
59
|
+
// vínculo do usuário com o app e o tenant solicitado.
|
|
60
|
+
if (requestedTenantId && verifiedTenantId && requestedTenantId !== verifiedTenantId) {
|
|
61
|
+
throw forbidden(
|
|
62
|
+
"TENANT_CONTEXT_MISMATCH",
|
|
63
|
+
"O tenant solicitado não corresponde ao contexto autorizado.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (tenancyModel === "multi_tenant" && !verifiedTenantId) {
|
|
68
|
+
throw forbidden(
|
|
69
|
+
"TENANT_CONTEXT_REQUIRED",
|
|
70
|
+
"Selecione um tenant autorizado para acessar este aplicativo.",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (tenancyModel === "none" && requestedTenantId) {
|
|
75
|
+
throw forbidden(
|
|
76
|
+
"TENANT_CONTEXT_NOT_ALLOWED",
|
|
77
|
+
"Este aplicativo não aceita contexto de tenant.",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const roleCode = String(
|
|
82
|
+
usuario?.roleCode || usuario?.perfil || usuario?.tipo || "",
|
|
83
|
+
).trim() || undefined;
|
|
84
|
+
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
userId: stringId(usuario?.usuarioId || usuario?.id || usuario?._id || usuario?.sub),
|
|
87
|
+
tenantId: tenancyModel === "none" ? undefined : verifiedTenantId,
|
|
88
|
+
appCode: String(
|
|
89
|
+
usuario?.appCode ||
|
|
90
|
+
usuario?.app?.codigo ||
|
|
91
|
+
usuario?.aplicativo?.codigo ||
|
|
92
|
+
usuario?.acesso?.app?.codigo ||
|
|
93
|
+
options.appCode ||
|
|
94
|
+
"",
|
|
95
|
+
).trim().toLowerCase() || undefined,
|
|
96
|
+
roleCode,
|
|
97
|
+
permissions: Object.freeze(inferPermissions(usuario)),
|
|
98
|
+
tenancyModel,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function dataScope(entryOrDefinition) {
|
|
103
|
+
const definition = entryOrDefinition?.definition || entryOrDefinition || {};
|
|
104
|
+
const value = definition.scope || definition.tenancy?.scope || "legacy";
|
|
105
|
+
return DATA_SCOPES.includes(value) ? value : "legacy";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function tenantField(entryOrDefinition) {
|
|
109
|
+
const definition = entryOrDefinition?.definition || entryOrDefinition || {};
|
|
110
|
+
return definition.tenancy?.tenantField || "tenantId";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function userField(entryOrDefinition) {
|
|
114
|
+
const definition = entryOrDefinition?.definition || entryOrDefinition || {};
|
|
115
|
+
return definition.tenancy?.userField || "usuarioId";
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function requireContextValue(value, code, message) {
|
|
119
|
+
if (!value) throw forbidden(code, message);
|
|
120
|
+
return value;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function scopeFilter(entryOrDefinition, accessContext) {
|
|
124
|
+
const scope = dataScope(entryOrDefinition);
|
|
125
|
+
if (scope === "legacy" || scope === "system") return {};
|
|
126
|
+
|
|
127
|
+
const tenantId = requireContextValue(
|
|
128
|
+
accessContext?.tenantId,
|
|
129
|
+
"TENANT_CONTEXT_REQUIRED",
|
|
130
|
+
"Contexto de tenant obrigatório para este recurso.",
|
|
131
|
+
);
|
|
132
|
+
const filter = { [tenantField(entryOrDefinition)]: tenantId };
|
|
133
|
+
|
|
134
|
+
if (scope === "user") {
|
|
135
|
+
filter[userField(entryOrDefinition)] = requireContextValue(
|
|
136
|
+
accessContext?.userId,
|
|
137
|
+
"USER_CONTEXT_REQUIRED",
|
|
138
|
+
"Contexto de usuário obrigatório para este recurso.",
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return filter;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function mergeScopedFilter(entryOrDefinition, filter = {}, accessContext) {
|
|
146
|
+
const clean = { ...(filter || {}) };
|
|
147
|
+
const enforced = scopeFilter(entryOrDefinition, accessContext);
|
|
148
|
+
|
|
149
|
+
// Remove tentativas de substituir escopo via query/body. O valor confiável
|
|
150
|
+
// sempre vence e é derivado de req.accessContext.
|
|
151
|
+
for (const [key, value] of Object.entries(enforced)) {
|
|
152
|
+
clean[key] = value;
|
|
153
|
+
}
|
|
154
|
+
return clean;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function scopeMutation(entryOrDefinition, data = {}, accessContext) {
|
|
158
|
+
const result = data && typeof data === "object" && !Array.isArray(data)
|
|
159
|
+
? { ...data }
|
|
160
|
+
: data;
|
|
161
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
|
|
162
|
+
|
|
163
|
+
const scope = dataScope(entryOrDefinition);
|
|
164
|
+
if (scope === "legacy" || scope === "system") return result;
|
|
165
|
+
|
|
166
|
+
const enforced = scopeFilter(entryOrDefinition, accessContext);
|
|
167
|
+
Object.assign(result, enforced);
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function scopedIdFilter(entryOrDefinition, id, accessContext) {
|
|
172
|
+
return mergeScopedFilter(entryOrDefinition, { _id: id }, accessContext);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function assertScopedEntry(entryOrDefinition) {
|
|
176
|
+
const scope = dataScope(entryOrDefinition);
|
|
177
|
+
if (scope === "legacy") return;
|
|
178
|
+
if (!DATA_SCOPES.includes(scope)) {
|
|
179
|
+
throw new Error(`Escopo de dados inválido: ${scope}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
TENANT_HEADER,
|
|
185
|
+
TENANCY_MODELS,
|
|
186
|
+
DATA_SCOPES,
|
|
187
|
+
stringId,
|
|
188
|
+
readRequestedTenantId,
|
|
189
|
+
inferVerifiedTenantId,
|
|
190
|
+
inferTenancyModel,
|
|
191
|
+
createAccessContext,
|
|
192
|
+
dataScope,
|
|
193
|
+
tenantField,
|
|
194
|
+
userField,
|
|
195
|
+
scopeFilter,
|
|
196
|
+
mergeScopedFilter,
|
|
197
|
+
scopeMutation,
|
|
198
|
+
scopedIdFilter,
|
|
199
|
+
assertScopedEntry,
|
|
200
|
+
};
|
package/src/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const { activate } = require("./boot/activation");
|
|
|
6
6
|
const define = require("./core/define");
|
|
7
7
|
const fields = require("./core/fields");
|
|
8
8
|
const registry = require("./core/registry");
|
|
9
|
+
const tenancy = require("./core/tenancy");
|
|
9
10
|
const GenericError = require("./core/errors/GenericError");
|
|
10
11
|
const { requirePermission } = require("./core/middlewares/permissions");
|
|
11
12
|
const rbacPolicy = require("./core/rbac/policy");
|
|
@@ -38,6 +39,16 @@ module.exports = {
|
|
|
38
39
|
roleByCode: rbacPolicy.roleByCode,
|
|
39
40
|
permissionGranted: rbacPolicy.granted,
|
|
40
41
|
|
|
42
|
+
TENANT_HEADER: tenancy.TENANT_HEADER,
|
|
43
|
+
TENANCY_MODELS: tenancy.TENANCY_MODELS,
|
|
44
|
+
DATA_SCOPES: tenancy.DATA_SCOPES,
|
|
45
|
+
createAccessContext: tenancy.createAccessContext,
|
|
46
|
+
readRequestedTenantId: tenancy.readRequestedTenantId,
|
|
47
|
+
scopeFilter: tenancy.scopeFilter,
|
|
48
|
+
mergeScopedFilter: tenancy.mergeScopedFilter,
|
|
49
|
+
scopeMutation: tenancy.scopeMutation,
|
|
50
|
+
scopedIdFilter: tenancy.scopedIdFilter,
|
|
51
|
+
|
|
41
52
|
APP_MANIFEST_FILENAME: appManifest.APP_MANIFEST_FILENAME,
|
|
42
53
|
APP_MANIFEST_SCHEMA_VERSION: appManifest.APP_MANIFEST_SCHEMA_VERSION,
|
|
43
54
|
SUPPORTED_APP_KINDS: appManifest.SUPPORTED_APP_KINDS,
|