diff --git a/dashboard/src/components/app/Eventlog.vue b/dashboard/src/components/app/Eventlog.vue new file mode 100644 index 000000000..faf22e6da --- /dev/null +++ b/dashboard/src/components/app/Eventlog.vue @@ -0,0 +1,53 @@ + + + diff --git a/dashboard/src/models/AppsModel.js b/dashboard/src/models/AppsModel.js index 6293392f1..45c8de34c 100644 --- a/dashboard/src/models/AppsModel.js +++ b/dashboard/src/models/AppsModel.js @@ -242,6 +242,17 @@ function create() { if (result.status !== 202) return [result]; return [null]; }, + async getEvents(id) { + let result; + try { + result = await fetcher.get(`${origin}/api/v1/apps/${id}/eventlog`, { page: 1, per_page: 100, access_token: accessToken }); + } catch (e) { + return [e]; + } + + if (result.status !== 200) return [result]; + return [null, result.body.eventlogs]; + }, }; } diff --git a/dashboard/src/utils.js b/dashboard/src/utils.js index 9a4439c57..616d43af4 100644 --- a/dashboard/src/utils.js +++ b/dashboard/src/utils.js @@ -1,4 +1,6 @@ +import { prettyBinarySize } from 'pankow/utils'; + // https://stackoverflow.com/questions/3665115/how-to-create-a-file-in-memory-for-user-to-download-but-not-through-server#18197341 function download(filename, text) { var element = document.createElement('a'); @@ -26,11 +28,467 @@ function s3like(provider) { || provider === 'contabo-objectstorage'; } +// TODO not sure what appIdContext is good for +function eventlogDetails(eventLog, app = null, appIdContext = '') { + const ACTION_ACTIVATE = 'cloudron.activate'; + const ACTION_PROVISION = 'cloudron.provision'; + const ACTION_RESTORE = 'cloudron.restore'; + + const ACTION_APP_CLONE = 'app.clone'; + const ACTION_APP_REPAIR = 'app.repair'; + const ACTION_APP_CONFIGURE = 'app.configure'; + const ACTION_APP_INSTALL = 'app.install'; + const ACTION_APP_RESTORE = 'app.restore'; + const ACTION_APP_IMPORT = 'app.import'; + const ACTION_APP_UNINSTALL = 'app.uninstall'; + const ACTION_APP_UPDATE = 'app.update'; + const ACTION_APP_UPDATE_FINISH = 'app.update.finish'; + const ACTION_APP_BACKUP = 'app.backup'; + const ACTION_APP_BACKUP_FINISH = 'app.backup.finish'; + const ACTION_APP_LOGIN = 'app.login'; + const ACTION_APP_OOM = 'app.oom'; + const ACTION_APP_UP = 'app.up'; + const ACTION_APP_DOWN = 'app.down'; + const ACTION_APP_START = 'app.start'; + const ACTION_APP_STOP = 'app.stop'; + const ACTION_APP_RESTART = 'app.restart'; + + const ACTION_ARCHIVES_ADD = 'archives.add'; + const ACTION_ARCHIVES_DEL = 'archives.del'; + + const ACTION_BACKUP_FINISH = 'backup.finish'; + const ACTION_BACKUP_START = 'backup.start'; + const ACTION_BACKUP_CLEANUP_START = 'backup.cleanup.start'; + const ACTION_BACKUP_CLEANUP_FINISH = 'backup.cleanup.finish'; + + const ACTION_BRANDING_AVATAR = 'branding.avatar'; + const ACTION_BRANDING_NAME = 'branding.name'; + const ACTION_BRANDING_FOOTER = 'branding.footer'; + + const ACTION_CERTIFICATE_NEW = 'certificate.new'; + const ACTION_CERTIFICATE_RENEWAL = 'certificate.renew'; + const ACTION_CERTIFICATE_CLEANUP = 'certificate.cleanup'; + + const ACTION_DASHBOARD_DOMAIN_UPDATE = 'dashboard.domain.update'; + + const ACTION_DIRECTORY_SERVER_CONFIGURE = 'directoryserver.configure'; + + const ACTION_DOMAIN_ADD = 'domain.add'; + const ACTION_DOMAIN_UPDATE = 'domain.update'; + const ACTION_DOMAIN_REMOVE = 'domain.remove'; + + const ACTION_EXTERNAL_LDAP_CONFIGURE = 'externalldap.configure'; + + const ACTION_GROUP_ADD = 'group.add'; + const ACTION_GROUP_UPDATE = 'group.update'; + const ACTION_GROUP_REMOVE = 'group.remove'; + const ACTION_GROUP_MEMBERSHIP = 'group.membership'; + + const ACTION_INSTALL_FINISH = 'cloudron.install.finish'; + + const ACTION_START = 'cloudron.start'; + const ACTION_SERVICE_CONFIGURE = 'service.configure'; + const ACTION_SERVICE_REBUILD = 'service.rebuild'; + const ACTION_SERVICE_RESTART = 'service.restart'; + const ACTION_UPDATE = 'cloudron.update'; + const ACTION_UPDATE_FINISH = 'cloudron.update.finish'; + const ACTION_USER_ADD = 'user.add'; + const ACTION_USER_LOGIN = 'user.login'; + const ACTION_USER_LOGIN_GHOST = 'user.login.ghost'; + const ACTION_USER_LOGOUT = 'user.logout'; + const ACTION_USER_REMOVE = 'user.remove'; + const ACTION_USER_UPDATE = 'user.update'; + const ACTION_USER_TRANSFER = 'user.transfer'; + + const ACTION_USER_DIRECTORY_PROFILE_CONFIG_UPDATE = 'userdirectory.profileconfig.update'; + + const ACTION_MAIL_LOCATION = 'mail.location'; + const ACTION_MAIL_ENABLED = 'mail.enabled'; + const ACTION_MAIL_DISABLED = 'mail.disabled'; + const ACTION_MAIL_MAILBOX_ADD = 'mail.box.add'; + const ACTION_MAIL_MAILBOX_UPDATE = 'mail.box.update'; + const ACTION_MAIL_MAILBOX_REMOVE = 'mail.box.remove'; + const ACTION_MAIL_LIST_ADD = 'mail.list.add'; + const ACTION_MAIL_LIST_UPDATE = 'mail.list.update'; + const ACTION_MAIL_LIST_REMOVE = 'mail.list.remove'; + + const ACTION_SUPPORT_TICKET = 'support.ticket'; + const ACTION_SUPPORT_SSH = 'support.ssh'; + + const ACTION_VOLUME_ADD = 'volume.add'; + const ACTION_VOLUME_UPDATE = 'volume.update'; + const ACTION_VOLUME_REMOVE = 'volume.remove'; + + const ACTION_DYNDNS_UPDATE = 'dyndns.update'; + + const data = eventLog.data; + const errorMessage = data.errorMessage; + let details; + + function appName(pre, app, defaultValue) { + if (appIdContext) return defaultValue || ''; + + pre = pre ? (pre + ' ') : ''; + + return pre + (app.label || app.fqdn || app.subdomain) + ' (' + app.manifest.title + ') '; + } + + function eventBy() { + if (eventLog.source && eventLog.source.username) return ' by ' + eventLog.source.username; + return ''; + } + + switch (eventLog.action) { + case ACTION_ACTIVATE: + return 'Cloudron was activated'; + + case ACTION_PROVISION: + return 'Cloudron was setup'; + + case ACTION_RESTORE: + return 'Cloudron was restored using backup at ' + data.remotePath; + + case ACTION_APP_CONFIGURE: { + if (!data.app) return ''; + app = data.app; + + if ('accessRestriction' in data) { // since it can be null + return 'Access restriction ' + appName('of', app) + ' was changed'; + } else if ('operators' in data) { + return 'Operators ' + appName('of', app) + ' was changed'; + } else if (data.label) { + return `Label ${appName('of', app)} was set to ${data.label}`; + } else if (data.tags) { + return `Tags ${appName('of', app)} was set to ${data.tags.join(', ')}`; + } else if (data.icon) { + return 'Icon ' + appName('of', app) + ' was changed'; + } else if (data.memoryLimit) { + return 'Memory limit ' + appName('of', app) + ' was set to ' + prettyBinarySize(data.memoryLimit); + } else if (data.cpuShares) { // replaced by cpuQuota in 8.0 + return 'CPU shares ' + appName('of', app) + ' was set to ' + Math.round((data.cpuShares * 100)/1024) + '%'; + } else if (data.cpuQuota) { + return 'CPU quota ' + appName('of', app) + ' was set to ' + data.cpuQuota + '%'; + } else if (data.env) { + return 'Env vars ' + appName('of', app) + ' was changed'; + } else if ('debugMode' in data) { // since it can be null + if (data.debugMode) return appName('', app, 'App') + ' was placed in repair mode'; + else return appName('', app, 'App') + ' was taken out of repair mode'; + } else if ('enableBackup' in data) { + return 'Automatic backups ' + appName('of', app) + ' was ' + (data.enableBackup ? 'enabled' : 'disabled'); + } else if ('enableAutomaticUpdate' in data) { + return 'Automatic updates ' + appName('of', app) + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled'); + } else if ('reverseProxyConfig' in data) { + return 'Reverse proxy configuration ' + appName('of', app) + ' was updated'; + } else if ('upstreamUri' in data) { + return 'Upstream URI ' + appName('of', app) + ' was updated'; + } else if ('cert' in data) { + if (data.cert) return 'Custom certificate was set ' + appName('for', app); + else return 'Certificate ' + appName('of', app) + ' was reset'; + } else if (data.subdomain) { + if (data.fqdn !== data.app.fqdn) { + return 'Location ' + appName('of', app) + ' was changed to ' + data.fqdn; + // TODO below we used to use angular.equals(data.redirectDomains, data.app.redirectDomains) + } else if (data.redirectDomains !== data.app.redirectDomains) { + const altFqdns = data.redirectDomains.map(a => a.fqdn); + return 'Alternate domains ' + appName('of', app) + ' was ' + (altFqdns.length ? 'set to ' + altFqdns.join(', ') : 'reset'); + // TODO below we used to use angular.equals(data.aliasDomains, data.app.aliasDomains) + } else if (data.aliasDomains !== data.app.aliasDomains) { + const aliasDomains = data.aliasDomains.map(a => a.fqdn); + return 'Alias domains ' + appName('of', app) + ' was ' + (aliasDomains.length ? 'set to ' + aliasDomains.join(', ') : 'reset'); + // TODO below we used to use angular.equals(data.portBindings, data.app.portBindings) + } else if (data.portBindings !== data.app.portBindings) { + return 'Port bindings ' + appName('of', app) + ' was changed'; + } + } else if ('dataDir' in data) { + if (data.dataDir) return 'Data directory ' + appName('of', app) + ' was set ' + data.dataDir; + else return 'Data directory ' + appName('of', app) + ' was reset'; + } else if ('icon' in data) { + if (data.icon) return 'Icon ' + appName('of', app) + ' was set'; + else return 'Icon ' + appName('of', app) + ' was reset'; + } else if ('mailboxName' in data) { + if (data.mailboxName) return `Mailbox ${appName('of', app)} was set to ${data.mailboxDisplayName || '' } ${data.mailboxName}@${data.mailboxDomain}`; + else return 'Mailbox ' + appName('of', app) + ' was disabled'; + } + + return appName('', app, 'App ') + 'was re-configured'; + } + + case ACTION_APP_INSTALL: + if (!data.app) return ''; + return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed ' + appName('at', data.app) + eventBy(); + + case ACTION_APP_RESTORE: + if (!data.app) return ''; + details = appName('', data.app, 'App') + ' was restored'; + // older versions (<3.5) did not have these fields + if (data.fromManifest) details += ' from version ' + data.fromManifest.version; + if (data.toManifest) details += ' to version ' + data.toManifest.version; + if (data.remotePath) details += ' using backup at ' + data.remotePath; + return details; + + case ACTION_APP_IMPORT: + if (!data.app) return ''; + details = appName('', data.app, 'App') + ' was imported'; + if (data.toManifest) details += ' to version ' + data.toManifest.version; + if (data.remotePath) details += ' using backup at ' + data.remotePath; + return details; + + case ACTION_APP_UNINSTALL: + if (!data.app) return ''; + return appName('', data.app, 'App') + ' (package v' + data.app.manifest.version + ') was uninstalled'; + + case ACTION_APP_UPDATE: + if (!data.app) return ''; + return 'Update ' + appName('of', data.app) + ' started from v' + data.fromManifest.version + ' to v' + data.toManifest.version; + + case ACTION_APP_UPDATE_FINISH: + if (!data.app) return ''; + return appName('', data.app, 'App') + ' was updated to v' + data.app.manifest.version; + + case ACTION_APP_BACKUP: + if (!data.app) return ''; + return 'Backup ' + appName('of', data.app) + ' started'; + + case ACTION_APP_BACKUP_FINISH: + if (!data.app) return ''; + if (data.errorMessage) return 'Backup ' + appName('of', data.app) + ' failed: ' + data.errorMessage; + else return 'Backup ' + appName('of', data.app) + ' succeeded with backup id ' + data.backupId + ' at ' + data.remotePath; + + case ACTION_APP_CLONE: + if (appIdContext === data.oldAppId) return 'App was cloned to ' + data.newApp.fqdn + ' using backup at ' + data.remotePath; + else if (appIdContext === data.appId) return 'App was cloned from ' + data.oldApp.fqdn + ' using backup at ' + data.remotePath; + else return appName('', data.newApp, 'App') + ' was cloned ' + appName('from', data.oldApp) + ' using backup at ' + data.remotePath; + + case ACTION_APP_REPAIR: + return appName('', data.app, 'App') + ' was re-configured'; // re-configure of email apps is more common? + + case ACTION_APP_LOGIN: { + // const app = getApp(data.appId); + if (!app) return ''; + return 'App ' + app.fqdn + ' logged in'; + } + + case ACTION_APP_OOM: + if (!data.app) return ''; + return appName('', data.app, 'App') + ' ran out of memory'; + + case ACTION_APP_DOWN: + if (!data.app) return ''; + return appName('', data.app, 'App') + ' is down'; + + case ACTION_APP_UP: + if (!data.app) return ''; + return appName('', data.app, 'App') + ' is back online'; + + case ACTION_APP_START: + if (!data.app) return ''; + return appName('', data.app, 'App') + ' was started'; + + case ACTION_APP_STOP: + if (!data.app) return ''; + return appName('', data.app, 'App') + ' was stopped'; + + case ACTION_APP_RESTART: + if (!data.app) return ''; + return appName('', data.app, 'App') + ' was restarted'; + + case ACTION_ARCHIVES_ADD: + return 'Backup ' + data.backupId + ' added to archive'; + + case ACTION_ARCHIVES_DEL: + return 'Backup ' + data.backupId + ' deleted from archive'; + + case ACTION_BACKUP_START: + return 'Backup started'; + + case ACTION_BACKUP_FINISH: + if (!errorMessage) return 'Cloudron backup created at ' + data.remotePath; + else return 'Cloudron backup errored with error: ' + errorMessage; + + case ACTION_BACKUP_CLEANUP_START: + return 'Backup cleaner started'; + + case ACTION_BACKUP_CLEANUP_FINISH: + return data.errorMessage ? 'Backup cleaner errored: ' + data.errorMessage : 'Backup cleaner removed ' + (data.removedBoxBackupPaths ? data.removedBoxBackupPaths.length : '0') + ' backups'; + + case ACTION_BRANDING_AVATAR: + return 'Cloudron Avatar Changed'; + + case ACTION_BRANDING_NAME: + return 'Cloudron Name set to ' + data.name; + + case ACTION_BRANDING_FOOTER: + return 'Cloudron Footer set to ' + data.footer; + + case ACTION_CERTIFICATE_NEW: + return 'Certificate install for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); + + case ACTION_CERTIFICATE_RENEWAL: + return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); + + case ACTION_CERTIFICATE_CLEANUP: + return 'Certificate(s) of ' + data.domains.join(',') + ' was cleaned up since they expired 6 months ago'; + + case ACTION_DASHBOARD_DOMAIN_UPDATE: + return 'Dashboard domain set to ' + data.fqdn || (data.subdomain + '.' + data.domain); + + case ACTION_DIRECTORY_SERVER_CONFIGURE: + if (data.fromEnabled !== data.toEnabled) return 'Directory server was ' + (data.toEnabled ? 'enabled' : 'disabled'); + else return 'Directory server configuration was changed'; + + case ACTION_DOMAIN_ADD: + return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was added'; + + case ACTION_DOMAIN_UPDATE: + return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was updated'; + + case ACTION_DOMAIN_REMOVE: + return 'Domain ' + data.domain + ' was removed'; + + case ACTION_EXTERNAL_LDAP_CONFIGURE: + if (data.config.provider === 'noop') return 'External Directory disabled'; + else return 'External Directory set to ' + data.config.url + ' (' + data.config.provider + ')'; + + case ACTION_GROUP_ADD: + return 'Group ' + data.name + ' was added'; + + case ACTION_GROUP_UPDATE: + return 'Group name changed from ' + data.oldName + ' to ' + data.group.name; + + case ACTION_GROUP_REMOVE: + return 'Group ' + data.group.name + ' was removed'; + + case ACTION_GROUP_MEMBERSHIP: + return 'Group membership of ' + data.group.name + ' changed. Now was ' + data.userIds.length + ' member(s).'; + + case ACTION_INSTALL_FINISH: + return 'Cloudron version ' + data.version + ' installed'; + + case ACTION_MAIL_LOCATION: + return 'Mail server location was changed to ' + data.subdomain + (data.subdomain ? '.' : '') + data.domain; + + case ACTION_MAIL_ENABLED: + return 'Mail was enabled for domain ' + data.domain; + + case ACTION_MAIL_DISABLED: + return 'Mail was disabled for domain ' + data.domain; + + case ACTION_MAIL_MAILBOX_ADD: + return 'Mailbox ' + data.name + '@' + data.domain + ' was added'; + + case ACTION_MAIL_MAILBOX_UPDATE: + if (data.aliases) return 'Mailbox aliases of ' + data.name + '@' + data.domain + ' was updated'; + else return 'Mailbox ' + data.name + '@' + data.domain + ' was updated'; + + case ACTION_MAIL_MAILBOX_REMOVE: + return 'Mailbox ' + data.name + '@' + data.domain + ' was removed'; + + case ACTION_MAIL_LIST_ADD: + return 'Mail list ' + data.name + '@' + data.domain + 'was added'; + + case ACTION_MAIL_LIST_UPDATE: + return 'Mail list ' + data.name + '@' + data.domain + ' was updated'; + + case ACTION_MAIL_LIST_REMOVE: + return 'Mail list ' + data.name + '@' + data.domain + ' was removed'; + + case ACTION_START: + return 'Cloudron started with version ' + data.version; + + case ACTION_SERVICE_CONFIGURE: + return 'Service ' + data.id + ' was configured'; + + case ACTION_SERVICE_REBUILD: + return 'Service ' + data.id + ' was rebuilt'; + + case ACTION_SERVICE_RESTART: + return 'Service ' + data.id + ' was restarted'; + + case ACTION_UPDATE: + return 'Cloudron update to version ' + data.boxUpdateInfo.version + ' was started'; + + case ACTION_UPDATE_FINISH: + if (data.errorMessage) return 'Cloudron update errored. Error: ' + data.errorMessage; + else return 'Cloudron updated to version ' + data.newVersion; + + case ACTION_USER_ADD: + return 'User ' + data.email + (data.user.username ? ' (' + data.user.username + ')' : '') + ' was added'; + + case ACTION_USER_UPDATE: + return 'User ' + (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was updated'; + + case ACTION_USER_REMOVE: + return 'User ' + (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was removed'; + + case ACTION_USER_TRANSFER: + return 'Apps of ' + data.oldOwnerId + ' was transferred to ' + data.newOwnerId; + + case ACTION_USER_LOGIN: + if (data.mailboxId) { + return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in to mailbox ' + data.mailboxId; + } else if (data.appId) { + // const app = getApp(data.appId); + return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in to ' + (app ? app.fqdn : data.appId); + } else { // can happen with directoryserver + return 'User ' + (data.user ? data.user.username : data.userId) + ' authenticated'; + } + + case ACTION_USER_LOGIN_GHOST: + return 'User ' + (data.user ? data.user.username : data.userId) + ' was impersonated'; + + case ACTION_USER_LOGOUT: + return 'User ' + (data.user ? data.user.username : data.userId) + ' logged out'; + + case ACTION_USER_DIRECTORY_PROFILE_CONFIG_UPDATE: + return 'User directory profile config updated. Mandatory 2FA: ' + (data.config.mandatory2FA) + ' Lock profiles: ' + (data.config.lockUserProfiles); + + case ACTION_DYNDNS_UPDATE: { + details = data.errorMessage ? 'Error updating DNS. ' : 'Updated DNS. '; + if (data.fromIpv4 !== data.toIpv4) details += 'From IPv4 ' + data.fromIpv4 + ' to ' + data.toIpv4 + '. '; + if (data.fromIpv6 !== data.toIpv6) details += 'From IPv6 ' + data.fromIpv6 + ' to ' + data.toIpv6 + '.'; + if (data.errorMessage) details += ' ' + data.errorMessage; + return details; + } + + case ACTION_SUPPORT_SSH: + return 'Remote Support was ' + (data.enable ? 'enabled' : 'disabled'); + + case ACTION_SUPPORT_TICKET: + return 'Support ticket was created'; + + case ACTION_VOLUME_ADD: + return 'Volume "' + data.volume.name + '" was added'; + + case ACTION_VOLUME_UPDATE: + return 'Volme "' + data.volume.name + '" was updated'; + + case ACTION_VOLUME_REMOVE: + return 'Volume "' + data.volume.name + '" was removed'; + + default: + return eventLog.action; + } +} + +function eventlogSource(eventlog, app = null) { + const source = eventlog.source; + + let line = source.username || source.userId || source.mailboxId || source.authType || 'system'; + if (source.appId) { + // const app = null; //getApp(source.appId); + line += ' - ' + (app ? app.fqdn : source.appId); + } + + return line; +} + // named exports export { download, mountlike, s3like, + eventlogDetails, + eventlogSource, }; // default export @@ -38,4 +496,6 @@ export default { download, mountlike, s3like, + eventlogDetails, + eventlogSource, }; diff --git a/dashboard/src/views/AppConfigureView.vue b/dashboard/src/views/AppConfigureView.vue index 3f0dae942..f80680b27 100644 --- a/dashboard/src/views/AppConfigureView.vue +++ b/dashboard/src/views/AppConfigureView.vue @@ -9,6 +9,7 @@ const t = i18n.t; import { ref, onMounted, onUnmounted, useTemplateRef, computed } from 'vue'; import { Button, ButtonGroup, TabView } from 'pankow'; import Info from '../components/app/Info.vue'; +import Eventlog from '../components/app/Eventlog.vue'; import Uninstall from '../components/app/Uninstall.vue'; import AppsModel from '../models/AppsModel.js'; import { APP_TYPES } from '../constants.js'; @@ -167,7 +168,7 @@ onUnmounted(() => { - + @@ -181,7 +182,7 @@ onUnmounted(() => { - + diff --git a/dashboard/src/views/EventlogView.vue b/dashboard/src/views/EventlogView.vue index 03580ca51..ac99570cf 100644 --- a/dashboard/src/views/EventlogView.vue +++ b/dashboard/src/views/EventlogView.vue @@ -6,9 +6,10 @@ const t = i18n.t; import { ref, reactive, onMounted, watch } from 'vue'; import { Button, ButtonGroup, Dropdown, Spinner, TextInput, MultiSelect } from 'pankow'; -import { useDebouncedRef, prettyDate, prettyLongDate, prettyBinarySize } from 'pankow/utils'; +import { useDebouncedRef, prettyDate, prettyLongDate } from 'pankow/utils'; import AppsModel from '../models/AppsModel.js'; import EventlogsModel from '../models/EventlogsModel.js'; +import { eventlogDetails, eventlogSource } from '../utils.js'; const appsModel = AppsModel.create(); const eventlogsModel = EventlogsModel.create(); @@ -17,459 +18,6 @@ function getApp(id) { return apps.value.find(a => a.id === id); } -function eventlogDetails(eventLog, appIdContext) { - const ACTION_ACTIVATE = 'cloudron.activate'; - const ACTION_PROVISION = 'cloudron.provision'; - const ACTION_RESTORE = 'cloudron.restore'; - - const ACTION_APP_CLONE = 'app.clone'; - const ACTION_APP_REPAIR = 'app.repair'; - const ACTION_APP_CONFIGURE = 'app.configure'; - const ACTION_APP_INSTALL = 'app.install'; - const ACTION_APP_RESTORE = 'app.restore'; - const ACTION_APP_IMPORT = 'app.import'; - const ACTION_APP_UNINSTALL = 'app.uninstall'; - const ACTION_APP_UPDATE = 'app.update'; - const ACTION_APP_UPDATE_FINISH = 'app.update.finish'; - const ACTION_APP_BACKUP = 'app.backup'; - const ACTION_APP_BACKUP_FINISH = 'app.backup.finish'; - const ACTION_APP_LOGIN = 'app.login'; - const ACTION_APP_OOM = 'app.oom'; - const ACTION_APP_UP = 'app.up'; - const ACTION_APP_DOWN = 'app.down'; - const ACTION_APP_START = 'app.start'; - const ACTION_APP_STOP = 'app.stop'; - const ACTION_APP_RESTART = 'app.restart'; - - const ACTION_ARCHIVES_ADD = 'archives.add'; - const ACTION_ARCHIVES_DEL = 'archives.del'; - - const ACTION_BACKUP_FINISH = 'backup.finish'; - const ACTION_BACKUP_START = 'backup.start'; - const ACTION_BACKUP_CLEANUP_START = 'backup.cleanup.start'; - const ACTION_BACKUP_CLEANUP_FINISH = 'backup.cleanup.finish'; - - const ACTION_BRANDING_AVATAR = 'branding.avatar'; - const ACTION_BRANDING_NAME = 'branding.name'; - const ACTION_BRANDING_FOOTER = 'branding.footer'; - - const ACTION_CERTIFICATE_NEW = 'certificate.new'; - const ACTION_CERTIFICATE_RENEWAL = 'certificate.renew'; - const ACTION_CERTIFICATE_CLEANUP = 'certificate.cleanup'; - - const ACTION_DASHBOARD_DOMAIN_UPDATE = 'dashboard.domain.update'; - - const ACTION_DIRECTORY_SERVER_CONFIGURE = 'directoryserver.configure'; - - const ACTION_DOMAIN_ADD = 'domain.add'; - const ACTION_DOMAIN_UPDATE = 'domain.update'; - const ACTION_DOMAIN_REMOVE = 'domain.remove'; - - const ACTION_EXTERNAL_LDAP_CONFIGURE = 'externalldap.configure'; - - const ACTION_GROUP_ADD = 'group.add'; - const ACTION_GROUP_UPDATE = 'group.update'; - const ACTION_GROUP_REMOVE = 'group.remove'; - const ACTION_GROUP_MEMBERSHIP = 'group.membership'; - - const ACTION_INSTALL_FINISH = 'cloudron.install.finish'; - - const ACTION_START = 'cloudron.start'; - const ACTION_SERVICE_CONFIGURE = 'service.configure'; - const ACTION_SERVICE_REBUILD = 'service.rebuild'; - const ACTION_SERVICE_RESTART = 'service.restart'; - const ACTION_UPDATE = 'cloudron.update'; - const ACTION_UPDATE_FINISH = 'cloudron.update.finish'; - const ACTION_USER_ADD = 'user.add'; - const ACTION_USER_LOGIN = 'user.login'; - const ACTION_USER_LOGIN_GHOST = 'user.login.ghost'; - const ACTION_USER_LOGOUT = 'user.logout'; - const ACTION_USER_REMOVE = 'user.remove'; - const ACTION_USER_UPDATE = 'user.update'; - const ACTION_USER_TRANSFER = 'user.transfer'; - - const ACTION_USER_DIRECTORY_PROFILE_CONFIG_UPDATE = 'userdirectory.profileconfig.update'; - - const ACTION_MAIL_LOCATION = 'mail.location'; - const ACTION_MAIL_ENABLED = 'mail.enabled'; - const ACTION_MAIL_DISABLED = 'mail.disabled'; - const ACTION_MAIL_MAILBOX_ADD = 'mail.box.add'; - const ACTION_MAIL_MAILBOX_UPDATE = 'mail.box.update'; - const ACTION_MAIL_MAILBOX_REMOVE = 'mail.box.remove'; - const ACTION_MAIL_LIST_ADD = 'mail.list.add'; - const ACTION_MAIL_LIST_UPDATE = 'mail.list.update'; - const ACTION_MAIL_LIST_REMOVE = 'mail.list.remove'; - - const ACTION_SUPPORT_TICKET = 'support.ticket'; - const ACTION_SUPPORT_SSH = 'support.ssh'; - - const ACTION_VOLUME_ADD = 'volume.add'; - const ACTION_VOLUME_UPDATE = 'volume.update'; - const ACTION_VOLUME_REMOVE = 'volume.remove'; - - const ACTION_DYNDNS_UPDATE = 'dyndns.update'; - - const data = eventLog.data; - const errorMessage = data.errorMessage; - let details, app; - - function appName(pre, app, defaultValue) { - if (appIdContext) return defaultValue || ''; - - pre = pre ? (pre + ' ') : ''; - - return pre + (app.label || app.fqdn || app.subdomain) + ' (' + app.manifest.title + ') '; - } - - function eventBy() { - if (eventLog.source && eventLog.source.username) return ' by ' + eventLog.source.username; - return ''; - } - - switch (eventLog.action) { - case ACTION_ACTIVATE: - return 'Cloudron was activated'; - - case ACTION_PROVISION: - return 'Cloudron was setup'; - - case ACTION_RESTORE: - return 'Cloudron was restored using backup at ' + data.remotePath; - - case ACTION_APP_CONFIGURE: { - if (!data.app) return ''; - app = data.app; - - if ('accessRestriction' in data) { // since it can be null - return 'Access restriction ' + appName('of', app) + ' was changed'; - } else if ('operators' in data) { - return 'Operators ' + appName('of', app) + ' was changed'; - } else if (data.label) { - return `Label ${appName('of', app)} was set to ${data.label}`; - } else if (data.tags) { - return `Tags ${appName('of', app)} was set to ${data.tags.join(', ')}`; - } else if (data.icon) { - return 'Icon ' + appName('of', app) + ' was changed'; - } else if (data.memoryLimit) { - return 'Memory limit ' + appName('of', app) + ' was set to ' + prettyBinarySize(data.memoryLimit); - } else if (data.cpuShares) { // replaced by cpuQuota in 8.0 - return 'CPU shares ' + appName('of', app) + ' was set to ' + Math.round((data.cpuShares * 100)/1024) + '%'; - } else if (data.cpuQuota) { - return 'CPU quota ' + appName('of', app) + ' was set to ' + data.cpuQuota + '%'; - } else if (data.env) { - return 'Env vars ' + appName('of', app) + ' was changed'; - } else if ('debugMode' in data) { // since it can be null - if (data.debugMode) return appName('', app, 'App') + ' was placed in repair mode'; - else return appName('', app, 'App') + ' was taken out of repair mode'; - } else if ('enableBackup' in data) { - return 'Automatic backups ' + appName('of', app) + ' was ' + (data.enableBackup ? 'enabled' : 'disabled'); - } else if ('enableAutomaticUpdate' in data) { - return 'Automatic updates ' + appName('of', app) + ' was ' + (data.enableAutomaticUpdate ? 'enabled' : 'disabled'); - } else if ('reverseProxyConfig' in data) { - return 'Reverse proxy configuration ' + appName('of', app) + ' was updated'; - } else if ('upstreamUri' in data) { - return 'Upstream URI ' + appName('of', app) + ' was updated'; - } else if ('cert' in data) { - if (data.cert) return 'Custom certificate was set ' + appName('for', app); - else return 'Certificate ' + appName('of', app) + ' was reset'; - } else if (data.subdomain) { - if (data.fqdn !== data.app.fqdn) { - return 'Location ' + appName('of', app) + ' was changed to ' + data.fqdn; - // TODO below we used to use angular.equals(data.redirectDomains, data.app.redirectDomains) - } else if (data.redirectDomains !== data.app.redirectDomains) { - const altFqdns = data.redirectDomains.map(a => a.fqdn); - return 'Alternate domains ' + appName('of', app) + ' was ' + (altFqdns.length ? 'set to ' + altFqdns.join(', ') : 'reset'); - // TODO below we used to use angular.equals(data.aliasDomains, data.app.aliasDomains) - } else if (data.aliasDomains !== data.app.aliasDomains) { - const aliasDomains = data.aliasDomains.map(a => a.fqdn); - return 'Alias domains ' + appName('of', app) + ' was ' + (aliasDomains.length ? 'set to ' + aliasDomains.join(', ') : 'reset'); - // TODO below we used to use angular.equals(data.portBindings, data.app.portBindings) - } else if (data.portBindings !== data.app.portBindings) { - return 'Port bindings ' + appName('of', app) + ' was changed'; - } - } else if ('dataDir' in data) { - if (data.dataDir) return 'Data directory ' + appName('of', app) + ' was set ' + data.dataDir; - else return 'Data directory ' + appName('of', app) + ' was reset'; - } else if ('icon' in data) { - if (data.icon) return 'Icon ' + appName('of', app) + ' was set'; - else return 'Icon ' + appName('of', app) + ' was reset'; - } else if ('mailboxName' in data) { - if (data.mailboxName) return `Mailbox ${appName('of', app)} was set to ${data.mailboxDisplayName || '' } ${data.mailboxName}@${data.mailboxDomain}`; - else return 'Mailbox ' + appName('of', app) + ' was disabled'; - } - - return appName('', app, 'App ') + 'was re-configured'; - } - - case ACTION_APP_INSTALL: - if (!data.app) return ''; - return data.app.manifest.title + ' (package v' + data.app.manifest.version + ') was installed ' + appName('at', data.app) + eventBy(); - - case ACTION_APP_RESTORE: - if (!data.app) return ''; - details = appName('', data.app, 'App') + ' was restored'; - // older versions (<3.5) did not have these fields - if (data.fromManifest) details += ' from version ' + data.fromManifest.version; - if (data.toManifest) details += ' to version ' + data.toManifest.version; - if (data.remotePath) details += ' using backup at ' + data.remotePath; - return details; - - case ACTION_APP_IMPORT: - if (!data.app) return ''; - details = appName('', data.app, 'App') + ' was imported'; - if (data.toManifest) details += ' to version ' + data.toManifest.version; - if (data.remotePath) details += ' using backup at ' + data.remotePath; - return details; - - case ACTION_APP_UNINSTALL: - if (!data.app) return ''; - return appName('', data.app, 'App') + ' (package v' + data.app.manifest.version + ') was uninstalled'; - - case ACTION_APP_UPDATE: - if (!data.app) return ''; - return 'Update ' + appName('of', data.app) + ' started from v' + data.fromManifest.version + ' to v' + data.toManifest.version; - - case ACTION_APP_UPDATE_FINISH: - if (!data.app) return ''; - return appName('', data.app, 'App') + ' was updated to v' + data.app.manifest.version; - - case ACTION_APP_BACKUP: - if (!data.app) return ''; - return 'Backup ' + appName('of', data.app) + ' started'; - - case ACTION_APP_BACKUP_FINISH: - if (!data.app) return ''; - if (data.errorMessage) return 'Backup ' + appName('of', data.app) + ' failed: ' + data.errorMessage; - else return 'Backup ' + appName('of', data.app) + ' succeeded with backup id ' + data.backupId + ' at ' + data.remotePath; - - case ACTION_APP_CLONE: - if (appIdContext === data.oldAppId) return 'App was cloned to ' + data.newApp.fqdn + ' using backup at ' + data.remotePath; - else if (appIdContext === data.appId) return 'App was cloned from ' + data.oldApp.fqdn + ' using backup at ' + data.remotePath; - else return appName('', data.newApp, 'App') + ' was cloned ' + appName('from', data.oldApp) + ' using backup at ' + data.remotePath; - - case ACTION_APP_REPAIR: - return appName('', data.app, 'App') + ' was re-configured'; // re-configure of email apps is more common? - - case ACTION_APP_LOGIN: { - const app = getApp(data.appId); - if (!app) return ''; - return 'App ' + app.fqdn + ' logged in'; - } - - case ACTION_APP_OOM: - if (!data.app) return ''; - return appName('', data.app, 'App') + ' ran out of memory'; - - case ACTION_APP_DOWN: - if (!data.app) return ''; - return appName('', data.app, 'App') + ' is down'; - - case ACTION_APP_UP: - if (!data.app) return ''; - return appName('', data.app, 'App') + ' is back online'; - - case ACTION_APP_START: - if (!data.app) return ''; - return appName('', data.app, 'App') + ' was started'; - - case ACTION_APP_STOP: - if (!data.app) return ''; - return appName('', data.app, 'App') + ' was stopped'; - - case ACTION_APP_RESTART: - if (!data.app) return ''; - return appName('', data.app, 'App') + ' was restarted'; - - case ACTION_ARCHIVES_ADD: - return 'Backup ' + data.backupId + ' added to archive'; - - case ACTION_ARCHIVES_DEL: - return 'Backup ' + data.backupId + ' deleted from archive'; - - case ACTION_BACKUP_START: - return 'Backup started'; - - case ACTION_BACKUP_FINISH: - if (!errorMessage) return 'Cloudron backup created at ' + data.remotePath; - else return 'Cloudron backup errored with error: ' + errorMessage; - - case ACTION_BACKUP_CLEANUP_START: - return 'Backup cleaner started'; - - case ACTION_BACKUP_CLEANUP_FINISH: - return data.errorMessage ? 'Backup cleaner errored: ' + data.errorMessage : 'Backup cleaner removed ' + (data.removedBoxBackupPaths ? data.removedBoxBackupPaths.length : '0') + ' backups'; - - case ACTION_BRANDING_AVATAR: - return 'Cloudron Avatar Changed'; - - case ACTION_BRANDING_NAME: - return 'Cloudron Name set to ' + data.name; - - case ACTION_BRANDING_FOOTER: - return 'Cloudron Footer set to ' + data.footer; - - case ACTION_CERTIFICATE_NEW: - return 'Certificate install for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); - - case ACTION_CERTIFICATE_RENEWAL: - return 'Certificate renewal for ' + data.domain + (errorMessage ? ' failed' : ' succeeded'); - - case ACTION_CERTIFICATE_CLEANUP: - return 'Certificate(s) of ' + data.domains.join(',') + ' was cleaned up since they expired 6 months ago'; - - case ACTION_DASHBOARD_DOMAIN_UPDATE: - return 'Dashboard domain set to ' + data.fqdn || (data.subdomain + '.' + data.domain); - - case ACTION_DIRECTORY_SERVER_CONFIGURE: - if (data.fromEnabled !== data.toEnabled) return 'Directory server was ' + (data.toEnabled ? 'enabled' : 'disabled'); - else return 'Directory server configuration was changed'; - - case ACTION_DOMAIN_ADD: - return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was added'; - - case ACTION_DOMAIN_UPDATE: - return 'Domain ' + data.domain + ' with ' + data.provider + ' provider was updated'; - - case ACTION_DOMAIN_REMOVE: - return 'Domain ' + data.domain + ' was removed'; - - case ACTION_EXTERNAL_LDAP_CONFIGURE: - if (data.config.provider === 'noop') return 'External Directory disabled'; - else return 'External Directory set to ' + data.config.url + ' (' + data.config.provider + ')'; - - case ACTION_GROUP_ADD: - return 'Group ' + data.name + ' was added'; - - case ACTION_GROUP_UPDATE: - return 'Group name changed from ' + data.oldName + ' to ' + data.group.name; - - case ACTION_GROUP_REMOVE: - return 'Group ' + data.group.name + ' was removed'; - - case ACTION_GROUP_MEMBERSHIP: - return 'Group membership of ' + data.group.name + ' changed. Now was ' + data.userIds.length + ' member(s).'; - - case ACTION_INSTALL_FINISH: - return 'Cloudron version ' + data.version + ' installed'; - - case ACTION_MAIL_LOCATION: - return 'Mail server location was changed to ' + data.subdomain + (data.subdomain ? '.' : '') + data.domain; - - case ACTION_MAIL_ENABLED: - return 'Mail was enabled for domain ' + data.domain; - - case ACTION_MAIL_DISABLED: - return 'Mail was disabled for domain ' + data.domain; - - case ACTION_MAIL_MAILBOX_ADD: - return 'Mailbox ' + data.name + '@' + data.domain + ' was added'; - - case ACTION_MAIL_MAILBOX_UPDATE: - if (data.aliases) return 'Mailbox aliases of ' + data.name + '@' + data.domain + ' was updated'; - else return 'Mailbox ' + data.name + '@' + data.domain + ' was updated'; - - case ACTION_MAIL_MAILBOX_REMOVE: - return 'Mailbox ' + data.name + '@' + data.domain + ' was removed'; - - case ACTION_MAIL_LIST_ADD: - return 'Mail list ' + data.name + '@' + data.domain + 'was added'; - - case ACTION_MAIL_LIST_UPDATE: - return 'Mail list ' + data.name + '@' + data.domain + ' was updated'; - - case ACTION_MAIL_LIST_REMOVE: - return 'Mail list ' + data.name + '@' + data.domain + ' was removed'; - - case ACTION_START: - return 'Cloudron started with version ' + data.version; - - case ACTION_SERVICE_CONFIGURE: - return 'Service ' + data.id + ' was configured'; - - case ACTION_SERVICE_REBUILD: - return 'Service ' + data.id + ' was rebuilt'; - - case ACTION_SERVICE_RESTART: - return 'Service ' + data.id + ' was restarted'; - - case ACTION_UPDATE: - return 'Cloudron update to version ' + data.boxUpdateInfo.version + ' was started'; - - case ACTION_UPDATE_FINISH: - if (data.errorMessage) return 'Cloudron update errored. Error: ' + data.errorMessage; - else return 'Cloudron updated to version ' + data.newVersion; - - case ACTION_USER_ADD: - return 'User ' + data.email + (data.user.username ? ' (' + data.user.username + ')' : '') + ' was added'; - - case ACTION_USER_UPDATE: - return 'User ' + (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was updated'; - - case ACTION_USER_REMOVE: - return 'User ' + (data.user ? (data.user.email + (data.user.username ? ' (' + data.user.username + ')' : '')) : data.userId) + ' was removed'; - - case ACTION_USER_TRANSFER: - return 'Apps of ' + data.oldOwnerId + ' was transferred to ' + data.newOwnerId; - - case ACTION_USER_LOGIN: - if (data.mailboxId) { - return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in to mailbox ' + data.mailboxId; - } else if (data.appId) { - const app = getApp(data.appId); - return 'User ' + (data.user ? data.user.username : data.userId) + ' logged in to ' + (app ? app.fqdn : data.appId); - } else { // can happen with directoryserver - return 'User ' + (data.user ? data.user.username : data.userId) + ' authenticated'; - } - - case ACTION_USER_LOGIN_GHOST: - return 'User ' + (data.user ? data.user.username : data.userId) + ' was impersonated'; - - case ACTION_USER_LOGOUT: - return 'User ' + (data.user ? data.user.username : data.userId) + ' logged out'; - - case ACTION_USER_DIRECTORY_PROFILE_CONFIG_UPDATE: - return 'User directory profile config updated. Mandatory 2FA: ' + (data.config.mandatory2FA) + ' Lock profiles: ' + (data.config.lockUserProfiles); - - case ACTION_DYNDNS_UPDATE: { - details = data.errorMessage ? 'Error updating DNS. ' : 'Updated DNS. '; - if (data.fromIpv4 !== data.toIpv4) details += 'From IPv4 ' + data.fromIpv4 + ' to ' + data.toIpv4 + '. '; - if (data.fromIpv6 !== data.toIpv6) details += 'From IPv6 ' + data.fromIpv6 + ' to ' + data.toIpv6 + '.'; - if (data.errorMessage) details += ' ' + data.errorMessage; - return details; - } - - case ACTION_SUPPORT_SSH: - return 'Remote Support was ' + (data.enable ? 'enabled' : 'disabled'); - - case ACTION_SUPPORT_TICKET: - return 'Support ticket was created'; - - case ACTION_VOLUME_ADD: - return 'Volume "' + data.volume.name + '" was added'; - - case ACTION_VOLUME_UPDATE: - return 'Volme "' + data.volume.name + '" was updated'; - - case ACTION_VOLUME_REMOVE: - return 'Volume "' + data.volume.name + '" was removed'; - - default: - return eventLog.action; - } -} - -function eventlogSource(eventlog) { - const source = eventlog.source; - - let line = source.username || source.userId || source.mailboxId || source.authType || 'system'; - if (source.appId) { - const app = null; //getApp(source.appId); - line += ' - ' + (app ? app.fqdn : source.appId); - } - - return line; -} - const availableActions = [ { id: 'app.backup' }, { id: 'app.backup.finish' }, @@ -563,8 +111,8 @@ async function onRefresh() { return { id: Symbol(), raw: e, - details: eventlogDetails(e), - source: eventlogSource(e) + details: eventlogDetails(e, e.appId ? getApp(e.appId) : null), + source: eventlogSource(e, e.appId ? getApp(e.appId) : null) }; });