import { diff } from 'deep-object-diff' ; import express from 'express' ; import flatten from 'flat' ; import { chain } from 'lodash' ; import { join } from 'path' ; import { addColors , config , createLogger , format , transports } from 'winston' ; import { AUDIT_LOG_FILENAME } from '@server/initializers/constants' ; import { AdminAbuse , CustomConfig , User , VideoChannel , VideoComment , VideoDetails , VideoImport } from '@shared/models' ; import { CONFIG } from '../initializers/config' ; import { jsonLoggerFormat , labelFormatter } from './logger' ; function getAuditIdFromRes ( res ) { return res . locals . oauth . token . User . username ; } enum AUDIT_TYPE { CREATE = 'create' , UPDATE = 'update' , DELETE = 'delete' } const colors = config . npm . colors ; colors . audit = config . npm . colors . info ; addColors ( colors ) ; const auditLogger = createLogger ( { levels : { audit : 0 } , transports : [ new transports . File ( { filename : join ( CONFIG . STORAGE . LOG_DIR , AUDIT_LOG_FILENAME ) , level : 'audit' , maxsize : 5242880 , maxFiles : 5 , format : format . combine ( format . timestamp ( ) , labelFormatter ( ) , format . splat ( ) , jsonLoggerFormat ) } ) ] , exitOnError : true } ) ; function auditLoggerWrapper ( domain , user , action , entity , oldEntity = null ) { let entityInfos ; if ( action === AUDIT_TYPE . UPDATE && oldEntity ) { const oldEntityKeys = oldEntity . toLogKeys ( ) ; const diffObject = diff ( oldEntityKeys , entity . toLogKeys ( ) ) ; const diffKeys = Object . entries ( diffObject ) . reduce ( ( newKeys , entry ) => { newKeys [ `new-${ entry [ 0 ] }` ] = entry [ 1 ] ; return newKeys ; } , { } ) ; entityInfos = { ... oldEntityKeys , ... diffKeys } ; } else { entityInfos = { ... entity . toLogKeys ( ) } ; } auditLogger . log ( 'audit' , JSON . stringify ( { user , domain , action , ... entityInfos } ) ) ; } function auditLoggerFactory ( domain ) { return { create ( user , entity ) { auditLoggerWrapper ( domain , user , AUDIT_TYPE . CREATE , entity ) ; } , update ( user , entity , oldEntity ) { auditLoggerWrapper ( domain , user , AUDIT_TYPE . UPDATE , entity , oldEntity ) ; } , delete ( user , entity ) { auditLoggerWrapper ( domain , user , AUDIT_TYPE . DELETE , entity ) ; } } ; } abstract class EntityAuditView { constructor ( private readonly keysToKeep , private readonly prefix , private readonly entityInfos ) { } toLogKeys ( ) { return chain ( flatten < object , any > ( this . entityInfos , { delimiter : '-' , safe : true } ) ) . pick ( this . keysToKeep ) . mapKeys ( ( _value , key ) => `${ this . prefix }-${ key }` ) . value ( ) ; } } const videoKeysToKeep = [ 'tags' , 'uuid' , 'id' , 'uuid' , 'createdAt' , 'updatedAt' , 'publishedAt' , 'category' , 'licence' , 'language' , 'privacy' , 'description' , 'duration' , 'isLocal' , 'name' , 'thumbnailPath' , 'previewPath' , 'nsfw' , 'waitTranscoding' , 'account-id' , 'account-uuid' , 'account-name' , 'channel-id' , 'channel-uuid' , 'channel-name' , 'support' , 'commentsEnabled' , 'downloadEnabled' ] ; class VideoAuditView extends EntityAuditView { constructor ( private readonly video ) { super ( videoKeysToKeep , 'video' , video ) ; } } const videoImportKeysToKeep = [ 'id' , 'targetUrl' , 'video-name' ] ; class VideoImportAuditView extends EntityAuditView { constructor ( private readonly videoImport ) { super ( videoImportKeysToKeep , 'video-import' , videoImport ) ; } } const commentKeysToKeep = [ 'id' , 'text' , 'threadId' , 'inReplyToCommentId' , 'videoId' , 'createdAt' , 'updatedAt' , 'totalReplies' , 'account-id' , 'account-uuid' , 'account-name' ] ; class CommentAuditView extends EntityAuditView { constructor ( private readonly comment ) { super ( commentKeysToKeep , 'comment' , comment ) ; } } const userKeysToKeep = [ 'id' , 'username' , 'email' , 'nsfwPolicy' , 'autoPlayVideo' , 'role' , 'videoQuota' , 'createdAt' , 'account-id' , 'account-uuid' , 'account-name' , 'account-followingCount' , 'account-followersCount' , 'account-createdAt' , 'account-updatedAt' , 'account-avatar-path' , 'account-avatar-createdAt' , 'account-avatar-updatedAt' , 'account-displayName' , 'account-description' , 'videoChannels' ] ; class UserAuditView extends EntityAuditView { constructor ( private readonly user ) { super ( userKeysToKeep , 'user' , user ) ; } } const channelKeysToKeep = [ 'id' , 'uuid' , 'name' , 'followingCount' , 'followersCount' , 'createdAt' , 'updatedAt' , 'avatar-path' , 'avatar-createdAt' , 'avatar-updatedAt' , 'displayName' , 'description' , 'support' , 'isLocal' , 'ownerAccount-id' , 'ownerAccount-uuid' , 'ownerAccount-name' , 'ownerAccount-displayedName' ] ; class VideoChannelAuditView extends EntityAuditView { constructor ( private readonly channel ) { super ( channelKeysToKeep , 'channel' , channel ) ; } } const abuseKeysToKeep = [ 'id' , 'reason' , 'reporterAccount' , 'createdAt' ] ; class AbuseAuditView extends EntityAuditView { constructor ( private readonly abuse ) { super ( abuseKeysToKeep , 'abuse' , abuse ) ; } } const customConfigKeysToKeep = [ 'instance-name' , 'instance-shortDescription' , 'instance-description' , 'instance-terms' , 'instance-defaultClientRoute' , 'instance-defaultNSFWPolicy' , 'instance-customizations-javascript' , 'instance-customizations-css' , 'services-twitter-username' , 'services-twitter-whitelisted' , 'cache-previews-size' , 'cache-captions-size' , 'signup-enabled' , 'signup-limit' , 'signup-requiresEmailVerification' , 'admin-email' , 'user-videoQuota' , 'transcoding-enabled' , 'transcoding-threads' , 'transcoding-resolutions' ] ; class CustomConfigAuditView extends EntityAuditView { constructor ( customConfig ) { const infos = customConfig ; const resolutionsDict = infos . transcoding . resolutions ; const resolutionsArray = [ ] ; Object . entries ( resolutionsDict ) . forEach ( ( [ resolution , isEnabled ] ) => { if ( isEnabled ) resolutionsArray . push ( resolution ) ; } ) ; Object . assign ( { } , infos , { transcoding : { resolutions : resolutionsArray } } ) ; super ( customConfigKeysToKeep , 'config' , infos ) ; } } export { getAuditIdFromRes , auditLoggerFactory , VideoImportAuditView , VideoChannelAuditView , CommentAuditView , UserAuditView , VideoAuditView , AbuseAuditView , CustomConfigAuditView } ;