@mostajs/audit 2.0.2 → 2.0.3
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 +19 -626
- package/dist/lib/audit-factory.d.ts +1 -1
- package/dist/lib/audit-factory.js +10 -96
- package/dist/lib/audit-init.js +4 -14
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -1,654 +1,47 @@
|
|
|
1
1
|
# @mostajs/audit
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Fire-and-forget audit logging with paginated consultation.
|
|
4
|
+
> Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
[](LICENSE)
|
|
7
|
-
|
|
8
|
-
Part of the [@mosta suite](https://mostajs.dev). Depend de `@mostajs/orm` pour l'abstraction multi-dialecte (MongoDB, PostgreSQL, MySQL, SQLite, etc.).
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## Table des matieres
|
|
13
|
-
|
|
14
|
-
1. [Installation](#installation)
|
|
15
|
-
2. [Quick Start](#quick-start)
|
|
16
|
-
3. [Integration complete dans une nouvelle app](#integration-complete)
|
|
17
|
-
4. [logAudit — Journalisation fire-and-forget](#logaudit)
|
|
18
|
-
5. [getAuditUser — Extraction session](#getaudituser)
|
|
19
|
-
6. [AuditLogRepository — Requetes avancees](#auditlogrepository)
|
|
20
|
-
7. [createAuditHandlers — Route API factory](#createaudithandlers)
|
|
21
|
-
8. [Schema et indexes](#schema-et-indexes)
|
|
22
|
-
9. [Cas d'usage courants](#cas-dusage-courants)
|
|
23
|
-
10. [API Reference](#api-reference)
|
|
24
|
-
11. [Architecture](#architecture)
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Installation
|
|
6
|
+
## Install
|
|
29
7
|
|
|
30
8
|
```bash
|
|
31
9
|
npm install @mostajs/audit @mostajs/orm
|
|
32
10
|
```
|
|
33
11
|
|
|
34
|
-
|
|
12
|
+
## How to Use
|
|
35
13
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
## Quick Start
|
|
14
|
+
### 1. Log an Action
|
|
39
15
|
|
|
40
16
|
```typescript
|
|
41
|
-
import { logAudit } from '@mostajs/audit'
|
|
17
|
+
import { logAudit, getAuditUser } from '@mostajs/audit'
|
|
42
18
|
|
|
43
|
-
// Fire-and-forget — ne throw jamais, ne bloque jamais
|
|
44
19
|
await logAudit({
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
userRole: 'admin',
|
|
48
|
-
action: 'create',
|
|
20
|
+
...getAuditUser(session),
|
|
21
|
+
action: 'user_create',
|
|
49
22
|
module: 'users',
|
|
50
|
-
resource: '
|
|
51
|
-
resourceId:
|
|
52
|
-
details: { email: '
|
|
53
|
-
})
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
C'est tout. Le schema est auto-enregistre dans l'ORM, la table/collection est creee automatiquement.
|
|
57
|
-
|
|
58
|
-
---
|
|
59
|
-
|
|
60
|
-
## Integration complete
|
|
61
|
-
|
|
62
|
-
Guide pas-a-pas pour integrer `@mostajs/audit` dans une nouvelle application Next.js.
|
|
63
|
-
|
|
64
|
-
### Etape 1 — Installer les packages
|
|
65
|
-
|
|
66
|
-
```bash
|
|
67
|
-
npm install @mostajs/audit @mostajs/orm
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### Etape 2 — Configurer la connexion DB
|
|
71
|
-
|
|
72
|
-
L'ORM doit etre configure avant d'utiliser audit. Dans votre `.env.local` :
|
|
73
|
-
|
|
74
|
-
```env
|
|
75
|
-
DATABASE_URL=mongodb://localhost:27017/myapp
|
|
76
|
-
# ou: DATABASE_URL=postgres://user:pass@localhost:5432/myapp
|
|
77
|
-
# ou: DATABASE_URL=sqlite:./data/myapp.db
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Initialiser l'ORM (une seule fois, au demarrage) :
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
// src/lib/db.ts
|
|
84
|
-
import { initDialect } from '@mostajs/orm'
|
|
85
|
-
|
|
86
|
-
let initialized = false
|
|
87
|
-
|
|
88
|
-
export async function ensureDB() {
|
|
89
|
-
if (initialized) return
|
|
90
|
-
await initDialect(process.env.DATABASE_URL!)
|
|
91
|
-
initialized = true
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### Etape 3 — Utiliser logAudit dans vos routes API
|
|
96
|
-
|
|
97
|
-
```typescript
|
|
98
|
-
// src/app/api/products/route.ts
|
|
99
|
-
import { NextRequest, NextResponse } from 'next/server'
|
|
100
|
-
import { logAudit, getAuditUser } from '@mostajs/audit/lib/audit'
|
|
101
|
-
import { ensureDB } from '@/lib/db'
|
|
102
|
-
import { getServerSession } from 'next-auth'
|
|
103
|
-
|
|
104
|
-
export async function POST(req: NextRequest) {
|
|
105
|
-
await ensureDB()
|
|
106
|
-
const session = await getServerSession()
|
|
107
|
-
if (!session) {
|
|
108
|
-
return NextResponse.json({ error: 'Non autorise' }, { status: 401 })
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const body = await req.json()
|
|
112
|
-
|
|
113
|
-
// ... creer le produit dans la DB ...
|
|
114
|
-
const product = { id: 'prod_123', name: body.name }
|
|
115
|
-
|
|
116
|
-
// Journaliser l'action — fire-and-forget
|
|
117
|
-
await logAudit({
|
|
118
|
-
...getAuditUser(session),
|
|
119
|
-
action: 'create',
|
|
120
|
-
module: 'products',
|
|
121
|
-
resource: 'Product',
|
|
122
|
-
resourceId: product.id,
|
|
123
|
-
details: { name: product.name },
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
return NextResponse.json({ data: product })
|
|
127
|
-
}
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
### Etape 4 — Route de consultation des logs
|
|
131
|
-
|
|
132
|
-
```typescript
|
|
133
|
-
// src/app/api/admin/audit/route.ts
|
|
134
|
-
import { createAuditHandlers } from '@mostajs/audit/api/route'
|
|
135
|
-
import { checkPermission } from '@/lib/auth'
|
|
136
|
-
|
|
137
|
-
export const { GET } = createAuditHandlers('audit:view', checkPermission)
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Cette route expose `GET /api/admin/audit` avec pagination et filtres automatiques.
|
|
141
|
-
|
|
142
|
-
### Etape 5 — Page d'administration (frontend)
|
|
143
|
-
|
|
144
|
-
```tsx
|
|
145
|
-
'use client'
|
|
146
|
-
import { useState, useEffect } from 'react'
|
|
147
|
-
|
|
148
|
-
interface AuditLog {
|
|
149
|
-
id: string
|
|
150
|
-
userName: string
|
|
151
|
-
action: string
|
|
152
|
-
module: string
|
|
153
|
-
resource: string
|
|
154
|
-
timestamp: string
|
|
155
|
-
status: 'success' | 'failure'
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export default function AuditPage() {
|
|
159
|
-
const [logs, setLogs] = useState<AuditLog[]>([])
|
|
160
|
-
const [meta, setMeta] = useState({ total: 0, page: 1, pages: 1 })
|
|
161
|
-
const [module, setModule] = useState('')
|
|
162
|
-
|
|
163
|
-
async function fetchLogs(page = 1) {
|
|
164
|
-
const params = new URLSearchParams({ page: String(page), limit: '20' })
|
|
165
|
-
if (module) params.set('module', module)
|
|
166
|
-
|
|
167
|
-
const res = await fetch(`/api/admin/audit?${params}`)
|
|
168
|
-
const json = await res.json()
|
|
169
|
-
setLogs(json.data)
|
|
170
|
-
setMeta(json.meta)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
useEffect(() => { fetchLogs() }, [module])
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<div>
|
|
177
|
-
<h1>Journal d'audit</h1>
|
|
178
|
-
|
|
179
|
-
{/* Filtre par module */}
|
|
180
|
-
<select value={module} onChange={e => setModule(e.target.value)}>
|
|
181
|
-
<option value="">Tous les modules</option>
|
|
182
|
-
<option value="users">Utilisateurs</option>
|
|
183
|
-
<option value="products">Produits</option>
|
|
184
|
-
<option value="orders">Commandes</option>
|
|
185
|
-
</select>
|
|
186
|
-
|
|
187
|
-
{/* Table des logs */}
|
|
188
|
-
<table>
|
|
189
|
-
<thead>
|
|
190
|
-
<tr>
|
|
191
|
-
<th>Date</th>
|
|
192
|
-
<th>Utilisateur</th>
|
|
193
|
-
<th>Action</th>
|
|
194
|
-
<th>Module</th>
|
|
195
|
-
<th>Ressource</th>
|
|
196
|
-
<th>Statut</th>
|
|
197
|
-
</tr>
|
|
198
|
-
</thead>
|
|
199
|
-
<tbody>
|
|
200
|
-
{logs.map(log => (
|
|
201
|
-
<tr key={log.id}>
|
|
202
|
-
<td>{new Date(log.timestamp).toLocaleString()}</td>
|
|
203
|
-
<td>{log.userName}</td>
|
|
204
|
-
<td>{log.action}</td>
|
|
205
|
-
<td>{log.module}</td>
|
|
206
|
-
<td>{log.resource}</td>
|
|
207
|
-
<td>{log.status}</td>
|
|
208
|
-
</tr>
|
|
209
|
-
))}
|
|
210
|
-
</tbody>
|
|
211
|
-
</table>
|
|
212
|
-
|
|
213
|
-
{/* Pagination */}
|
|
214
|
-
<div>
|
|
215
|
-
<button
|
|
216
|
-
disabled={meta.page <= 1}
|
|
217
|
-
onClick={() => fetchLogs(meta.page - 1)}
|
|
218
|
-
>Precedent</button>
|
|
219
|
-
<span>Page {meta.page} / {meta.pages} ({meta.total} total)</span>
|
|
220
|
-
<button
|
|
221
|
-
disabled={meta.page >= meta.pages}
|
|
222
|
-
onClick={() => fetchLogs(meta.page + 1)}
|
|
223
|
-
>Suivant</button>
|
|
224
|
-
</div>
|
|
225
|
-
</div>
|
|
226
|
-
)
|
|
227
|
-
}
|
|
228
|
-
```
|
|
229
|
-
|
|
230
|
-
### Etape 6 — Verification
|
|
231
|
-
|
|
232
|
-
```bash
|
|
233
|
-
# Demarrer l'app
|
|
234
|
-
npm run dev
|
|
235
|
-
|
|
236
|
-
# Creer un produit (declenche un audit log)
|
|
237
|
-
curl -X POST http://localhost:3000/api/products \
|
|
238
|
-
-H 'Content-Type: application/json' \
|
|
239
|
-
-d '{"name": "Widget"}'
|
|
240
|
-
|
|
241
|
-
# Consulter les logs
|
|
242
|
-
curl 'http://localhost:3000/api/admin/audit?limit=5'
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
Reponse attendue :
|
|
246
|
-
|
|
247
|
-
```json
|
|
248
|
-
{
|
|
249
|
-
"data": [
|
|
250
|
-
{
|
|
251
|
-
"id": "...",
|
|
252
|
-
"userName": "Dr Madani",
|
|
253
|
-
"userRole": "admin",
|
|
254
|
-
"action": "create",
|
|
255
|
-
"module": "products",
|
|
256
|
-
"resource": "Product",
|
|
257
|
-
"resourceId": "prod_123",
|
|
258
|
-
"details": { "name": "Widget" },
|
|
259
|
-
"status": "success",
|
|
260
|
-
"timestamp": "2026-03-05T14:30:00.000Z"
|
|
261
|
-
}
|
|
262
|
-
],
|
|
263
|
-
"meta": { "total": 1, "page": 1, "limit": 5, "pages": 1 }
|
|
264
|
-
}
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
---
|
|
268
|
-
|
|
269
|
-
## logAudit
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
import { logAudit } from '@mostajs/audit/lib/audit'
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
Journalise une action. **Fire-and-forget** : ne throw jamais, ne bloque jamais. Si l'ecriture echoue, l'erreur est loggee dans la console sans interrompre le flux.
|
|
276
|
-
|
|
277
|
-
```typescript
|
|
278
|
-
await logAudit({
|
|
279
|
-
userId: '507f1f77bcf86cd799439011',
|
|
280
|
-
userName: 'Alice',
|
|
281
|
-
userRole: 'manager',
|
|
282
|
-
action: 'update',
|
|
283
|
-
module: 'clients',
|
|
284
|
-
resource: 'Client',
|
|
285
|
-
resourceId: 'cli_456',
|
|
286
|
-
details: { field: 'status', oldValue: 'active', newValue: 'suspended' },
|
|
287
|
-
ipAddress: '192.168.1.100',
|
|
288
|
-
status: 'success', // 'success' | 'failure' (defaut: 'success')
|
|
289
|
-
})
|
|
290
|
-
```
|
|
291
|
-
|
|
292
|
-
### Parametres (AuditParams)
|
|
293
|
-
|
|
294
|
-
| Champ | Type | Requis | Description |
|
|
295
|
-
|-------|------|--------|-------------|
|
|
296
|
-
| `userId` | `string` | oui | ID de l'utilisateur |
|
|
297
|
-
| `userName` | `string` | oui | Nom affichable |
|
|
298
|
-
| `userRole` | `string` | oui | Role (admin, manager, etc.) |
|
|
299
|
-
| `action` | `string` | oui | Action effectuee (create, update, delete, login, etc.) |
|
|
300
|
-
| `module` | `string` | oui | Module concerne (users, clients, lockers, etc.) |
|
|
301
|
-
| `resource` | `string` | non | Type de ressource (User, Client, etc.) |
|
|
302
|
-
| `resourceId` | `string` | non | ID de la ressource |
|
|
303
|
-
| `details` | `Record<string, any>` | non | Donnees supplementaires |
|
|
304
|
-
| `ipAddress` | `string` | non | Adresse IP du client |
|
|
305
|
-
| `status` | `'success' \| 'failure'` | non | Resultat (defaut: `'success'`) |
|
|
306
|
-
|
|
307
|
-
---
|
|
308
|
-
|
|
309
|
-
## getAuditUser
|
|
310
|
-
|
|
311
|
-
```typescript
|
|
312
|
-
import { getAuditUser } from '@mostajs/audit/lib/audit'
|
|
313
|
-
```
|
|
314
|
-
|
|
315
|
-
Extrait `userId`, `userName` et `userRole` d'une session NextAuth. Compatible avec tout objet session ayant `session.user.id`, `session.user.name` et `session.user.role`.
|
|
316
|
-
|
|
317
|
-
```typescript
|
|
318
|
-
const session = await getServerSession()
|
|
319
|
-
|
|
320
|
-
await logAudit({
|
|
321
|
-
...getAuditUser(session), // { userId, userName, userRole }
|
|
322
|
-
action: 'delete',
|
|
323
|
-
module: 'products',
|
|
324
|
-
resourceId: productId,
|
|
325
|
-
})
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Le helper gere les cas :
|
|
329
|
-
- `session.user.role` (string) → utilise directement
|
|
330
|
-
- `session.user.roles` (array) → joint avec `, `
|
|
331
|
-
- Ni l'un ni l'autre → `'unknown'`
|
|
332
|
-
- `session.user.name` absent → utilise `session.user.email`
|
|
333
|
-
|
|
334
|
-
---
|
|
335
|
-
|
|
336
|
-
## AuditLogRepository
|
|
337
|
-
|
|
338
|
-
```typescript
|
|
339
|
-
import { AuditLogRepository } from '@mostajs/audit'
|
|
340
|
-
import { getDialect } from '@mostajs/orm'
|
|
341
|
-
|
|
342
|
-
const repo = new AuditLogRepository(await getDialect())
|
|
343
|
-
```
|
|
344
|
-
|
|
345
|
-
### findPaginated(filters)
|
|
346
|
-
|
|
347
|
-
Recherche paginee avec filtres optionnels.
|
|
348
|
-
|
|
349
|
-
```typescript
|
|
350
|
-
const { data, total } = await repo.findPaginated({
|
|
351
|
-
module: 'users', // Filtrer par module
|
|
352
|
-
action: 'delete', // Filtrer par action (regex, case-insensitive)
|
|
353
|
-
userId: '507f1f77...', // Filtrer par utilisateur
|
|
354
|
-
status: 'failure', // Filtrer par statut
|
|
355
|
-
from: new Date('2026-01-01'), // Date debut
|
|
356
|
-
to: new Date('2026-03-01'), // Date fin
|
|
357
|
-
page: 2, // Page (defaut: 1)
|
|
358
|
-
limit: 20, // Limite (defaut: 50)
|
|
23
|
+
resource: 'John Doe',
|
|
24
|
+
resourceId: user.id,
|
|
25
|
+
details: { email: 'john@test.com' },
|
|
359
26
|
})
|
|
360
|
-
|
|
361
|
-
console.log(`${total} logs trouves, page 2`)
|
|
362
|
-
data.forEach(log => console.log(log.action, log.module, log.timestamp))
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
### findByResource(resourceId, modules?)
|
|
366
|
-
|
|
367
|
-
Tous les logs lies a une ressource specifique.
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
// Tous les logs du client CLI_123
|
|
371
|
-
const logs = await repo.findByResource('CLI_123')
|
|
372
|
-
|
|
373
|
-
// Logs du client CLI_123 dans les modules 'clients' et 'lockers'
|
|
374
|
-
const filtered = await repo.findByResource('CLI_123', ['clients', 'lockers'])
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
### deleteOlderThan(days)
|
|
378
|
-
|
|
379
|
-
Nettoyage des anciens logs. Utile en cron job ou maintenance.
|
|
380
|
-
|
|
381
|
-
```typescript
|
|
382
|
-
// Supprimer les logs de plus de 90 jours
|
|
383
|
-
const deleted = await repo.deleteOlderThan(90)
|
|
384
|
-
console.log(`${deleted} logs supprimes`)
|
|
27
|
+
// Fire-and-forget — never throws, never blocks
|
|
385
28
|
```
|
|
386
29
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
## createAuditHandlers
|
|
30
|
+
### 2. API Handler (paginated consultation)
|
|
390
31
|
|
|
391
32
|
```typescript
|
|
392
33
|
import { createAuditHandlers } from '@mostajs/audit/api/route'
|
|
393
|
-
```
|
|
394
|
-
|
|
395
|
-
Factory pour creer une route `GET` paginee avec controle d'acces.
|
|
396
|
-
|
|
397
|
-
```typescript
|
|
398
|
-
// src/app/api/admin/audit/route.ts
|
|
399
|
-
import { createAuditHandlers } from '@mostajs/audit/api/route'
|
|
400
|
-
import { checkPermission } from '@/lib/auth'
|
|
401
|
-
|
|
402
34
|
export const { GET } = createAuditHandlers('audit:view', checkPermission)
|
|
35
|
+
// GET /api/audit-log?page=1&limit=50&module=users&action=create
|
|
403
36
|
```
|
|
404
37
|
|
|
405
|
-
###
|
|
406
|
-
|
|
407
|
-
```typescript
|
|
408
|
-
type PermissionChecker = (permission: string) => Promise<{
|
|
409
|
-
error: NextResponse | null // null = autorise
|
|
410
|
-
session: any // session utilisateur
|
|
411
|
-
}>
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
### Query params supportes
|
|
415
|
-
|
|
416
|
-
| Param | Type | Description |
|
|
417
|
-
|-------|------|-------------|
|
|
418
|
-
| `module` | string | Filtrer par module |
|
|
419
|
-
| `action` | string | Filtrer par action (regex) |
|
|
420
|
-
| `userId` | string | Filtrer par utilisateur |
|
|
421
|
-
| `status` | `success \| failure` | Filtrer par statut |
|
|
422
|
-
| `from` | ISO date | Date debut |
|
|
423
|
-
| `to` | ISO date | Date fin |
|
|
424
|
-
| `page` | number | Numero de page (defaut: 1) |
|
|
425
|
-
| `limit` | number | Elements par page (defaut: 50) |
|
|
426
|
-
|
|
427
|
-
### Exemples de requetes
|
|
428
|
-
|
|
429
|
-
```bash
|
|
430
|
-
# Tous les logs, page 1
|
|
431
|
-
GET /api/admin/audit
|
|
432
|
-
|
|
433
|
-
# Logs du module 'users', page 2, 20 par page
|
|
434
|
-
GET /api/admin/audit?module=users&page=2&limit=20
|
|
435
|
-
|
|
436
|
-
# Logs en echec du dernier mois
|
|
437
|
-
GET /api/admin/audit?status=failure&from=2026-02-01&to=2026-03-01
|
|
438
|
-
|
|
439
|
-
# Logs d'un utilisateur specifique
|
|
440
|
-
GET /api/admin/audit?userId=507f1f77bcf86cd799439011
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
---
|
|
444
|
-
|
|
445
|
-
## Schema et indexes
|
|
446
|
-
|
|
447
|
-
La collection/table `auditlogs` est creee automatiquement avec le schema suivant :
|
|
448
|
-
|
|
449
|
-
| Champ | Type | Requis | Defaut |
|
|
450
|
-
|-------|------|--------|--------|
|
|
451
|
-
| `userId` | relation → User | oui | — |
|
|
452
|
-
| `userName` | string | oui | — |
|
|
453
|
-
| `userRole` | string | oui | — |
|
|
454
|
-
| `action` | string | oui | — |
|
|
455
|
-
| `module` | string | oui | — |
|
|
456
|
-
| `resource` | string | non | `''` |
|
|
457
|
-
| `resourceId` | string | non | `''` |
|
|
458
|
-
| `details` | json | non | — |
|
|
459
|
-
| `ipAddress` | string | non | `''` |
|
|
460
|
-
| `status` | enum: success, failure | non | `'success'` |
|
|
461
|
-
| `timestamp` | date | non | `now` |
|
|
462
|
-
|
|
463
|
-
### Indexes
|
|
464
|
-
|
|
465
|
-
| Index | Champs | Usage |
|
|
466
|
-
|-------|--------|-------|
|
|
467
|
-
| 1 | `{ timestamp: desc }` | Tri chronologique |
|
|
468
|
-
| 2 | `{ module: asc, timestamp: desc }` | Filtrage par module |
|
|
469
|
-
| 3 | `{ userId: asc, timestamp: desc }` | Historique utilisateur |
|
|
470
|
-
|
|
471
|
-
Compatible avec tous les dialectes supportes par `@mostajs/orm` (MongoDB, PostgreSQL, MySQL, SQLite, MariaDB, MSSQL, Oracle, etc.).
|
|
472
|
-
|
|
473
|
-
---
|
|
474
|
-
|
|
475
|
-
## Cas d'usage courants
|
|
476
|
-
|
|
477
|
-
### Auditer les operations CRUD
|
|
478
|
-
|
|
479
|
-
```typescript
|
|
480
|
-
// Dans chaque route API
|
|
481
|
-
export async function POST(req: NextRequest) {
|
|
482
|
-
const { error, session } = await checkPermission(PERMISSIONS.CLIENT_CREATE)
|
|
483
|
-
if (error) return error
|
|
484
|
-
|
|
485
|
-
const data = await req.json()
|
|
486
|
-
const client = await clientRepo.create(data)
|
|
487
|
-
|
|
488
|
-
// Audit
|
|
489
|
-
await logAudit({
|
|
490
|
-
...getAuditUser(session),
|
|
491
|
-
action: 'create',
|
|
492
|
-
module: 'clients',
|
|
493
|
-
resource: 'Client',
|
|
494
|
-
resourceId: client.id,
|
|
495
|
-
details: { firstName: data.firstName, lastName: data.lastName },
|
|
496
|
-
})
|
|
497
|
-
|
|
498
|
-
return NextResponse.json({ data: client })
|
|
499
|
-
}
|
|
500
|
-
```
|
|
501
|
-
|
|
502
|
-
### Auditer les connexions
|
|
503
|
-
|
|
504
|
-
```typescript
|
|
505
|
-
// Dans le callback NextAuth signIn
|
|
506
|
-
import { logAudit } from '@mostajs/audit/lib/audit'
|
|
507
|
-
|
|
508
|
-
callbacks: {
|
|
509
|
-
async signIn({ user }) {
|
|
510
|
-
await logAudit({
|
|
511
|
-
userId: user.id,
|
|
512
|
-
userName: user.name || user.email,
|
|
513
|
-
userRole: user.role || 'user',
|
|
514
|
-
action: 'login',
|
|
515
|
-
module: 'auth',
|
|
516
|
-
})
|
|
517
|
-
return true
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
### Auditer les echecs
|
|
523
|
-
|
|
524
|
-
```typescript
|
|
525
|
-
try {
|
|
526
|
-
await dangerousOperation()
|
|
527
|
-
await logAudit({ ...getAuditUser(session), action: 'export_data', module: 'reports', status: 'success' })
|
|
528
|
-
} catch (err) {
|
|
529
|
-
await logAudit({ ...getAuditUser(session), action: 'export_data', module: 'reports', status: 'failure', details: { error: err.message } })
|
|
530
|
-
throw err
|
|
531
|
-
}
|
|
532
|
-
```
|
|
533
|
-
|
|
534
|
-
### Nettoyage automatique (cron)
|
|
38
|
+
### 3. Module Info (for @mostajs/setup)
|
|
535
39
|
|
|
536
40
|
```typescript
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
import { getDialect } from '@mostajs/orm'
|
|
540
|
-
|
|
541
|
-
export async function POST() {
|
|
542
|
-
const repo = new AuditLogRepository(await getDialect())
|
|
543
|
-
const deleted = await repo.deleteOlderThan(180) // 6 mois
|
|
544
|
-
return Response.json({ data: { deleted } })
|
|
545
|
-
}
|
|
546
|
-
```
|
|
547
|
-
|
|
548
|
-
### Historique d'une ressource
|
|
549
|
-
|
|
550
|
-
```typescript
|
|
551
|
-
// src/app/api/clients/[id]/history/route.ts
|
|
552
|
-
import { AuditLogRepository } from '@mostajs/audit'
|
|
553
|
-
import { getDialect } from '@mostajs/orm'
|
|
554
|
-
|
|
555
|
-
export async function GET(req: Request, { params }: { params: { id: string } }) {
|
|
556
|
-
const repo = new AuditLogRepository(await getDialect())
|
|
557
|
-
const logs = await repo.findByResource(params.id, ['clients', 'lockers', 'rfid'])
|
|
558
|
-
return Response.json({ data: logs })
|
|
559
|
-
}
|
|
41
|
+
import { getSchemas } from '@mostajs/audit/lib/module-info'
|
|
42
|
+
const schemas = getSchemas() // [AuditLogSchema]
|
|
560
43
|
```
|
|
561
44
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
## API Reference
|
|
565
|
-
|
|
566
|
-
### Core
|
|
567
|
-
|
|
568
|
-
| Export | Import | Description |
|
|
569
|
-
|--------|--------|-------------|
|
|
570
|
-
| `logAudit(params)` | `@mostajs/audit/lib/audit` | Fire-and-forget audit logging |
|
|
571
|
-
| `getAuditUser(session)` | `@mostajs/audit/lib/audit` | Extraire userId/userName/userRole |
|
|
572
|
-
|
|
573
|
-
### Data Layer
|
|
574
|
-
|
|
575
|
-
| Export | Import | Description |
|
|
576
|
-
|--------|--------|-------------|
|
|
577
|
-
| `AuditLogRepository` | `@mostajs/audit` | Repository avec findPaginated, findByResource, deleteOlderThan |
|
|
578
|
-
| `AuditLogSchema` | `@mostajs/audit` | Schema d'entite ORM |
|
|
579
|
-
|
|
580
|
-
### Route Factory
|
|
581
|
-
|
|
582
|
-
| Export | Import | Description |
|
|
583
|
-
|--------|--------|-------------|
|
|
584
|
-
| `createAuditHandlers(perm, checker)` | `@mostajs/audit/api/route` | Factory GET pagine avec auth |
|
|
585
|
-
|
|
586
|
-
### Types
|
|
587
|
-
|
|
588
|
-
| Type | Description |
|
|
589
|
-
|------|-------------|
|
|
590
|
-
| `AuditParams` | Parametres de logAudit() |
|
|
591
|
-
| `AuditFilters` | Filtres pour findPaginated() |
|
|
592
|
-
| `AuditLogDTO` | Objet retourne (id, userName, action, module, timestamp, ...) |
|
|
593
|
-
| `MostaAuditConfig` | Config optionnelle (modules, actions connus) |
|
|
594
|
-
|
|
595
|
-
---
|
|
596
|
-
|
|
597
|
-
## Architecture
|
|
598
|
-
|
|
599
|
-
```
|
|
600
|
-
@mostajs/audit
|
|
601
|
-
├── lib/
|
|
602
|
-
│ └── audit.ts # logAudit() + getAuditUser()
|
|
603
|
-
├── api/
|
|
604
|
-
│ └── route.ts # createAuditHandlers() factory
|
|
605
|
-
├── repositories/
|
|
606
|
-
│ └── audit-log.repository.ts # findPaginated, findByResource, deleteOlderThan
|
|
607
|
-
├── schemas/
|
|
608
|
-
│ └── audit-log.schema.ts # EntitySchema ORM (collection: auditlogs)
|
|
609
|
-
├── types/
|
|
610
|
-
│ └── index.ts # AuditParams, AuditFilters, AuditLogDTO
|
|
611
|
-
└── index.ts # Barrel exports
|
|
612
|
-
|
|
613
|
-
Dependances :
|
|
614
|
-
@mostajs/orm (required — abstraction DB multi-dialecte)
|
|
615
|
-
next >= 14 (peer, optionnel — pour createAuditHandlers)
|
|
616
|
-
```
|
|
617
|
-
|
|
618
|
-
### Pattern d'injection
|
|
619
|
-
|
|
620
|
-
```
|
|
621
|
-
┌─────────────────────┐ inject permission ┌──────────────────────┐
|
|
622
|
-
│ @mostajs/audit │ ◄───────────────────────── │ Votre app │
|
|
623
|
-
│ │ │ │
|
|
624
|
-
│ createAuditHandlers(│ │ 'audit:view', │
|
|
625
|
-
│ permission, │ │ checkPermission │
|
|
626
|
-
│ checkPermission │ │ │
|
|
627
|
-
│ ) │ │ │
|
|
628
|
-
└─────────────────────┘ └──────────────────────┘
|
|
629
|
-
|
|
630
|
-
┌─────────────────────┐ call anywhere ┌──────────────────────┐
|
|
631
|
-
│ logAudit({...}) │ ◄───────────────────────── │ Route API, callback, │
|
|
632
|
-
│ fire-and-forget │ │ middleware, cron... │
|
|
633
|
-
└─────────────────────┘ └──────────────────────┘
|
|
634
|
-
```
|
|
635
|
-
|
|
636
|
-
Modifications effectuees :
|
|
637
|
-
|
|
638
|
-
1. README.md — Tutoriel complet avec :
|
|
639
|
-
- Integration pas-a-pas dans une nouvelle app (6 etapes)
|
|
640
|
-
- Documentation detaillee de logAudit, getAuditUser, AuditLogRepository,
|
|
641
|
-
createAuditHandlers
|
|
642
|
-
- Schema et indexes documentes
|
|
643
|
-
- 5 cas d'usage courants (CRUD, login, echecs, cron cleanup, historique
|
|
644
|
-
ressource)
|
|
645
|
-
- API Reference, architecture ASCII, pattern d'injection
|
|
646
|
-
2. package.json — Ajout des subpath exports manquants : ./lib/*, ./api/*, ./types
|
|
647
|
-
(corrige l'import @mostajs/audit/lib/audit utilise dans 15 routes de l'app)
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
---
|
|
651
|
-
|
|
652
|
-
## License
|
|
45
|
+
### 4. Dual ORM/NET Mode
|
|
653
46
|
|
|
654
|
-
|
|
47
|
+
Works automatically via `getAuditRepo()` — reads `MOSTA_DATA=orm|net` from environment.
|
|
@@ -8,7 +8,7 @@ export interface IAuditLogRepository {
|
|
|
8
8
|
findByResource(resourceId: string, modules?: string[]): Promise<AuditLogDTO[]>;
|
|
9
9
|
deleteOlderThan(days: number): Promise<number>;
|
|
10
10
|
}
|
|
11
|
-
/** Get audit repository
|
|
11
|
+
/** Get audit repository — dialect resolved by octoswitcher (ORM or NET) */
|
|
12
12
|
export declare function getAuditRepo(): Promise<IAuditLogRepository>;
|
|
13
13
|
/** Reset cache (for tests) */
|
|
14
14
|
export declare function resetAuditRepo(): void;
|
|
@@ -1,108 +1,22 @@
|
|
|
1
|
-
// audit-factory.ts — Centralized repository factory
|
|
2
|
-
//
|
|
1
|
+
// audit-factory.ts — Centralized repository factory
|
|
2
|
+
// Uses @mostajs/octoswitcher to get the right dialect (ORM or NET)
|
|
3
3
|
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
-
import { isNetMode } from './data-mode.js';
|
|
5
4
|
import { AuditLogSchema } from '../schemas/audit-log.schema.js';
|
|
6
5
|
// ============================================================
|
|
7
|
-
// Factory
|
|
6
|
+
// Factory — dialect resolved by octoswitcher
|
|
8
7
|
// ============================================================
|
|
9
8
|
let _cached = null;
|
|
10
|
-
|
|
11
|
-
/** Get audit repository for the current data mode (ORM or NET) */
|
|
9
|
+
/** Get audit repository — dialect resolved by octoswitcher (ORM or NET) */
|
|
12
10
|
export async function getAuditRepo() {
|
|
13
11
|
if (_cached)
|
|
14
12
|
return _cached;
|
|
15
|
-
|
|
16
|
-
await ensureSchemaNet();
|
|
17
|
-
_cached = createNetRepo();
|
|
18
|
-
}
|
|
19
|
-
else {
|
|
20
|
-
await ensureSchemaOrm();
|
|
21
|
-
_cached = await createOrmRepo();
|
|
22
|
-
}
|
|
23
|
-
return _cached;
|
|
24
|
-
}
|
|
25
|
-
/** Reset cache (for tests) */
|
|
26
|
-
export function resetAuditRepo() { _cached = null; _schemaReady = false; }
|
|
27
|
-
// ============================================================
|
|
28
|
-
// Schema init
|
|
29
|
-
// ============================================================
|
|
30
|
-
async function ensureSchemaOrm() {
|
|
31
|
-
if (_schemaReady)
|
|
32
|
-
return;
|
|
13
|
+
const { getDialect } = await import('@mostajs/octoswitcher');
|
|
33
14
|
const { registerSchemas } = await import('@mostajs/orm');
|
|
34
|
-
registerSchemas([AuditLogSchema]);
|
|
35
|
-
_schemaReady = true;
|
|
36
|
-
}
|
|
37
|
-
async function ensureSchemaNet() {
|
|
38
|
-
if (_schemaReady)
|
|
39
|
-
return;
|
|
40
|
-
const { NetClient } = await import('@mostajs/net/client');
|
|
41
|
-
const client = new NetClient({ url: process.env.MOSTA_NET_URL });
|
|
42
|
-
const result = await client.compareSchema(AuditLogSchema);
|
|
43
|
-
if (!result.exists || !result.compatible) {
|
|
44
|
-
await client.applySchema([AuditLogSchema]);
|
|
45
|
-
}
|
|
46
|
-
_schemaReady = true;
|
|
47
|
-
}
|
|
48
|
-
// ============================================================
|
|
49
|
-
// ORM mode
|
|
50
|
-
// ============================================================
|
|
51
|
-
async function createOrmRepo() {
|
|
52
|
-
const { getDialect } = await import('@mostajs/orm');
|
|
53
15
|
const { AuditLogRepository } = await import('../repositories/audit-log.repository.js');
|
|
16
|
+
registerSchemas([AuditLogSchema]);
|
|
54
17
|
const dialect = await getDialect();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// ============================================================
|
|
58
|
-
// NET mode
|
|
59
|
-
// ============================================================
|
|
60
|
-
function createNetRepo() {
|
|
61
|
-
const clientPromise = import('@mostajs/net/client').then(m => new m.NetClient({ url: process.env.MOSTA_NET_URL }));
|
|
62
|
-
return {
|
|
63
|
-
async create(data) {
|
|
64
|
-
const c = await clientPromise;
|
|
65
|
-
return c.create('auditlogs', data);
|
|
66
|
-
},
|
|
67
|
-
async findPaginated(filters) {
|
|
68
|
-
const c = await clientPromise;
|
|
69
|
-
const netFilter = {};
|
|
70
|
-
if (filters.module)
|
|
71
|
-
netFilter.module = filters.module;
|
|
72
|
-
if (filters.action)
|
|
73
|
-
netFilter.action = { $regex: filters.action, $regexFlags: 'i' };
|
|
74
|
-
if (filters.userId)
|
|
75
|
-
netFilter.userId = filters.userId;
|
|
76
|
-
if (filters.status)
|
|
77
|
-
netFilter.status = filters.status;
|
|
78
|
-
if (filters.from || filters.to) {
|
|
79
|
-
const ts = {};
|
|
80
|
-
if (filters.from)
|
|
81
|
-
ts.$gte = new Date(filters.from).toISOString();
|
|
82
|
-
if (filters.to)
|
|
83
|
-
ts.$lte = new Date(filters.to).toISOString();
|
|
84
|
-
netFilter.timestamp = ts;
|
|
85
|
-
}
|
|
86
|
-
const page = filters.page || 1;
|
|
87
|
-
const limit = filters.limit || 50;
|
|
88
|
-
const options = { sort: { timestamp: -1 }, skip: (page - 1) * limit, limit };
|
|
89
|
-
const [data, total] = await Promise.all([
|
|
90
|
-
c.findAll('auditlogs', netFilter, options),
|
|
91
|
-
c.count('auditlogs', netFilter),
|
|
92
|
-
]);
|
|
93
|
-
return { data, total };
|
|
94
|
-
},
|
|
95
|
-
async findByResource(resourceId, modules) {
|
|
96
|
-
const c = await clientPromise;
|
|
97
|
-
const filter = { resourceId };
|
|
98
|
-
if (modules?.length)
|
|
99
|
-
filter.module = { $in: modules };
|
|
100
|
-
return c.findAll('auditlogs', filter, { sort: { timestamp: -1 } });
|
|
101
|
-
},
|
|
102
|
-
async deleteOlderThan(days) {
|
|
103
|
-
const c = await clientPromise;
|
|
104
|
-
const cutoff = new Date(Date.now() - days * 86400000).toISOString();
|
|
105
|
-
return c.deleteMany('auditlogs', { timestamp: { $lt: cutoff } });
|
|
106
|
-
},
|
|
107
|
-
};
|
|
18
|
+
_cached = new AuditLogRepository(dialect);
|
|
19
|
+
return _cached;
|
|
108
20
|
}
|
|
21
|
+
/** Reset cache (for tests) */
|
|
22
|
+
export function resetAuditRepo() { _cached = null; }
|
package/dist/lib/audit-init.js
CHANGED
|
@@ -1,22 +1,12 @@
|
|
|
1
|
-
// audit-init.ts — Ensure AuditLog schema is ready
|
|
1
|
+
// audit-init.ts — Ensure AuditLog schema is ready
|
|
2
|
+
// Uses octoswitcher — schema registration works transparently in ORM and NET mode
|
|
2
3
|
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
-
import { isNetMode } from './data-mode.js';
|
|
4
4
|
import { AuditLogSchema } from '../schemas/audit-log.schema.js';
|
|
5
5
|
let initialized = false;
|
|
6
6
|
export async function ensureAuditSchema() {
|
|
7
7
|
if (initialized)
|
|
8
8
|
return;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const client = new NetClient({ url: process.env.MOSTA_NET_URL });
|
|
12
|
-
const result = await client.compareSchema(AuditLogSchema);
|
|
13
|
-
if (!result.exists || !result.compatible) {
|
|
14
|
-
await client.applySchema([AuditLogSchema]);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
else {
|
|
18
|
-
const { registerSchemas } = await import('@mostajs/orm');
|
|
19
|
-
registerSchemas([AuditLogSchema]);
|
|
20
|
-
}
|
|
9
|
+
const { registerSchemas } = await import('@mostajs/orm');
|
|
10
|
+
registerSchemas([AuditLogSchema]);
|
|
21
11
|
initialized = true;
|
|
22
12
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/audit",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
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",
|
|
@@ -119,6 +119,7 @@
|
|
|
119
119
|
},
|
|
120
120
|
"dependencies": {
|
|
121
121
|
"@mostajs/net": "^2.0.0",
|
|
122
|
+
"@mostajs/octoswitcher": "^1.0.0",
|
|
122
123
|
"@mostajs/orm": "^1.7.0"
|
|
123
124
|
}
|
|
124
125
|
}
|