|
|
|
|
@@ -0,0 +1,669 @@
|
|
|
|
|
<script setup>
|
|
|
|
|
|
|
|
|
|
const API_ORIGIN = import.meta.env.VITE_API_ORIGIN ? import.meta.env.VITE_API_ORIGIN : window.location.origin;
|
|
|
|
|
|
|
|
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
|
const i18n = useI18n();
|
|
|
|
|
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 AppsModel from '../models/AppsModel.js';
|
|
|
|
|
import EventlogsModel from '../models/EventlogsModel.js';
|
|
|
|
|
|
|
|
|
|
const appsModel = AppsModel.create(API_ORIGIN, localStorage.token);
|
|
|
|
|
const eventlogsModel = EventlogsModel.create(API_ORIGIN, localStorage.token);
|
|
|
|
|
|
|
|
|
|
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' },
|
|
|
|
|
{ id: 'app.configure' },
|
|
|
|
|
{ id: 'app.install' },
|
|
|
|
|
{ id: 'app.restore' },
|
|
|
|
|
{ id: 'app.uninstall' },
|
|
|
|
|
{ id: 'app.update' },
|
|
|
|
|
{ id: 'app.update.finish' },
|
|
|
|
|
{ id: 'app.login' },
|
|
|
|
|
{ id: 'app.oom' },
|
|
|
|
|
{ id: 'app.down' },
|
|
|
|
|
{ id: 'app.up' },
|
|
|
|
|
{ id: 'app.start' },
|
|
|
|
|
{ id: 'app.stop' },
|
|
|
|
|
{ id: 'app.restart' },
|
|
|
|
|
{ id: 'backup.cleanup' },
|
|
|
|
|
{ id: 'backup.cleanup.finish' },
|
|
|
|
|
{ id: 'backup.finish' },
|
|
|
|
|
{ id: 'backup.start' },
|
|
|
|
|
{ id: 'branding.avatar' },
|
|
|
|
|
{ id: 'branding.footer' },
|
|
|
|
|
{ id: 'branding.name' },
|
|
|
|
|
{ id: 'certificate.new' },
|
|
|
|
|
{ id: 'certificate.renew' },
|
|
|
|
|
{ id: 'certificate.cleanup' },
|
|
|
|
|
{ id: 'cloudron.activate' },
|
|
|
|
|
{ id: 'cloudron.provision' },
|
|
|
|
|
{ id: 'cloudron.restore' },
|
|
|
|
|
{ id: 'cloudron.start' },
|
|
|
|
|
{ id: 'cloudron.update' },
|
|
|
|
|
{ id: 'cloudron.update.finish' },
|
|
|
|
|
{ id: 'dashboard.domain.update' },
|
|
|
|
|
{ id: 'directoryserver.configure' },
|
|
|
|
|
{ id: 'dyndns.update' },
|
|
|
|
|
{ id: 'domain.add' },
|
|
|
|
|
{ id: 'domain.update' },
|
|
|
|
|
{ id: 'domain.remove' },
|
|
|
|
|
{ id: 'externalldap.configure' },
|
|
|
|
|
{ id: 'group.add' },
|
|
|
|
|
{ id: 'group.update' },
|
|
|
|
|
{ id: 'group.remove' },
|
|
|
|
|
{ id: 'mail.location' },
|
|
|
|
|
{ id: 'mail.enabled' },
|
|
|
|
|
{ id: 'mail.box.add' },
|
|
|
|
|
{ id: 'mail.box.update' },
|
|
|
|
|
{ id: 'mail.box.remove' },
|
|
|
|
|
{ id: 'mail.list.add' },
|
|
|
|
|
{ id: 'mail.list.update' },
|
|
|
|
|
{ id: 'mail.list.remove' },
|
|
|
|
|
{ id: 'service.configure' },
|
|
|
|
|
{ id: 'service.rebuild' },
|
|
|
|
|
{ id: 'service.restart' },
|
|
|
|
|
{ id: 'support.ticket' },
|
|
|
|
|
{ id: 'support.ssh' },
|
|
|
|
|
{ id: 'user.add' },
|
|
|
|
|
{ id: 'user.login' },
|
|
|
|
|
{ id: 'user.login.ghost' },
|
|
|
|
|
{ id: 'user.logout' },
|
|
|
|
|
{ id: 'user.remove' },
|
|
|
|
|
{ id: 'user.transfer' },
|
|
|
|
|
{ id: 'user.update' },
|
|
|
|
|
{ id: 'userdirectory.profileconfig.update' },
|
|
|
|
|
{ id: 'volume.add' },
|
|
|
|
|
{ id: 'volume.update' },
|
|
|
|
|
{ id: 'volume.remove' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const perPageOptions = [
|
|
|
|
|
{ name: t('main.pagination.perPageSelector', { n: 20 }), value: 20 },
|
|
|
|
|
{ name: t('main.pagination.perPageSelector', { n: 50 }), value: 50 },
|
|
|
|
|
{ name: t('main.pagination.perPageSelector', { n: 100 }), value: 100 }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const busy = ref(false);
|
|
|
|
|
const refreshBusy = ref(false);
|
|
|
|
|
const apps = ref([]);
|
|
|
|
|
const eventlogs = ref([]);
|
|
|
|
|
const activeId = ref(null);
|
|
|
|
|
const search = useDebouncedRef('');
|
|
|
|
|
const page = ref(1);
|
|
|
|
|
const perPage = ref(20);
|
|
|
|
|
const actions = reactive([]);
|
|
|
|
|
|
|
|
|
|
async function onRefresh() {
|
|
|
|
|
refreshBusy.value = true;
|
|
|
|
|
const [error, result] = await eventlogsModel.search(actions.map(a => a.id).join(','), search.value, page.value, perPage.value);
|
|
|
|
|
if (error) return console.error(error);
|
|
|
|
|
|
|
|
|
|
eventlogs.value = result.map(e => {
|
|
|
|
|
return {
|
|
|
|
|
id: Symbol(),
|
|
|
|
|
raw: e,
|
|
|
|
|
details: eventlogDetails(e),
|
|
|
|
|
source: eventlogSource(e)
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
refreshBusy.value = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function onEventLogDetails(id) {
|
|
|
|
|
if (activeId.value === id) activeId.value = null;
|
|
|
|
|
else activeId.value = id;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onPrevPage() {
|
|
|
|
|
if (page.value > 1) page.value--;
|
|
|
|
|
else page.value = 1;
|
|
|
|
|
await onRefresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function onNextPage() {
|
|
|
|
|
page.value++;
|
|
|
|
|
await onRefresh();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
watch(perPage, onRefresh);
|
|
|
|
|
watch(actions, onRefresh);
|
|
|
|
|
watch(search, onRefresh);
|
|
|
|
|
|
|
|
|
|
onMounted(async () => {
|
|
|
|
|
busy.value = true;
|
|
|
|
|
|
|
|
|
|
const [error, result] = await appsModel.list();
|
|
|
|
|
if (error) console.error(error);
|
|
|
|
|
else apps.value = result;
|
|
|
|
|
|
|
|
|
|
await onRefresh();
|
|
|
|
|
busy.value = false;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<template>
|
|
|
|
|
<div class="content">
|
|
|
|
|
<h1>{{ $t('eventlog.title') }}</h1>
|
|
|
|
|
|
|
|
|
|
<div class="eventlog-filter">
|
|
|
|
|
<TextInput :placeholder="$t('main.searchPlaceholder')" style="flex-grow: 1;" v-model="search"/>
|
|
|
|
|
|
|
|
|
|
<MultiSelect v-model="actions" :options="availableActions" option-label="id" :selected-label="actions.length ? $t('main.multiselect.selected', { n: actions.length }) : $t('eventlog.filterAllEvents')"/>
|
|
|
|
|
<Dropdown v-model="perPage" :options="perPageOptions" option-key="value" option-label="name"/>
|
|
|
|
|
|
|
|
|
|
<ButtonGroup>
|
|
|
|
|
<Button tool @click="onPrevPage()" :disabled="busy || page <= 1" icon="fa-solid fa-angle-double-left" v-tooltip="$t('main.pagination.prev')" />
|
|
|
|
|
<Button tool @click="onRefresh()" :loading="refreshBusy" icon="fa-solid fa-sync-alt" />
|
|
|
|
|
<Button tool @click="onNextPage()" :disabled="busy || perPage > eventlogs.length" icon="fa-solid fa-angle-double-right" v-tooltip="$t('main.pagination.next')" />
|
|
|
|
|
</ButtonGroup>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<center v-show="busy"><Spinner class="pankow-spinner-large" /></center>
|
|
|
|
|
<table v-show="!busy" class="table table-condensed table-hover">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr>
|
|
|
|
|
<th>{{ $t('eventlog.time') }}</th>
|
|
|
|
|
<th>{{ $t('eventlog.source') }}</th>
|
|
|
|
|
<th>{{ $t('eventlog.details') }}</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody v-for="eventlog in eventlogs" :key="eventlog.id">
|
|
|
|
|
<tr @click="onEventLogDetails(eventlog.id)" class="hand">
|
|
|
|
|
<td><span v-tooltip="prettyLongDate(eventlog.raw.creationTime)" class="arrow">{{ prettyDate(eventlog.raw.creationTime) }}</span></td>
|
|
|
|
|
<td>{{ eventlog.source }}</td>
|
|
|
|
|
<td v-html="eventlog.details"></td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr v-show="activeId === eventlog.id">
|
|
|
|
|
<td colspan="4">
|
|
|
|
|
<p v-show="eventlog.raw.source.ip">Source IP: <code>{{ eventlog.raw.source.ip }}</code></p>
|
|
|
|
|
<pre class="eventlog-details">{{ JSON.stringify(eventlog.raw.data, null, 4) }}</pre>
|
|
|
|
|
</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
|
|
|
|
.eventlog-filter {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 5px;
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.eventlog-details {
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
color: var(--pankow-text-color);
|
|
|
|
|
background-color: var(--pankow-input-background-color);
|
|
|
|
|
border: none;
|
|
|
|
|
border-radius: var(--pankow-border-radius);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
</style>
|