@oondemand/oon-core-back 0.2.0 → 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.
- package/README.md +13 -126
- package/package.json +1 -1
- package/src/api/activation.routes.js +522 -0
- package/src/api/auth.routes.js +105 -6
- package/src/boot/createApp.js +4 -0
- package/src/core/fields.js +4 -1
- package/src/core/middlewares/activationGuard.js +43 -0
- package/src/core/middlewares/auth.js +88 -34
- package/src/core/models/InstanciaEcossistema.js +60 -0
- package/src/core/utils/activationProvider.js +54 -0
- package/src/core/utils/helpers.js +2 -2
- package/src/core/utils/instanceActivation.js +477 -0
- package/src/deploy/render.js +3 -0
- package/src/deploy/templates/configmap.yaml +3 -0
package/src/api/auth.routes.js
CHANGED
|
@@ -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;
|
package/src/boot/createApp.js
CHANGED
|
@@ -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
|
|
package/src/core/fields.js
CHANGED
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
*/
|
|
8
8
|
const mongoose = require("mongoose");
|
|
9
9
|
|
|
10
|
-
const withMeta = (mongooseDef, 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
throw
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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)
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
8
|
+
error: error || (code ? { code, message } : undefined),
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
11
|
|