@oondemand/oon-core-back 0.3.61 → 0.3.63
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
CHANGED
package/src/api/core.routes.js
CHANGED
|
@@ -105,6 +105,156 @@ function runtimeSummary() {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
const DASHBOARD_FILTER_OPERATORS = Object.freeze({
|
|
109
|
+
eq: "$eq",
|
|
110
|
+
ne: "$ne",
|
|
111
|
+
in: "$in",
|
|
112
|
+
nin: "$nin",
|
|
113
|
+
gt: "$gt",
|
|
114
|
+
gte: "$gte",
|
|
115
|
+
lt: "$lt",
|
|
116
|
+
lte: "$lte",
|
|
117
|
+
exists: "$exists",
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function dashboardError(message, statusCode = 422) {
|
|
121
|
+
const error = new Error(message);
|
|
122
|
+
error.statusCode = statusCode;
|
|
123
|
+
return error;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function dashboardField(entry, field, usage) {
|
|
127
|
+
const normalized = String(field || "").trim();
|
|
128
|
+
if (!normalized || normalized.startsWith("$") || normalized.includes(".")) {
|
|
129
|
+
throw dashboardError(`Campo inválido para ${usage}.`);
|
|
130
|
+
}
|
|
131
|
+
if (normalized !== "_id" && !entry.metaFields?.[normalized]) {
|
|
132
|
+
throw dashboardError(`Campo não declarado em ${entry.name}: ${normalized}.`);
|
|
133
|
+
}
|
|
134
|
+
return normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function primitive(value) {
|
|
138
|
+
return value === null || ["string", "number", "boolean"].includes(typeof value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeDashboardFilterValue(value) {
|
|
142
|
+
if (primitive(value)) return value;
|
|
143
|
+
if (!value || Array.isArray(value) || typeof value !== "object") {
|
|
144
|
+
throw dashboardError("Valor de filtro inválido.");
|
|
145
|
+
}
|
|
146
|
+
const output = {};
|
|
147
|
+
for (const [operator, operand] of Object.entries(value)) {
|
|
148
|
+
const mongoOperator = DASHBOARD_FILTER_OPERATORS[operator];
|
|
149
|
+
if (!mongoOperator) throw dashboardError(`Operador de filtro não permitido: ${operator}.`);
|
|
150
|
+
if (["in", "nin"].includes(operator)) {
|
|
151
|
+
if (!Array.isArray(operand) || !operand.every(primitive)) {
|
|
152
|
+
throw dashboardError(`${operator} exige uma lista de valores simples.`);
|
|
153
|
+
}
|
|
154
|
+
} else if (operator === "exists") {
|
|
155
|
+
if (typeof operand !== "boolean") throw dashboardError("exists exige true ou false.");
|
|
156
|
+
} else if (!primitive(operand)) {
|
|
157
|
+
throw dashboardError(`${operator} exige um valor simples.`);
|
|
158
|
+
}
|
|
159
|
+
output[mongoOperator] = operand;
|
|
160
|
+
}
|
|
161
|
+
return output;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function normalizeDashboardFilter(entry, raw = {}) {
|
|
165
|
+
if (!raw || Array.isArray(raw) || typeof raw !== "object") {
|
|
166
|
+
throw dashboardError("Filtros do Dashboard devem ser um objeto.");
|
|
167
|
+
}
|
|
168
|
+
const entries = Object.entries(raw);
|
|
169
|
+
if (entries.length > 20) throw dashboardError("O Dashboard aceita no máximo 20 filtros por indicador.");
|
|
170
|
+
return Object.fromEntries(entries.map(([field, value]) => [
|
|
171
|
+
dashboardField(entry, field, "filtro"),
|
|
172
|
+
normalizeDashboardFilterValue(value),
|
|
173
|
+
]));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function executeDashboardMetric(entry, query = {}) {
|
|
177
|
+
if (!entry?.mongooseModel) throw dashboardError("Model sem persistência para o Dashboard.");
|
|
178
|
+
const id = String(query.id || "").trim();
|
|
179
|
+
if (!id) throw dashboardError("Todo indicador do Dashboard deve possuir id.");
|
|
180
|
+
const kind = String(query.kind || "count");
|
|
181
|
+
if (!["count", "sum", "groupCount", "groupSum"].includes(kind)) {
|
|
182
|
+
throw dashboardError(`Agregação de Dashboard não permitida: ${kind}.`);
|
|
183
|
+
}
|
|
184
|
+
const filter = normalizeDashboardFilter(entry, query.filters || {});
|
|
185
|
+
|
|
186
|
+
if (kind === "count") {
|
|
187
|
+
return { id, kind, value: Number(await entry.mongooseModel.countDocuments(filter)) };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const field = ["sum", "groupSum"].includes(kind)
|
|
191
|
+
? dashboardField(entry, query.field, "soma")
|
|
192
|
+
: undefined;
|
|
193
|
+
if (field) {
|
|
194
|
+
const fieldKind = entry.metaFields?.[field]?.kind;
|
|
195
|
+
if (!["number", "currency", "currencyConverted"].includes(fieldKind)) {
|
|
196
|
+
throw dashboardError(`O campo ${entry.name}.${field} não é numérico.`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (kind === "sum") {
|
|
201
|
+
const [row] = await entry.mongooseModel.aggregate([
|
|
202
|
+
{ $match: filter },
|
|
203
|
+
{ $group: { _id: null, value: { $sum: { $ifNull: [`$${field}`, 0] } } } },
|
|
204
|
+
]);
|
|
205
|
+
return { id, kind, value: Number(row?.value || 0) };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const groupBy = dashboardField(entry, query.groupBy, "agrupamento");
|
|
209
|
+
const accumulator = kind === "groupSum"
|
|
210
|
+
? { $sum: { $ifNull: [`$${field}`, 0] } }
|
|
211
|
+
: { $sum: 1 };
|
|
212
|
+
const rows = await entry.mongooseModel.aggregate([
|
|
213
|
+
{ $match: filter },
|
|
214
|
+
{ $group: { _id: `$${groupBy}`, value: accumulator } },
|
|
215
|
+
{ $sort: { _id: 1 } },
|
|
216
|
+
]);
|
|
217
|
+
return {
|
|
218
|
+
id,
|
|
219
|
+
kind,
|
|
220
|
+
groups: rows.map((row) => ({
|
|
221
|
+
key: row._id == null || row._id === "" ? "Sem status" : String(row._id),
|
|
222
|
+
value: Number(row.value || 0),
|
|
223
|
+
})),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function canReadDashboardModel(req, entry) {
|
|
228
|
+
const roles = entry.definition?.crud?.roles?.read || [];
|
|
229
|
+
if (!roles.length) return true;
|
|
230
|
+
const role = req.user?.tipo || req.user?.role;
|
|
231
|
+
return roles.includes(role);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
router.post("/dashboard/query", async (req, res, next) => {
|
|
235
|
+
try {
|
|
236
|
+
const queries = req.body?.queries;
|
|
237
|
+
if (!Array.isArray(queries) || !queries.length) {
|
|
238
|
+
throw dashboardError("Informe ao menos um indicador do Dashboard.", 400);
|
|
239
|
+
}
|
|
240
|
+
if (queries.length > 40) throw dashboardError("O Dashboard aceita no máximo 40 indicadores.", 400);
|
|
241
|
+
const ids = queries.map((query) => String(query?.id || ""));
|
|
242
|
+
if (new Set(ids).size !== ids.length) throw dashboardError("Ids de indicadores não podem se repetir.", 400);
|
|
243
|
+
|
|
244
|
+
const results = await Promise.all(queries.map(async (query) => {
|
|
245
|
+
const entry = registry.getModel(query.model);
|
|
246
|
+
if (!entry) throw dashboardError(`Model não registrada: ${query.model || "não informada"}.`, 404);
|
|
247
|
+
if (!canReadDashboardModel(req, entry)) {
|
|
248
|
+
throw dashboardError(`Sem permissão de leitura para ${entry.name}.`, 403);
|
|
249
|
+
}
|
|
250
|
+
return executeDashboardMetric(entry, query);
|
|
251
|
+
}));
|
|
252
|
+
res.json({ results });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
next(error);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
108
258
|
router.get("/metadata", (_req, res) => {
|
|
109
259
|
res.json({
|
|
110
260
|
service: { name: ctx.config.serviceName, version: ctx.config.serviceVersion },
|
|
@@ -152,3 +302,5 @@ router.get("/features", (_req, res) => res.json({ results: registry.listFeatures
|
|
|
152
302
|
|
|
153
303
|
module.exports = router;
|
|
154
304
|
module.exports.runtimeSummary = runtimeSummary;
|
|
305
|
+
module.exports.executeDashboardMetric = executeDashboardMetric;
|
|
306
|
+
module.exports.normalizeDashboardFilter = normalizeDashboardFilter;
|
|
@@ -91,6 +91,16 @@ function buildIntegrationRouter() {
|
|
|
91
91
|
requestId: req.requestId || "",
|
|
92
92
|
}));
|
|
93
93
|
}));
|
|
94
|
+
router.get("/providers/:provider/configuration/:instanceId/webhook", rbac(READ_ROLES), asyncHandler(async (req, res) => {
|
|
95
|
+
const provider = getIntegrationProvider(req.params.provider);
|
|
96
|
+
const getWebhookAccess = requireProviderExtension(provider, "getWebhookAccess");
|
|
97
|
+
res.json({ provider: provider.key, ...await getWebhookAccess(req.params.instanceId) });
|
|
98
|
+
}));
|
|
99
|
+
router.post("/providers/:provider/configuration/:instanceId/webhook/rotate", rbac(WRITE_ROLES), asyncHandler(async (req, res) => {
|
|
100
|
+
const provider = getIntegrationProvider(req.params.provider);
|
|
101
|
+
const rotateWebhookToken = requireProviderExtension(provider, "rotateWebhookToken");
|
|
102
|
+
res.json({ provider: provider.key, ...await rotateWebhookToken(req.params.instanceId) });
|
|
103
|
+
}));
|
|
94
104
|
router.get("/providers/:provider/mappings", rbac(READ_ROLES), asyncHandler(async (req, res) => {
|
|
95
105
|
const provider = getIntegrationProvider(req.params.provider);
|
|
96
106
|
const describe = requireProviderExtension(provider, "describe");
|
|
@@ -63,6 +63,10 @@ function maskSecret(value) {
|
|
|
63
63
|
return `${clear.slice(0, 2)}${"•".repeat(Math.min(8, clear.length - 4))}${clear.slice(-2)}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function generateWebhookToken() {
|
|
67
|
+
return crypto.randomBytes(32).toString("base64url");
|
|
68
|
+
}
|
|
69
|
+
|
|
66
70
|
function normalizeInstanceId(value) {
|
|
67
71
|
const instanceId = String(value || "default").trim().toLowerCase();
|
|
68
72
|
if (!/^[a-z0-9][a-z0-9._-]*$/.test(instanceId)) {
|
|
@@ -83,6 +87,7 @@ function configurationSchema() {
|
|
|
83
87
|
webhookTokenEncrypted: { type: String, default: "", select: false },
|
|
84
88
|
appKeyMasked: { type: String, default: "" },
|
|
85
89
|
credentialsConfigured: { type: Boolean, default: false },
|
|
90
|
+
webhookConfigured: { type: Boolean, default: false },
|
|
86
91
|
connectionStatus: { type: String, enum: ["not-tested", "ok", "error"], default: "not-tested" },
|
|
87
92
|
lastConnectionError: { type: String, default: "" },
|
|
88
93
|
lastConnectionTestAt: { type: Date, default: null },
|
|
@@ -134,6 +139,7 @@ function environmentCredentials(instanceId) {
|
|
|
134
139
|
function publicConfiguration(document) {
|
|
135
140
|
if (!document) return null;
|
|
136
141
|
const value = typeof document.toObject === "function" ? document.toObject() : { ...document };
|
|
142
|
+
value.webhookConfigured = Boolean(value.webhookConfigured || value.webhookTokenEncrypted);
|
|
137
143
|
delete value.appKeyEncrypted;
|
|
138
144
|
delete value.appSecretEncrypted;
|
|
139
145
|
delete value.webhookTokenEncrypted;
|
|
@@ -142,7 +148,10 @@ function publicConfiguration(document) {
|
|
|
142
148
|
|
|
143
149
|
async function listOmieConfigurations() {
|
|
144
150
|
const { Configuration } = getOmieModels();
|
|
145
|
-
const persisted = await Configuration.find({})
|
|
151
|
+
const persisted = await Configuration.find({})
|
|
152
|
+
.select("+webhookTokenEncrypted")
|
|
153
|
+
.sort({ label: 1, instanceId: 1 })
|
|
154
|
+
.lean();
|
|
146
155
|
if (persisted.length) return persisted.map(publicConfiguration);
|
|
147
156
|
const env = environmentCredentials("default");
|
|
148
157
|
return [{
|
|
@@ -153,6 +162,7 @@ async function listOmieConfigurations() {
|
|
|
153
162
|
baseUrl: env.baseUrl,
|
|
154
163
|
appKeyMasked: maskSecret(env.appKey),
|
|
155
164
|
credentialsConfigured: Boolean(env.appKey && env.appSecret),
|
|
165
|
+
webhookConfigured: Boolean(env.webhookToken),
|
|
156
166
|
connectionStatus: "not-tested",
|
|
157
167
|
lastConnectionError: "",
|
|
158
168
|
lastConnectionTestAt: null,
|
|
@@ -180,6 +190,7 @@ async function saveOmieConfiguration(instanceId, input = {}) {
|
|
|
180
190
|
const appKey = String(input.appKey || "").trim();
|
|
181
191
|
const appSecret = String(input.appSecret || "").trim();
|
|
182
192
|
const webhookToken = String(input.webhookToken || "").trim();
|
|
193
|
+
const env = environmentCredentials(normalized);
|
|
183
194
|
if (appKey) {
|
|
184
195
|
update.appKeyEncrypted = encryptSecret(appKey);
|
|
185
196
|
update.appKeyMasked = maskSecret(appKey);
|
|
@@ -189,10 +200,12 @@ async function saveOmieConfiguration(instanceId, input = {}) {
|
|
|
189
200
|
}
|
|
190
201
|
if (appSecret) update.appSecretEncrypted = encryptSecret(appSecret);
|
|
191
202
|
else if (current?.appSecretEncrypted) update.appSecretEncrypted = current.appSecretEncrypted;
|
|
192
|
-
if (webhookToken) update.webhookTokenEncrypted = encryptSecret(webhookToken);
|
|
193
|
-
else if (current?.webhookTokenEncrypted) update.webhookTokenEncrypted = current.webhookTokenEncrypted;
|
|
194
203
|
|
|
195
|
-
const
|
|
204
|
+
const effectiveWebhookToken = webhookToken
|
|
205
|
+
|| (current?.webhookTokenEncrypted ? decryptSecret(current.webhookTokenEncrypted) : env.webhookToken)
|
|
206
|
+
|| generateWebhookToken();
|
|
207
|
+
update.webhookTokenEncrypted = encryptSecret(effectiveWebhookToken);
|
|
208
|
+
update.webhookConfigured = true;
|
|
196
209
|
const effectiveKey = appKey || (current?.appKeyEncrypted ? decryptSecret(current.appKeyEncrypted) : env.appKey);
|
|
197
210
|
const effectiveSecret = appSecret
|
|
198
211
|
|| (current?.appSecretEncrypted ? decryptSecret(current.appSecretEncrypted) : env.appSecret);
|
|
@@ -238,6 +251,7 @@ async function resolveOmieConfiguration(instanceId = "default", options = {}) {
|
|
|
238
251
|
webhookToken: options.includeSecrets ? env.webhookToken : undefined,
|
|
239
252
|
appKeyMasked: maskSecret(env.appKey),
|
|
240
253
|
credentialsConfigured: Boolean(env.appKey && env.appSecret),
|
|
254
|
+
webhookConfigured: Boolean(env.webhookToken),
|
|
241
255
|
connectionStatus: "not-tested",
|
|
242
256
|
source: "environment",
|
|
243
257
|
};
|
|
@@ -253,6 +267,57 @@ async function resolveOmieConfiguration(instanceId = "default", options = {}) {
|
|
|
253
267
|
return result;
|
|
254
268
|
}
|
|
255
269
|
|
|
270
|
+
async function ensureOmieWebhookAccess(instanceId = "default", options = {}) {
|
|
271
|
+
const normalized = normalizeInstanceId(instanceId);
|
|
272
|
+
const rotate = Boolean(options.rotate);
|
|
273
|
+
const { Configuration } = getOmieModels();
|
|
274
|
+
const env = environmentCredentials(normalized);
|
|
275
|
+
const current = await Configuration.findOne({ instanceId: normalized })
|
|
276
|
+
.select("+appKeyEncrypted +appSecretEncrypted +webhookTokenEncrypted");
|
|
277
|
+
|
|
278
|
+
let token = !rotate && current?.webhookTokenEncrypted
|
|
279
|
+
? decryptSecret(current.webhookTokenEncrypted)
|
|
280
|
+
: (!rotate ? env.webhookToken : "");
|
|
281
|
+
if (!token) token = generateWebhookToken();
|
|
282
|
+
|
|
283
|
+
if (!current) {
|
|
284
|
+
await saveOmieConfiguration(normalized, {
|
|
285
|
+
label: normalized === "default" ? "Omie" : normalized,
|
|
286
|
+
active: Boolean(env.appKey && env.appSecret),
|
|
287
|
+
baseUrl: env.baseUrl,
|
|
288
|
+
...(env.appKey ? { appKey: env.appKey } : {}),
|
|
289
|
+
...(env.appSecret ? { appSecret: env.appSecret } : {}),
|
|
290
|
+
webhookToken: token,
|
|
291
|
+
});
|
|
292
|
+
} else if (rotate || !current.webhookTokenEncrypted) {
|
|
293
|
+
await Configuration.updateOne(
|
|
294
|
+
{ instanceId: normalized },
|
|
295
|
+
{
|
|
296
|
+
$set: {
|
|
297
|
+
webhookTokenEncrypted: encryptSecret(token),
|
|
298
|
+
webhookConfigured: true,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
);
|
|
302
|
+
} else if (!current.webhookConfigured) {
|
|
303
|
+
await Configuration.updateOne(
|
|
304
|
+
{ instanceId: normalized },
|
|
305
|
+
{ $set: { webhookConfigured: true } },
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return {
|
|
310
|
+
instanceId: normalized,
|
|
311
|
+
webhookConfigured: true,
|
|
312
|
+
rotated: rotate,
|
|
313
|
+
webhookPath: `/integrations/webhooks/omie/${encodeURIComponent(token)}`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async function rotateOmieWebhookToken(instanceId = "default") {
|
|
318
|
+
return ensureOmieWebhookAccess(instanceId, { rotate: true });
|
|
319
|
+
}
|
|
320
|
+
|
|
256
321
|
async function updateConnectionStatus(instanceId, result = {}) {
|
|
257
322
|
const normalized = normalizeInstanceId(instanceId);
|
|
258
323
|
const { Configuration } = getOmieModels();
|
|
@@ -297,6 +362,8 @@ module.exports = {
|
|
|
297
362
|
DEFAULT_BASE_URL,
|
|
298
363
|
decryptSecret,
|
|
299
364
|
encryptSecret,
|
|
365
|
+
ensureOmieWebhookAccess,
|
|
366
|
+
generateWebhookToken,
|
|
300
367
|
getOmieModels,
|
|
301
368
|
listOmieConfigurations,
|
|
302
369
|
maskSecret,
|
|
@@ -304,6 +371,7 @@ module.exports = {
|
|
|
304
371
|
publicConfiguration,
|
|
305
372
|
resolveInstanceByWebhookToken,
|
|
306
373
|
resolveOmieConfiguration,
|
|
374
|
+
rotateOmieWebhookToken,
|
|
307
375
|
saveOmieConfiguration,
|
|
308
376
|
updateConnectionStatus,
|
|
309
377
|
};
|
|
@@ -8,11 +8,13 @@ const { sanitizeIntegrationError, sanitizeIntegrationValue } = require("../integ
|
|
|
8
8
|
const { createOmieClient, getPath, resolveTemplate, OmieApiError } = require("./client");
|
|
9
9
|
const { collectOmieDefinitions, describeOmieDefinitions } = require("./definitions");
|
|
10
10
|
const {
|
|
11
|
+
ensureOmieWebhookAccess,
|
|
11
12
|
getOmieModels,
|
|
12
13
|
listOmieConfigurations,
|
|
13
14
|
normalizeInstanceId,
|
|
14
15
|
resolveInstanceByWebhookToken,
|
|
15
16
|
resolveOmieConfiguration,
|
|
17
|
+
rotateOmieWebhookToken,
|
|
16
18
|
saveOmieConfiguration,
|
|
17
19
|
updateConnectionStatus,
|
|
18
20
|
} = require("./models");
|
|
@@ -450,6 +452,8 @@ function ensureOmieProviderRegistered(config = {}) {
|
|
|
450
452
|
getConfiguration: listOmieConfigurations,
|
|
451
453
|
saveConfiguration: saveOmieConfiguration,
|
|
452
454
|
testConnection: (instanceId, options) => testOmieConnection(definitions, instanceId, options),
|
|
455
|
+
getWebhookAccess: (instanceId) => ensureOmieWebhookAccess(instanceId),
|
|
456
|
+
rotateWebhookToken: (instanceId) => rotateOmieWebhookToken(instanceId),
|
|
453
457
|
executeCall: (callKey, input, options) => runTrackedOperation({
|
|
454
458
|
provider: "omie",
|
|
455
459
|
resource: callKey,
|