@oondemand/oon-core-back 0.7.6 → 0.7.9
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 +36 -0
- package/package.json +1 -1
- package/src/api/access.routes.js +41 -21
- package/src/api/status.routes.js +2 -1
- package/src/boot/database.js +24 -5
- package/src/capabilities/secrets/index.js +2 -1
- package/src/capabilities/transactionalEmail/models.js +2 -1
- package/src/core/database/errors.js +15 -0
- package/src/core/database/metrics.js +66 -0
- package/src/core/database/mongoose.js +40 -0
- package/src/core/database/policy.js +41 -0
- package/src/core/define.js +2 -1
- package/src/core/fields.js +2 -1
- package/src/core/middlewares/auth.js +8 -0
- package/src/core/middlewares/error.js +12 -0
- package/src/core/middlewares/requestLog.js +3 -2
- package/src/core/models/ControleAlteracao.js +2 -1
- package/src/core/models/InstanciaEcossistema.js +2 -1
- package/src/core/models/Log.js +2 -1
- package/src/core/models/Sistema.js +2 -1
- package/src/core/rbac/centralInvitations.js +70 -0
- package/src/core/rbac/identityProvider.js +17 -2
- package/src/core/rbac/storage.js +6 -4
- package/src/core/runtime/localDevelopment.js +2 -1
- package/src/core/utils/instanceActivation.js +2 -1
- package/src/index.js +2 -0
- package/src/manifest/processRuntime.js +2 -2
- package/src/modules/assistants/index.js +2 -1
- package/src/modules/currencies/index.js +2 -1
package/README.md
CHANGED
|
@@ -97,3 +97,39 @@ O Core omite todo o corpo de entrada e resposta no `requestLog` para esse métod
|
|
|
97
97
|
## Segredos V1 (0.7.6)
|
|
98
98
|
|
|
99
99
|
`capabilities.secrets.put/status/use/remove` armazena objetos cifrados para Apps single_tenant dedicados. Declare `core.secrets` e `capabilitySettings["core.secrets"]={enabled:true}`. Rotas exigem permissão e sensitiveBody; use `req.accessContext`. Chave provisionada fora do banco, preservada em republicações. Veja o guia distribuído `SECRETS.md` para assinaturas, limites e recadastro explícito.
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
### Aceite de convite (C2, SDK 0.7.7)
|
|
103
|
+
|
|
104
|
+
`PUBLIC_APP_URL` deve conter a origem pública HTTPS do aplicativo, sem credenciais,
|
|
105
|
+
query, fragmento ou subcaminho. HTTP é permitido apenas em loopback fora de produção.
|
|
106
|
+
A configuração é validada antes de emitir/rotacionar o convite; o header Host não
|
|
107
|
+
é usado como destino. Novos links usam `/accept-invite#token=...`.
|
|
108
|
+
|
|
109
|
+
O frontend consulta `POST /core/invitations/inspect` com `{ token }`. Somente um
|
|
110
|
+
convite pendente válido permite consultar a existência da identidade. O aceite
|
|
111
|
+
usa `POST /core/invitations/accept` com `{ token, intent, password? }`:
|
|
112
|
+
`authenticate` verifica a senha atual; `register` cria uma identidade com senha
|
|
113
|
+
nova; `session` exige uma sessão válida do destinatário. Nenhum desses caminhos
|
|
114
|
+
redefine a senha de uma identidade existente. Recuperação de senha continua no
|
|
115
|
+
login da plataforma. O retorno após aceite é local e fixo, sem aceitar redirect
|
|
116
|
+
arbitrário. Os endpoints antigos continuam disponíveis até o cutover coordenado.
|
|
117
|
+
|
|
118
|
+
Provedores locais devem fornecer `auth.inspectInvitation({ req, invitation })`,
|
|
119
|
+
retornando `{ accountExists: boolean, sessionUser?: { id, email } | null }`, além de
|
|
120
|
+
`auth.provisionInvitation`. A sessão opcional deve ser validada como identidade
|
|
121
|
+
(assinatura, expiração, versão de credencial e escopo), sem exigir o acesso que
|
|
122
|
+
o convite ainda concederá. Uma sessão inválida retorna `sessionUser: null`.
|
|
123
|
+
O provisionamento recebe também `intent` e deve verificar a prova correspondente,
|
|
124
|
+
retornando `usuario` com `id` e `email` do destinatário. Sem hook de inspeção,
|
|
125
|
+
um provedor local falha fechado e nunca delega identidades administrativas ao
|
|
126
|
+
provedor de aplicativos.
|
|
127
|
+
|
|
128
|
+
Publicar primeiro o suporte da Central (consulta e prova de identidade), depois
|
|
129
|
+
lançar front/back do SDK juntos e atualizar consumidores. O bloqueio de integração
|
|
130
|
+
comercial permanece até C3a/C3b; C2 não executa cutover nem reenvia convites.
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
## Contenção MongoDB
|
|
134
|
+
|
|
135
|
+
O backend governa o pool e os timeouts dos métodos Mongoose cobertos. Veja a [política distribuída](../create-central-oon/docs/DATABASE_CONTAINMENT.md) e as [evidências/gates de adoção](../../docs/database-containment-validation.md). A release precisa ser adotada e republicada por cada consumidor; não fornece isolamento físico nem limite global do cluster.
|
package/package.json
CHANGED
package/src/api/access.routes.js
CHANGED
|
@@ -22,9 +22,10 @@ const {
|
|
|
22
22
|
recoverPendingInvitation,
|
|
23
23
|
assertNotLastAdmin,
|
|
24
24
|
} = require("../core/rbac/storage");
|
|
25
|
-
const { provisionInvitation } = require("../core/rbac/identityProvider");
|
|
25
|
+
const { provisionInvitation, inspectInvitation } = require("../core/rbac/identityProvider");
|
|
26
26
|
|
|
27
27
|
const router = express.Router();
|
|
28
|
+
router.use(require("../core/rbac/centralInvitations").createRouter({ inviteUrl }));
|
|
28
29
|
|
|
29
30
|
function requiredRbac() {
|
|
30
31
|
if (!policy().enabled) {
|
|
@@ -36,10 +37,16 @@ function requiredRbac() {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
function inviteUrl(req, token) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
let base;
|
|
41
|
+
try {
|
|
42
|
+
base = new URL(String(process.env.PUBLIC_APP_URL || ""));
|
|
43
|
+
const local = ["localhost", "127.0.0.1", "[::1]"].includes(base.hostname);
|
|
44
|
+
if ((base.protocol !== "https:" && !(base.protocol === "http:" && local && process.env.NODE_ENV !== "production"))
|
|
45
|
+
|| base.username || base.password || base.search || base.hash || base.pathname !== "/") throw new Error();
|
|
46
|
+
} catch {
|
|
47
|
+
throw new GenericError("Configure PUBLIC_APP_URL com a origem HTTPS pública deste aplicativo.", { statusCode: 503, code: "INVITATION_PUBLIC_URL_INVALID" });
|
|
48
|
+
}
|
|
49
|
+
return `${base.origin}/accept-invite#token=${encodeURIComponent(token)}`;
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
function invitationView(invitation) {
|
|
@@ -139,6 +146,7 @@ router.post(
|
|
|
139
146
|
asyncHandler(async (req, res) => {
|
|
140
147
|
requiredRbac();
|
|
141
148
|
const role = assertAssignable(req.usuario, req.body?.roleCode);
|
|
149
|
+
inviteUrl(req, ""); // Validate configuration before creating or rotating a token.
|
|
142
150
|
const result = await createInvitation({
|
|
143
151
|
email: req.body?.email,
|
|
144
152
|
name: req.body?.name,
|
|
@@ -192,6 +200,7 @@ router.post(
|
|
|
192
200
|
const previous = await Invitation.findOne({ _id: req.params.id, appCode: appCode() });
|
|
193
201
|
if (!previous) throw new GenericError("Convite não encontrado.", { statusCode: 404 });
|
|
194
202
|
assertAssignable(req.usuario, previous.roleCode);
|
|
203
|
+
inviteUrl(req, ""); // Validate configuration before creating or rotating a token.
|
|
195
204
|
const result = await createInvitation({
|
|
196
205
|
email: previous.email,
|
|
197
206
|
name: previous.name,
|
|
@@ -277,11 +286,9 @@ router.patch(
|
|
|
277
286
|
}),
|
|
278
287
|
);
|
|
279
288
|
|
|
280
|
-
|
|
281
|
-
"/invitations/:token",
|
|
282
|
-
asyncHandler(async (req, res) => {
|
|
289
|
+
const inspectHandler = asyncHandler(async (req, res) => {
|
|
283
290
|
requiredRbac();
|
|
284
|
-
const invitation = await findInvitationByToken(req.params.token);
|
|
291
|
+
const invitation = await findInvitationByToken(req.params.token || req.body?.token);
|
|
285
292
|
if (!invitation || invitation.status !== "pending") {
|
|
286
293
|
throw new GenericError("Convite inválido, expirado ou já utilizado.", {
|
|
287
294
|
statusCode: 410,
|
|
@@ -289,8 +296,12 @@ router.get(
|
|
|
289
296
|
});
|
|
290
297
|
}
|
|
291
298
|
res.set("Cache-Control", "no-store");
|
|
299
|
+
const identity = await inspectInvitation({ req, invitation });
|
|
300
|
+
if (typeof identity?.accountExists !== "boolean") throw new GenericError("Resposta inválida do provedor de identidade.", { statusCode: 502 });
|
|
292
301
|
res.json({
|
|
293
302
|
invitation: {
|
|
303
|
+
accountExists: identity.accountExists,
|
|
304
|
+
sessionUser: identity.sessionUser || null,
|
|
294
305
|
email: invitation.email,
|
|
295
306
|
name: invitation.name,
|
|
296
307
|
roleCode: invitation.roleCode,
|
|
@@ -298,19 +309,23 @@ router.get(
|
|
|
298
309
|
expiresAt: invitation.expiresAt,
|
|
299
310
|
},
|
|
300
311
|
});
|
|
301
|
-
})
|
|
302
|
-
);
|
|
312
|
+
});
|
|
313
|
+
router.get("/invitations/:token", inspectHandler);
|
|
314
|
+
router.post("/invitations/inspect", inspectHandler);
|
|
303
315
|
|
|
304
|
-
|
|
305
|
-
"/invitations/:token/accept",
|
|
306
|
-
asyncHandler(async (req, res) => {
|
|
316
|
+
const acceptHandler = asyncHandler(async (req, res) => {
|
|
307
317
|
requiredRbac();
|
|
308
318
|
const password = String(req.body?.password || "");
|
|
309
|
-
|
|
310
|
-
|
|
319
|
+
const intent = req.body?.intent;
|
|
320
|
+
if (intent !== undefined && !["register", "authenticate", "session"].includes(intent)) {
|
|
321
|
+
throw new GenericError("Forma de autenticação inválida.", { statusCode: 422 });
|
|
322
|
+
}
|
|
323
|
+
if ((intent === "register" || intent === undefined) && password.length < 8) {
|
|
324
|
+
throw new GenericError("A senha de cadastro deve possuir pelo menos 8 caracteres.", { statusCode: 422 });
|
|
311
325
|
}
|
|
326
|
+
if (intent !== "session" && !password) throw new GenericError("Informe a senha.", { statusCode: 422 });
|
|
312
327
|
|
|
313
|
-
const invitation = await findInvitationByToken(req.params.token);
|
|
328
|
+
const invitation = await findInvitationByToken(req.params.token || req.body?.token);
|
|
314
329
|
if (!invitation || invitation.status !== "pending") {
|
|
315
330
|
throw new GenericError("Convite inválido, expirado ou já utilizado.", {
|
|
316
331
|
statusCode: 410,
|
|
@@ -324,8 +339,12 @@ router.post(
|
|
|
324
339
|
throw new GenericError("O acesso associado ao convite não está mais disponível.", { statusCode: 410 });
|
|
325
340
|
}
|
|
326
341
|
|
|
327
|
-
const identity = await provisionInvitation({ req, invitation, password });
|
|
342
|
+
const identity = await provisionInvitation({ req, invitation, password, intent });
|
|
328
343
|
const providerUser = identity?.usuario || identity?.user || identity;
|
|
344
|
+
if (!providerUser?.email || String(providerUser.email).trim().toLowerCase() !== invitation.email.toLowerCase()
|
|
345
|
+
|| !(providerUser.id || providerUser._id)) {
|
|
346
|
+
throw new GenericError("Identidade retornada não corresponde ao convite.", { statusCode: 502, code: "INVITATION_IDENTITY_MISMATCH" });
|
|
347
|
+
}
|
|
329
348
|
access.identityId = String(providerUser?.id || providerUser?._id || access.identityId || "") || undefined;
|
|
330
349
|
access.name = providerUser?.nome || providerUser?.name || access.name;
|
|
331
350
|
access.status = "active";
|
|
@@ -347,11 +366,12 @@ router.post(
|
|
|
347
366
|
|
|
348
367
|
res.set("Cache-Control", "no-store");
|
|
349
368
|
res.json({
|
|
350
|
-
message: "Convite aceito.
|
|
369
|
+
message: "Convite aceito. O acesso está ativo.",
|
|
351
370
|
email: access.email,
|
|
352
371
|
});
|
|
353
|
-
})
|
|
354
|
-
);
|
|
372
|
+
});
|
|
373
|
+
router.post("/invitations/:token/accept", acceptHandler);
|
|
374
|
+
router.post("/invitations/accept", acceptHandler);
|
|
355
375
|
|
|
356
376
|
module.exports = router;
|
|
357
377
|
module.exports.inviteUrl = inviteUrl;
|
package/src/api/status.routes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const express = require("express");
|
|
4
|
-
const mongoose = require("mongoose");
|
|
4
|
+
const mongoose = require("../core/database/mongoose");
|
|
5
5
|
const ctx = require("../core/context");
|
|
6
6
|
const { runtimeSummary } = require("./core.routes");
|
|
7
7
|
const { isLocalRuntime, localRuntimeSummary } = require("../core/runtime/localDevelopment");
|
|
@@ -79,3 +79,4 @@ module.exports = router;
|
|
|
79
79
|
module.exports.databaseState = databaseState;
|
|
80
80
|
module.exports.readinessSummary = readinessSummary;
|
|
81
81
|
module.exports.versionSummary = versionSummary;
|
|
82
|
+
|
package/src/boot/database.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("../core/database/mongoose");
|
|
2
2
|
const registry = require("../core/registry");
|
|
3
|
+
const ctx = require("../core/context");
|
|
4
|
+
const { DEFAULTS, poolOptions } = require("../core/database/policy");
|
|
5
|
+
const { attachPoolMetrics } = require("../core/database/metrics");
|
|
3
6
|
|
|
4
7
|
function buildMongoUri() {
|
|
5
8
|
const cfg = {
|
|
@@ -20,7 +23,8 @@ function buildMongoUri() {
|
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
function connectionOptions() {
|
|
23
|
-
|
|
26
|
+
const pool = poolOptions();
|
|
27
|
+
if (process.env.MONGO_URI) return pool;
|
|
24
28
|
|
|
25
29
|
const hasUser = Boolean(process.env.DB_USER);
|
|
26
30
|
const hasPassword = Boolean(process.env.DB_PASSWORD);
|
|
@@ -30,8 +34,8 @@ function connectionOptions() {
|
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
return hasUser
|
|
33
|
-
? { user: process.env.DB_USER, pass: process.env.DB_PASSWORD }
|
|
34
|
-
:
|
|
37
|
+
? { ...pool, user: process.env.DB_USER, pass: process.env.DB_PASSWORD }
|
|
38
|
+
: pool;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
function databaseLabel(uri) {
|
|
@@ -71,7 +75,21 @@ async function syncDeclaredIndexes(entries = registry.listModels()) {
|
|
|
71
75
|
|
|
72
76
|
async function connectDB() {
|
|
73
77
|
const uri = buildMongoUri();
|
|
74
|
-
|
|
78
|
+
const options = connectionOptions();
|
|
79
|
+
// Explicit driver options override conflicting URI pool options; tested with the real driver.
|
|
80
|
+
const connecting = mongoose.connect(uri, options);
|
|
81
|
+
const client = mongoose.connection.getClient();
|
|
82
|
+
const metrics = client && attachPoolMetrics(client, mongoose.connection, {
|
|
83
|
+
app: ctx.config.appCode || ctx.config.slug || process.env.APP_CODE,
|
|
84
|
+
environment: process.env.APP_ENVIRONMENT || process.env.NODE_ENV,
|
|
85
|
+
});
|
|
86
|
+
try {
|
|
87
|
+
await connecting;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
metrics?.dispose();
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
console.log("[OonCore] database policy", JSON.stringify({ ...DEFAULTS, uncoveredModels: Object.values(mongoose.models).filter(model => !model.schema[Symbol.for("oon.database.policy.schema.v1")]).length }));
|
|
75
93
|
await syncDeclaredIndexes();
|
|
76
94
|
console.log(`Conectado ao MongoDB (${databaseLabel(uri)})`);
|
|
77
95
|
return mongoose.connection;
|
|
@@ -85,3 +103,4 @@ module.exports = {
|
|
|
85
103
|
shouldSyncDeclaredIndexes,
|
|
86
104
|
syncDeclaredIndexes,
|
|
87
105
|
};
|
|
106
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const crypto = require("node:crypto");
|
|
3
|
-
const mongoose = require("mongoose");
|
|
3
|
+
const mongoose = require("../../core/database/mongoose");
|
|
4
4
|
const ctx = require("../../core/context");
|
|
5
5
|
const GenericError = require("../../core/errors/GenericError");
|
|
6
6
|
const MAX_BYTES = 16384;
|
|
@@ -111,3 +111,4 @@ async function remove({ name, context, correlationId } = {}) {
|
|
|
111
111
|
return safe(async () => { const s = resolveScope(context, name); await audit("remove", s, context, correlationId); await models().Secret.deleteOne({ _id: id(s) }); return { state: "absent" }; });
|
|
112
112
|
}
|
|
113
113
|
module.exports = Object.freeze({ put, status, use, remove });
|
|
114
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const mongoose = require("mongoose");
|
|
3
|
+
const mongoose = require("../../core/database/mongoose");
|
|
4
4
|
|
|
5
5
|
const envelope = { keyId: String, version: Number, algorithm: String, iv: String, authTag: String, ciphertext: String };
|
|
6
6
|
const common = { scopeKey: { type: String, required: true, index: true } };
|
|
@@ -19,3 +19,4 @@ function getModels() {
|
|
|
19
19
|
}
|
|
20
20
|
async function syncIndexes() { const models = getModels(); return Promise.all(Object.values(models).map((entry) => entry.syncIndexes())); }
|
|
21
21
|
module.exports = { getModels, syncIndexes };
|
|
22
|
+
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
function databaseError(error) {
|
|
4
|
+
if (error?.name === "MongoWaitQueueTimeoutError") return {
|
|
5
|
+
code: "DATABASE_POOL_BUSY", statusCode: 503,
|
|
6
|
+
message: "O banco está ocupado. Aguarde antes de tentar novamente.",
|
|
7
|
+
};
|
|
8
|
+
if (error?.code === 50 && error?.name === "MongoServerError") return {
|
|
9
|
+
code: "DATABASE_OPERATION_TIMEOUT", statusCode: 504,
|
|
10
|
+
message: "A operação excedeu o tempo permitido. Consulte o estado antes de repetir uma alteração.",
|
|
11
|
+
};
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
module.exports = { databaseError };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const monitors = new WeakMap();
|
|
4
|
+
const connections = new WeakMap();
|
|
5
|
+
|
|
6
|
+
function observeOperationError(connection, error) {
|
|
7
|
+
if (error?.name === "MongoServerError" && error.code === 50) {
|
|
8
|
+
const monitor = connections.get(connection);
|
|
9
|
+
if (monitor) monitor.operationTimeout();
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function safeLabel(value) {
|
|
14
|
+
return typeof value === "string" && /^[a-zA-Z0-9_.-]{1,80}$/.test(value) ? value : "unknown";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function attachPoolMetrics(client, connection, { app, environment, emit = record => console.log("[OonCore] database", JSON.stringify(record)), intervalMS = 60000 } = {}) {
|
|
18
|
+
if (monitors.has(client)) return monitors.get(client);
|
|
19
|
+
const inUse = new Set();
|
|
20
|
+
let waiting = 0;
|
|
21
|
+
let counters;
|
|
22
|
+
const reset = () => { counters = { checkouts: 0, checkoutFailures: 0, checkoutTimeouts: 0, mongooseOperationTimeouts: 0, waitTotalMS: 0, waitMaxMS: 0, peakWaiting: waiting, peakInUse: inUse.size }; };
|
|
23
|
+
reset();
|
|
24
|
+
const key = event => `${event.address}/${event.connectionId}`;
|
|
25
|
+
const duration = event => {
|
|
26
|
+
const ms = Number.isFinite(event.durationMS) && event.durationMS >= 0 ? event.durationMS : 0;
|
|
27
|
+
counters.waitTotalMS += ms;
|
|
28
|
+
counters.waitMaxMS = Math.max(counters.waitMaxMS, ms);
|
|
29
|
+
};
|
|
30
|
+
const listeners = {
|
|
31
|
+
connectionCheckOutStarted() { waiting++; counters.peakWaiting = Math.max(counters.peakWaiting, waiting); },
|
|
32
|
+
connectionCheckedOut(event) { waiting = Math.max(0, waiting - 1); inUse.add(key(event)); counters.checkouts++; counters.peakInUse = Math.max(counters.peakInUse, inUse.size); duration(event); },
|
|
33
|
+
connectionCheckOutFailed(event) { waiting = Math.max(0, waiting - 1); counters.checkoutFailures++; if (event.reason === "timeout") counters.checkoutTimeouts++; duration(event); },
|
|
34
|
+
connectionCheckedIn(event) { inUse.delete(key(event)); },
|
|
35
|
+
connectionClosed(event) { inUse.delete(key(event)); },
|
|
36
|
+
};
|
|
37
|
+
for (const [event, listener] of Object.entries(listeners)) client.on(event, listener);
|
|
38
|
+
const snapshot = () => ({ app: safeLabel(app), environment: safeLabel(environment), pid: process.pid, intervalMS, inUse: inUse.size, waiting, ...counters });
|
|
39
|
+
const flush = () => {
|
|
40
|
+
if (counters.checkouts || counters.checkoutFailures || counters.mongooseOperationTimeouts || inUse.size || waiting) {
|
|
41
|
+
try { emit(snapshot()); } catch { /* Telemetry must never fail a database operation. */ }
|
|
42
|
+
}
|
|
43
|
+
reset();
|
|
44
|
+
};
|
|
45
|
+
const timer = setInterval(flush, intervalMS);
|
|
46
|
+
timer.unref();
|
|
47
|
+
let disposed = false;
|
|
48
|
+
const dispose = () => {
|
|
49
|
+
if (disposed) return;
|
|
50
|
+
disposed = true;
|
|
51
|
+
clearInterval(timer);
|
|
52
|
+
flush();
|
|
53
|
+
for (const [event, listener] of Object.entries(listeners)) client.removeListener(event, listener);
|
|
54
|
+
connection.removeListener("close", dispose);
|
|
55
|
+
monitors.delete(client);
|
|
56
|
+
if (connections.get(connection) === monitor) connections.delete(connection);
|
|
57
|
+
inUse.clear(); waiting = 0;
|
|
58
|
+
};
|
|
59
|
+
const monitor = { snapshot, flush, dispose, operationTimeout() { counters.mongooseOperationTimeouts++; } };
|
|
60
|
+
monitors.set(client, monitor);
|
|
61
|
+
connections.set(connection, monitor);
|
|
62
|
+
connection.once("close", dispose);
|
|
63
|
+
return monitor;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { attachPoolMetrics, observeOperationError };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const mongoose = require("mongoose");
|
|
4
|
+
const { boundedMaxTimeMS } = require("./policy");
|
|
5
|
+
const { observeOperationError } = require("./metrics");
|
|
6
|
+
const INSTALLED = Symbol.for("oon.database.policy.installed.v1");
|
|
7
|
+
const COVERED = Symbol.for("oon.database.policy.schema.v1");
|
|
8
|
+
const QUERY_METHODS = Object.freeze([
|
|
9
|
+
"find", "findOne", "countDocuments", "estimatedDocumentCount", "distinct",
|
|
10
|
+
"findOneAndUpdate", "findOneAndReplace", "findOneAndDelete",
|
|
11
|
+
"updateOne", "updateMany", "replaceOne", "deleteOne", "deleteMany",
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function install(instance) {
|
|
15
|
+
if (instance[INSTALLED]) return;
|
|
16
|
+
instance[INSTALLED] = true;
|
|
17
|
+
instance.plugin(function databasePolicy(schema) {
|
|
18
|
+
if (schema[COVERED]) return;
|
|
19
|
+
schema[COVERED] = true;
|
|
20
|
+
for (const method of QUERY_METHODS) {
|
|
21
|
+
schema.pre(method, { query: true, document: false }, function applyQueryBudget() {
|
|
22
|
+
if (this.model.db === instance.connection) this.setOptions({ maxTimeMS: boundedMaxTimeMS(this.getOptions().maxTimeMS) });
|
|
23
|
+
});
|
|
24
|
+
schema.post(method, { query: true, document: false }, function observeQueryError(error, result, next) {
|
|
25
|
+
observeOperationError(this.model.db, error);
|
|
26
|
+
next(error);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
schema.pre("aggregate", function applyAggregateBudget() {
|
|
30
|
+
if (this.model().db === instance.connection) this.option({ maxTimeMS: boundedMaxTimeMS(this.options.maxTimeMS) });
|
|
31
|
+
});
|
|
32
|
+
schema.post("aggregate", function observeAggregateError(error, result, next) {
|
|
33
|
+
observeOperationError(this.model().db, error);
|
|
34
|
+
next(error);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
install(mongoose);
|
|
40
|
+
module.exports = mongoose;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Versioned Core policy, deliberately not an app manifest/env extension point.
|
|
4
|
+
// Initial calibration profile: not a claim of safe shared-cluster capacity.
|
|
5
|
+
const DEFAULTS = Object.freeze({
|
|
6
|
+
maxPoolSize: 4,
|
|
7
|
+
minPoolSize: 0,
|
|
8
|
+
maxConnecting: 2,
|
|
9
|
+
waitQueueTimeoutMS: 2000,
|
|
10
|
+
maxTimeMS: 5000,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function configurationError() {
|
|
14
|
+
const error = new Error("Política de contenção MongoDB inválida.");
|
|
15
|
+
error.code = "DATABASE_POLICY_INVALID";
|
|
16
|
+
return error;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function validatePolicy(input = DEFAULTS) {
|
|
20
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) throw configurationError();
|
|
21
|
+
const policy = { ...DEFAULTS, ...input };
|
|
22
|
+
if (Object.keys(policy).some(key => !Object.hasOwn(DEFAULTS, key))) throw configurationError();
|
|
23
|
+
for (const [key, value] of Object.entries(policy)) {
|
|
24
|
+
if (!Number.isSafeInteger(value) || value < (key === "minPoolSize" ? 0 : 1)) throw configurationError();
|
|
25
|
+
}
|
|
26
|
+
if (policy.minPoolSize > policy.maxPoolSize || policy.maxConnecting > policy.maxPoolSize) throw configurationError();
|
|
27
|
+
return Object.freeze(policy);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function poolOptions(policy = DEFAULTS) {
|
|
31
|
+
const { maxTimeMS, ...pool } = validatePolicy(policy);
|
|
32
|
+
return pool;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function boundedMaxTimeMS(value, ceiling = DEFAULTS.maxTimeMS) {
|
|
36
|
+
if (value === undefined || value === 0) return ceiling;
|
|
37
|
+
if (!Number.isSafeInteger(value) || value < 0) throw configurationError();
|
|
38
|
+
return Math.min(value, ceiling);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = { DEFAULTS, validatePolicy, poolOptions, boundedMaxTimeMS };
|
package/src/core/define.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("./database/mongoose");
|
|
2
2
|
const express = require("express");
|
|
3
3
|
const registry = require("./registry");
|
|
4
4
|
const ctx = require("./context");
|
|
@@ -212,3 +212,4 @@ module.exports = {
|
|
|
212
212
|
definePipeline,
|
|
213
213
|
defineRoutes,
|
|
214
214
|
};
|
|
215
|
+
|
package/src/core/fields.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `__meta` é consumido pelo registry para gerar metadata/OpenAPI para o
|
|
6
6
|
* front, sem vazar para o schema Mongoose final.
|
|
7
7
|
*/
|
|
8
|
-
const mongoose = require("mongoose");
|
|
8
|
+
const mongoose = require("./database/mongoose");
|
|
9
9
|
|
|
10
10
|
const withMeta = (mongooseDef, meta) => ({
|
|
11
11
|
...mongooseDef,
|
|
@@ -93,3 +93,4 @@ module.exports = {
|
|
|
93
93
|
currencyCode,
|
|
94
94
|
currencyConverted,
|
|
95
95
|
};
|
|
96
|
+
|
|
@@ -137,6 +137,14 @@ async function applyEffectiveAccess(usuario) {
|
|
|
137
137
|
rbacMode: "app-commercial", commercialAccess: { ...authority,
|
|
138
138
|
roleComplete: !local.includes("*") && local.every(p => permissions.includes(p)) } };
|
|
139
139
|
}
|
|
140
|
+
if (usuario?.canonicalAccess && typeof ctx.config.auth?.verifyToken !== "function") {
|
|
141
|
+
const source = usuario.canonicalAccess;
|
|
142
|
+
const role = roleByCode(source.roleCode);
|
|
143
|
+
if (!source.grantId || source.tenantId !== usuario.tenantId || !role) {
|
|
144
|
+
throw Object.assign(Error("Concessão central inválida."), { statusCode: 403, code: "CENTRAL_ACCESS_INVALID" });
|
|
145
|
+
}
|
|
146
|
+
return { ...usuario, roleCode: role.code, permissions: rolePermissions(role), accessStatus: "active", mustChangePassword: false, rbacMode: "central-invitation" };
|
|
147
|
+
}
|
|
140
148
|
const access = await resolveUserAccess(usuario);
|
|
141
149
|
return {
|
|
142
150
|
...usuario,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const { databaseError } = require("../database/errors");
|
|
1
2
|
const helpers = require("../utils/helpers");
|
|
2
3
|
const GenericError = require("../errors/GenericError");
|
|
3
4
|
|
|
@@ -74,6 +75,16 @@ const errorMiddleware = (error, req, res, next) => {
|
|
|
74
75
|
});
|
|
75
76
|
}
|
|
76
77
|
|
|
78
|
+
const databaseFailure = databaseError(error);
|
|
79
|
+
if (databaseFailure) {
|
|
80
|
+
const { statusCode, ...details } = databaseFailure;
|
|
81
|
+
return helpers.sendErrorResponse({
|
|
82
|
+
res, statusCode,
|
|
83
|
+
error: { ...details, ...(requestId ? { requestId } : {}) },
|
|
84
|
+
message: details.message,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
if (error?.code === 11000 || error?.name === "MongoServerError" && error?.code === 11000) {
|
|
78
89
|
const details = duplicateKeyDetails(error, requestId);
|
|
79
90
|
return helpers.sendErrorResponse({
|
|
@@ -135,3 +146,4 @@ const errorMiddleware = (error, req, res, next) => {
|
|
|
135
146
|
module.exports = errorMiddleware;
|
|
136
147
|
module.exports.firstDuplicateField = firstDuplicateField;
|
|
137
148
|
module.exports.duplicateKeyDetails = duplicateKeyDetails;
|
|
149
|
+
|
|
@@ -8,10 +8,11 @@ const registry = require("../registry");
|
|
|
8
8
|
const requestLog = async (req, res, next) => {
|
|
9
9
|
if (req.method === "GET") return next();
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const invitationRequest = /\/(?:core\/invitations|convites)\//.test(String(req.originalUrl || req.url));
|
|
12
|
+
const sensitiveBody = invitationRequest || await registry.isSensitiveRequest(req.method, req.url || req.originalUrl);
|
|
12
13
|
const log = new Log({
|
|
13
14
|
usuario: req.usuario || null,
|
|
14
|
-
endpoint: req.originalUrl,
|
|
15
|
+
endpoint: invitationRequest ? String(req.originalUrl).replace(/(\/core\/invitations\/)[^/?]+/, "$1[redacted]").split("?")[0] : req.originalUrl,
|
|
15
16
|
metodo: req.method,
|
|
16
17
|
ip: req.ip || req.headers["x-forwarded-for"] || req.connection?.remoteAddress,
|
|
17
18
|
correlationId: req.correlationId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("../database/mongoose");
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Trilha de auditoria de mutações. Diferente da versão do CST, no Core
|
|
@@ -26,3 +26,4 @@ const ControleAlteracao =
|
|
|
26
26
|
module.exports = ControleAlteracao;
|
|
27
27
|
module.exports.ACOES_PADRAO = ACOES_PADRAO;
|
|
28
28
|
module.exports.ORIGENS_PADRAO = ORIGENS_PADRAO;
|
|
29
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("../database/mongoose");
|
|
2
2
|
|
|
3
3
|
const statuses = ["nao_ativada", "validando", "configurando", "ativa", "suspensa", "revogada", "erro"];
|
|
4
4
|
const roles = ["root", "member"];
|
|
@@ -65,3 +65,4 @@ schema.index({ singletonKey: 1 }, { unique: true });
|
|
|
65
65
|
module.exports = mongoose.models.InstanciaEcossistema || mongoose.model("InstanciaEcossistema", schema);
|
|
66
66
|
module.exports.STATUSES = statuses;
|
|
67
67
|
module.exports.ROLES = roles;
|
|
68
|
+
|
package/src/core/models/Log.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("../database/mongoose");
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Log de requisições mutantes. Model interna do Core — registrada
|
|
@@ -20,3 +20,4 @@ const logSchema = new mongoose.Schema(
|
|
|
20
20
|
|
|
21
21
|
module.exports =
|
|
22
22
|
mongoose.models.Log || mongoose.model("Log", logSchema);
|
|
23
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("../database/mongoose");
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Configuração singleton da Central. Credenciais funcionais pertencem às
|
|
@@ -17,3 +17,4 @@ const sistemaSchema = new mongoose.Schema(
|
|
|
17
17
|
|
|
18
18
|
module.exports =
|
|
19
19
|
mongoose.models.Sistema || mongoose.model("Sistema", sistemaSchema);
|
|
20
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const express = require("express");
|
|
3
|
+
const ctx = require("../context");
|
|
4
|
+
const auth = require("../middlewares/auth");
|
|
5
|
+
const { asyncHandler } = require("../utils/helpers");
|
|
6
|
+
const GenericError = require("../errors/GenericError");
|
|
7
|
+
const { defaultProviderCall } = require("./identityProvider");
|
|
8
|
+
const { models, appCode, audit } = require("./storage");
|
|
9
|
+
const base = "/integracao/autenticacao-aplicativos/convites/v1";
|
|
10
|
+
function enabled() { return process.env.OON_CENTRAL_INVITATIONS_ENABLED === "true" && typeof ctx.config.auth?.provisionInvitation !== "function" && typeof ctx.config.auth?.verifyToken !== "function" && process.env.OON_RUNTIME_MODE !== "local"; }
|
|
11
|
+
async function cutover() {
|
|
12
|
+
// No import or token recovery. Idempotent state transition touches only old pending invitations.
|
|
13
|
+
const { Invitation } = models();
|
|
14
|
+
const session = await Invitation.db.startSession();
|
|
15
|
+
try { await session.withTransaction(async () => {
|
|
16
|
+
const rows = await Invitation.find({ appCode: appCode(), status: "pending" }).session(session);
|
|
17
|
+
for (const row of rows) {
|
|
18
|
+
const changed = await Invitation.updateOne({ _id: row._id, status: "pending" }, { $set: { status: "revoked", revokedAt: new Date() } }, { session });
|
|
19
|
+
if (changed.modifiedCount) await audit("invitation.cutover.v1", { actor: "system:cutover", target: row.email, context: { invitationId: String(row._id), cutoverId: "central-invitations-v1" }, session });
|
|
20
|
+
}
|
|
21
|
+
}); } finally { await session.endSession(); }
|
|
22
|
+
}
|
|
23
|
+
function createRouter({ inviteUrl }) {
|
|
24
|
+
const router = express.Router();
|
|
25
|
+
router.use((req, _res, next) => enabled() ? next() : next("router"));
|
|
26
|
+
const call = (req, payload) => defaultProviderCall(req, `${base}/command`, payload);
|
|
27
|
+
const management = handler => [auth, asyncHandler(async (req, res) => { await call(req, { command: "list" }); await cutover(); res.set("Cache-Control", "no-store"); await handler(req, res); })];
|
|
28
|
+
function identifier(value) {
|
|
29
|
+
const match = /^central:([a-f\d]{24})$/i.exec(String(value));
|
|
30
|
+
if (!match) throw new GenericError("Convite anterior ao corte. Solicite um novo convite.", { statusCode: 410, code: "INVITATION_LEGACY_DISABLED" });
|
|
31
|
+
return match[1];
|
|
32
|
+
}
|
|
33
|
+
function invitation(row) { return { ...row, id: `central:${row.id}` }; }
|
|
34
|
+
const access = row => ({ id: `central:${row.id}`, email: row.email, name: row.name, roleCode: row.roleCode, status: row.status === "accepted" ? "active" : row.status === "pending" ? "invited" : "revoked", origin: "central", invitation: invitation(row) });
|
|
35
|
+
async function withLink(req, result) {
|
|
36
|
+
if (result.invitation.status && result.invitation.status !== "pending") return { invitation: invitation(result.invitation), access: access(result.invitation) };
|
|
37
|
+
const copied = await call(req, { command: "copy", id: result.invitation.id });
|
|
38
|
+
return { invitation: invitation(copied.invitation), access: access(copied.invitation), inviteUrl: inviteUrl(req, copied.token) };
|
|
39
|
+
}
|
|
40
|
+
router.get("/users", ...management(async (req, res) => {
|
|
41
|
+
const result = await call(req, { command: "list" });
|
|
42
|
+
const canonical = result.invitations.map(access);
|
|
43
|
+
const identities = new Set(canonical.map(row => row.email));
|
|
44
|
+
const local = req.usuario?.commercialAccess ? [] : await models().UserAccess.find({ appCode: appCode(), status: { $ne: "invited" } }).lean();
|
|
45
|
+
res.json({ results: [...canonical, ...local.filter(row => !identities.has(row.email)).map(row => ({ id: String(row._id), email: row.email, name: row.name, roleCode: row.roleCode, status: row.status, origin: "local" }))] });
|
|
46
|
+
}));
|
|
47
|
+
router.post("/users/invitations", ...management(async (req, res) => {
|
|
48
|
+
inviteUrl(req, "");
|
|
49
|
+
const result = await call(req, { command: "create", email: req.body?.email, roleCode: req.body?.roleCode, version: 0 });
|
|
50
|
+
res.status(201).json(await withLink(req, result));
|
|
51
|
+
}));
|
|
52
|
+
router.get("/users/invitations/:id/link", ...management(async (req, res) => res.json(await withLink(req, { invitation: { id: identifier(req.params.id) } }))));
|
|
53
|
+
router.post("/users/invitations/:id/regenerate", ...management(async (req, res) => {
|
|
54
|
+
inviteUrl(req, "");
|
|
55
|
+
res.json(await withLink(req, await call(req, { command: "reissue", id: identifier(req.params.id), version: req.body?.version })));
|
|
56
|
+
}));
|
|
57
|
+
router.delete("/users/invitations/:id", ...management(async (req, res) => res.json(await call(req, { command: "revoke", id: identifier(req.params.id), version: req.body?.version }))));
|
|
58
|
+
router.patch("/users/:id", auth, asyncHandler(async (req, _res, next) => { if (!String(req.params.id).startsWith("central:")) return next("router"); throw new GenericError("Gerencie a concessão na Central; esta lista é uma projeção de leitura.", { statusCode: 409, code: "CENTRAL_ACCESS_COMMAND_REQUIRED" }); }));
|
|
59
|
+
for (const action of ["inspect", "accept", "decline"]) router.post(`/invitations/${action}`, asyncHandler(async (req, res) => {
|
|
60
|
+
if (!String(req.body?.token || "").startsWith("c3.")) throw new GenericError("Convite anterior ao corte. Solicite um novo convite.", { statusCode: 410, code: "INVITATION_LEGACY_DISABLED" });
|
|
61
|
+
res.set("Cache-Control", "no-store");
|
|
62
|
+
if (action === "inspect") return res.json(await defaultProviderCall(req, `${base}/inspect`, { token: req.body.token }));
|
|
63
|
+
const result = await call(req, { command: action, token: req.body.token, version: req.body.version, intent: req.body.intent, password: req.body.password, registration: req.body.registration });
|
|
64
|
+
res.json({ ...result, message: "Convite processado pela Central." });
|
|
65
|
+
}));
|
|
66
|
+
router.all("/invitations/:token/accept", (_req, res) => res.status(410).json({ code: "INVITATION_LEGACY_DISABLED", message: "Solicite um novo convite." }));
|
|
67
|
+
router.get("/invitations/:token", (_req, res) => res.status(410).json({ code: "INVITATION_LEGACY_DISABLED", message: "Solicite um novo convite." }));
|
|
68
|
+
return router;
|
|
69
|
+
}
|
|
70
|
+
module.exports = { enabled, cutover, createRouter };
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
} = require("../utils/instanceActivation");
|
|
11
11
|
|
|
12
12
|
const IDENTITY_PATHS = Object.freeze({
|
|
13
|
+
inspectInvitation: "/integracao/autenticacao-aplicativos/convites/inspecionar",
|
|
13
14
|
provisionInvitation: "/integracao/autenticacao-aplicativos/convites/provisionar",
|
|
14
15
|
changeFirstAccessPassword: "/integracao/autenticacao-aplicativos/primeiro-acesso/senha",
|
|
15
16
|
});
|
|
@@ -25,6 +26,9 @@ async function defaultProviderCall(req, path, payload) {
|
|
|
25
26
|
|
|
26
27
|
try {
|
|
27
28
|
const headers = instanceHeaders(instance);
|
|
29
|
+
for (const key of ["idempotency-key", "x-correlation-id", "x-oon-tenant-id"]) {
|
|
30
|
+
if (req?.headers?.[key]) headers[key] = req.headers[key];
|
|
31
|
+
}
|
|
28
32
|
if (req?.headers?.authorization) headers.Authorization = req.headers.authorization;
|
|
29
33
|
const response = await providerPost(req, path, payload, headers);
|
|
30
34
|
return response.data;
|
|
@@ -38,14 +42,24 @@ async function defaultProviderCall(req, path, payload) {
|
|
|
38
42
|
}
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
async function
|
|
45
|
+
async function inspectInvitation({ req, invitation }) {
|
|
46
|
+
const hook = ctx.config.auth?.inspectInvitation;
|
|
47
|
+
if (typeof hook === "function") return hook({ req, invitation });
|
|
48
|
+
if (typeof ctx.config.auth?.provisionInvitation === "function") {
|
|
49
|
+
throw new GenericError("O provedor local precisa disponibilizar a consulta de convite.", { statusCode: 503, code: "INVITATION_INSPECTION_REQUIRED" });
|
|
50
|
+
}
|
|
51
|
+
return defaultProviderCall(req, IDENTITY_PATHS.inspectInvitation, { email: invitation.email });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function provisionInvitation({ req, invitation, password, intent }) {
|
|
42
55
|
const hook = ctx.config.auth?.provisionInvitation;
|
|
43
|
-
if (typeof hook === "function") return hook({ req, invitation, password });
|
|
56
|
+
if (typeof hook === "function") return hook({ req, invitation, password, intent });
|
|
44
57
|
return defaultProviderCall(req, IDENTITY_PATHS.provisionInvitation, {
|
|
45
58
|
email: invitation.email,
|
|
46
59
|
nome: invitation.name,
|
|
47
60
|
roleCode: invitation.roleCode,
|
|
48
61
|
password,
|
|
62
|
+
intent,
|
|
49
63
|
invitationFingerprint: invitation.tokenFingerprint,
|
|
50
64
|
});
|
|
51
65
|
}
|
|
@@ -63,6 +77,7 @@ async function changeFirstAccessPassword({ req, user, password }) {
|
|
|
63
77
|
module.exports = {
|
|
64
78
|
IDENTITY_PATHS,
|
|
65
79
|
defaultProviderCall,
|
|
80
|
+
inspectInvitation,
|
|
66
81
|
provisionInvitation,
|
|
67
82
|
changeFirstAccessPassword,
|
|
68
83
|
};
|
package/src/core/rbac/storage.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const crypto = require("node:crypto");
|
|
4
|
-
const mongoose = require("mongoose");
|
|
4
|
+
const mongoose = require("../database/mongoose");
|
|
5
5
|
const ctx = require("../context");
|
|
6
6
|
const GenericError = require("../errors/GenericError");
|
|
7
7
|
const { encrypt, decrypt } = require("../utils/instanceActivation");
|
|
@@ -105,10 +105,10 @@ function sanitizeSnapshot(value) {
|
|
|
105
105
|
return plain;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
async function audit(event, { actor, target, before, after, context } = {}) {
|
|
108
|
+
async function audit(event, { actor, target, before, after, context, session } = {}) {
|
|
109
109
|
if (mongoose.connection.readyState !== 1) return null;
|
|
110
110
|
const { Audit } = models();
|
|
111
|
-
|
|
111
|
+
const data = {
|
|
112
112
|
appCode: appCode(),
|
|
113
113
|
event,
|
|
114
114
|
actor: typeof actor === "string" ? actor : actorKey(actor),
|
|
@@ -117,7 +117,8 @@ async function audit(event, { actor, target, before, after, context } = {}) {
|
|
|
117
117
|
after: sanitizeSnapshot(after),
|
|
118
118
|
context,
|
|
119
119
|
at: new Date(),
|
|
120
|
-
}
|
|
120
|
+
};
|
|
121
|
+
return session ? (await Audit.create([data], { session }))[0] : Audit.create(data);
|
|
121
122
|
}
|
|
122
123
|
|
|
123
124
|
function accessError(message, code = "RBAC_ACCESS_DENIED", statusCode = 403) {
|
|
@@ -404,3 +405,4 @@ module.exports = {
|
|
|
404
405
|
activeAdminCount,
|
|
405
406
|
assertNotLastAdmin,
|
|
406
407
|
};
|
|
408
|
+
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const crypto = require("node:crypto");
|
|
4
|
-
const mongoose = require("mongoose");
|
|
4
|
+
const mongoose = require("../database/mongoose");
|
|
5
5
|
const ctx = require("../context");
|
|
6
6
|
const GenericError = require("../errors/GenericError");
|
|
7
7
|
const { policy, roleByCode, rolePermissions } = require("../rbac/policy");
|
|
@@ -338,3 +338,4 @@ module.exports = {
|
|
|
338
338
|
clearCookie, assertLocalRequest, localRequestBoundary, localRuntimeSummary,
|
|
339
339
|
resetLocalDevelopmentForTests,
|
|
340
340
|
};
|
|
341
|
+
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const crypto = require("node:crypto");
|
|
2
|
-
const mongoose = require("mongoose");
|
|
2
|
+
const mongoose = require("../database/mongoose");
|
|
3
3
|
const axios = require("axios");
|
|
4
4
|
const ctx = require("../context");
|
|
5
5
|
const GenericError = require("../errors/GenericError");
|
|
@@ -641,3 +641,4 @@ module.exports = {
|
|
|
641
641
|
event,
|
|
642
642
|
};
|
|
643
643
|
|
|
644
|
+
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require("dotenv").config();
|
|
2
|
+
require("./core/database/mongoose"); // Before any Core or consumer model compilation.
|
|
2
3
|
|
|
3
4
|
const { start } = require("./boot/startServer");
|
|
4
5
|
const { createApp } = require("./boot/createApp");
|
|
@@ -82,3 +83,4 @@ module.exports = {
|
|
|
82
83
|
TransactionalEmailError: capabilities.TransactionalEmailError,
|
|
83
84
|
GenericError, registry,
|
|
84
85
|
};
|
|
86
|
+
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
const mongoose = require("mongoose");
|
|
3
|
+
const mongoose = require("../core/database/mongoose");
|
|
4
4
|
const registry = require("../core/registry");
|
|
5
5
|
const {
|
|
6
6
|
DomainRuleError,
|
|
@@ -588,4 +588,4 @@ module.exports = {
|
|
|
588
588
|
recalculateTarget,
|
|
589
589
|
drainProcessJobs,
|
|
590
590
|
toPlainRecord,
|
|
591
|
-
};
|
|
591
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("../../core/database/mongoose");
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Módulo Assistentes IA (Seção 13) — catálogo de assistentes, prompts e
|
|
@@ -31,3 +31,4 @@ function mascarar(texto, segredos = []) {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
module.exports = { Assistente, mascarar };
|
|
34
|
+
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const mongoose = require("mongoose");
|
|
1
|
+
const mongoose = require("../../core/database/mongoose");
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Módulo Multi-moedas (Seção 14) — cadastro/cotação/conversão. As models
|
|
@@ -29,3 +29,4 @@ async function converter({ valor, de = "BRL", para = "BRL" }) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
module.exports = { Moeda, converter };
|
|
32
|
+
|