@oondemand/oon-core-back 0.7.6 → 0.7.8
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 +31 -0
- package/package.json +1 -1
- package/src/api/access.routes.js +41 -21
- package/src/core/middlewares/auth.js +8 -0
- package/src/core/middlewares/requestLog.js +3 -2
- package/src/core/rbac/centralInvitations.js +70 -0
- package/src/core/rbac/identityProvider.js +17 -2
- package/src/core/rbac/storage.js +4 -3
package/README.md
CHANGED
|
@@ -97,3 +97,34 @@ 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.
|
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;
|
|
@@ -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,
|
|
@@ -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,
|
|
@@ -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
|
@@ -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) {
|