@oondemand/oon-core-back 0.1.4 → 0.3.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.
@@ -1,20 +1,119 @@
1
1
  const express = require("express");
2
+ const axios = require("axios");
3
+ const ctx = require("../core/context");
2
4
  const auth = require("../core/middlewares/auth");
5
+ const GenericError = require("../core/errors/GenericError");
6
+ const {
7
+ LOGIN_PATH,
8
+ getBaseUrl,
9
+ getAppCode,
10
+ getTimeout,
11
+ } = require("../core/utils/activationProvider");
12
+ const {
13
+ ensureInstance,
14
+ instanceHeaders,
15
+ } = require("../core/utils/instanceActivation");
3
16
  const { asyncHandler, sendResponse } = require("../core/utils/helpers");
4
17
 
5
- /**
6
- * Rotas de autenticação do Core. O login propriamente dito é delegado ao
7
- * provedor configurado (Meus Apps por padrão); aqui validamos o token e
8
- * devolvemos o usuário resolvido.
9
- */
10
18
  const router = express.Router();
11
19
 
20
+ function readBasicAuthorization(req) {
21
+ const authorization = String(req.headers.authorization || "");
22
+ if (!authorization.startsWith("Basic ")) {
23
+ throw new GenericError("Credenciais de autenticação ausentes.", {
24
+ statusCode: 401,
25
+ });
26
+ }
27
+ return authorization;
28
+ }
29
+
30
+ function resolveInstanceHeaders(instance) {
31
+ if (instance.ecosystemRole !== "member") return {};
32
+
33
+ if (!instance.instanceId) {
34
+ if (process.env.NODE_TEST_CONTEXT) return {};
35
+
36
+ throw new GenericError(
37
+ "Instância member ativa sem identidade do ecossistema.",
38
+ { statusCode: 423 },
39
+ );
40
+ }
41
+
42
+ return instanceHeaders(instance);
43
+ }
44
+
45
+ function translateProviderError(error) {
46
+ if (error instanceof GenericError) return error;
47
+ const timedOut = error.code === "ECONNABORTED";
48
+ const statusCode = timedOut ? 504 : error.response?.status || 502;
49
+ const message =
50
+ error.response?.data?.message ||
51
+ (timedOut
52
+ ? "A Central de Ativações não respondeu dentro do prazo."
53
+ : "Não foi possível autenticar o usuário na Central de Ativações.");
54
+ return new GenericError(message, { statusCode });
55
+ }
56
+
57
+ async function authenticateRoot(req, res, authorization) {
58
+ const authenticate = ctx.config.auth?.authenticate;
59
+ if (typeof authenticate !== "function") {
60
+ throw new GenericError(
61
+ "A aplicação root não configurou a autenticação local.",
62
+ { statusCode: 500 },
63
+ );
64
+ }
65
+
66
+ const result = await authenticate({ authorization, req });
67
+ const statusCode = result?.statusCode || 200;
68
+ const data = result?.data || result;
69
+
70
+ res.set("Cache-Control", "no-store");
71
+ res.status(statusCode).json(data);
72
+ }
73
+
74
+ router.post(
75
+ "/autenticar",
76
+ asyncHandler(async (req, res) => {
77
+ const authorization = readBasicAuthorization(req);
78
+ const instance = await ensureInstance();
79
+
80
+ if (instance.ecosystemRole === "root") {
81
+ return authenticateRoot(req, res, authorization);
82
+ }
83
+
84
+ const baseUrl = getBaseUrl(req);
85
+ const appCode = getAppCode();
86
+
87
+ try {
88
+ const response = await axios.post(
89
+ `${baseUrl}${LOGIN_PATH}`,
90
+ {},
91
+ {
92
+ timeout: getTimeout(),
93
+ maxRedirects: 0,
94
+ headers: {
95
+ Authorization: authorization,
96
+ "Content-Type": "application/json",
97
+ "x-app-code": appCode,
98
+ ...resolveInstanceHeaders(instance),
99
+ },
100
+ },
101
+ );
102
+
103
+ res.set("Cache-Control", "no-store");
104
+ res.status(response.status).json(response.data);
105
+ } catch (error) {
106
+ throw translateProviderError(error);
107
+ }
108
+ }),
109
+ );
110
+
12
111
  router.get(
13
112
  "/validar-token",
14
113
  auth,
15
114
  asyncHandler(async (req, res) => {
16
115
  sendResponse({ res, statusCode: 200, usuario: req.usuario });
17
- })
116
+ }),
18
117
  );
19
118
 
20
119
  module.exports = router;
@@ -13,6 +13,8 @@ const { buildModelRouter } = require("../core/factories/modelRouter");
13
13
  const coreRoutes = require("../api/core.routes");
14
14
  const statusRoutes = require("../api/status.routes");
15
15
  const authRoutes = require("../api/auth.routes");
16
+ const activationRoutes = require("../api/activation.routes");
17
+ const activationGuard = require("../core/middlewares/activationGuard");
16
18
 
17
19
  /**
18
20
  * Cria a app Express do Core: middlewares transversais, rotas públicas,
@@ -31,6 +33,8 @@ function createApp() {
31
33
 
32
34
  // Públicas.
33
35
  app.use("/", statusRoutes);
36
+ app.use("/ativacao", activationRoutes);
37
+ app.use(activationGuard);
34
38
  app.use("/auth", authRoutes);
35
39
  app.use("/core", coreRoutes);
36
40
 
@@ -7,7 +7,10 @@
7
7
  */
8
8
  const mongoose = require("mongoose");
9
9
 
10
- const withMeta = (mongooseDef, meta) => ({ ...mongooseDef, __meta: meta });
10
+ const withMeta = (mongooseDef, meta) => ({
11
+ ...mongooseDef,
12
+ __meta: { required: !!mongooseDef.required, ...meta },
13
+ });
11
14
 
12
15
  const string = (opts = {}) =>
13
16
  withMeta(
@@ -0,0 +1,43 @@
1
+ const { ensureInstance } = require("../utils/instanceActivation");
2
+ const Helpers = require("../utils/helpers");
3
+
4
+ const allowed = [
5
+ /^\/ativacao(\/|$)/,
6
+ /^\/health$/,
7
+ /^\/version$/,
8
+ /^\/favicon\.ico$/,
9
+ ];
10
+
11
+ module.exports = async function activationGuard(req, res, next) {
12
+ try {
13
+ if (allowed.some((pattern) => pattern.test(req.path))) return next();
14
+
15
+ const instance = await ensureInstance();
16
+ if (instance.status === "ativa") return next();
17
+
18
+ const code =
19
+ instance.status === "suspensa"
20
+ ? "INSTANCE_SUSPENDED"
21
+ : instance.status === "revogada"
22
+ ? "INSTANCE_REVOKED"
23
+ : "INSTANCE_NOT_ACTIVE";
24
+
25
+ const message =
26
+ instance.status === "suspensa"
27
+ ? instance.motivoSuspensao || "Esta Central está suspensa."
28
+ : instance.status === "revogada"
29
+ ? "Esta Central foi revogada."
30
+ : instance.ecosystemRole === "root"
31
+ ? "O aplicativo root ainda não foi ativado."
32
+ : "Esta Central ainda não foi ativada.";
33
+
34
+ return Helpers.sendErrorResponse({
35
+ res,
36
+ statusCode: 423,
37
+ message,
38
+ code,
39
+ });
40
+ } catch (error) {
41
+ return next(error);
42
+ }
43
+ };
@@ -1,39 +1,84 @@
1
1
  const axios = require("axios");
2
- const Sistema = require("../models/Sistema");
3
2
  const Helpers = require("../utils/helpers");
4
3
  const ctx = require("../context");
4
+ const {
5
+ PERMISSION_PATH,
6
+ getBaseUrl,
7
+ getAppCode,
8
+ getTimeout,
9
+ } = require("../utils/activationProvider");
10
+ const {
11
+ ensureInstance,
12
+ instanceHeaders,
13
+ } = require("../utils/instanceActivation");
5
14
 
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;
15
+ function resolveInstanceHeaders(instance) {
16
+ if (instance.ecosystemRole !== "member") return {};
17
+
18
+ if (!instance.instanceId) {
19
+ if (process.env.NODE_TEST_CONTEXT) return {};
20
+
21
+ const error = new Error(
22
+ "Instância member ativa sem identidade do ecossistema.",
23
+ );
24
+ error.statusCode = 423;
25
+ throw error;
17
26
  }
18
27
 
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
- };
28
+ return instanceHeaders(instance);
29
+ }
30
+
31
+ async function defaultVerifyToken(token, context = {}) {
32
+ const baseUrl = getBaseUrl(context.req);
33
+ const appCode = getAppCode();
34
+ const instance = await ensureInstance();
35
+
36
+ try {
37
+ const headers = {
38
+ "x-app-code": appCode,
39
+ Authorization: `Bearer ${token}`,
40
+ ...resolveInstanceHeaders(instance),
41
+ };
42
+
43
+ const response = await axios.get(`${baseUrl}${PERMISSION_PATH}`, {
44
+ headers,
45
+ timeout: getTimeout(),
46
+ maxRedirects: 0,
47
+ });
48
+
49
+ const data = response.data?.usuario;
50
+ if (!data) {
51
+ const error = new Error(
52
+ "Usuário não retornado pela Central de Ativações.",
53
+ );
54
+ error.statusCode = 502;
55
+ throw error;
56
+ }
57
+
58
+ return {
59
+ ...data,
60
+ tipo:
61
+ data.tipo === "master" || data.perfil === "administrador"
62
+ ? "admin"
63
+ : data.tipo || data.perfil,
64
+ };
65
+ } catch (error) {
66
+ if (error.statusCode) throw error;
67
+
68
+ const timedOut = error.code === "ECONNABORTED";
69
+ const translated = new Error(
70
+ error.response?.data?.message ||
71
+ (timedOut
72
+ ? "A Central de Ativações não respondeu dentro do prazo."
73
+ : "Não foi possível validar a permissão do aplicativo."),
74
+ );
75
+ translated.statusCode = timedOut
76
+ ? 504
77
+ : error.response?.status || 502;
78
+ throw translated;
79
+ }
31
80
  }
32
81
 
33
- /**
34
- * Middleware de autenticação. Sem token => 401 (invariante de segurança).
35
- * Resolve `req.usuario` e segue.
36
- */
37
82
  const authMiddleware = async (req, res, next) => {
38
83
  const token = req.headers.authorization?.split(" ")[1];
39
84
 
@@ -49,15 +94,24 @@ const authMiddleware = async (req, res, next) => {
49
94
 
50
95
  try {
51
96
  const usuario = await verify(token, { req });
52
- if (!usuario) throw new Error("Usuário não resolvido.");
97
+ if (!usuario) {
98
+ const error = new Error("Usuário não resolvido.");
99
+ error.statusCode = 401;
100
+ throw error;
101
+ }
53
102
  req.usuario = usuario;
54
103
  next();
55
104
  } catch (error) {
56
- return Helpers.sendErrorResponse({
57
- res,
58
- message: "Token inválido ou erro na autenticação.",
59
- statusCode: error.statusCode || 401,
60
- });
105
+ const statusCode = error.statusCode || error.response?.status || 502;
106
+ const message =
107
+ statusCode === 403
108
+ ? "Usuário sem permissão para acessar este aplicativo."
109
+ : statusCode === 401
110
+ ? "Token inválido ou expirado."
111
+ : error.message ||
112
+ "Erro ao validar autenticação e permissão do aplicativo.";
113
+
114
+ return Helpers.sendErrorResponse({ res, message, statusCode });
61
115
  }
62
116
  };
63
117
 
@@ -0,0 +1,60 @@
1
+ const mongoose = require("mongoose");
2
+
3
+ const statuses = ["nao_ativada", "validando", "configurando", "ativa", "suspensa", "revogada", "erro"];
4
+ const roles = ["root", "member"];
5
+
6
+ const schema = new mongoose.Schema(
7
+ {
8
+ singletonKey: { type: String, default: "default", immutable: true },
9
+ status: { type: String, enum: statuses, default: "nao_ativada", index: true },
10
+ ecosystemRole: { type: String, enum: roles, default: "member", index: true },
11
+ instanceId: { type: String, index: true },
12
+ instanceCode: String,
13
+ appCode: { type: String, index: true },
14
+ appId: String,
15
+ tenantId: String,
16
+ licencaId: String,
17
+ ambiente: { type: String, enum: ["desenvolvimento", "homologacao", "producao"] },
18
+ nomeInstancia: String,
19
+ ecosystemUrl: String,
20
+ publicUrl: String,
21
+ coreVersion: String,
22
+ appVersion: String,
23
+ fingerprint: String,
24
+ credentialEncrypted: { type: String, select: false },
25
+ credentialIv: { type: String, select: false },
26
+ credentialAuthTag: { type: String, select: false },
27
+ credentialVersion: Number,
28
+ configurationVersion: Number,
29
+ parametrosGlobais: { type: mongoose.Schema.Types.Mixed, default: {} },
30
+ parametrosAplicacao: { type: mongoose.Schema.Types.Mixed, default: {} },
31
+ tenant: { type: mongoose.Schema.Types.Mixed, default: {} },
32
+ app: { type: mongoose.Schema.Types.Mixed, default: {} },
33
+ licenca: { type: mongoose.Schema.Types.Mixed, default: {} },
34
+ ativadaEm: Date,
35
+ concluidaEm: Date,
36
+ ultimaValidacaoEm: Date,
37
+ ultimoHeartbeatEm: Date,
38
+ ultimoErro: String,
39
+ motivoSuspensao: String,
40
+ motivoRevogacao: String,
41
+ },
42
+ {
43
+ timestamps: true,
44
+ collection: "oon_instancia_ecossistema",
45
+ toJSON: {
46
+ transform(_doc, ret) {
47
+ delete ret.credentialEncrypted;
48
+ delete ret.credentialIv;
49
+ delete ret.credentialAuthTag;
50
+ return ret;
51
+ },
52
+ },
53
+ },
54
+ );
55
+
56
+ schema.index({ singletonKey: 1 }, { unique: true });
57
+
58
+ module.exports = mongoose.models.InstanciaEcossistema || mongoose.model("InstanciaEcossistema", schema);
59
+ module.exports.STATUSES = statuses;
60
+ module.exports.ROLES = roles;
@@ -0,0 +1,54 @@
1
+ const GenericError = require("../errors/GenericError");
2
+
3
+ const LOGIN_PATH = "/integracao/autenticacao-aplicativos/autenticar";
4
+ const PERMISSION_PATH = "/integracao/autenticacao-aplicativos/verificar-permissao";
5
+
6
+ function getBaseUrl(req) {
7
+ const value = process.env.CENTRAL_ATIVACAO_URL || process.env.MEUS_APPS_BACKEND_URL;
8
+ if (!value) {
9
+ throw new GenericError("CENTRAL_ATIVACAO_URL não configurada.", { statusCode: 500 });
10
+ }
11
+
12
+ let parsed;
13
+ try {
14
+ parsed = new URL(value);
15
+ } catch {
16
+ throw new GenericError("CENTRAL_ATIVACAO_URL inválida.", { statusCode: 500 });
17
+ }
18
+
19
+ const currentHost = String(
20
+ req?.headers?.["x-forwarded-host"] || req?.headers?.host || "",
21
+ )
22
+ .split(",")[0]
23
+ .trim()
24
+ .toLowerCase();
25
+
26
+ if (
27
+ currentHost &&
28
+ parsed.host.toLowerCase() === currentHost &&
29
+ process.env.AUTH_ALLOW_SAME_HOST !== "true"
30
+ ) {
31
+ throw new GenericError(
32
+ "CENTRAL_ATIVACAO_URL aponta para o próprio Core. Configure o endereço da Central de Ativações.",
33
+ { statusCode: 500 },
34
+ );
35
+ }
36
+
37
+ return parsed.toString().replace(/\/$/, "");
38
+ }
39
+
40
+ function getAppCode() {
41
+ const value = String(process.env.APP_CODE || process.env.APP_CODIGO || process.env.APP_KEY || "")
42
+ .trim()
43
+ .toLowerCase();
44
+ if (value) return value;
45
+ if (process.env.NODE_TEST_CONTEXT) return "oon-core-test";
46
+ throw new GenericError("APP_CODE não configurada no backend.", { statusCode: 500 });
47
+ }
48
+
49
+ function getTimeout() {
50
+ const value = Number(process.env.AUTH_PROVIDER_TIMEOUT_MS);
51
+ return Number.isFinite(value) && value > 0 ? value : 10000;
52
+ }
53
+
54
+ module.exports = { LOGIN_PATH, PERMISSION_PATH, getBaseUrl, getAppCode, getTimeout };
@@ -2,10 +2,10 @@ exports.sendResponse = ({ res, statusCode = 200, message = "OK", ...rest }) => {
2
2
  res.status(statusCode).send({ message, ...rest });
3
3
  };
4
4
 
5
- exports.sendErrorResponse = ({ res, statusCode = 500, message, error }) => {
5
+ exports.sendErrorResponse = ({ res, statusCode = 500, message, error, code }) => {
6
6
  res.status(statusCode).json({
7
7
  message,
8
- ...(error ? { error } : {}),
8
+ error: error || (code ? { code, message } : undefined),
9
9
  });
10
10
  };
11
11