@mostajs/audit 2.0.3 → 2.1.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/dist/index.d.ts +5 -0
- package/dist/index.js +7 -0
- package/dist/lib/audit-factory.js +4 -0
- package/dist/lib/audit-stats.d.ts +15 -0
- package/dist/lib/audit-stats.js +33 -0
- package/dist/lib/export.d.ts +10 -0
- package/dist/lib/export.js +26 -0
- package/dist/lib/handlers/audit-export.d.ts +1 -0
- package/dist/lib/handlers/audit-export.js +47 -0
- package/dist/test-scripts/test-unit.d.ts +1 -0
- package/dist/test-scripts/test-unit.js +75 -0
- package/package.json +16 -1
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,9 @@ export { createAuditHandlers } from './api/route';
|
|
|
5
5
|
export { auditMenuContribution } from './lib/menu';
|
|
6
6
|
export { default as AuditPage } from './pages/AuditPage';
|
|
7
7
|
export { t as auditT } from './lib/i18n';
|
|
8
|
+
export { auditToCsv, auditToJson } from './lib/export';
|
|
9
|
+
export type { ExportOptions } from './lib/export';
|
|
10
|
+
export { createAuditExportHandler } from './lib/handlers/audit-export';
|
|
11
|
+
export { getAuditStats } from './lib/audit-stats';
|
|
12
|
+
export type { AuditStats } from './lib/audit-stats';
|
|
8
13
|
export type { MostaAuditConfig, AuditParams, AuditFilters, AuditLogDTO, } from './types/index';
|
package/dist/index.js
CHANGED
|
@@ -13,3 +13,10 @@ export { auditMenuContribution } from './lib/menu';
|
|
|
13
13
|
export { default as AuditPage } from './pages/AuditPage';
|
|
14
14
|
// I18n
|
|
15
15
|
export { t as auditT } from './lib/i18n';
|
|
16
|
+
// Cloud modules
|
|
17
|
+
// Export
|
|
18
|
+
export { auditToCsv, auditToJson } from './lib/export';
|
|
19
|
+
// Export handler
|
|
20
|
+
export { createAuditExportHandler } from './lib/handlers/audit-export';
|
|
21
|
+
// Audit stats
|
|
22
|
+
export { getAuditStats } from './lib/audit-stats';
|
|
@@ -15,6 +15,10 @@ export async function getAuditRepo() {
|
|
|
15
15
|
const { AuditLogRepository } = await import('../repositories/audit-log.repository.js');
|
|
16
16
|
registerSchemas([AuditLogSchema]);
|
|
17
17
|
const dialect = await getDialect();
|
|
18
|
+
if (typeof dialect.initSchema === 'function') {
|
|
19
|
+
const { getAllSchemas } = await import('@mostajs/orm');
|
|
20
|
+
await dialect.initSchema(getAllSchemas());
|
|
21
|
+
}
|
|
18
22
|
_cached = new AuditLogRepository(dialect);
|
|
19
23
|
return _cached;
|
|
20
24
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AuditStats {
|
|
2
|
+
totalLogs: number;
|
|
3
|
+
byModule: Record<string, number>;
|
|
4
|
+
byAction: Record<string, number>;
|
|
5
|
+
byStatus: {
|
|
6
|
+
success: number;
|
|
7
|
+
failure: number;
|
|
8
|
+
};
|
|
9
|
+
topUsers: {
|
|
10
|
+
userName: string;
|
|
11
|
+
count: number;
|
|
12
|
+
}[];
|
|
13
|
+
recentErrors: any[];
|
|
14
|
+
}
|
|
15
|
+
export declare function getAuditStats(repo: any, days?: number): Promise<AuditStats>;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// @mostajs/audit — Audit statistics helpers
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
export async function getAuditStats(repo, days = 30) {
|
|
4
|
+
const cutoff = new Date(Date.now() - days * 86400000);
|
|
5
|
+
// Get all logs in period
|
|
6
|
+
const { data, total } = await repo.findPaginated({
|
|
7
|
+
page: 1,
|
|
8
|
+
limit: 10000,
|
|
9
|
+
dateFrom: cutoff.toISOString(),
|
|
10
|
+
});
|
|
11
|
+
const byModule = {};
|
|
12
|
+
const byAction = {};
|
|
13
|
+
const byStatus = { success: 0, failure: 0 };
|
|
14
|
+
const userCounts = {};
|
|
15
|
+
for (const log of data) {
|
|
16
|
+
byModule[log.module] = (byModule[log.module] ?? 0) + 1;
|
|
17
|
+
byAction[log.action] = (byAction[log.action] ?? 0) + 1;
|
|
18
|
+
if (log.status === 'success')
|
|
19
|
+
byStatus.success++;
|
|
20
|
+
else
|
|
21
|
+
byStatus.failure++;
|
|
22
|
+
if (log.userName)
|
|
23
|
+
userCounts[log.userName] = (userCounts[log.userName] ?? 0) + 1;
|
|
24
|
+
}
|
|
25
|
+
const topUsers = Object.entries(userCounts)
|
|
26
|
+
.sort((a, b) => b[1] - a[1])
|
|
27
|
+
.slice(0, 10)
|
|
28
|
+
.map(([userName, count]) => ({ userName, count }));
|
|
29
|
+
const recentErrors = data
|
|
30
|
+
.filter((l) => l.status === 'failure')
|
|
31
|
+
.slice(0, 20);
|
|
32
|
+
return { totalLogs: total, byModule, byAction, byStatus, topUsers, recentErrors };
|
|
33
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface ExportOptions {
|
|
2
|
+
format: 'csv' | 'json';
|
|
3
|
+
from?: string;
|
|
4
|
+
to?: string;
|
|
5
|
+
module?: string;
|
|
6
|
+
action?: string;
|
|
7
|
+
limit?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function auditToCsv(logs: any[]): string;
|
|
10
|
+
export declare function auditToJson(logs: any[]): string;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// @mostajs/audit — Export audit logs to CSV/JSON
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
export function auditToCsv(logs) {
|
|
4
|
+
if (logs.length === 0)
|
|
5
|
+
return '';
|
|
6
|
+
const headers = ['timestamp', 'userName', 'userRole', 'module', 'action', 'resource', 'resourceId', 'status', 'ipAddress', 'details'];
|
|
7
|
+
const rows = logs.map(log => {
|
|
8
|
+
return headers.map(h => {
|
|
9
|
+
let val = log[h] ?? '';
|
|
10
|
+
if (h === 'details' && typeof val === 'object')
|
|
11
|
+
val = JSON.stringify(val);
|
|
12
|
+
if (h === 'timestamp' && val instanceof Date)
|
|
13
|
+
val = val.toISOString();
|
|
14
|
+
// Escape CSV
|
|
15
|
+
const str = String(val);
|
|
16
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n')) {
|
|
17
|
+
return '"' + str.replace(/"/g, '""') + '"';
|
|
18
|
+
}
|
|
19
|
+
return str;
|
|
20
|
+
}).join(',');
|
|
21
|
+
});
|
|
22
|
+
return [headers.join(','), ...rows].join('\n');
|
|
23
|
+
}
|
|
24
|
+
export function auditToJson(logs) {
|
|
25
|
+
return JSON.stringify(logs, null, 2);
|
|
26
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function createAuditExportHandler(getRepo: () => any, checkPermission?: (perm: string) => Promise<any>): (req: Request) => Promise<Response>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// @mostajs/audit — Export handler
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
import { auditToCsv, auditToJson } from '../export.js';
|
|
4
|
+
export function createAuditExportHandler(getRepo, checkPermission) {
|
|
5
|
+
return async function GET(req) {
|
|
6
|
+
// Check permission if provided
|
|
7
|
+
if (checkPermission) {
|
|
8
|
+
const check = await checkPermission('audit:export');
|
|
9
|
+
if (check?.error)
|
|
10
|
+
return check.error;
|
|
11
|
+
}
|
|
12
|
+
const url = new URL(req.url);
|
|
13
|
+
const format = url.searchParams.get('format') ?? 'json';
|
|
14
|
+
const from = url.searchParams.get('from') ?? undefined;
|
|
15
|
+
const to = url.searchParams.get('to') ?? undefined;
|
|
16
|
+
const module = url.searchParams.get('module') ?? undefined;
|
|
17
|
+
const limit = parseInt(url.searchParams.get('limit') ?? '10000', 10);
|
|
18
|
+
const repo = getRepo();
|
|
19
|
+
const filter = {};
|
|
20
|
+
if (module)
|
|
21
|
+
filter.module = module;
|
|
22
|
+
if (from || to) {
|
|
23
|
+
filter.timestamp = {};
|
|
24
|
+
if (from)
|
|
25
|
+
filter.timestamp.$gte = new Date(from);
|
|
26
|
+
if (to)
|
|
27
|
+
filter.timestamp.$lte = new Date(to);
|
|
28
|
+
}
|
|
29
|
+
const { data } = await repo.findPaginated({ ...filter, page: 1, limit });
|
|
30
|
+
if (format === 'csv') {
|
|
31
|
+
const csv = auditToCsv(data);
|
|
32
|
+
return new Response(csv, {
|
|
33
|
+
headers: {
|
|
34
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
35
|
+
'Content-Disposition': `attachment; filename="audit-export-${new Date().toISOString().slice(0, 10)}.csv"`,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const json = auditToJson(data);
|
|
40
|
+
return new Response(json, {
|
|
41
|
+
headers: {
|
|
42
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
43
|
+
'Content-Disposition': `attachment; filename="audit-export-${new Date().toISOString().slice(0, 10)}.json"`,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// @mostajs/audit — Tests unitaires (no DB needed)
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
import { auditToCsv, auditToJson, } from '../lib/export.js';
|
|
4
|
+
let passed = 0;
|
|
5
|
+
let failed = 0;
|
|
6
|
+
function assert(condition, label) {
|
|
7
|
+
if (condition) {
|
|
8
|
+
passed++;
|
|
9
|
+
console.log(' ✅', label);
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
failed++;
|
|
13
|
+
console.error(' ❌', label);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function run() {
|
|
17
|
+
// ── T1 — CSV export ──
|
|
18
|
+
console.log('T1 — CSV export');
|
|
19
|
+
const sampleLogs = [
|
|
20
|
+
{
|
|
21
|
+
timestamp: '2026-04-01T10:00:00.000Z',
|
|
22
|
+
userName: 'admin', userRole: 'superadmin',
|
|
23
|
+
module: 'cloud', action: 'project_create',
|
|
24
|
+
resource: 'Project', resourceId: 'proj-1',
|
|
25
|
+
status: 'success', ipAddress: '192.168.1.1',
|
|
26
|
+
details: { name: 'MyProject' },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
timestamp: '2026-04-02T12:00:00.000Z',
|
|
30
|
+
userName: 'user1', userRole: 'admin',
|
|
31
|
+
module: 'billing', action: 'payment_success',
|
|
32
|
+
resource: 'Subscription', resourceId: 'sub-42',
|
|
33
|
+
status: 'success', ipAddress: '10.0.0.5',
|
|
34
|
+
details: null,
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
const csv = auditToCsv(sampleLogs);
|
|
38
|
+
const lines = csv.split('\n');
|
|
39
|
+
assert(lines.length === 3, 'CSV has 3 lines (header + 2 rows)');
|
|
40
|
+
assert(lines[0] === 'timestamp,userName,userRole,module,action,resource,resourceId,status,ipAddress,details', 'CSV header correct');
|
|
41
|
+
assert(lines[1].includes('admin'), 'row 1 contains admin');
|
|
42
|
+
assert(lines[2].includes('user1'), 'row 2 contains user1');
|
|
43
|
+
console.log('');
|
|
44
|
+
// ── T2 — JSON export ──
|
|
45
|
+
console.log('T2 — JSON export');
|
|
46
|
+
const json = auditToJson(sampleLogs);
|
|
47
|
+
const parsed = JSON.parse(json);
|
|
48
|
+
assert(Array.isArray(parsed), 'JSON output is array');
|
|
49
|
+
assert(parsed.length === 2, 'JSON has 2 entries');
|
|
50
|
+
assert(parsed[0].userName === 'admin', 'first entry userName = admin');
|
|
51
|
+
console.log('');
|
|
52
|
+
// ── T3 — CSV empty ──
|
|
53
|
+
console.log('T3 — CSV empty');
|
|
54
|
+
assert(auditToCsv([]) === '', 'empty array → empty string');
|
|
55
|
+
console.log('');
|
|
56
|
+
// ── T4 — CSV escaping ──
|
|
57
|
+
console.log('T4 — CSV escaping');
|
|
58
|
+
const logsWithComma = [{
|
|
59
|
+
timestamp: '2026-04-01T10:00:00.000Z',
|
|
60
|
+
userName: 'user, with comma', userRole: 'admin',
|
|
61
|
+
module: 'cloud', action: 'project_create',
|
|
62
|
+
resource: 'Project', resourceId: 'proj-1',
|
|
63
|
+
status: 'success', ipAddress: '192.168.1.1', details: null,
|
|
64
|
+
}];
|
|
65
|
+
const csvEscaped = auditToCsv(logsWithComma);
|
|
66
|
+
assert(csvEscaped.includes('"user, with comma"'), 'value with comma is quoted');
|
|
67
|
+
console.log('');
|
|
68
|
+
// ── Summary ──
|
|
69
|
+
console.log('════════════════════════════════════════');
|
|
70
|
+
console.log(` Resultats: ${passed} passed, ${failed} failed`);
|
|
71
|
+
console.log('════════════════════════════════════════');
|
|
72
|
+
if (failed > 0)
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
run().catch(e => { console.error('❌ Fatal:', e.message); process.exit(1); });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/audit",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Reusable audit logging module — fire-and-forget logAudit() with paginated consultation",
|
|
5
5
|
"author": "Dr Hamid MADANI <drmdh@msn.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,6 +52,21 @@
|
|
|
52
52
|
"types": "./dist/lib/module-info.d.ts",
|
|
53
53
|
"import": "./dist/lib/module-info.js",
|
|
54
54
|
"default": "./dist/lib/module-info.js"
|
|
55
|
+
},
|
|
56
|
+
"./lib/export": {
|
|
57
|
+
"types": "./dist/lib/export.d.ts",
|
|
58
|
+
"import": "./dist/lib/export.js",
|
|
59
|
+
"default": "./dist/lib/export.js"
|
|
60
|
+
},
|
|
61
|
+
"./lib/audit-stats": {
|
|
62
|
+
"types": "./dist/lib/audit-stats.d.ts",
|
|
63
|
+
"import": "./dist/lib/audit-stats.js",
|
|
64
|
+
"default": "./dist/lib/audit-stats.js"
|
|
65
|
+
},
|
|
66
|
+
"./lib/handlers/audit-export": {
|
|
67
|
+
"types": "./dist/lib/handlers/audit-export.d.ts",
|
|
68
|
+
"import": "./dist/lib/handlers/audit-export.js",
|
|
69
|
+
"default": "./dist/lib/handlers/audit-export.js"
|
|
55
70
|
}
|
|
56
71
|
},
|
|
57
72
|
"files": [
|