diff --git a/jslib b/jslib index 306aef73d4..92dbf24ab8 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit 306aef73d459dfad8a7a06c32442c9ed2d56922e +Subproject commit 92dbf24ab895443d8f5bd404e749d4fd83f32207 diff --git a/src/app/organizations/manage/entity-events.component.ts b/src/app/organizations/manage/entity-events.component.ts index 007fcdf54c..d69bddc623 100644 --- a/src/app/organizations/manage/entity-events.component.ts +++ b/src/app/organizations/manage/entity-events.component.ts @@ -94,9 +94,9 @@ export class EntityEventsComponent implements OnInit { } catch { } this.continuationToken = response.continuationToken; - const events = response.data.map(r => { + const events = await Promise.all(response.data.map(async r => { const userId = r.actingUserId == null ? r.userId : r.actingUserId; - const eventInfo = this.eventService.getEventInfo(r); + const eventInfo = await this.eventService.getEventInfo(r); const user = this.showUser && userId != null && this.orgUsersUserIdMap.has(userId) ? this.orgUsersUserIdMap.get(userId) : null; return { @@ -110,7 +110,7 @@ export class EntityEventsComponent implements OnInit { ip: r.ipAddress, type: r.type, }; - }); + })); if (!clearExisting && this.events != null && this.events.length > 0) { this.events = this.events.concat(events); diff --git a/src/app/organizations/manage/events.component.html b/src/app/organizations/manage/events.component.html index d09a877415..ed09ca4561 100644 --- a/src/app/organizations/manage/events.component.html +++ b/src/app/organizations/manage/events.component.html @@ -15,6 +15,10 @@ {{'refresh' | i18n}} + diff --git a/src/app/organizations/manage/events.component.ts b/src/app/organizations/manage/events.component.ts index 512b410d1d..3d4a62cf54 100644 --- a/src/app/organizations/manage/events.component.ts +++ b/src/app/organizations/manage/events.component.ts @@ -7,13 +7,16 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; import { ApiService } from 'jslib/abstractions/api.service'; +import { ExportService } from 'jslib/abstractions/export.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { UserService } from 'jslib/abstractions/user.service'; import { EventService } from '../../services/event.service'; import { EventResponse } from 'jslib/models/response/eventResponse'; import { ListResponse } from 'jslib/models/response/listResponse'; +import { EventView } from 'jslib/models/view/eventView'; @Component({ selector: 'app-org-events', @@ -23,19 +26,20 @@ export class EventsComponent implements OnInit { loading = true; loaded = false; organizationId: string; - events: any[]; + events: EventView[]; start: string; end: string; continuationToken: string; refreshPromise: Promise; + exportPromise: Promise; morePromise: Promise; private orgUsersUserIdMap = new Map(); private orgUsersIdMap = new Map(); - constructor(private apiService: ApiService, private route: ActivatedRoute, - private eventService: EventService, private i18nService: I18nService, - private toasterService: ToasterService, private userService: UserService, + constructor(private apiService: ApiService, private route: ActivatedRoute, private eventService: EventService, + private i18nService: I18nService, private toasterService: ToasterService, private userService: UserService, + private exportService: ExportService, private platformUtilsService: PlatformUtilsService, private router: Router) { } async ngOnInit() { @@ -64,8 +68,26 @@ export class EventsComponent implements OnInit { this.loaded = true; } + async exportEvents() { + if (this.appApiPromiseUnfulfilled()) { + return; + } + + this.loading = true; + this.exportPromise = this.exportService.getEventExport(this.events).then(data => { + const fileName = this.exportService.getFileName('org-events', 'csv'); + this.platformUtilsService.saveFile(window, data, { type: 'text/plain' }, fileName); + }); + try { + await this.exportPromise; + } catch { } + + this.exportPromise = null; + this.loading = false; + } + async loadEvents(clearExisting: boolean) { - if (this.refreshPromise != null || this.morePromise != null) { + if (this.appApiPromiseUnfulfilled()) { return; } @@ -92,13 +114,14 @@ export class EventsComponent implements OnInit { } catch { } this.continuationToken = response.continuationToken; - const events = response.data.map(r => { + const events = await Promise.all(response.data.map(async r => { const userId = r.actingUserId == null ? r.userId : r.actingUserId; - const eventInfo = this.eventService.getEventInfo(r); + const eventInfo = await this.eventService.getEventInfo(r); const user = userId != null && this.orgUsersUserIdMap.has(userId) ? this.orgUsersUserIdMap.get(userId) : null; - return { + return new EventView({ message: eventInfo.message, + humanReadableMessage: eventInfo.humanReadableMessage, appIcon: eventInfo.appIcon, appName: eventInfo.appName, userId: userId, @@ -107,8 +130,8 @@ export class EventsComponent implements OnInit { date: r.date, ip: r.ipAddress, type: r.type, - }; - }); + }); + })); if (!clearExisting && this.events != null && this.events.length > 0) { this.events = this.events.concat(events); @@ -120,4 +143,8 @@ export class EventsComponent implements OnInit { this.morePromise = null; this.refreshPromise = null; } + + private appApiPromiseUnfulfilled() { + return this.refreshPromise != null || this.morePromise != null || this.exportPromise != null; + } } diff --git a/src/app/services/event.service.ts b/src/app/services/event.service.ts index 1eefe62222..4c75ff69a1 100644 --- a/src/app/services/event.service.ts +++ b/src/app/services/event.service.ts @@ -1,15 +1,17 @@ import { Injectable } from '@angular/core'; import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PolicyService } from 'jslib/abstractions/policy.service'; import { DeviceType } from 'jslib/enums/deviceType'; import { EventType } from 'jslib/enums/eventType'; +import { PolicyType } from 'jslib/enums/policyType'; import { EventResponse } from 'jslib/models/response/eventResponse'; @Injectable() export class EventService { - constructor(private i18nService: I18nService) { } + constructor(private i18nService: I18nService, private policyService: PolicyService) { } getDefaultDateFilters() { const d = new Date(); @@ -28,146 +30,180 @@ export class EventService { return [start.toISOString(), end.toISOString()]; } - getEventInfo(ev: EventResponse, options = new EventOptions()): EventInfo { + async getEventInfo(ev: EventResponse, options = new EventOptions()): Promise { const appInfo = this.getAppInfo(ev.deviceType); + const { message, humanReadableMessage } = await this.getEventMessage(ev, options); return { - message: this.getEventMessage(ev, options), + message: message, + humanReadableMessage: humanReadableMessage, appIcon: appInfo[0], appName: appInfo[1], }; } - private getEventMessage(ev: EventResponse, options: EventOptions) { + private async getEventMessage(ev: EventResponse, options: EventOptions) { let msg = ''; + let humanReadableMsg = ''; switch (ev.type) { // User case EventType.User_LoggedIn: - msg = this.i18nService.t('loggedIn'); + msg = humanReadableMsg = this.i18nService.t('loggedIn'); break; case EventType.User_ChangedPassword: - msg = this.i18nService.t('changedPassword'); + msg = humanReadableMsg = this.i18nService.t('changedPassword'); break; case EventType.User_Updated2fa: - msg = this.i18nService.t('enabledUpdated2fa'); + msg = humanReadableMsg = this.i18nService.t('enabledUpdated2fa'); break; case EventType.User_Disabled2fa: - msg = this.i18nService.t('disabled2fa'); + msg = humanReadableMsg = this.i18nService.t('disabled2fa'); break; case EventType.User_Recovered2fa: - msg = this.i18nService.t('recovered2fa'); + msg = humanReadableMsg = this.i18nService.t('recovered2fa'); break; case EventType.User_FailedLogIn: - msg = this.i18nService.t('failedLogin'); + msg = humanReadableMsg = this.i18nService.t('failedLogin'); break; case EventType.User_FailedLogIn2fa: - msg = this.i18nService.t('failedLogin2fa'); + msg = humanReadableMsg = this.i18nService.t('failedLogin2fa'); break; case EventType.User_ClientExportedVault: - msg = this.i18nService.t('exportedVault'); + msg = humanReadableMsg = this.i18nService.t('exportedVault'); break; // Cipher case EventType.Cipher_Created: msg = this.i18nService.t('createdItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('createdItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_Updated: msg = this.i18nService.t('editedItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('editedItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_Deleted: msg = this.i18nService.t('permanentlyDeletedItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('permanentlyDeletedItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_SoftDeleted: msg = this.i18nService.t('deletedItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('deletedItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_Restored: msg = this.i18nService.t('restoredItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('restoredItemId', this.formatCipherId(ev, options)); break; case EventType.Cipher_AttachmentCreated: msg = this.i18nService.t('createdAttachmentForItem', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('createdAttachmentForItem', this.getShortId(ev.cipherId)); break; case EventType.Cipher_AttachmentDeleted: msg = this.i18nService.t('deletedAttachmentForItem', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('deletedAttachmentForItem', this.getShortId(ev.cipherId)); break; case EventType.Cipher_Shared: msg = this.i18nService.t('sharedItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('sharedItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_ClientViewed: msg = this.i18nService.t('viewedItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('viewedItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_ClientToggledPasswordVisible: msg = this.i18nService.t('viewedPasswordItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('viewedPasswordItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_ClientToggledHiddenFieldVisible: msg = this.i18nService.t('viewedHiddenFieldItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('viewedHiddenFieldItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_ClientToggledCardCodeVisible: msg = this.i18nService.t('viewedSecurityCodeItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('viewedSecurityCodeItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_ClientCopiedHiddenField: msg = this.i18nService.t('copiedHiddenFieldItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('copiedHiddenFieldItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_ClientCopiedPassword: msg = this.i18nService.t('copiedPasswordItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('copiedPasswordItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_ClientCopiedCardCode: msg = this.i18nService.t('copiedSecurityCodeItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('copiedSecurityCodeItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_ClientAutofilled: msg = this.i18nService.t('autofilledItemId', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('autofilledItemId', this.getShortId(ev.cipherId)); break; case EventType.Cipher_UpdatedCollections: msg = this.i18nService.t('editedCollectionsForItem', this.formatCipherId(ev, options)); + humanReadableMsg = this.i18nService.t('editedCollectionsForItem', this.getShortId(ev.cipherId)); break; // Collection case EventType.Collection_Created: msg = this.i18nService.t('createdCollectionId', this.formatCollectionId(ev)); + humanReadableMsg = this.i18nService.t('createdCollectionId', this.getShortId(ev.collectionId)); break; case EventType.Collection_Updated: msg = this.i18nService.t('editedCollectionId', this.formatCollectionId(ev)); + humanReadableMsg = this.i18nService.t('editedCollectionId', this.getShortId(ev.collectionId)); break; case EventType.Collection_Deleted: msg = this.i18nService.t('deletedCollectionId', this.formatCollectionId(ev)); + humanReadableMsg = this.i18nService.t('deletedCollectionId', this.getShortId(ev.collectionId)); break; // Group case EventType.Group_Created: msg = this.i18nService.t('createdGroupId', this.formatGroupId(ev)); + humanReadableMsg = this.i18nService.t('createdGroupId', this.getShortId(ev.groupId)); break; case EventType.Group_Updated: msg = this.i18nService.t('editedGroupId', this.formatGroupId(ev)); + humanReadableMsg = this.i18nService.t('editedGroupId', this.getShortId(ev.groupId)); break; case EventType.Group_Deleted: msg = this.i18nService.t('deletedGroupId', this.formatGroupId(ev)); + humanReadableMsg = this.i18nService.t('deletedGroupId', this.getShortId(ev.groupId)); break; // Org user case EventType.OrganizationUser_Invited: msg = this.i18nService.t('invitedUserId', this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t('invitedUserId', this.getShortId(ev.organizationUserId)); break; case EventType.OrganizationUser_Confirmed: msg = this.i18nService.t('confirmedUserId', this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t('confirmedUserId', this.getShortId(ev.organizationUserId)); break; case EventType.OrganizationUser_Updated: msg = this.i18nService.t('editedUserId', this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t('editedUserId', this.getShortId(ev.organizationUserId)); break; case EventType.OrganizationUser_Removed: msg = this.i18nService.t('removedUserId', this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t('removedUserId', this.getShortId(ev.organizationUserId)); break; case EventType.OrganizationUser_UpdatedGroups: msg = this.i18nService.t('editedGroupsForUser', this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t('editedGroupsForUser', this.getShortId(ev.organizationUserId)); break; case EventType.OrganizationUser_UnlinkedSso: msg = this.i18nService.t('unlinkedSsoUser', this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t('unlinkedSsoUser', this.getShortId(ev.organizationUserId)); break; case EventType.OrganizationUser_ResetPassword_Enroll: msg = this.i18nService.t('eventEnrollPasswordReset', this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t('eventEnrollPasswordReset', this.getShortId(ev.organizationUserId)); break; case EventType.OrganizationUser_ResetPassword_Withdraw: msg = this.i18nService.t('eventWithdrawPasswordReset', this.formatOrgUserId(ev)); + humanReadableMsg = this.i18nService.t('eventWithdrawPasswordReset', this.getShortId(ev.organizationUserId)); break; // Org case EventType.Organization_Updated: - msg = this.i18nService.t('editedOrgSettings'); + msg = humanReadableMsg = this.i18nService.t('editedOrgSettings'); break; case EventType.Organization_PurgedVault: - msg = this.i18nService.t('purgedOrganizationVault'); + msg = humanReadableMsg = this.i18nService.t('purgedOrganizationVault'); break; /* case EventType.Organization_ClientExportedVault: @@ -176,13 +212,25 @@ export class EventService { */ // Policies case EventType.Policy_Updated: - msg = this.i18nService.t('modifiedPolicy', this.formatPolicyId(ev)); + msg = this.i18nService.t('modifiedPolicyId', this.formatPolicyId(ev)); + + const policies = await this.policyService.getAll(); + const policy = policies.filter(p => p.id === ev.policyId)[0]; + let p1 = this.getShortId(ev.policyId); + if (policy !== null) { + p1 = PolicyType[policy.type]; + } + + humanReadableMsg = this.i18nService.t('modifiedPolicyId', p1); break; default: break; } - return msg === '' ? null : msg; + return { + message: msg === '' ? null : msg, + humanReadableMessage: humanReadableMsg === '' ? null : humanReadableMsg, + }; } private getAppInfo(deviceType: DeviceType): [string, string] { @@ -299,6 +347,7 @@ export class EventService { export class EventInfo { message: string; + humanReadableMessage: string; appIcon: string; appName: string; } diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 719199fa1e..531dd28307 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -817,6 +817,9 @@ "exportMasterPassword": { "message": "Enter your master password to export your vault data." }, + "export": { + "message": "Export" + }, "exportVault": { "message": "Export Vault" },