rl-core-api 0.6.0 → 0.8.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 +54 -0
- package/dist/core/auth/ownershipScope.util.d.ts +10 -0
- package/dist/core/auth/ownershipScope.util.js +14 -0
- package/dist/core/auth/permissionScope.util.d.ts +2 -0
- package/dist/core/auth/permissionScope.util.js +7 -1
- package/dist/core/config/env.schema.d.ts +3 -0
- package/dist/core/config/env.schema.js +3 -0
- package/dist/core/core.module.js +3 -1
- package/dist/core/database/migrations/1710000000000-InitialSchema.js +8 -1
- package/dist/core/database/migrations/index.js +0 -2
- package/dist/core/database/seeds/seedPermissions.d.ts +20 -1
- package/dist/core/database/seeds/seedPermissions.js +31 -38
- package/dist/core/events/appEvent.enum.d.ts +6 -0
- package/dist/core/events/appEvent.enum.js +1 -0
- package/dist/core/health/health.controller.d.ts +4 -1
- package/dist/core/health/health.controller.js +9 -3
- package/dist/core/query/applyOwnershipScope.util.d.ts +7 -0
- package/dist/core/query/applyOwnershipScope.util.js +16 -0
- package/dist/core/queue/bullConnection.module.d.ts +2 -0
- package/dist/core/queue/bullConnection.module.js +27 -0
- package/dist/core/queue/job.interface.d.ts +70 -0
- package/dist/core/queue/job.interface.js +2 -0
- package/dist/core/queue/job.producer.d.ts +15 -0
- package/dist/core/queue/job.producer.js +124 -0
- package/dist/core/queue/jobEvents.interface.d.ts +4 -0
- package/dist/core/queue/jobEvents.interface.js +4 -0
- package/dist/core/queue/jobEvents.service.d.ts +12 -0
- package/dist/core/queue/jobEvents.service.js +53 -0
- package/dist/core/queue/jobStatus.controller.d.ts +8 -0
- package/dist/core/queue/jobStatus.controller.js +44 -0
- package/dist/core/queue/jobStatus.response.d.ts +20 -0
- package/dist/core/queue/jobStatus.response.js +84 -0
- package/dist/core/queue/jobWorkerHost.d.ts +17 -0
- package/dist/core/queue/jobWorkerHost.js +72 -0
- package/dist/core/queue/queue.constants.d.ts +11 -0
- package/dist/core/queue/queue.constants.js +18 -0
- package/dist/core/queue/queue.module.d.ts +2 -0
- package/dist/core/queue/queue.module.js +38 -0
- package/dist/core/queue/queueAdmin.controller.d.ts +11 -0
- package/dist/core/queue/queueAdmin.controller.js +93 -0
- package/dist/core/queue/queueAdmin.response.d.ts +23 -0
- package/dist/core/queue/queueAdmin.response.js +87 -0
- package/dist/core/queue/queueAdmin.service.d.ts +15 -0
- package/dist/core/queue/queueAdmin.service.js +85 -0
- package/dist/core/queue/redisConnection.d.ts +2 -0
- package/dist/core/queue/redisConnection.js +11 -0
- package/dist/core/websocket/baseGateway.d.ts +1 -0
- package/dist/core/websocket/baseGateway.js +3 -0
- package/dist/features/audit/audit.module.js +1 -0
- package/dist/features/audit/presentation/index.d.ts +1 -0
- package/dist/features/audit/presentation/index.js +1 -0
- package/dist/features/audit/presentation/userPermissionsChanged.listener.d.ts +7 -0
- package/dist/features/audit/presentation/userPermissionsChanged.listener.js +43 -0
- package/dist/features/notifications/notifications.module.js +1 -1
- package/dist/features/notifications/presentation/domainEvents.listener.d.ts +2 -1
- package/dist/features/notifications/presentation/domainEvents.listener.js +19 -0
- package/dist/features/rbac/domain/rbac.catalog.js +19 -16
- package/dist/features/rbac/domain/rbac.service.d.ts +11 -1
- package/dist/features/rbac/domain/rbac.service.js +76 -3
- package/dist/features/rbac/domain/teamScope.service.d.ts +1 -0
- package/dist/features/rbac/domain/teamScope.service.js +10 -4
- package/dist/features/rbac/infra/schema/scope.schema.js +1 -1
- package/dist/features/rbac/presentation/commands/createScope.command.js +1 -1
- package/dist/features/rbac/presentation/commands/setUserPermissions.command.d.ts +3 -0
- package/dist/features/rbac/presentation/commands/setUserPermissions.command.js +24 -0
- package/dist/features/rbac/presentation/index.d.ts +1 -0
- package/dist/features/rbac/presentation/index.js +1 -0
- package/dist/features/rbac/presentation/rbac.controller.d.ts +5 -0
- package/dist/features/rbac/presentation/rbac.controller.js +57 -0
- package/dist/index.d.ts +17 -3
- package/dist/index.js +48 -2
- package/dist/rlCore.module.js +8 -0
- package/package.json +4 -1
- package/dist/core/database/migrations/1787270400000-GroupManager.d.ts +0 -6
- package/dist/core/database/migrations/1787270400000-GroupManager.js +0 -26
package/README.md
CHANGED
|
@@ -61,6 +61,7 @@ migrations do core vêm no pacote — você nunca copia nem reescreve nenhuma.
|
|
|
61
61
|
| **Auditoria** | trilha de alterações de dados, log de requisições e log de erros, mantidos indefinidamente (sem retenção automática) |
|
|
62
62
|
| **Notificações** | WebSocket com handshake por cookie ou Bearer, escopo resolvido na leitura |
|
|
63
63
|
| **Listagens** | paginação, ordenação e filtro dinâmico com catálogo por listagem (`$AND`/`$OR` aninhados, 17 operadores) |
|
|
64
|
+
| **Fila** | processamento em segundo plano com progresso em tempo real (BullMQ + Redis), opcional por ambiente |
|
|
64
65
|
| **Infra** | config validada com Zod, mailer, agendador, rate-limit, CSRF, filtro global de exceções, logger Winston |
|
|
65
66
|
|
|
66
67
|
## Configuração
|
|
@@ -69,6 +70,59 @@ O pacote **não traz `.env`** — ele declara o que precisa e quebra no boot se
|
|
|
69
70
|
faltar. Cada projeto tem os valores dele (banco próprio, segredo JWT próprio).
|
|
70
71
|
Comece pelo `.env.example` do repositório.
|
|
71
72
|
|
|
73
|
+
### Fila de processamento (opcional)
|
|
74
|
+
|
|
75
|
+
Sem `REDIS_HOST` a aplicação sobe **sem fila e sem erro** — é o padrão.
|
|
76
|
+
Declarado, o Redis passa a ser obrigatório: a subida falha se ele não responder,
|
|
77
|
+
em vez de a falha aparecer no primeiro job.
|
|
78
|
+
|
|
79
|
+
```env
|
|
80
|
+
REDIS_HOST=localhost
|
|
81
|
+
REDIS_PORT=6379
|
|
82
|
+
REDIS_PASSWORD=redis
|
|
83
|
+
REDIS_PREFIX=controlx # um Redis para vários projetos: o prefixo os separa
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Concorrência, tentativas, backoff e retenção não são variáveis de ambiente —
|
|
87
|
+
são constantes em `queue.constants.ts`. São decisão de arquitetura da fila, e um
|
|
88
|
+
número desses ajustado no `.env` de produção sem ninguém revisar é como se
|
|
89
|
+
descobre, tarde, por que os jobs empilharam.
|
|
90
|
+
|
|
91
|
+
O processador é sempre do projeto — o core não conhece o que a feature faz:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
@Processor(JOBS_QUEUE, { concurrency: JOB_CONCURRENCY, autorun: false })
|
|
95
|
+
export class FunkosImportProcessor extends JobWorkerHost<Payload, Summary> {
|
|
96
|
+
constructor(
|
|
97
|
+
jobEvents: JobEventsService,
|
|
98
|
+
private readonly imports: FunkosImportService,
|
|
99
|
+
) {
|
|
100
|
+
super(jobEvents);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async handle(job: JobContext<Payload>): Promise<Summary> {
|
|
104
|
+
return this.imports.run(job.payload, (done, total) => job.report(done, total));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Registre a classe como provider do módulo da feature, **atrás do
|
|
110
|
+
`isRedisConfigured()`** — sem Redis não há conexão para o worker abrir:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
@Module({ providers: isRedisConfigured() ? [FunkosImportProcessor] : [] })
|
|
114
|
+
export class FunkosModule {}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Na rota, `jobs.enqueue(name, payload, { ownerId })` devolve o `jobId` na hora. O
|
|
118
|
+
progresso vai para a sala do dono pela **mesma** conexão de socket das
|
|
119
|
+
notificações (`job:progress`, `job:completed`, `job:failed`), e `GET /jobs/:id`
|
|
120
|
+
responde a quem perdeu o socket.
|
|
121
|
+
|
|
122
|
+
Quem opera vê tudo pela tela de **Filas** (`QueuesScreen`, no `rl-core-front`):
|
|
123
|
+
contagem por estado, o motivo cru da falha, reprocessar e remover — com
|
|
124
|
+
`queues:read:any` e `queues:manage:any`.
|
|
125
|
+
|
|
72
126
|
### Identidade do sistema
|
|
73
127
|
|
|
74
128
|
O core não tem nome próprio. `APP_NAME` é o do projeto, e dele saem o título do
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveOwnershipScope = void 0;
|
|
4
|
+
const permissionScope_util_1 = require("./permissionScope.util");
|
|
5
|
+
const resolveOwnershipScope = (actor, action) => {
|
|
6
|
+
if (actor.permissions.includes(`${action}:${permissionScope_util_1.SCOPE_ANY}`)) {
|
|
7
|
+
return { kind: "all" };
|
|
8
|
+
}
|
|
9
|
+
if (actor.permissions.includes(`${action}:${permissionScope_util_1.SCOPE_OWN}`)) {
|
|
10
|
+
return { kind: "own", userId: actor.id };
|
|
11
|
+
}
|
|
12
|
+
return { kind: "none" };
|
|
13
|
+
};
|
|
14
|
+
exports.resolveOwnershipScope = resolveOwnershipScope;
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.anyOrTeam = exports.SCOPE_TEAM = exports.SCOPE_ANY = void 0;
|
|
3
|
+
exports.anyOrOwn = exports.anyOrTeam = exports.SCOPE_OWN = exports.SCOPE_TEAM = exports.SCOPE_ANY = void 0;
|
|
4
4
|
exports.SCOPE_ANY = "any";
|
|
5
5
|
exports.SCOPE_TEAM = "team";
|
|
6
|
+
exports.SCOPE_OWN = "own";
|
|
6
7
|
const anyOrTeam = (action) => [
|
|
7
8
|
`${action}:${exports.SCOPE_ANY}`,
|
|
8
9
|
`${action}:${exports.SCOPE_TEAM}`,
|
|
9
10
|
];
|
|
10
11
|
exports.anyOrTeam = anyOrTeam;
|
|
12
|
+
const anyOrOwn = (action) => [
|
|
13
|
+
`${action}:${exports.SCOPE_ANY}`,
|
|
14
|
+
`${action}:${exports.SCOPE_OWN}`,
|
|
15
|
+
];
|
|
16
|
+
exports.anyOrOwn = anyOrOwn;
|
|
@@ -57,6 +57,9 @@ export declare const envSchema: z.ZodObject<{
|
|
|
57
57
|
PASSWORD_RESET_TOKEN_EXPIRY: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
58
58
|
FIRST_ACCESS_TOKEN_EXPIRY: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
59
59
|
PASSWORD_MAX_AGE_DAYS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
60
|
+
REDIS_HOST: z.ZodOptional<z.ZodString>;
|
|
61
|
+
REDIS_PORT: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
62
|
+
REDIS_PASSWORD: z.ZodOptional<z.ZodString>;
|
|
60
63
|
TOKEN_CLEANUP_CRON: z.ZodDefault<z.ZodString>;
|
|
61
64
|
AUDIT_SLOW_REQUEST_MS: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
62
65
|
AUDIT_IP_SALT: z.ZodOptional<z.ZodString>;
|
|
@@ -68,6 +68,9 @@ exports.envSchema = zod_1.z
|
|
|
68
68
|
.positive()
|
|
69
69
|
.default(1440),
|
|
70
70
|
PASSWORD_MAX_AGE_DAYS: zod_1.z.coerce.number().int().positive().default(90),
|
|
71
|
+
REDIS_HOST: zod_1.z.string().min(1).optional(),
|
|
72
|
+
REDIS_PORT: zod_1.z.coerce.number().int().positive().default(6379),
|
|
73
|
+
REDIS_PASSWORD: zod_1.z.string().optional(),
|
|
71
74
|
TOKEN_CLEANUP_CRON: zod_1.z.string().default("0 */6 * * *"),
|
|
72
75
|
AUDIT_SLOW_REQUEST_MS: zod_1.z.coerce.number().int().positive().default(1000),
|
|
73
76
|
AUDIT_IP_SALT: zod_1.z.string().optional(),
|
package/dist/core/core.module.js
CHANGED
|
@@ -21,6 +21,7 @@ const config_module_1 = require("./config/config.module");
|
|
|
21
21
|
const env_service_1 = require("./config/env.service");
|
|
22
22
|
const health_controller_1 = require("./health/health.controller");
|
|
23
23
|
const mailer_module_1 = require("./mailer/mailer.module");
|
|
24
|
+
const queue_module_1 = require("./queue/queue.module");
|
|
24
25
|
let CoreModule = CoreModule_1 = class CoreModule {
|
|
25
26
|
static forRoot(options) {
|
|
26
27
|
return {
|
|
@@ -28,6 +29,7 @@ let CoreModule = CoreModule_1 = class CoreModule {
|
|
|
28
29
|
imports: [
|
|
29
30
|
config_module_1.ConfigModule.forRoot({ validateEnv: options.validateEnv }),
|
|
30
31
|
mailer_module_1.MailerModule,
|
|
32
|
+
queue_module_1.QueueModule,
|
|
31
33
|
schedule_1.ScheduleModule.forRoot(),
|
|
32
34
|
event_emitter_1.EventEmitterModule.forRoot(),
|
|
33
35
|
typeorm_1.TypeOrmModule.forRootAsync({
|
|
@@ -63,7 +65,7 @@ let CoreModule = CoreModule_1 = class CoreModule {
|
|
|
63
65
|
{ provide: core_1.APP_GUARD, useClass: csrf_guard_1.CsrfGuard },
|
|
64
66
|
{ provide: core_1.APP_FILTER, useClass: allExceptions_filter_1.AllExceptionsFilter },
|
|
65
67
|
],
|
|
66
|
-
exports: [mailer_module_1.MailerModule],
|
|
68
|
+
exports: [mailer_module_1.MailerModule, queue_module_1.QueueModule],
|
|
67
69
|
};
|
|
68
70
|
}
|
|
69
71
|
};
|
|
@@ -133,11 +133,18 @@ class InitialSchema1710000000000 {
|
|
|
133
133
|
description varchar(255) NULL,
|
|
134
134
|
type varchar(50) NOT NULL DEFAULT 'team',
|
|
135
135
|
parent_id char(36) NULL,
|
|
136
|
+
manager_id char(36) NULL,
|
|
136
137
|
is_active tinyint NOT NULL DEFAULT 1,
|
|
137
138
|
${ts},
|
|
138
139
|
PRIMARY KEY (id),
|
|
139
140
|
KEY IDX_groups_parent (parent_id),
|
|
140
|
-
|
|
141
|
+
KEY IDX_groups_manager (manager_id),
|
|
142
|
+
CONSTRAINT FK_groups_parent FOREIGN KEY (parent_id) REFERENCES \`groups\` (id),
|
|
143
|
+
-- Quem manda no grupo, base do escopo \`team\`. \`ON DELETE SET NULL\` de
|
|
144
|
+
-- propósito: apagar a pessoa não pode apagar o grupo junto — o time
|
|
145
|
+
-- continua existindo e fica sem gerente até alguém assumir.
|
|
146
|
+
CONSTRAINT FK_groups_manager FOREIGN KEY (manager_id)
|
|
147
|
+
REFERENCES users (id) ON DELETE SET NULL
|
|
141
148
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
|
142
149
|
`);
|
|
143
150
|
await queryRunner.query(`
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.coreMigrations = void 0;
|
|
4
4
|
const _1710000000000_InitialSchema_1 = require("./1710000000000-InitialSchema");
|
|
5
|
-
const _1787270400000_GroupManager_1 = require("./1787270400000-GroupManager");
|
|
6
5
|
exports.coreMigrations = [
|
|
7
6
|
_1710000000000_InitialSchema_1.InitialSchema1710000000000,
|
|
8
|
-
_1787270400000_GroupManager_1.GroupManager1787270400000,
|
|
9
7
|
];
|
|
@@ -1,2 +1,21 @@
|
|
|
1
1
|
import { DataSource } from "typeorm";
|
|
2
|
-
|
|
2
|
+
interface CatalogEntry {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
}
|
|
6
|
+
export interface RbacCatalogExtension {
|
|
7
|
+
resources?: CatalogEntry[];
|
|
8
|
+
actions?: CatalogEntry[];
|
|
9
|
+
scopes?: CatalogEntry[];
|
|
10
|
+
permissions?: {
|
|
11
|
+
code: string;
|
|
12
|
+
description: string;
|
|
13
|
+
}[];
|
|
14
|
+
roles?: {
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
permissions: string[] | "*";
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
export declare function seedPermissions(ds: DataSource, extension?: RbacCatalogExtension): Promise<void>;
|
|
21
|
+
export {};
|
|
@@ -7,47 +7,40 @@ const permission_schema_1 = require("../../../features/rbac/infra/schema/permiss
|
|
|
7
7
|
const resource_schema_1 = require("../../../features/rbac/infra/schema/resource.schema");
|
|
8
8
|
const role_schema_1 = require("../../../features/rbac/infra/schema/role.schema");
|
|
9
9
|
const scope_schema_1 = require("../../../features/rbac/infra/schema/scope.schema");
|
|
10
|
-
async function
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const roleRepo = ds.getRepository(role_schema_1.Role);
|
|
16
|
-
const resources = new Map();
|
|
17
|
-
for (const r of rbac_catalog_1.RESOURCES) {
|
|
18
|
-
let entity = await resourceRepo.findOne({
|
|
19
|
-
where: { name: r.name },
|
|
20
|
-
withDeleted: true,
|
|
21
|
-
});
|
|
22
|
-
if (!entity) {
|
|
23
|
-
entity = await resourceRepo.save(resourceRepo.create(r));
|
|
24
|
-
}
|
|
25
|
-
resources.set(r.name, entity);
|
|
26
|
-
}
|
|
27
|
-
const actions = new Map();
|
|
28
|
-
for (const a of rbac_catalog_1.ACTIONS) {
|
|
29
|
-
let entity = await actionRepo.findOne({
|
|
30
|
-
where: { name: a.name },
|
|
10
|
+
async function seedCatalog(repo, entries) {
|
|
11
|
+
const byName = new Map();
|
|
12
|
+
for (const entry of entries) {
|
|
13
|
+
let row = await repo.findOne({
|
|
14
|
+
where: { name: entry.name },
|
|
31
15
|
withDeleted: true,
|
|
32
16
|
});
|
|
33
|
-
if (!
|
|
34
|
-
|
|
17
|
+
if (!row) {
|
|
18
|
+
row = await repo.save(repo.create(entry));
|
|
35
19
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for (const s of rbac_catalog_1.SCOPES) {
|
|
40
|
-
let entity = await scopeRepo.findOne({
|
|
41
|
-
where: { name: s.name },
|
|
42
|
-
withDeleted: true,
|
|
43
|
-
});
|
|
44
|
-
if (!entity) {
|
|
45
|
-
entity = await scopeRepo.save(scopeRepo.create(s));
|
|
20
|
+
else if (row.description !== entry.description) {
|
|
21
|
+
row.description = entry.description;
|
|
22
|
+
row = await repo.save(row);
|
|
46
23
|
}
|
|
47
|
-
|
|
24
|
+
byName.set(entry.name, row);
|
|
48
25
|
}
|
|
26
|
+
return byName;
|
|
27
|
+
}
|
|
28
|
+
async function seedPermissions(ds, extension = {}) {
|
|
29
|
+
const allResources = [...rbac_catalog_1.RESOURCES, ...(extension.resources ?? [])];
|
|
30
|
+
const allActions = [...rbac_catalog_1.ACTIONS, ...(extension.actions ?? [])];
|
|
31
|
+
const allScopes = [...rbac_catalog_1.SCOPES, ...(extension.scopes ?? [])];
|
|
32
|
+
const allPermissions = [...rbac_catalog_1.PERMISSIONS, ...(extension.permissions ?? [])];
|
|
33
|
+
const allRoles = [...rbac_catalog_1.ROLES, ...(extension.roles ?? [])];
|
|
34
|
+
const resourceRepo = ds.getRepository(resource_schema_1.Resource);
|
|
35
|
+
const actionRepo = ds.getRepository(action_schema_1.Action);
|
|
36
|
+
const scopeRepo = ds.getRepository(scope_schema_1.Scope);
|
|
37
|
+
const permissionRepo = ds.getRepository(permission_schema_1.Permission);
|
|
38
|
+
const roleRepo = ds.getRepository(role_schema_1.Role);
|
|
39
|
+
const resources = await seedCatalog(resourceRepo, allResources);
|
|
40
|
+
const actions = await seedCatalog(actionRepo, allActions);
|
|
41
|
+
const scopes = await seedCatalog(scopeRepo, allScopes);
|
|
49
42
|
const permissions = new Map();
|
|
50
|
-
for (const p of
|
|
43
|
+
for (const p of allPermissions) {
|
|
51
44
|
let entity = await permissionRepo.findOne({
|
|
52
45
|
where: { code: p.code },
|
|
53
46
|
withDeleted: true,
|
|
@@ -70,7 +63,7 @@ async function seedPermissions(ds) {
|
|
|
70
63
|
}
|
|
71
64
|
permissions.set(p.code, entity);
|
|
72
65
|
}
|
|
73
|
-
for (const r of
|
|
66
|
+
for (const r of allRoles) {
|
|
74
67
|
let role = await roleRepo.findOne({
|
|
75
68
|
where: { name: r.name },
|
|
76
69
|
relations: { permissions: true },
|
|
@@ -83,11 +76,11 @@ async function seedPermissions(ds) {
|
|
|
83
76
|
permissions: [],
|
|
84
77
|
});
|
|
85
78
|
}
|
|
86
|
-
const codes = r.permissions === "*" ?
|
|
79
|
+
const codes = r.permissions === "*" ? allPermissions.map((p) => p.code) : r.permissions;
|
|
87
80
|
role.permissions = codes
|
|
88
81
|
.map((code) => permissions.get(code))
|
|
89
82
|
.filter(Boolean);
|
|
90
83
|
await roleRepo.save(role);
|
|
91
84
|
}
|
|
92
|
-
console.log(`[seed] RBAC: ${resources.size} resources, ${actions.size} actions, ${scopes.size} scopes, ${permissions.size} permissions, ${
|
|
85
|
+
console.log(`[seed] RBAC: ${resources.size} resources, ${actions.size} actions, ${scopes.size} scopes, ${permissions.size} permissions, ${allRoles.length} roles`);
|
|
93
86
|
}
|
|
@@ -2,6 +2,7 @@ export declare enum AppEvent {
|
|
|
2
2
|
USER_LOCKED = "user.locked",
|
|
3
3
|
USER_TWO_FACTOR_RESET = "user.two-factor-reset",
|
|
4
4
|
USER_ROLES_CHANGED = "user.roles-changed",
|
|
5
|
+
USER_PERMISSIONS_CHANGED = "user.permissions-changed",
|
|
5
6
|
USER_PASSWORD_RESET_REQUESTED = "user.password-reset-requested",
|
|
6
7
|
USER_PASSWORD_EXPIRING = "user.password-expiring",
|
|
7
8
|
SYSTEM_CLEANUP_FAILED = "system.cleanup-failed",
|
|
@@ -20,6 +21,11 @@ export interface UserRolesChangedEvent {
|
|
|
20
21
|
roleNames: string[];
|
|
21
22
|
previousRoleNames: string[];
|
|
22
23
|
}
|
|
24
|
+
export interface UserPermissionsChangedEvent {
|
|
25
|
+
userId: string;
|
|
26
|
+
codes: string[];
|
|
27
|
+
previousCodes: string[];
|
|
28
|
+
}
|
|
23
29
|
export interface UserPasswordResetRequestedEvent {
|
|
24
30
|
userId: string;
|
|
25
31
|
}
|
|
@@ -6,6 +6,7 @@ var AppEvent;
|
|
|
6
6
|
AppEvent["USER_LOCKED"] = "user.locked";
|
|
7
7
|
AppEvent["USER_TWO_FACTOR_RESET"] = "user.two-factor-reset";
|
|
8
8
|
AppEvent["USER_ROLES_CHANGED"] = "user.roles-changed";
|
|
9
|
+
AppEvent["USER_PERMISSIONS_CHANGED"] = "user.permissions-changed";
|
|
9
10
|
AppEvent["USER_PASSWORD_RESET_REQUESTED"] = "user.password-reset-requested";
|
|
10
11
|
AppEvent["USER_PASSWORD_EXPIRING"] = "user.password-expiring";
|
|
11
12
|
AppEvent["SYSTEM_CLEANUP_FAILED"] = "system.cleanup-failed";
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { DataSource } from "typeorm";
|
|
2
|
+
import { JobProducer } from "../queue/job.producer";
|
|
2
3
|
export declare class HealthController {
|
|
3
4
|
private readonly ds;
|
|
5
|
+
private readonly jobs;
|
|
4
6
|
private readonly logger;
|
|
5
|
-
constructor(ds: DataSource);
|
|
7
|
+
constructor(ds: DataSource, jobs: JobProducer);
|
|
6
8
|
check(): Promise<{
|
|
7
9
|
status: string;
|
|
8
10
|
database: string;
|
|
11
|
+
queue: string;
|
|
9
12
|
timestamp: string;
|
|
10
13
|
}>;
|
|
11
14
|
}
|
|
@@ -19,9 +19,11 @@ const swagger_1 = require("@nestjs/swagger");
|
|
|
19
19
|
const typeorm_1 = require("@nestjs/typeorm");
|
|
20
20
|
const typeorm_2 = require("typeorm");
|
|
21
21
|
const public_decorator_1 = require("../auth/decorators/public.decorator");
|
|
22
|
+
const job_producer_1 = require("../queue/job.producer");
|
|
22
23
|
let HealthController = HealthController_1 = class HealthController {
|
|
23
|
-
constructor(ds) {
|
|
24
|
+
constructor(ds, jobs) {
|
|
24
25
|
this.ds = ds;
|
|
26
|
+
this.jobs = jobs;
|
|
25
27
|
this.logger = new common_1.Logger(HealthController_1.name);
|
|
26
28
|
}
|
|
27
29
|
async check() {
|
|
@@ -33,9 +35,12 @@ let HealthController = HealthController_1 = class HealthController {
|
|
|
33
35
|
this.logger.error(`Healthcheck do banco falhou: ${err.message}`);
|
|
34
36
|
database = "down";
|
|
35
37
|
}
|
|
38
|
+
const queue = await this.jobs.redisStatus();
|
|
39
|
+
const healthy = database === "up" && queue !== "down";
|
|
36
40
|
return {
|
|
37
|
-
status:
|
|
41
|
+
status: healthy ? "ok" : "degraded",
|
|
38
42
|
database,
|
|
43
|
+
queue,
|
|
39
44
|
timestamp: new Date().toISOString(),
|
|
40
45
|
};
|
|
41
46
|
}
|
|
@@ -52,5 +57,6 @@ exports.HealthController = HealthController = HealthController_1 = __decorate([
|
|
|
52
57
|
(0, swagger_1.ApiTags)("Health"),
|
|
53
58
|
(0, common_1.Controller)({ path: "health", version: "1" }),
|
|
54
59
|
__param(0, (0, typeorm_1.InjectDataSource)()),
|
|
55
|
-
__metadata("design:paramtypes", [typeorm_2.DataSource
|
|
60
|
+
__metadata("design:paramtypes", [typeorm_2.DataSource,
|
|
61
|
+
job_producer_1.JobProducer])
|
|
56
62
|
], HealthController);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ObjectLiteral, SelectQueryBuilder } from "typeorm";
|
|
2
|
+
import { OwnershipScope } from "../auth/ownershipScope.util";
|
|
3
|
+
export interface OwnershipScopeOptions {
|
|
4
|
+
alias: string;
|
|
5
|
+
column?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const applyOwnershipScope: <T extends ObjectLiteral>(qb: SelectQueryBuilder<T>, scope: OwnershipScope, options: OwnershipScopeOptions) => SelectQueryBuilder<T>;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.applyOwnershipScope = void 0;
|
|
4
|
+
const applyOwnershipScope = (qb, scope, options) => {
|
|
5
|
+
const { alias, column = "ownerId" } = options;
|
|
6
|
+
if (scope.kind === "all") {
|
|
7
|
+
return qb;
|
|
8
|
+
}
|
|
9
|
+
if (scope.kind === "none") {
|
|
10
|
+
return qb.andWhere("1 = 0");
|
|
11
|
+
}
|
|
12
|
+
return qb.andWhere(`${alias}.${column} = :ownershipScopeUserId`, {
|
|
13
|
+
ownershipScopeUserId: scope.userId,
|
|
14
|
+
});
|
|
15
|
+
};
|
|
16
|
+
exports.applyOwnershipScope = applyOwnershipScope;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.BullConnectionModule = void 0;
|
|
10
|
+
const bullmq_1 = require("@nestjs/bullmq");
|
|
11
|
+
const common_1 = require("@nestjs/common");
|
|
12
|
+
const queue_constants_1 = require("./queue.constants");
|
|
13
|
+
const redisConnection_1 = require("./redisConnection");
|
|
14
|
+
let BullConnectionModule = class BullConnectionModule {
|
|
15
|
+
};
|
|
16
|
+
exports.BullConnectionModule = BullConnectionModule;
|
|
17
|
+
exports.BullConnectionModule = BullConnectionModule = __decorate([
|
|
18
|
+
(0, common_1.Module)({
|
|
19
|
+
imports: [
|
|
20
|
+
bullmq_1.BullModule.forRoot({
|
|
21
|
+
connection: (0, redisConnection_1.buildRedisOptions)(),
|
|
22
|
+
prefix: (0, queue_constants_1.queuePrefix)(),
|
|
23
|
+
}),
|
|
24
|
+
],
|
|
25
|
+
exports: [bullmq_1.BullModule],
|
|
26
|
+
})
|
|
27
|
+
], BullConnectionModule);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type JobState = "waiting" | "active" | "completed" | "failed" | "delayed" | "unknown";
|
|
2
|
+
export interface JobRef {
|
|
3
|
+
jobId: string;
|
|
4
|
+
name: string;
|
|
5
|
+
}
|
|
6
|
+
export interface JobProgress {
|
|
7
|
+
jobId: string;
|
|
8
|
+
name: string;
|
|
9
|
+
processed: number;
|
|
10
|
+
total: number;
|
|
11
|
+
percent: number;
|
|
12
|
+
message?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface JobCompleted<TSummary = unknown> {
|
|
15
|
+
jobId: string;
|
|
16
|
+
name: string;
|
|
17
|
+
summary: TSummary;
|
|
18
|
+
}
|
|
19
|
+
export interface JobFailed {
|
|
20
|
+
jobId: string;
|
|
21
|
+
name: string;
|
|
22
|
+
errorCode: string | null;
|
|
23
|
+
message: string;
|
|
24
|
+
}
|
|
25
|
+
export interface JobStatus<TSummary = unknown> {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
state: JobState;
|
|
29
|
+
progress: Omit<JobProgress, "jobId" | "name"> | null;
|
|
30
|
+
summary: TSummary | null;
|
|
31
|
+
error: {
|
|
32
|
+
code: string | null;
|
|
33
|
+
message: string;
|
|
34
|
+
} | null;
|
|
35
|
+
createdAt: string;
|
|
36
|
+
finishedAt: string | null;
|
|
37
|
+
}
|
|
38
|
+
export interface EnqueueOptions {
|
|
39
|
+
ownerId: string;
|
|
40
|
+
jobId?: string;
|
|
41
|
+
attempts?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface QueueJobSummary {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
state: JobState;
|
|
47
|
+
ownerId: string | null;
|
|
48
|
+
attemptsMade: number;
|
|
49
|
+
percent: number | null;
|
|
50
|
+
failedReason: string | null;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
finishedAt: string | null;
|
|
53
|
+
}
|
|
54
|
+
export interface QueueCounts {
|
|
55
|
+
waiting: number;
|
|
56
|
+
active: number;
|
|
57
|
+
completed: number;
|
|
58
|
+
failed: number;
|
|
59
|
+
delayed: number;
|
|
60
|
+
}
|
|
61
|
+
export interface JobContext<TPayload = unknown> {
|
|
62
|
+
jobId: string;
|
|
63
|
+
ownerId: string;
|
|
64
|
+
payload: TPayload;
|
|
65
|
+
report: (processed: number, total: number, message?: string) => Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
export interface JobEnvelope<TPayload = unknown> {
|
|
68
|
+
ownerId: string;
|
|
69
|
+
payload: TPayload;
|
|
70
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { OnApplicationBootstrap } from "@nestjs/common";
|
|
2
|
+
import { Queue } from "bullmq";
|
|
3
|
+
import { EnqueueOptions, JobEnvelope, JobRef, JobStatus } from "./job.interface";
|
|
4
|
+
export declare class JobProducer implements OnApplicationBootstrap {
|
|
5
|
+
private readonly queue?;
|
|
6
|
+
private readonly logger;
|
|
7
|
+
constructor(queue?: Queue<JobEnvelope> | undefined);
|
|
8
|
+
onApplicationBootstrap(): Promise<void>;
|
|
9
|
+
isAvailable(): boolean;
|
|
10
|
+
enqueue<TPayload>(name: string, payload: TPayload, options: EnqueueOptions): Promise<JobRef>;
|
|
11
|
+
status<TSummary>(jobId: string, ownerId: string): Promise<JobStatus<TSummary>>;
|
|
12
|
+
redisStatus(): Promise<"up" | "down" | "off">;
|
|
13
|
+
requireQueue(): Queue<JobEnvelope>;
|
|
14
|
+
private assertRedisReachable;
|
|
15
|
+
}
|