diff --git a/src/plugin.ts b/src/plugin.ts index ff097a4..02de0bf 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -23,17 +23,26 @@ export interface OrgEvent { export type OrgEventSink = (event: OrgEvent) => void; +export interface RoleEvent { + action: 'created' | 'updated' | 'deleted'; + role: { id: string; org_id?: string; name?: string; description?: string | null }; + actorId: string | null; +} + +export type RoleEventSink = (event: RoleEvent) => void; + export interface MgmtPluginOptions { db: Pool; checkLimit?: CheckLimitFn; emitOrgEvent?: OrgEventSink; + emitRoleEvent?: RoleEventSink; } export function createMgmtRoutes(opts: MgmtPluginOptions) { - const { db, checkLimit, emitOrgEvent } = opts; + const { db, checkLimit, emitOrgEvent, emitRoleEvent } = opts; return async function mgmtRoutes(app: FastifyInstance) { await app.register(createOrgRoutes(db, checkLimit, emitOrgEvent)); - await app.register(createRoleRoutes(db, checkLimit)); + await app.register(createRoleRoutes(db, checkLimit, emitRoleEvent)); await app.register(createRuleRoutes(db)); await app.register(createUserRoutes(db, checkLimit)); await app.register(createGlobalRoutes(db)); diff --git a/src/routes/roles.ts b/src/routes/roles.ts index 0972402..92f6c0f 100644 --- a/src/routes/roles.ts +++ b/src/routes/roles.ts @@ -1,12 +1,12 @@ import type { FastifyInstance } from 'fastify'; import type { Pool } from 'pg'; -import type { CheckLimitFn } from '../plugin.js'; +import type { CheckLimitFn, RoleEventSink } from '../plugin.js'; import { getUser, getOrgScope, orgScopeExpr, hasPermission, SYSTEM_ORG_ID, } from '../permissions.js'; import { roleResponse, notFound, forbidden, unauthorized, badRequest } from '../schemas.js'; -export function createRoleRoutes(db: Pool, checkLimit?: CheckLimitFn) { +export function createRoleRoutes(db: Pool, checkLimit?: CheckLimitFn, emitRoleEvent?: RoleEventSink) { return async function roleRoutes(app: FastifyInstance) { app.get('/roles', { schema: { @@ -80,6 +80,7 @@ export function createRoleRoutes(db: Pool, checkLimit?: CheckLimitFn) { [org_id, name, description ?? null], ); const org = await db.query('SELECT name FROM morbac.orgs WHERE id = $1', [org_id]); + emitRoleEvent?.({ action: 'created', role: rows[0], actorId: user.id }); return reply.code(201).send({ ...rows[0], org_name: org.rows[0]?.name }); }); @@ -125,6 +126,7 @@ export function createRoleRoutes(db: Pool, checkLimit?: CheckLimitFn) { [name ?? null, description ?? null, id], ); if (!rows[0]) return reply.code(404).send({ error: 'Not found' }); + emitRoleEvent?.({ action: 'updated', role: rows[0], actorId: user.id }); return rows[0]; }); @@ -153,6 +155,7 @@ export function createRoleRoutes(db: Pool, checkLimit?: CheckLimitFn) { if (!await hasPermission(db, req, reply, roleOrg.rows[0].org_id, 'delete', 'roles')) return reply; await db.query('DELETE FROM morbac.roles WHERE id = $1', [id]); + emitRoleEvent?.({ action: 'deleted', role: { id, org_id: roleOrg.rows[0].org_id }, actorId: user.id }); return reply.code(204).send(); });