allow limiting video-comments rss feeds to an account or video channel
[oweals/peertube.git] / server / helpers / audit-logger.ts
1 import * as path from 'path'
2 import * as express from 'express'
3 import { diff } from 'deep-object-diff'
4 import { chain } from 'lodash'
5 import * as flatten from 'flat'
6 import * as winston from 'winston'
7 import { jsonLoggerFormat, labelFormatter } from './logger'
8 import { User, VideoAbuse, VideoChannel, VideoDetails, VideoImport } from '../../shared'
9 import { VideoComment } from '../../shared/models/videos/video-comment.model'
10 import { CustomConfig } from '../../shared/models/server/custom-config.model'
11 import { CONFIG } from '../initializers/config'
12 import { AUDIT_LOG_FILENAME } from '@server/initializers/constants'
13
14 function getAuditIdFromRes (res: express.Response) {
15   return res.locals.oauth.token.User.username
16 }
17
18 enum AUDIT_TYPE {
19   CREATE = 'create',
20   UPDATE = 'update',
21   DELETE = 'delete'
22 }
23
24 const colors = winston.config.npm.colors
25 colors.audit = winston.config.npm.colors.info
26
27 winston.addColors(colors)
28
29 const auditLogger = winston.createLogger({
30   levels: { audit: 0 },
31   transports: [
32     new winston.transports.File({
33       filename: path.join(CONFIG.STORAGE.LOG_DIR, AUDIT_LOG_FILENAME),
34       level: 'audit',
35       maxsize: 5242880,
36       maxFiles: 5,
37       format: winston.format.combine(
38         winston.format.timestamp(),
39         labelFormatter(),
40         winston.format.splat(),
41         jsonLoggerFormat
42       )
43     })
44   ],
45   exitOnError: true
46 })
47
48 function auditLoggerWrapper (domain: string, user: string, action: AUDIT_TYPE, entity: EntityAuditView, oldEntity: EntityAuditView = null) {
49   let entityInfos: object
50   if (action === AUDIT_TYPE.UPDATE && oldEntity) {
51     const oldEntityKeys = oldEntity.toLogKeys()
52     const diffObject = diff(oldEntityKeys, entity.toLogKeys())
53     const diffKeys = Object.entries(diffObject).reduce((newKeys, entry) => {
54       newKeys[`new-${entry[0]}`] = entry[1]
55       return newKeys
56     }, {})
57     entityInfos = { ...oldEntityKeys, ...diffKeys }
58   } else {
59     entityInfos = { ...entity.toLogKeys() }
60   }
61   auditLogger.log('audit', JSON.stringify({
62     user,
63     domain,
64     action,
65     ...entityInfos
66   }))
67 }
68
69 function auditLoggerFactory (domain: string) {
70   return {
71     create (user: string, entity: EntityAuditView) {
72       auditLoggerWrapper(domain, user, AUDIT_TYPE.CREATE, entity)
73     },
74     update (user: string, entity: EntityAuditView, oldEntity: EntityAuditView) {
75       auditLoggerWrapper(domain, user, AUDIT_TYPE.UPDATE, entity, oldEntity)
76     },
77     delete (user: string, entity: EntityAuditView) {
78       auditLoggerWrapper(domain, user, AUDIT_TYPE.DELETE, entity)
79     }
80   }
81 }
82
83 abstract class EntityAuditView {
84   constructor (private readonly keysToKeep: string[], private readonly prefix: string, private readonly entityInfos: object) { }
85
86   toLogKeys (): object {
87     return chain(flatten(this.entityInfos, { delimiter: '-', safe: true }))
88       .pick(this.keysToKeep)
89       .mapKeys((value, key) => `${this.prefix}-${key}`)
90       .value()
91   }
92 }
93
94 const videoKeysToKeep = [
95   'tags',
96   'uuid',
97   'id',
98   'uuid',
99   'createdAt',
100   'updatedAt',
101   'publishedAt',
102   'category',
103   'licence',
104   'language',
105   'privacy',
106   'description',
107   'duration',
108   'isLocal',
109   'name',
110   'thumbnailPath',
111   'previewPath',
112   'nsfw',
113   'waitTranscoding',
114   'account-id',
115   'account-uuid',
116   'account-name',
117   'channel-id',
118   'channel-uuid',
119   'channel-name',
120   'support',
121   'commentsEnabled',
122   'downloadEnabled'
123 ]
124 class VideoAuditView extends EntityAuditView {
125   constructor (private readonly video: VideoDetails) {
126     super(videoKeysToKeep, 'video', video)
127   }
128 }
129
130 const videoImportKeysToKeep = [
131   'id',
132   'targetUrl',
133   'video-name'
134 ]
135 class VideoImportAuditView extends EntityAuditView {
136   constructor (private readonly videoImport: VideoImport) {
137     super(videoImportKeysToKeep, 'video-import', videoImport)
138   }
139 }
140
141 const commentKeysToKeep = [
142   'id',
143   'text',
144   'threadId',
145   'inReplyToCommentId',
146   'videoId',
147   'createdAt',
148   'updatedAt',
149   'totalReplies',
150   'account-id',
151   'account-uuid',
152   'account-name'
153 ]
154 class CommentAuditView extends EntityAuditView {
155   constructor (private readonly comment: VideoComment) {
156     super(commentKeysToKeep, 'comment', comment)
157   }
158 }
159
160 const userKeysToKeep = [
161   'id',
162   'username',
163   'email',
164   'nsfwPolicy',
165   'autoPlayVideo',
166   'role',
167   'videoQuota',
168   'createdAt',
169   'account-id',
170   'account-uuid',
171   'account-name',
172   'account-followingCount',
173   'account-followersCount',
174   'account-createdAt',
175   'account-updatedAt',
176   'account-avatar-path',
177   'account-avatar-createdAt',
178   'account-avatar-updatedAt',
179   'account-displayName',
180   'account-description',
181   'videoChannels'
182 ]
183 class UserAuditView extends EntityAuditView {
184   constructor (private readonly user: User) {
185     super(userKeysToKeep, 'user', user)
186   }
187 }
188
189 const channelKeysToKeep = [
190   'id',
191   'uuid',
192   'name',
193   'followingCount',
194   'followersCount',
195   'createdAt',
196   'updatedAt',
197   'avatar-path',
198   'avatar-createdAt',
199   'avatar-updatedAt',
200   'displayName',
201   'description',
202   'support',
203   'isLocal',
204   'ownerAccount-id',
205   'ownerAccount-uuid',
206   'ownerAccount-name',
207   'ownerAccount-displayedName'
208 ]
209 class VideoChannelAuditView extends EntityAuditView {
210   constructor (private readonly channel: VideoChannel) {
211     super(channelKeysToKeep, 'channel', channel)
212   }
213 }
214
215 const videoAbuseKeysToKeep = [
216   'id',
217   'reason',
218   'reporterAccount',
219   'video-id',
220   'video-name',
221   'video-uuid',
222   'createdAt'
223 ]
224 class VideoAbuseAuditView extends EntityAuditView {
225   constructor (private readonly videoAbuse: VideoAbuse) {
226     super(videoAbuseKeysToKeep, 'abuse', videoAbuse)
227   }
228 }
229
230 const customConfigKeysToKeep = [
231   'instance-name',
232   'instance-shortDescription',
233   'instance-description',
234   'instance-terms',
235   'instance-defaultClientRoute',
236   'instance-defaultNSFWPolicy',
237   'instance-customizations-javascript',
238   'instance-customizations-css',
239   'services-twitter-username',
240   'services-twitter-whitelisted',
241   'cache-previews-size',
242   'cache-captions-size',
243   'signup-enabled',
244   'signup-limit',
245   'signup-requiresEmailVerification',
246   'admin-email',
247   'user-videoQuota',
248   'transcoding-enabled',
249   'transcoding-threads',
250   'transcoding-resolutions'
251 ]
252 class CustomConfigAuditView extends EntityAuditView {
253   constructor (customConfig: CustomConfig) {
254     const infos: any = customConfig
255     const resolutionsDict = infos.transcoding.resolutions
256     const resolutionsArray = []
257
258     Object.entries(resolutionsDict)
259           .forEach(([ resolution, isEnabled ]) => {
260             if (isEnabled) resolutionsArray.push(resolution)
261           })
262
263     Object.assign({}, infos, { transcoding: { resolutions: resolutionsArray } })
264     super(customConfigKeysToKeep, 'config', infos)
265   }
266 }
267
268 export {
269   getAuditIdFromRes,
270
271   auditLoggerFactory,
272   VideoImportAuditView,
273   VideoChannelAuditView,
274   CommentAuditView,
275   UserAuditView,
276   VideoAuditView,
277   VideoAbuseAuditView,
278   CustomConfigAuditView
279 }