@oondemand/oon-core-back 0.1.0
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/LICENSE +91 -0
- package/README.md +130 -0
- package/bin/cli.js +95 -0
- package/bin/dev-runner.js +8 -0
- package/package.json +53 -0
- package/src/api/auth.routes.js +20 -0
- package/src/api/core.routes.js +75 -0
- package/src/api/status.routes.js +21 -0
- package/src/boot/activation.js +31 -0
- package/src/boot/createApp.js +56 -0
- package/src/boot/database.js +35 -0
- package/src/boot/loadCentral.js +51 -0
- package/src/boot/startServer.js +31 -0
- package/src/core/context.js +35 -0
- package/src/core/define.js +171 -0
- package/src/core/errors/GenericError.js +14 -0
- package/src/core/factories/crudController.js +66 -0
- package/src/core/factories/crudService.js +96 -0
- package/src/core/factories/modelRouter.js +67 -0
- package/src/core/fields.js +92 -0
- package/src/core/middlewares/auth.js +65 -0
- package/src/core/middlewares/correlationId.js +12 -0
- package/src/core/middlewares/error.js +46 -0
- package/src/core/middlewares/mutationAudit.js +40 -0
- package/src/core/middlewares/rbac.js +22 -0
- package/src/core/middlewares/requestLog.js +28 -0
- package/src/core/models/ControleAlteracao.js +28 -0
- package/src/core/models/Log.js +22 -0
- package/src/core/models/Sistema.js +27 -0
- package/src/core/registry.js +117 -0
- package/src/core/utils/helpers.js +44 -0
- package/src/deploy/render.js +88 -0
- package/src/deploy/templates/Dockerfile +19 -0
- package/src/deploy/templates/certificate.yaml +12 -0
- package/src/deploy/templates/configmap.yaml +16 -0
- package/src/deploy/templates/deployment.yaml +32 -0
- package/src/deploy/templates/hpa.yaml +19 -0
- package/src/deploy/templates/ingress.yaml +24 -0
- package/src/deploy/templates/secret.yaml +12 -0
- package/src/deploy/templates/service.yaml +14 -0
- package/src/index.js +43 -0
- package/src/modules/assistants/index.js +33 -0
- package/src/modules/currencies/index.js +31 -0
- package/src/modules/documents/index.js +19 -0
- package/src/modules/omie/index.js +69 -0
- package/src/modules/pipelines/index.js +19 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const ctx = require("../core/context");
|
|
2
|
+
const { connectDB } = require("./database");
|
|
3
|
+
const { createApp } = require("./createApp");
|
|
4
|
+
const { loadCentral } = require("./loadCentral");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sobe a aplicação completa: carrega a Central do consumidor, conecta no
|
|
8
|
+
* banco, monta a app e escuta a porta. É o `start` da API pública e o
|
|
9
|
+
* alvo do `oonCore-back start`.
|
|
10
|
+
*
|
|
11
|
+
* @param {{ cwd?: string, listen?: boolean }} opts
|
|
12
|
+
*/
|
|
13
|
+
async function start({ cwd = process.cwd(), listen = true } = {}) {
|
|
14
|
+
loadCentral(cwd);
|
|
15
|
+
await connectDB();
|
|
16
|
+
const app = createApp();
|
|
17
|
+
|
|
18
|
+
if (!listen) return { app };
|
|
19
|
+
|
|
20
|
+
const port = ctx.config.port;
|
|
21
|
+
return new Promise((resolve) => {
|
|
22
|
+
const server = app.listen(port, () => {
|
|
23
|
+
console.log("****************************************************************");
|
|
24
|
+
console.log(`${ctx.config.serviceName} rodando na porta ${port} e conectado ao MongoDB`);
|
|
25
|
+
console.log("****************************************************************");
|
|
26
|
+
resolve({ app, server });
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { start };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Estado de runtime do Core. Singleton de processo: guarda a config da
|
|
3
|
+
* Central resolvida no boot e dá acesso aos registries para as factories,
|
|
4
|
+
* rotas /core/* e CLI.
|
|
5
|
+
*/
|
|
6
|
+
const defaults = {
|
|
7
|
+
serviceName: process.env.SERVICE_NAME || "central-oon",
|
|
8
|
+
serviceVersion: process.env.SERVICE_VERSION || "0.0.0",
|
|
9
|
+
port: Number(process.env.PORT) || 4000,
|
|
10
|
+
// Função de verificação de token. Recebe (token, { sistema }) e deve
|
|
11
|
+
// resolver com o objeto `usuario` ({ tipo, nome, email }) ou lançar.
|
|
12
|
+
// Default: delega ao Meus Apps (comportamento histórico do CST).
|
|
13
|
+
auth: { verifyToken: null },
|
|
14
|
+
cors: { origin: "*" },
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
let config = { ...defaults };
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
get config() {
|
|
21
|
+
return config;
|
|
22
|
+
},
|
|
23
|
+
setConfig(partial = {}) {
|
|
24
|
+
config = {
|
|
25
|
+
...config,
|
|
26
|
+
...partial,
|
|
27
|
+
auth: { ...config.auth, ...(partial.auth || {}) },
|
|
28
|
+
cors: { ...config.cors, ...(partial.cors || {}) },
|
|
29
|
+
};
|
|
30
|
+
return config;
|
|
31
|
+
},
|
|
32
|
+
reset() {
|
|
33
|
+
config = { ...defaults };
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const mongoose = require("mongoose");
|
|
2
|
+
const express = require("express");
|
|
3
|
+
const registry = require("./registry");
|
|
4
|
+
const ctx = require("./context");
|
|
5
|
+
const { asyncHandler } = require("./utils/helpers");
|
|
6
|
+
const auth = require("./middlewares/auth");
|
|
7
|
+
const { rbac } = require("./middlewares/rbac");
|
|
8
|
+
const { mutationAudit } = require("./middlewares/mutationAudit");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Constrói o schema Mongoose a partir dos descriptors de `fields`,
|
|
12
|
+
* separando o bloco `__meta` (consumido apenas por /core/metadata).
|
|
13
|
+
*/
|
|
14
|
+
function buildSchema(schemaDef, options = {}) {
|
|
15
|
+
const mongooseShape = {};
|
|
16
|
+
const metaFields = {};
|
|
17
|
+
const searchableFields = [];
|
|
18
|
+
|
|
19
|
+
for (const [key, descriptor] of Object.entries(schemaDef)) {
|
|
20
|
+
if (descriptor && typeof descriptor === "object" && descriptor.__meta) {
|
|
21
|
+
const { __meta, ...mongooseDef } = descriptor;
|
|
22
|
+
mongooseShape[key] = mongooseDef;
|
|
23
|
+
metaFields[key] = { name: key, ...__meta };
|
|
24
|
+
if (__meta.kind === "string" && __meta.searchable) searchableFields.push(key);
|
|
25
|
+
} else {
|
|
26
|
+
// Descriptor cru (compatível com Mongoose puro).
|
|
27
|
+
mongooseShape[key] = descriptor;
|
|
28
|
+
metaFields[key] = { name: key, kind: "raw" };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const schema = new mongoose.Schema(mongooseShape, {
|
|
33
|
+
timestamps: true,
|
|
34
|
+
...options,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
return { schema, metaFields, searchableFields };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* defineModel — registra a model e gera derivações automáticas
|
|
42
|
+
* (schema, CRUD, RBAC, paginação, validação, auditoria, metadata).
|
|
43
|
+
*/
|
|
44
|
+
function defineModel(definition) {
|
|
45
|
+
if (!definition?.name) throw new Error("defineModel requer `name`.");
|
|
46
|
+
if (!definition?.schema) throw new Error(`defineModel("${definition.name}") requer \`schema\`.`);
|
|
47
|
+
|
|
48
|
+
const { schema, metaFields, searchableFields } = buildSchema(
|
|
49
|
+
definition.schema,
|
|
50
|
+
definition.options
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// Hooks de domínio (defineTrigger) ligados ao ciclo do documento.
|
|
54
|
+
schema.pre("save", async function (next) {
|
|
55
|
+
try {
|
|
56
|
+
const { before } = registry.getTriggers(definition.name);
|
|
57
|
+
for (const fn of before) await fn(this, { op: this.isNew ? "create" : "update" });
|
|
58
|
+
next();
|
|
59
|
+
} catch (err) {
|
|
60
|
+
next(err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
schema.post("save", async function (doc) {
|
|
64
|
+
const { after } = registry.getTriggers(definition.name);
|
|
65
|
+
for (const fn of after) await fn(doc, { op: "save" });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const mongooseModel =
|
|
69
|
+
mongoose.models[definition.name] || mongoose.model(definition.name, schema);
|
|
70
|
+
|
|
71
|
+
const entry = registry.registerModel({
|
|
72
|
+
name: definition.name,
|
|
73
|
+
singular: (definition.singular || definition.name).toLowerCase(),
|
|
74
|
+
mongooseModel,
|
|
75
|
+
definition,
|
|
76
|
+
metaFields,
|
|
77
|
+
searchableFields,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Permissões/actions derivadas para o front.
|
|
81
|
+
if (definition.crud?.enabled !== false) {
|
|
82
|
+
registry.registerFeature(definition.name, {
|
|
83
|
+
model: definition.name,
|
|
84
|
+
crud: true,
|
|
85
|
+
roles: definition.crud?.roles || {},
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return entry;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* defineCentral — declara metadados da Central (módulos ativos + paths de
|
|
94
|
+
* domínio). Mescla na config de runtime.
|
|
95
|
+
*/
|
|
96
|
+
function defineCentral(config = {}) {
|
|
97
|
+
ctx.setConfig(config);
|
|
98
|
+
return ctx.config;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function defineValidation(modelName, fn) {
|
|
102
|
+
registry.registerValidation(modelName, fn);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function defineTrigger(modelName, hooks) {
|
|
106
|
+
registry.registerTrigger(modelName, hooks);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function defineCollection(name, config) {
|
|
110
|
+
return registry.registerCollection(name, { name, ...config });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function defineDocument(name, config) {
|
|
114
|
+
return registry.registerDocument(name, { name, ...config });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function definePipeline(name, config) {
|
|
118
|
+
return registry.registerPipeline(name, { name, ...config });
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function defineOmieMapping(name, config) {
|
|
122
|
+
return registry.registerOmieMapping(name, { name, ...config });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* defineRoutes — rotas customizadas com o contrato de segurança do Core.
|
|
127
|
+
* Entrega `router.private.*` (auth + RBAC + mutationAudit) e
|
|
128
|
+
* `router.public.*`. NUNCA expõe `express.Router()` cru.
|
|
129
|
+
*/
|
|
130
|
+
function defineRoutes(basePath, builder) {
|
|
131
|
+
const expressRouter = express.Router();
|
|
132
|
+
|
|
133
|
+
const wrapPrivate = (method) => (path, opts = {}, handler) => {
|
|
134
|
+
if (typeof opts === "function") {
|
|
135
|
+
handler = opts;
|
|
136
|
+
opts = {};
|
|
137
|
+
}
|
|
138
|
+
const chain = [auth, rbac(opts.roles || [])];
|
|
139
|
+
if (["post", "put", "patch", "delete"].includes(method) && opts.audit) {
|
|
140
|
+
chain.push(mutationAudit(opts.audit));
|
|
141
|
+
}
|
|
142
|
+
expressRouter[method](path, ...chain, asyncHandler(handler));
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const wrapPublic = (method) => (path, handler) => {
|
|
146
|
+
expressRouter[method](path, asyncHandler(handler));
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const methods = ["get", "post", "put", "patch", "delete"];
|
|
150
|
+
const router = { private: {}, public: {} };
|
|
151
|
+
for (const m of methods) {
|
|
152
|
+
router.private[m] = wrapPrivate(m);
|
|
153
|
+
router.public[m] = wrapPublic(m);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
builder(router);
|
|
157
|
+
registry.registerRoutes({ basePath, expressRouter });
|
|
158
|
+
return expressRouter;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = {
|
|
162
|
+
defineModel,
|
|
163
|
+
defineCentral,
|
|
164
|
+
defineValidation,
|
|
165
|
+
defineTrigger,
|
|
166
|
+
defineCollection,
|
|
167
|
+
defineDocument,
|
|
168
|
+
definePipeline,
|
|
169
|
+
defineOmieMapping,
|
|
170
|
+
defineRoutes,
|
|
171
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Erro de domínio reconhecido pelo error middleware do Core.
|
|
3
|
+
* Permite que services/triggers sinalizem statusCode e detalhes.
|
|
4
|
+
*/
|
|
5
|
+
class GenericError extends Error {
|
|
6
|
+
constructor(message, { statusCode = 400, details = null } = {}) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "GenericError";
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.details = details;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = GenericError;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const Helpers = require("../utils/helpers");
|
|
2
|
+
const registry = require("../registry");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Controllers HTTP de CRUD ligados a um service. Aplica validação
|
|
6
|
+
* declarada via `defineValidation` antes de create/update.
|
|
7
|
+
*/
|
|
8
|
+
function createCrudController(entry, service) {
|
|
9
|
+
const runValidation = async (data, op) => {
|
|
10
|
+
const validate = registry.getValidation(entry.name);
|
|
11
|
+
if (validate) await validate(data, { op });
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
async list(req, res) {
|
|
16
|
+
const { pageIndex, pageSize, searchTerm, sort, ...filtros } = req.query;
|
|
17
|
+
const { results, pagination } = await service.listPaginated({
|
|
18
|
+
pageIndex,
|
|
19
|
+
pageSize,
|
|
20
|
+
searchTerm,
|
|
21
|
+
sort,
|
|
22
|
+
filtros,
|
|
23
|
+
});
|
|
24
|
+
Helpers.sendPaginatedResponse({ res, statusCode: 200, results, pagination });
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
async getById(req, res) {
|
|
28
|
+
const registro = await service.getById(req.params.id);
|
|
29
|
+
Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async create(req, res) {
|
|
33
|
+
await runValidation(req.body, "create");
|
|
34
|
+
const registro = await service.create(req.body);
|
|
35
|
+
Helpers.sendResponse({ res, statusCode: 201, [entry.singular]: registro });
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async update(req, res) {
|
|
39
|
+
await runValidation(req.body, "update");
|
|
40
|
+
const registro = await service.update(req.params.id, req.body);
|
|
41
|
+
Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
async patch(req, res) {
|
|
45
|
+
const registro = await service.update(req.params.id, req.body);
|
|
46
|
+
Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
async remove(req, res) {
|
|
50
|
+
const registro = await service.remove(req.params.id);
|
|
51
|
+
Helpers.sendResponse({ res, statusCode: 200, [entry.singular]: registro });
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
async importMany(req, res) {
|
|
55
|
+
const result = await service.importMany(req.body?.rows || req.body);
|
|
56
|
+
Helpers.sendResponse({ res, statusCode: 201, ...result });
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
async exportAll(req, res) {
|
|
60
|
+
const results = await service.exportAll(req.query);
|
|
61
|
+
Helpers.sendResponse({ res, statusCode: 200, results });
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { createCrudController };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const GenericError = require("../errors/GenericError");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Gera um service de CRUD genérico para uma model registrada.
|
|
5
|
+
* Cobre paginação, filtros, busca textual, ordenação, import e export.
|
|
6
|
+
*/
|
|
7
|
+
function createCrudService(entry) {
|
|
8
|
+
const { mongooseModel: Model, searchableFields } = entry;
|
|
9
|
+
|
|
10
|
+
async function listPaginated({ pageIndex = 0, pageSize = 20, searchTerm, sort, filtros = {} } = {}) {
|
|
11
|
+
const page = Math.max(0, Number(pageIndex) || 0);
|
|
12
|
+
const limit = Math.min(200, Math.max(1, Number(pageSize) || 20));
|
|
13
|
+
|
|
14
|
+
const query = { ...cleanFilters(filtros) };
|
|
15
|
+
if (searchTerm && searchableFields.length) {
|
|
16
|
+
const rx = new RegExp(escapeRegex(searchTerm), "i");
|
|
17
|
+
query.$or = searchableFields.map((f) => ({ [f]: rx }));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const sortSpec = parseSort(sort);
|
|
21
|
+
const [results, totalItems] = await Promise.all([
|
|
22
|
+
Model.find(query).sort(sortSpec).skip(page * limit).limit(limit).lean(),
|
|
23
|
+
Model.countDocuments(query),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
results,
|
|
28
|
+
pagination: {
|
|
29
|
+
currentPage: page,
|
|
30
|
+
itemsPerPage: limit,
|
|
31
|
+
totalItems,
|
|
32
|
+
totalPages: Math.ceil(totalItems / limit) || 1,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function getById(id) {
|
|
38
|
+
const doc = await Model.findById(id);
|
|
39
|
+
if (!doc) throw new GenericError("Registro não encontrado.", { statusCode: 404 });
|
|
40
|
+
return doc;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function create(data) {
|
|
44
|
+
return Model.create(data);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function update(id, data) {
|
|
48
|
+
const doc = await Model.findByIdAndUpdate(id, data, {
|
|
49
|
+
new: true,
|
|
50
|
+
runValidators: true,
|
|
51
|
+
});
|
|
52
|
+
if (!doc) throw new GenericError("Registro não encontrado.", { statusCode: 404 });
|
|
53
|
+
return doc;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function remove(id) {
|
|
57
|
+
const doc = await Model.findByIdAndDelete(id);
|
|
58
|
+
if (!doc) throw new GenericError("Registro não encontrado.", { statusCode: 404 });
|
|
59
|
+
return doc;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function importMany(rows = []) {
|
|
63
|
+
if (!Array.isArray(rows)) {
|
|
64
|
+
throw new GenericError("Esperado um array de registros.", { statusCode: 400 });
|
|
65
|
+
}
|
|
66
|
+
const docs = await Model.insertMany(rows, { ordered: false });
|
|
67
|
+
return { inserted: docs.length };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function exportAll(filtros = {}) {
|
|
71
|
+
return Model.find(cleanFilters(filtros)).lean();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { listPaginated, getById, create, update, remove, importMany, exportAll };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function cleanFilters(filtros) {
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const [k, v] of Object.entries(filtros || {})) {
|
|
80
|
+
if (v !== undefined && v !== "" && v !== null) out[k] = v;
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseSort(sort) {
|
|
86
|
+
if (!sort) return { createdAt: -1 };
|
|
87
|
+
// "campo:asc" | "campo:desc" | "campo"
|
|
88
|
+
const [field, dir] = String(sort).split(":");
|
|
89
|
+
return { [field]: dir === "asc" ? 1 : -1 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function escapeRegex(str) {
|
|
93
|
+
return String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { createCrudService };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const express = require("express");
|
|
2
|
+
const { asyncHandler } = require("../utils/helpers");
|
|
3
|
+
const auth = require("../middlewares/auth");
|
|
4
|
+
const { rbac } = require("../middlewares/rbac");
|
|
5
|
+
const { mutationAudit } = require("../middlewares/mutationAudit");
|
|
6
|
+
const { createCrudService } = require("./crudService");
|
|
7
|
+
const { createCrudController } = require("./crudController");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Monta o router de CRUD de uma model. Todas as rotas são privadas
|
|
11
|
+
* (auth obrigatório). Mutações passam por RBAC + mutationAudit.
|
|
12
|
+
*
|
|
13
|
+
* Endpoints (Seção 2.9): GET list, GET :id, POST, PUT :id, PATCH :id,
|
|
14
|
+
* DELETE :id, POST /import, GET /export.
|
|
15
|
+
*/
|
|
16
|
+
function buildModelRouter(entry) {
|
|
17
|
+
const router = express.Router();
|
|
18
|
+
const service = createCrudService(entry);
|
|
19
|
+
const controller = createCrudController(entry, service);
|
|
20
|
+
|
|
21
|
+
const crud = entry.definition.crud || {};
|
|
22
|
+
const roles = crud.roles || {};
|
|
23
|
+
const readRoles = roles.read || [];
|
|
24
|
+
const writeRoles = roles.write || [];
|
|
25
|
+
const entidade = entry.name;
|
|
26
|
+
|
|
27
|
+
router.use(auth);
|
|
28
|
+
|
|
29
|
+
router.get("/", rbac(readRoles), asyncHandler(controller.list));
|
|
30
|
+
router.get("/export", rbac(readRoles), asyncHandler(controller.exportAll));
|
|
31
|
+
router.get("/:id", rbac(readRoles), asyncHandler(controller.getById));
|
|
32
|
+
|
|
33
|
+
router.post(
|
|
34
|
+
"/import",
|
|
35
|
+
rbac(writeRoles),
|
|
36
|
+
mutationAudit({ entidade, acao: "adicionado" }),
|
|
37
|
+
asyncHandler(controller.importMany)
|
|
38
|
+
);
|
|
39
|
+
router.post(
|
|
40
|
+
"/",
|
|
41
|
+
rbac(writeRoles),
|
|
42
|
+
mutationAudit({ entidade, acao: "adicionado" }),
|
|
43
|
+
asyncHandler(controller.create)
|
|
44
|
+
);
|
|
45
|
+
router.put(
|
|
46
|
+
"/:id",
|
|
47
|
+
rbac(writeRoles),
|
|
48
|
+
mutationAudit({ entidade, acao: "alterado" }),
|
|
49
|
+
asyncHandler(controller.update)
|
|
50
|
+
);
|
|
51
|
+
router.patch(
|
|
52
|
+
"/:id",
|
|
53
|
+
rbac(writeRoles),
|
|
54
|
+
mutationAudit({ entidade, acao: "alterado" }),
|
|
55
|
+
asyncHandler(controller.patch)
|
|
56
|
+
);
|
|
57
|
+
router.delete(
|
|
58
|
+
"/:id",
|
|
59
|
+
rbac(writeRoles),
|
|
60
|
+
mutationAudit({ entidade, acao: "excluido" }),
|
|
61
|
+
asyncHandler(controller.remove)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return router;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { buildModelRouter };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `fields` — tipos prontos para declarar schemas em `defineModel` (Seção 2.4).
|
|
3
|
+
*
|
|
4
|
+
* Cada helper retorna um descriptor `{ ...mongoose, __meta }`. O bloco
|
|
5
|
+
* `__meta` é consumido pelo registry para gerar metadata/OpenAPI para o
|
|
6
|
+
* front, sem vazar para o schema Mongoose final.
|
|
7
|
+
*/
|
|
8
|
+
const mongoose = require("mongoose");
|
|
9
|
+
|
|
10
|
+
const withMeta = (mongooseDef, meta) => ({ ...mongooseDef, __meta: meta });
|
|
11
|
+
|
|
12
|
+
const string = (opts = {}) =>
|
|
13
|
+
withMeta(
|
|
14
|
+
{ type: String, required: !!opts.required, default: opts.default, trim: true },
|
|
15
|
+
{ kind: "string", label: opts.label, searchable: opts.searchable !== false }
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const number = (opts = {}) =>
|
|
19
|
+
withMeta(
|
|
20
|
+
{ type: Number, required: !!opts.required, default: opts.default },
|
|
21
|
+
{ kind: "number", label: opts.label }
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const boolean = (opts = {}) =>
|
|
25
|
+
withMeta(
|
|
26
|
+
{ type: Boolean, required: !!opts.required, default: opts.default ?? false },
|
|
27
|
+
{ kind: "boolean", label: opts.label }
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const date = (opts = {}) =>
|
|
31
|
+
withMeta(
|
|
32
|
+
{ type: Date, required: !!opts.required, default: opts.default },
|
|
33
|
+
{ kind: "date", label: opts.label }
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const ref = (modelName, opts = {}) =>
|
|
37
|
+
withMeta(
|
|
38
|
+
{
|
|
39
|
+
type: mongoose.Schema.Types.ObjectId,
|
|
40
|
+
ref: modelName,
|
|
41
|
+
required: !!opts.required,
|
|
42
|
+
},
|
|
43
|
+
{ kind: "ref", ref: modelName, label: opts.label }
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const enumField = (values, opts = {}) =>
|
|
47
|
+
withMeta(
|
|
48
|
+
{
|
|
49
|
+
type: String,
|
|
50
|
+
enum: values,
|
|
51
|
+
required: !!opts.required,
|
|
52
|
+
default: opts.default,
|
|
53
|
+
},
|
|
54
|
+
{ kind: "enum", options: values, label: opts.label }
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const currency = (opts = {}) =>
|
|
58
|
+
withMeta(
|
|
59
|
+
{ type: Number, required: !!opts.required, default: opts.default ?? 0, min: 0 },
|
|
60
|
+
{ kind: "currency", label: opts.label }
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const currencyCode = (opts = {}) =>
|
|
64
|
+
withMeta(
|
|
65
|
+
{
|
|
66
|
+
type: String,
|
|
67
|
+
uppercase: true,
|
|
68
|
+
minlength: 3,
|
|
69
|
+
maxlength: 3,
|
|
70
|
+
default: opts.default || "BRL",
|
|
71
|
+
required: !!opts.required,
|
|
72
|
+
},
|
|
73
|
+
{ kind: "currencyCode", label: opts.label }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const currencyConverted = (opts = {}) =>
|
|
77
|
+
withMeta(
|
|
78
|
+
{ type: Number, default: opts.default ?? 0, min: 0 },
|
|
79
|
+
{ kind: "currencyConverted", base: opts.base || "BRL", label: opts.label }
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
string,
|
|
84
|
+
number,
|
|
85
|
+
boolean,
|
|
86
|
+
date,
|
|
87
|
+
ref,
|
|
88
|
+
enum: enumField,
|
|
89
|
+
currency,
|
|
90
|
+
currencyCode,
|
|
91
|
+
currencyConverted,
|
|
92
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
const Sistema = require("../models/Sistema");
|
|
3
|
+
const Helpers = require("../utils/helpers");
|
|
4
|
+
const ctx = require("../context");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Verificador padrão: delega ao Meus Apps (comportamento histórico do CST).
|
|
8
|
+
* Pode ser sobrescrito via `central.config.js -> auth.verifyToken`.
|
|
9
|
+
*/
|
|
10
|
+
async function defaultVerifyToken(token) {
|
|
11
|
+
const sistema = await Sistema.findOne().catch(() => null);
|
|
12
|
+
const baseUrl = process.env.MEUS_APPS_BACKEND_URL;
|
|
13
|
+
if (!baseUrl) {
|
|
14
|
+
const err = new Error("MEUS_APPS_BACKEND_URL não configurada.");
|
|
15
|
+
err.statusCode = 401;
|
|
16
|
+
throw err;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const response = await axios.get(
|
|
20
|
+
`${baseUrl}/auth/autenticar-aplicativo/`,
|
|
21
|
+
{ headers: { Authorization: `Bearer ${token}`, origin: sistema?.appKey } }
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const data = response.data.usuario;
|
|
25
|
+
return {
|
|
26
|
+
tipo:
|
|
27
|
+
data.aplicativo.tipoAcesso === "master" ? "admin" : data.aplicativo.tipoAcesso,
|
|
28
|
+
nome: data.nome,
|
|
29
|
+
email: data.email,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Middleware de autenticação. Sem token => 401 (invariante de segurança).
|
|
35
|
+
* Resolve `req.usuario` e segue.
|
|
36
|
+
*/
|
|
37
|
+
const authMiddleware = async (req, res, next) => {
|
|
38
|
+
const token = req.headers.authorization?.split(" ")[1];
|
|
39
|
+
|
|
40
|
+
if (!token) {
|
|
41
|
+
return Helpers.sendErrorResponse({
|
|
42
|
+
res,
|
|
43
|
+
message: "Acesso não autorizado. Token ausente.",
|
|
44
|
+
statusCode: 401,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const verify = ctx.config.auth?.verifyToken || defaultVerifyToken;
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const usuario = await verify(token, { req });
|
|
52
|
+
if (!usuario) throw new Error("Usuário não resolvido.");
|
|
53
|
+
req.usuario = usuario;
|
|
54
|
+
next();
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return Helpers.sendErrorResponse({
|
|
57
|
+
res,
|
|
58
|
+
message: "Token inválido ou erro na autenticação.",
|
|
59
|
+
statusCode: error.statusCode || 401,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
module.exports = authMiddleware;
|
|
65
|
+
module.exports.defaultVerifyToken = defaultVerifyToken;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Garante um correlationId por requisição (header `x-correlation-id` ou
|
|
5
|
+
* gerado). Exposto em `req.correlationId` e ecoado na resposta.
|
|
6
|
+
*/
|
|
7
|
+
module.exports = function correlationId(req, res, next) {
|
|
8
|
+
const id = req.headers["x-correlation-id"] || crypto.randomUUID();
|
|
9
|
+
req.correlationId = id;
|
|
10
|
+
res.setHeader("x-correlation-id", id);
|
|
11
|
+
next();
|
|
12
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const helpers = require("../utils/helpers");
|
|
2
|
+
const GenericError = require("../errors/GenericError");
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error handler central. Trata GenericError (domínio), erros de validação
|
|
6
|
+
* do Mongoose e cai num 500 genérico para o resto.
|
|
7
|
+
*/
|
|
8
|
+
const errorMiddleware = (error, _req, res, next) => {
|
|
9
|
+
if (!error) return next();
|
|
10
|
+
|
|
11
|
+
if (error instanceof GenericError) {
|
|
12
|
+
return helpers.sendErrorResponse({
|
|
13
|
+
res,
|
|
14
|
+
statusCode: error.statusCode,
|
|
15
|
+
error: error.details,
|
|
16
|
+
message: error.message,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (error.name === "ValidationError") {
|
|
21
|
+
return helpers.sendErrorResponse({
|
|
22
|
+
res,
|
|
23
|
+
statusCode: 422,
|
|
24
|
+
error: Object.values(error.errors || {}).map((e) => e.message),
|
|
25
|
+
message: "Erro de validação.",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (error.name === "CastError") {
|
|
30
|
+
return helpers.sendErrorResponse({
|
|
31
|
+
res,
|
|
32
|
+
statusCode: 400,
|
|
33
|
+
message: "Identificador inválido.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log("🆘 [UNHANDLED ERROR]:", error?.message || error);
|
|
38
|
+
return helpers.sendErrorResponse({
|
|
39
|
+
res,
|
|
40
|
+
statusCode: 500,
|
|
41
|
+
error: error.message,
|
|
42
|
+
message: "Houve um erro inesperado!",
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
module.exports = errorMiddleware;
|