Add audit logs module
[oweals/peertube.git] / server / helpers / audit-logger.ts
1 import * as path from 'path'
2 import { diff } from 'deep-object-diff'
3 import { chain } from 'lodash'
4 import * as flatten from 'flat'
5 import * as winston from 'winston'
6 import { CONFIG } from '../initializers'
7 import { jsonLoggerFormat, labelFormatter } from './logger'
8 import { VideoDetails } from '../../shared'
9
10 enum AUDIT_TYPE {
11   CREATE = 'create',
12   UPDATE = 'update',
13   DELETE = 'delete'
14 }
15
16 const colors = winston.config.npm.colors
17 colors.audit = winston.config.npm.colors.info
18
19 winston.addColors(colors)
20
21 const auditLogger = winston.createLogger({
22   levels: { audit: 0 },
23   transports: [
24     new winston.transports.File({
25       filename: path.join(CONFIG.STORAGE.LOG_DIR, 'peertube-audit.log'),
26       level: 'audit',
27       maxsize: 5242880,
28       maxFiles: 5,
29       format: winston.format.combine(
30         winston.format.timestamp(),
31         labelFormatter,
32         winston.format.splat(),
33         jsonLoggerFormat
34       )
35     })
36   ],
37   exitOnError: true
38 })
39
40 function auditLoggerWrapper (domain: string, user: string, action: AUDIT_TYPE, entity: EntityAuditView, oldEntity: EntityAuditView = null) {
41   let entityInfos: object
42   if (action === AUDIT_TYPE.UPDATE && oldEntity) {
43     const oldEntityKeys = oldEntity.toLogKeys()
44     const diffObject = diff(oldEntityKeys, entity.toLogKeys())
45     const diffKeys = Object.entries(diffObject).reduce((newKeys, entry) => {
46       newKeys[`new-${entry[0]}`] = entry[1]
47       return newKeys
48     }, {})
49     entityInfos = { ...oldEntityKeys, ...diffKeys }
50   } else {
51     entityInfos = { ...entity.toLogKeys() }
52   }
53   auditLogger.log('audit', JSON.stringify({
54     user,
55     domain,
56     action,
57     ...entityInfos
58   }))
59 }
60
61 function auditLoggerFactory (domain: string) {
62   return {
63     create (user: string, entity: EntityAuditView) {
64       auditLoggerWrapper(domain, user, AUDIT_TYPE.CREATE, entity)
65     },
66     update (user: string, entity: EntityAuditView, oldEntity: EntityAuditView) {
67       auditLoggerWrapper(domain, user, AUDIT_TYPE.UPDATE, entity, oldEntity)
68     },
69     delete (user: string, entity: EntityAuditView) {
70       auditLoggerWrapper(domain, user, AUDIT_TYPE.DELETE, entity)
71     }
72   }
73 }
74
75 abstract class EntityAuditView {
76   constructor (private keysToKeep: Array<string>, private prefix: string, private entityInfos: object) { }
77   toLogKeys (): object {
78     return chain(flatten(this.entityInfos, { delimiter: '-', safe: true }))
79       .pick(this.keysToKeep)
80       .mapKeys((value, key) => `${this.prefix}-${key}`)
81       .value()
82   }
83 }
84
85 const videoKeysToKeep = [
86   'tags',
87   'uuid',
88   'id',
89   'uuid',
90   'createdAt',
91   'updatedAt',
92   'publishedAt',
93   'category',
94   'licence',
95   'language',
96   'privacy',
97   'description',
98   'duration',
99   'isLocal',
100   'name',
101   'thumbnailPath',
102   'previewPath',
103   'nsfw',
104   'waitTranscoding',
105   'account-id',
106   'account-uuid',
107   'account-name',
108   'channel-id',
109   'channel-uuid',
110   'channel-name',
111   'support',
112   'commentsEnabled'
113 ]
114 class VideoAuditView extends AuditEntity {
115   constructor (private video: VideoDetails) {
116     super(videoKeysToKeep, 'video', video)
117   }
118 }
119
120 export {
121   auditLoggerFactory,
122   VideoAuditView
123 }