@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oondemand/oon-core-back",
3
- "version": "0.3.61",
3
+ "version": "0.3.63",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/oondemand/oon-platform.git",
@@ -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({}).sort({ label: 1, instanceId: 1 }).lean();
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 env = environmentCredentials(normalized);
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,