@mostajs/audit 1.0.0 → 1.0.2
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 +610 -27
- package/dist/api/route.js +6 -9
- package/dist/index.d.ts +2 -1
- package/dist/index.js +6 -12
- package/dist/lib/audit.d.ts +1 -1
- package/dist/lib/audit.js +8 -9
- package/dist/lib/menu.d.ts +2 -0
- package/dist/lib/menu.js +16 -0
- package/dist/repositories/audit-log.repository.d.ts +1 -1
- package/dist/repositories/audit-log.repository.js +4 -8
- package/dist/schemas/audit-log.schema.js +1 -4
- package/dist/types/index.d.ts +0 -4
- package/dist/types/index.js +1 -2
- package/package.json +43 -10
package/README.md
CHANGED
|
@@ -5,7 +5,23 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@mostajs/audit)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
|
|
8
|
-
Part of the [@mosta suite](https://mostajs.dev).
|
|
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)
|
|
9
25
|
|
|
10
26
|
---
|
|
11
27
|
|
|
@@ -15,57 +31,624 @@ Part of the [@mosta suite](https://mostajs.dev).
|
|
|
15
31
|
npm install @mostajs/audit @mostajs/orm
|
|
16
32
|
```
|
|
17
33
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
### 1. Register the schema
|
|
34
|
+
`@mostajs/orm` est la seule dependance requise. Elle gere la connexion DB et les operations CRUD quel que soit le dialecte (MongoDB, PostgreSQL, MySQL, SQLite, etc.).
|
|
21
35
|
|
|
22
|
-
|
|
23
|
-
import { registerSchema } from '@mostajs/orm'
|
|
24
|
-
import { AuditLogSchema } from '@mostajs/audit'
|
|
25
|
-
|
|
26
|
-
registerSchema(AuditLogSchema)
|
|
27
|
-
```
|
|
36
|
+
---
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
## Quick Start
|
|
30
39
|
|
|
31
40
|
```typescript
|
|
32
41
|
import { logAudit } from '@mostajs/audit'
|
|
33
42
|
|
|
43
|
+
// Fire-and-forget — ne throw jamais, ne bloque jamais
|
|
34
44
|
await logAudit({
|
|
35
|
-
userId:
|
|
36
|
-
userName:
|
|
45
|
+
userId: '507f1f77bcf86cd799439011',
|
|
46
|
+
userName: 'Dr Madani',
|
|
37
47
|
userRole: 'admin',
|
|
38
48
|
action: 'create',
|
|
39
49
|
module: 'users',
|
|
40
50
|
resource: 'User',
|
|
41
|
-
resourceId:
|
|
51
|
+
resourceId: 'abc123',
|
|
52
|
+
details: { email: 'new@example.com' },
|
|
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)
|
|
42
359
|
})
|
|
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`)
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## createAuditHandlers
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
import { createAuditHandlers } from '@mostajs/audit/api/route'
|
|
43
393
|
```
|
|
44
394
|
|
|
45
|
-
|
|
395
|
+
Factory pour creer une route `GET` paginee avec controle d'acces.
|
|
46
396
|
|
|
47
397
|
```typescript
|
|
48
|
-
|
|
398
|
+
// src/app/api/admin/audit/route.ts
|
|
399
|
+
import { createAuditHandlers } from '@mostajs/audit/api/route'
|
|
49
400
|
import { checkPermission } from '@/lib/auth'
|
|
50
401
|
|
|
51
402
|
export const { GET } = createAuditHandlers('audit:view', checkPermission)
|
|
52
403
|
```
|
|
53
404
|
|
|
405
|
+
### Signature du checkPermission attendu
|
|
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)
|
|
535
|
+
|
|
536
|
+
```typescript
|
|
537
|
+
// src/app/api/cron/cleanup-audit/route.ts
|
|
538
|
+
import { AuditLogRepository } from '@mostajs/audit'
|
|
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
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
54
564
|
## API Reference
|
|
55
565
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
|
59
|
-
|
|
60
|
-
| `
|
|
61
|
-
| `
|
|
62
|
-
|
|
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
|
|
63
574
|
|
|
64
|
-
|
|
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
|
+
---
|
|
65
596
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
---
|
|
68
651
|
|
|
69
652
|
## License
|
|
70
653
|
|
|
71
|
-
MIT —
|
|
654
|
+
MIT — Dr Hamid MADANI <drmdh@msn.com>
|
package/dist/api/route.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
// @mosta/audit — API Route template
|
|
3
2
|
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
3
|
//
|
|
@@ -7,15 +6,13 @@
|
|
|
7
6
|
// import { createAuditHandlers } from '@mosta/audit/api/route'
|
|
8
7
|
// import { checkPermission } from '@mosta/auth'
|
|
9
8
|
// export const { GET } = createAuditHandlers('audit:view', checkPermission)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const orm_1 = require("@mostajs/orm");
|
|
14
|
-
const audit_log_repository_1 = require("../repositories/audit-log.repository");
|
|
9
|
+
import { NextResponse } from 'next/server';
|
|
10
|
+
import { getDialect } from '@mostajs/orm';
|
|
11
|
+
import { AuditLogRepository } from '../repositories/audit-log.repository';
|
|
15
12
|
/**
|
|
16
13
|
* Creates a GET handler for paginated audit log consultation.
|
|
17
14
|
*/
|
|
18
|
-
function createAuditHandlers(permission, checkPermission) {
|
|
15
|
+
export function createAuditHandlers(permission, checkPermission) {
|
|
19
16
|
async function GET(req) {
|
|
20
17
|
const { error } = await checkPermission(permission);
|
|
21
18
|
if (error)
|
|
@@ -31,9 +28,9 @@ function createAuditHandlers(permission, checkPermission) {
|
|
|
31
28
|
page: parseInt(url.searchParams.get('page') || '1', 10),
|
|
32
29
|
limit: parseInt(url.searchParams.get('limit') || '50', 10),
|
|
33
30
|
};
|
|
34
|
-
const repo = new
|
|
31
|
+
const repo = new AuditLogRepository(await getDialect());
|
|
35
32
|
const { data, total } = await repo.findPaginated(filters);
|
|
36
|
-
return
|
|
33
|
+
return NextResponse.json({
|
|
37
34
|
data,
|
|
38
35
|
meta: {
|
|
39
36
|
total,
|
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,5 @@ export { logAudit, getAuditUser } from './lib/audit';
|
|
|
2
2
|
export { AuditLogRepository } from './repositories/audit-log.repository';
|
|
3
3
|
export { AuditLogSchema } from './schemas/audit-log.schema';
|
|
4
4
|
export { createAuditHandlers } from './api/route';
|
|
5
|
-
export
|
|
5
|
+
export { auditMenuContribution } from './lib/menu';
|
|
6
|
+
export type { MostaAuditConfig, AuditParams, AuditFilters, AuditLogDTO, } from './types/index';
|
package/dist/index.js
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
// @mosta/audit — Barrel exports
|
|
3
2
|
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
4
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.createAuditHandlers = exports.AuditLogSchema = exports.AuditLogRepository = exports.getAuditUser = exports.logAudit = void 0;
|
|
6
3
|
// Core
|
|
7
|
-
|
|
8
|
-
Object.defineProperty(exports, "logAudit", { enumerable: true, get: function () { return audit_1.logAudit; } });
|
|
9
|
-
Object.defineProperty(exports, "getAuditUser", { enumerable: true, get: function () { return audit_1.getAuditUser; } });
|
|
4
|
+
export { logAudit, getAuditUser } from './lib/audit';
|
|
10
5
|
// Repository & Schema
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
var audit_log_schema_1 = require("./schemas/audit-log.schema");
|
|
14
|
-
Object.defineProperty(exports, "AuditLogSchema", { enumerable: true, get: function () { return audit_log_schema_1.AuditLogSchema; } });
|
|
6
|
+
export { AuditLogRepository } from './repositories/audit-log.repository';
|
|
7
|
+
export { AuditLogSchema } from './schemas/audit-log.schema';
|
|
15
8
|
// API helpers
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
export { createAuditHandlers } from './api/route';
|
|
10
|
+
// Menu contribution
|
|
11
|
+
export { auditMenuContribution } from './lib/menu';
|
package/dist/lib/audit.d.ts
CHANGED
package/dist/lib/audit.js
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.logAudit = logAudit;
|
|
4
|
-
exports.getAuditUser = getAuditUser;
|
|
5
1
|
// @mosta/audit — Core audit functions
|
|
6
2
|
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import { getDialect, registerSchemas } from '@mostajs/orm';
|
|
4
|
+
import { AuditLogRepository } from '../repositories/audit-log.repository';
|
|
5
|
+
import { AuditLogSchema } from '../schemas/audit-log.schema';
|
|
6
|
+
// Auto-register audit schema into ORM registry (idempotent)
|
|
7
|
+
registerSchemas([AuditLogSchema]);
|
|
9
8
|
/**
|
|
10
9
|
* Log an audit entry. Fire-and-forget — never throws, never blocks.
|
|
11
10
|
*/
|
|
12
|
-
async function logAudit(params) {
|
|
11
|
+
export async function logAudit(params) {
|
|
13
12
|
try {
|
|
14
|
-
const repo = new
|
|
13
|
+
const repo = new AuditLogRepository(await getDialect());
|
|
15
14
|
await repo.create({
|
|
16
15
|
userId: params.userId,
|
|
17
16
|
userName: params.userName,
|
|
@@ -33,7 +32,7 @@ async function logAudit(params) {
|
|
|
33
32
|
/**
|
|
34
33
|
* Extract audit user info from a NextAuth-style session.
|
|
35
34
|
*/
|
|
36
|
-
function getAuditUser(session) {
|
|
35
|
+
export function getAuditUser(session) {
|
|
37
36
|
const { role, roles } = session.user;
|
|
38
37
|
return {
|
|
39
38
|
userId: session.user.id,
|
package/dist/lib/menu.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// @mostajs/audit — Menu contribution
|
|
2
|
+
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
3
|
+
import { FileText } from 'lucide-react';
|
|
4
|
+
export const auditMenuContribution = {
|
|
5
|
+
moduleKey: 'audit',
|
|
6
|
+
mergeIntoGroup: 'Administration',
|
|
7
|
+
order: 80,
|
|
8
|
+
items: [
|
|
9
|
+
{
|
|
10
|
+
label: 'audit.title',
|
|
11
|
+
href: '/dashboard/audit',
|
|
12
|
+
icon: FileText,
|
|
13
|
+
permission: 'audit:view',
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseRepository } from '@mostajs/orm';
|
|
2
2
|
import type { IDialect, QueryOptions } from '@mostajs/orm';
|
|
3
|
-
import type { AuditLogDTO, AuditFilters } from '../types';
|
|
3
|
+
import type { AuditLogDTO, AuditFilters } from '../types/index';
|
|
4
4
|
export declare class AuditLogRepository extends BaseRepository<AuditLogDTO> {
|
|
5
5
|
constructor(dialect: IDialect);
|
|
6
6
|
/** Find paginated audit logs with optional filters */
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.AuditLogRepository = void 0;
|
|
4
1
|
// @mosta/audit — AuditLogRepository
|
|
5
2
|
// Author: Dr Hamid MADANI drmdh@msn.com
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class AuditLogRepository extends
|
|
3
|
+
import { BaseRepository } from '@mostajs/orm';
|
|
4
|
+
import { AuditLogSchema } from '../schemas/audit-log.schema';
|
|
5
|
+
export class AuditLogRepository extends BaseRepository {
|
|
9
6
|
constructor(dialect) {
|
|
10
|
-
super(
|
|
7
|
+
super(AuditLogSchema, dialect);
|
|
11
8
|
}
|
|
12
9
|
/** Find paginated audit logs with optional filters */
|
|
13
10
|
async findPaginated(filters = {}, options) {
|
|
@@ -50,4 +47,3 @@ class AuditLogRepository extends orm_1.BaseRepository {
|
|
|
50
47
|
return this.deleteMany({ timestamp: { $lt: cutoff } });
|
|
51
48
|
}
|
|
52
49
|
}
|
|
53
|
-
exports.AuditLogRepository = AuditLogRepository;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
export interface MostaAuditConfig {
|
|
2
|
-
/** Collection/table name (default: 'auditlogs') */
|
|
3
|
-
collection?: string;
|
|
4
|
-
/** Retention in days, 0 = unlimited (default: 0) */
|
|
5
|
-
retentionDays?: number;
|
|
6
2
|
/** Known modules for UI filters */
|
|
7
3
|
modules?: string[];
|
|
8
4
|
/** Known actions for UI filters */
|
package/dist/types/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,30 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostajs/audit",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
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",
|
|
7
|
+
"type": "module",
|
|
7
8
|
"main": "dist/index.js",
|
|
8
9
|
"types": "dist/index.d.ts",
|
|
9
10
|
"exports": {
|
|
10
11
|
".": {
|
|
11
12
|
"types": "./dist/index.d.ts",
|
|
12
13
|
"import": "./dist/index.js",
|
|
13
|
-
"require": "./dist/index.js",
|
|
14
14
|
"default": "./dist/index.js"
|
|
15
15
|
},
|
|
16
|
+
"./lib/audit": {
|
|
17
|
+
"types": "./dist/lib/audit.d.ts",
|
|
18
|
+
"import": "./dist/lib/audit.js",
|
|
19
|
+
"default": "./dist/lib/audit.js"
|
|
20
|
+
},
|
|
16
21
|
"./api/route": {
|
|
17
22
|
"types": "./dist/api/route.d.ts",
|
|
18
23
|
"import": "./dist/api/route.js",
|
|
19
|
-
"require": "./dist/api/route.js",
|
|
20
24
|
"default": "./dist/api/route.js"
|
|
25
|
+
},
|
|
26
|
+
"./types": {
|
|
27
|
+
"types": "./dist/types/index.d.ts",
|
|
28
|
+
"import": "./dist/types/index.js",
|
|
29
|
+
"default": "./dist/types/index.js"
|
|
21
30
|
}
|
|
22
31
|
},
|
|
23
|
-
"files": [
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"keywords": [
|
|
38
|
+
"audit",
|
|
39
|
+
"audit-log",
|
|
40
|
+
"logging",
|
|
41
|
+
"nextjs",
|
|
42
|
+
"mosta"
|
|
43
|
+
],
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "https://github.com/apolocine/mosta-audit"
|
|
47
|
+
},
|
|
26
48
|
"homepage": "https://mostajs.dev/packages/audit",
|
|
27
|
-
"engines": {
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=18.0.0"
|
|
51
|
+
},
|
|
28
52
|
"scripts": {
|
|
29
53
|
"build": "tsc",
|
|
30
54
|
"prepublishOnly": "npm run build"
|
|
@@ -33,16 +57,25 @@
|
|
|
33
57
|
"@mostajs/orm": "^1.0.0"
|
|
34
58
|
},
|
|
35
59
|
"peerDependencies": {
|
|
60
|
+
"@mostajs/menu": ">=1.0.2",
|
|
36
61
|
"next": ">=14",
|
|
37
62
|
"react": ">=18"
|
|
38
63
|
},
|
|
39
64
|
"peerDependenciesMeta": {
|
|
40
|
-
"next": {
|
|
41
|
-
|
|
65
|
+
"next": {
|
|
66
|
+
"optional": true
|
|
67
|
+
},
|
|
68
|
+
"react": {
|
|
69
|
+
"optional": true
|
|
70
|
+
},
|
|
71
|
+
"@mostajs/menu": {
|
|
72
|
+
"optional": true
|
|
73
|
+
}
|
|
42
74
|
},
|
|
43
75
|
"devDependencies": {
|
|
76
|
+
"@mostajs/menu": "^1.0.2",
|
|
44
77
|
"@types/react": "^19.0.0",
|
|
45
|
-
"next": "^
|
|
78
|
+
"next": "^16.1.6",
|
|
46
79
|
"react": "^19.0.0",
|
|
47
80
|
"typescript": "^5.6.0"
|
|
48
81
|
}
|