Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,46 @@ Breaking changes and upgrade notes for downstream projects.

---

## Audit route→type map is now config-driven (2026-04-07)

The hardcoded `route→type` map in `audit.middleware.js` has been removed. Each module now declares its own mapping via `audit.routeTypeMap` in its module config.

### Rationale

The previous hardcoded map forced optional modules (tasks, billing) to appear in core audit middleware — a violation of module isolation. Moving the map to config means each module owns its audit-type mapping, reducing coupling and keeping cross-module dependencies explicit. New modules can add their own mapping without modifying core code.

### What changed

- `modules/audit/middlewares/audit.middleware.js` — `deriveTargetType` reads `config.audit.routeTypeMap` instead of a hardcoded object
- `modules/audit/config/audit.development.config.js` — added empty `routeTypeMap: {}` base
- `modules/auth/config/auth.development.config.js` — added `audit.routeTypeMap: { auth: 'User' }`
- `modules/users/config/users.development.config.js` — added `audit.routeTypeMap: { users: 'User' }`
- `modules/billing/config/billing.development.config.js` — added `audit.routeTypeMap: { billing: 'Organization' }`
- `modules/organizations/config/organizations.development.config.js` — added `audit.routeTypeMap: { organizations: 'Organization' }`
- `modules/tasks/config/tasks.development.config.js` — added `audit.routeTypeMap: { tasks: 'Task' }`

### Action for downstream

1. Run `/update-stack` to pull the change
2. If your project has custom modules that need audit-type labelling, add `audit.routeTypeMap` to the module's development config:

```js
// modules/payments/config/payments.development.config.js
const config = {
audit: {
routeTypeMap: {
payments: 'Payment',
},
},
// ... rest of module config
};
export default config;
```

3. If no `routeTypeMap` entry exists for a route segment, the segment is capitalised as a fallback (same behaviour as before for unknown segments)

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
---

## Per-module project config overrides (2026-04-07)

The config loader now supports per-module project config files in addition to the existing global `config/defaults/{project}.config.js`.
Expand Down
2 changes: 2 additions & 0 deletions modules/audit/config/audit.development.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const config = {
ttlDays: 90,
captureIp: true,
captureUserAgent: true,
// Route segment → entity type map (merged from all module configs)
routeTypeMap: {},
},
};

Expand Down
11 changes: 3 additions & 8 deletions modules/audit/middlewares/audit.middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ const deriveAction = (routePath, baseUrl) => {

/**
* Derive the target type from the route path (first meaningful segment).
* Maps common path segments to model names.
* Reads the route→type map from config (audit.routeTypeMap) so that each
* module declares its own mapping without touching audit middleware code.
* @param {string} routePath - The Express matched route path
* @param {string} baseUrl - The Express baseUrl
* @returns {string} The target type (capitalized) or empty string
Expand All @@ -56,13 +57,7 @@ const deriveTargetType = (routePath, baseUrl) => {
if (segments.length === 0) return '';

const segment = segments[0];
const map = {
auth: 'User',
users: 'User',
billing: 'Organization',
organizations: 'Organization',
tasks: 'Task',
};
const map = config.audit?.routeTypeMap || {};
return map[segment] || segment.charAt(0).toUpperCase() + segment.slice(1);
};

Expand Down
26 changes: 25 additions & 1 deletion modules/audit/tests/audit.middleware.unit.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,24 @@ jest.unstable_mockModule('../../../lib/services/logger.js', () => ({
default: { error: mockLoggerError, warn: jest.fn(), info: jest.fn() },
}));

// Mock config with a routeTypeMap that mirrors module configs
jest.unstable_mockModule('../../../config/index.js', () => ({
default: {
audit: {
enabled: true,
captureIp: true,
captureUserAgent: true,
routeTypeMap: {
auth: 'User',
users: 'User',
billing: 'Organization',
organizations: 'Organization',
tasks: 'Task',
},
},
},
}));

/**
* Unit tests for audit middleware
*/
Expand Down Expand Up @@ -208,11 +226,17 @@ describe('Audit middleware unit tests:', () => {
expect(deriveAction('/:token', '/api/auth/password/reset')).toBe('auth.reset');
});

test('should derive targetType from route', () => {
test('should derive targetType from config routeTypeMap', () => {
expect(deriveTargetType('/signin', '/api/auth')).toBe('User');
expect(deriveTargetType('/checkout', '/api/billing')).toBe('Organization');
expect(deriveTargetType('/:orgId', '/api/organizations')).toBe('Organization');
expect(deriveTargetType('/', '/api/tasks')).toBe('Task');
expect(deriveTargetType('/', '/api/users')).toBe('User');
});

test('should capitalise unknown segments not in routeTypeMap', () => {
expect(deriveTargetType('/list', '/api/uploads')).toBe('Uploads');
expect(deriveTargetType('/', '/api/custom')).toBe('Custom');
});

test('should derive targetId from params', () => {
Expand Down
5 changes: 5 additions & 0 deletions modules/auth/config/auth.development.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
const config = {
audit: {
routeTypeMap: {
auth: 'User',
},
},
auth: {
lockout: {
maxAttempts: 5, // lock account after N consecutive failed login attempts
Expand Down
5 changes: 5 additions & 0 deletions modules/billing/config/billing.development.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
const config = {
audit: {
routeTypeMap: {
billing: 'Organization',
},
},
billing: {
activated: true,
// Plans available for subscriptions — extend as needed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
const config = {
audit: {
routeTypeMap: {
organizations: 'Organization',
},
},
organizations: {
activated: true,
enabled: true, // false → B2C mode, organizations invisible
Expand Down
5 changes: 5 additions & 0 deletions modules/tasks/config/tasks.development.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
const config = {
audit: {
routeTypeMap: {
tasks: 'Task',
},
},
tasks: {
activated: true,
},
Expand Down
5 changes: 5 additions & 0 deletions modules/users/config/users.development.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
const config = {
audit: {
routeTypeMap: {
users: 'User',
},
},
// Data filter whitelist & Blacklist
blacklists: {},
whitelists: {
Expand Down
Loading