'use strict'; exports = module.exports = { add, upsertLoginEvent, get, getActivationEvent, listPaged, cleanup, _clear: clear, // keep in sync with webadmin index.js filter ACTION_ACTIVATE: 'cloudron.activate', ACTION_APP_CLONE: 'app.clone', ACTION_APP_CONFIGURE: 'app.configure', ACTION_APP_REPAIR: 'app.repair', ACTION_APP_INSTALL: 'app.install', ACTION_APP_RESTORE: 'app.restore', ACTION_APP_IMPORT: 'app.import', ACTION_APP_UNINSTALL: 'app.uninstall', ACTION_APP_UPDATE: 'app.update', ACTION_APP_UPDATE_FINISH: 'app.update.finish', ACTION_APP_BACKUP: 'app.backup', ACTION_APP_BACKUP_FINISH: 'app.backup.finish', ACTION_APP_LOGIN: 'app.login', ACTION_APP_OOM: 'app.oom', ACTION_APP_UP: 'app.up', ACTION_APP_DOWN: 'app.down', ACTION_APP_START: 'app.start', ACTION_APP_STOP: 'app.stop', ACTION_APP_RESTART: 'app.restart', ACTION_BACKUP_FINISH: 'backup.finish', ACTION_BACKUP_START: 'backup.start', ACTION_BACKUP_CLEANUP_START: 'backup.cleanup.start', // obsolete ACTION_BACKUP_CLEANUP_FINISH: 'backup.cleanup.finish', ACTION_CERTIFICATE_NEW: 'certificate.new', ACTION_CERTIFICATE_RENEWAL: 'certificate.renew', // obsolete ACTION_CERTIFICATE_CLEANUP: 'certificate.cleanup', ACTION_DASHBOARD_DOMAIN_UPDATE: 'dashboard.domain.update', ACTION_DIRECTORY_SERVER_CONFIGURE: 'directoryserver.configure', ACTION_DOMAIN_ADD: 'domain.add', ACTION_DOMAIN_UPDATE: 'domain.update', ACTION_DOMAIN_REMOVE: 'domain.remove', ACTION_EXTERNAL_LDAP_CONFIGURE: 'externalldap.configure', ACTION_INSTALL_FINISH: 'cloudron.install.finish', ACTION_MAIL_LOCATION: 'mail.location', ACTION_MAIL_ENABLED: 'mail.enabled', ACTION_MAIL_DISABLED: 'mail.disabled', ACTION_MAIL_MAILBOX_ADD: 'mail.box.add', ACTION_MAIL_MAILBOX_REMOVE: 'mail.box.remove', ACTION_MAIL_MAILBOX_UPDATE: 'mail.box.update', ACTION_MAIL_LIST_ADD: 'mail.list.add', ACTION_MAIL_LIST_REMOVE: 'mail.list.remove', ACTION_MAIL_LIST_UPDATE: 'mail.list.update', ACTION_PROVISION: 'cloudron.provision', ACTION_RESTORE: 'cloudron.restore', // unused ACTION_START: 'cloudron.start', ACTION_SERVICE_CONFIGURE: 'service.configure', ACTION_SERVICE_REBUILD: 'service.rebuild', ACTION_SERVICE_RESTART: 'service.restart', ACTION_UPDATE: 'cloudron.update', ACTION_UPDATE_FINISH: 'cloudron.update.finish', ACTION_USER_ADD: 'user.add', ACTION_USER_LOGIN: 'user.login', ACTION_USER_LOGIN_GHOST: 'user.login.ghost', ACTION_USER_LOGOUT: 'user.logout', ACTION_USER_REMOVE: 'user.remove', ACTION_USER_UPDATE: 'user.update', ACTION_USER_TRANSFER: 'user.transfer', ACTION_VOLUME_ADD: 'volume.add', ACTION_VOLUME_UPDATE: 'volume.update', ACTION_VOLUME_REMOUNT: 'volume.remount', ACTION_VOLUME_REMOVE: 'volume.remove', ACTION_DYNDNS_UPDATE: 'dyndns.update', ACTION_SUPPORT_TICKET: 'support.ticket', ACTION_SUPPORT_SSH: 'support.ssh', ACTION_PROCESS_CRASH: 'system.crash' // obsolete }; const assert = require('assert'), database = require('./database.js'), debug = require('debug')('box:eventlog'), mysql = require('mysql'), notifications = require('./notifications.js'), safe = require('safetydance'), uuid = require('uuid'); const EVENTLOG_FIELDS = [ 'id', 'action', 'sourceJson', 'dataJson', 'creationTime' ].join(','); function postProcess(record) { // usually we have sourceJson and dataJson, however since this used to be the JSON data type, we don't record.source = safe.JSON.parse(record.sourceJson); delete record.sourceJson; record.data = safe.JSON.parse(record.dataJson); delete record.dataJson; return record; } // never throws, only logs because previously code did not take a callback async function add(action, source, data) { assert.strictEqual(typeof action, 'string'); assert.strictEqual(typeof source, 'object'); // an AuditSource assert.strictEqual(typeof data, 'object'); const id = uuid.v4(); await database.query('INSERT INTO eventlog (id, action, sourceJson, dataJson) VALUES (?, ?, ?, ?)', [ id, action, JSON.stringify(source), JSON.stringify(data) ]); await notifications.onEvent(id, action, source, data); return id; } // never throws, only logs because previously code did not take a callback async function upsertLoginEvent(action, source, data) { assert.strictEqual(typeof action, 'string'); assert.strictEqual(typeof source, 'object'); // an AuditSource assert.strictEqual(typeof data, 'object'); // can't do a real sql upsert, for frequent eventlog entries we only have to do 2 queries once a day const queries = [{ query: 'UPDATE eventlog SET creationTime=NOW(), dataJson=? WHERE action = ? AND sourceJson LIKE ? AND DATE(creationTime)=CURDATE()', args: [ JSON.stringify(data), action, JSON.stringify(source) ] }, { query: 'SELECT ' + EVENTLOG_FIELDS + ' FROM eventlog WHERE action = ? AND sourceJson LIKE ? AND DATE(creationTime)=CURDATE()', args: [ action, JSON.stringify(source) ] }]; const result = await database.transaction(queries); if (result[0].affectedRows >= 1) return result[1][0].id; // no existing eventlog found, create one return await add(action, source, data); } async function get(id) { assert.strictEqual(typeof id, 'string'); const result = await database.query(`SELECT ${EVENTLOG_FIELDS} FROM eventlog WHERE id = ?`, [ id ]); if (result.length === 0) return null; return postProcess(result[0]); } async function getActivationEvent() { const result = await database.query(`SELECT ${EVENTLOG_FIELDS} FROM eventlog WHERE action = ? ORDER BY creationTime`, [ exports.ACTION_ACTIVATE ]); if (result.length === 0) return null; return postProcess(result[0]); } async function listPaged(actions, search, page, perPage) { assert(Array.isArray(actions)); assert(typeof search === 'string' || search === null); assert.strictEqual(typeof page, 'number'); assert.strictEqual(typeof perPage, 'number'); let data = []; let query = `SELECT ${EVENTLOG_FIELDS} FROM eventlog`; if (actions.length || search) query += ' WHERE'; if (search) query += ' (sourceJson LIKE ' + mysql.escape('%' + search + '%') + ' OR dataJson LIKE ' + mysql.escape('%' + search + '%') + ')'; if (actions.length && search) query += ' AND ( '; actions.forEach(function (action, i) { query += ' (action LIKE ' + mysql.escape(`%${action}%`) + ') '; if (i < actions.length-1) query += ' OR '; }); if (actions.length && search) query += ' ) '; query += ' ORDER BY creationTime DESC LIMIT ?,?'; data.push((page-1)*perPage); data.push(perPage); const results = await database.query(query, data); results.forEach(postProcess); return results; } async function cleanup(options) { assert.strictEqual(typeof options, 'object'); const creationTime = options.creationTime; debug(`cleanup: pruning events. creationTime: ${creationTime.toString()}`); // only these actions are pruned const actions = [ exports.ACTION_USER_LOGIN, exports.ACTION_USER_LOGIN_GHOST, exports.ACTION_USER_LOGOUT, ]; let query = `SELECT ${EVENTLOG_FIELDS} FROM eventlog WHERE creationTime <= ? AND (`; let data = [ creationTime ]; actions.forEach(function (action, i) { query += ' action = ? '; data.push(action); if (i < actions.length-1) query += ' OR '; }); query += ' ) '; const results = await database.query(query, data); for (const result of results) { await database.query('DELETE FROM notifications WHERE eventId=?', [ result.id ]); // remove notifications that reference the events as well await database.query('DELETE FROM eventlog WHERE id=?', [ result.id ]); } } async function clear() { await database.query('DELETE FROM eventlog'); }