From ddee5908f1486c43de8ca908d6b38232d8f4dbfd Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 20 Aug 2018 13:45:32 -0400 Subject: [PATCH] notification service --- package-lock.json | 72 ++++++++------- package.json | 3 +- src/abstractions/environment.service.ts | 1 + src/abstractions/notifications.service.ts | 6 ++ .../components/environment.component.ts | 4 + src/enums/notificationType.ts | 14 +++ src/models/response/notificationResponse.ts | 71 +++++++++++++++ src/services/environment.service.ts | 6 ++ src/services/notifications.service.ts | 90 +++++++++++++++++++ 9 files changed, 236 insertions(+), 31 deletions(-) create mode 100644 src/abstractions/notifications.service.ts create mode 100644 src/enums/notificationType.ts create mode 100644 src/models/response/notificationResponse.ts create mode 100644 src/services/notifications.service.ts diff --git a/package-lock.json b/package-lock.json index c63ec41307..2a9b8547ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,6 +84,11 @@ "tslib": "^1.7.1" } }, + "@aspnet/signalr": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@aspnet/signalr/-/signalr-1.0.2.tgz", + "integrity": "sha512-sXleqUCCbodCOqUA8MjLSvtAgDTvDhEq6j3JyAq/w4RMJhpZ+dXK9+6xEMbzag2hisq5e/8vDC82JYutkcOISQ==" + }, "@types/form-data": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", @@ -1937,9 +1942,9 @@ "dev": true }, "electron": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.5.tgz", - "integrity": "sha512-NbWsgAvcxxQrDNaLA2L5adZTKWO6mZwC57uSPQiZiFjpO0K6uVNCjFyRbLnhq8AWq2tmcuzs6mFpIzQXmvlnUQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-2.0.7.tgz", + "integrity": "sha512-MRrDE6mrp+ZrIBpZM27pxbO2yEDKYfkmc6Ll79BtedMNEZsY4+oblupeDJL6RM6meUIp82KMo63W7fP65Tb89Q==", "dev": true, "requires": { "@types/node": "^8.0.24", @@ -1948,9 +1953,9 @@ }, "dependencies": { "@types/node": { - "version": "8.10.24", - "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.24.tgz", - "integrity": "sha512-5YaBKa6oFuWy7ptIFMATyftIcpZTZtvgrzPThEbs+kl4Uu41oUxiRunG0k32QZjD6MXMELls//ry/epNxc11aQ==", + "version": "8.10.26", + "resolved": "https://registry.npmjs.org/@types/node/-/node-8.10.26.tgz", + "integrity": "sha512-opk6bLLErLSwyVVJeSH5Ek7ZWOBSsN0JrvXTNVGLXLAXKB9xlTYajrplR44xVyMrmbut94H6uJ9jqzM/12jxkA==", "dev": true } } @@ -3301,12 +3306,12 @@ "dev": true }, "har-validator": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", - "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", "dev": true, "requires": { - "ajv": "^5.1.0", + "ajv": "^5.3.0", "har-schema": "^2.0.0" } }, @@ -5358,9 +5363,9 @@ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", "dev": true }, "object-assign": { @@ -5878,6 +5883,12 @@ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", "dev": true }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==", + "dev": true + }, "pstree.remy": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.0.tgz", @@ -6214,31 +6225,31 @@ } }, "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", "dev": true, "requires": { "aws-sign2": "~0.7.0", - "aws4": "^1.6.0", + "aws4": "^1.8.0", "caseless": "~0.12.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.1", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", "forever-agent": "~0.6.1", - "form-data": "~2.3.1", - "har-validator": "~5.0.3", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", "http-signature": "~1.2.0", "is-typedarray": "~1.0.0", "isstream": "~0.1.2", "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.17", - "oauth-sign": "~0.8.2", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", "performance-now": "^2.1.0", - "qs": "~6.5.1", - "safe-buffer": "^5.1.1", - "tough-cookie": "~2.3.3", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", - "uuid": "^3.1.0" + "uuid": "^3.3.2" } }, "requires-port": { @@ -7293,11 +7304,12 @@ } }, "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", "dev": true, "requires": { + "psl": "^1.1.24", "punycode": "^1.4.1" } }, diff --git a/package.json b/package.json index df787ee65e..1ca6ee9ca0 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@types/papaparse": "4.1.31", "@types/webcrypto": "0.0.28", "concurrently": "3.5.1", - "electron": "2.0.5", + "electron": "2.0.7", "jasmine": "^3.1.0", "jasmine-core": "^2.8.0", "jasmine-spec-reporter": "^4.2.1", @@ -67,6 +67,7 @@ "@angular/platform-browser-dynamic": "5.2.0", "@angular/router": "5.2.0", "@angular/upgrade": "5.2.0", + "@aspnet/signalr": "1.0.2", "angular2-toaster": "4.0.2", "angulartics2": "5.0.1", "core-js": "2.4.1", diff --git a/src/abstractions/environment.service.ts b/src/abstractions/environment.service.ts index 8c27710f2c..e561341d36 100644 --- a/src/abstractions/environment.service.ts +++ b/src/abstractions/environment.service.ts @@ -4,6 +4,7 @@ export abstract class EnvironmentService { apiUrl: string; identityUrl: string; iconsUrl: string; + notificationsUrl: string; getWebVaultUrl: () => string; setUrlsFromStorage: () => Promise; diff --git a/src/abstractions/notifications.service.ts b/src/abstractions/notifications.service.ts new file mode 100644 index 0000000000..540b9885fc --- /dev/null +++ b/src/abstractions/notifications.service.ts @@ -0,0 +1,6 @@ +import { EnvironmentService } from './environment.service'; + +export abstract class NotificationsService { + init: (environmentService: EnvironmentService) => Promise; + updateConnection: () => Promise; +} diff --git a/src/angular/components/environment.component.ts b/src/angular/components/environment.component.ts index 3bc31043f1..d7d4226d9b 100644 --- a/src/angular/components/environment.component.ts +++ b/src/angular/components/environment.component.ts @@ -16,6 +16,7 @@ export class EnvironmentComponent { identityUrl: string; apiUrl: string; webVaultUrl: string; + notificationsUrl: string; baseUrl: string; showCustom = false; @@ -26,6 +27,7 @@ export class EnvironmentComponent { this.apiUrl = environmentService.apiUrl || ''; this.identityUrl = environmentService.identityUrl || ''; this.iconsUrl = environmentService.iconsUrl || ''; + this.notificationsUrl = environmentService.notificationsUrl || ''; } async submit() { @@ -35,6 +37,7 @@ export class EnvironmentComponent { identity: this.identityUrl, webVault: this.webVaultUrl, icons: this.iconsUrl, + notifications: this.notificationsUrl, }); // re-set urls since service can change them, ex: prefixing https:// @@ -43,6 +46,7 @@ export class EnvironmentComponent { this.identityUrl = resUrls.identity; this.webVaultUrl = resUrls.webVault; this.iconsUrl = resUrls.icons; + this.notificationsUrl = resUrls.notifications; this.analytics.eventTrack.next({ action: 'Set Environment URLs' }); this.toasterService.popAsync('success', null, this.i18nService.t('environmentSaved')); diff --git a/src/enums/notificationType.ts b/src/enums/notificationType.ts new file mode 100644 index 0000000000..2ceddadd5d --- /dev/null +++ b/src/enums/notificationType.ts @@ -0,0 +1,14 @@ +export enum NotificationType { + SyncCipherUpdate = 0, + SyncCipherCreate = 1, + SyncLoginDelete = 2, + SyncFolderDelete = 3, + SyncCiphers = 4, + + SyncVault = 5, + SyncOrgKeys = 6, + SyncFolderCreate = 7, + SyncFolderUpdate = 8, + SyncCipherDelete = 9, + SyncSettings = 10, +} diff --git a/src/models/response/notificationResponse.ts b/src/models/response/notificationResponse.ts new file mode 100644 index 0000000000..2f8a2b01fa --- /dev/null +++ b/src/models/response/notificationResponse.ts @@ -0,0 +1,71 @@ +import { NotificationType } from '../../enums/notificationType'; + +export class NotificationResponse { + contextId: string; + type: NotificationType; + payload: any; + + constructor(response: any) { + this.contextId = response.contextId || response.ContextId; + this.type = response.type != null ? response.type : response.Type; + + const payload = response.payload || response.Payload; + switch (this.type) { + case NotificationType.SyncCipherCreate: + case NotificationType.SyncCipherDelete: + case NotificationType.SyncCipherUpdate: + case NotificationType.SyncLoginDelete: + this.payload = new SyncCipherNotification(payload); + break; + case NotificationType.SyncFolderCreate: + case NotificationType.SyncFolderDelete: + case NotificationType.SyncFolderUpdate: + this.payload = new SyncFolderNotification(payload); + break; + case NotificationType.SyncVault: + case NotificationType.SyncCiphers: + case NotificationType.SyncOrgKeys: + case NotificationType.SyncSettings: + this.payload = new SyncUserNotification(payload); + break; + default: + break; + } + } +} + +export class SyncCipherNotification { + id: string; + userId: string; + organizationId: string; + revisionDate: Date; + + constructor(response: any) { + this.id = response.Id; + this.userId = response.UserId; + this.organizationId = response.OrganizationId; + this.revisionDate = new Date(response.RevisionDate); + } +} + +export class SyncFolderNotification { + id: string; + userId: string; + revisionDate: Date; + + constructor(response: any) { + this.id = response.Id; + this.userId = response.UserId; + this.revisionDate = new Date(response.RevisionDate); + } +} + +export class SyncUserNotification { + userId: string; + date: Date; + + constructor(response: any) { + this.userId = response.UserId; + this.date = new Date(response.Date); + } +} diff --git a/src/services/environment.service.ts b/src/services/environment.service.ts index e41a27c54c..a50909eb23 100644 --- a/src/services/environment.service.ts +++ b/src/services/environment.service.ts @@ -12,6 +12,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { apiUrl: string; identityUrl: string; iconsUrl: string; + notificationsUrl: string; constructor(private apiService: ApiService, private storageService: StorageService) {} @@ -31,6 +32,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { api: null, identity: null, icons: null, + notifications: null, webVault: null, }; @@ -46,6 +48,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.apiUrl = envUrls.api = urls.api; this.identityUrl = envUrls.identity = urls.identity; this.iconsUrl = urls.icons; + this.notificationsUrl = urls.notifications; await this.apiService.setUrls(envUrls); } @@ -55,6 +58,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { urls.api = this.formatUrl(urls.api); urls.identity = this.formatUrl(urls.identity); urls.icons = this.formatUrl(urls.icons); + urls.notifications = this.formatUrl(urls.notifications); await this.storageService.save(ConstantsService.environmentUrlsKey, { base: urls.base, @@ -62,6 +66,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { identity: urls.identity, webVault: urls.webVault, icons: urls.icons, + notifications: urls.notifications, }); this.baseUrl = urls.base; @@ -69,6 +74,7 @@ export class EnvironmentService implements EnvironmentServiceAbstraction { this.apiUrl = urls.api; this.identityUrl = urls.identity; this.iconsUrl = urls.icons; + this.notificationsUrl = urls.notifications; const envUrls = new EnvironmentUrls(); if (this.baseUrl) { diff --git a/src/services/notifications.service.ts b/src/services/notifications.service.ts new file mode 100644 index 0000000000..55327c33a8 --- /dev/null +++ b/src/services/notifications.service.ts @@ -0,0 +1,90 @@ +import * as signalR from '@aspnet/signalr'; + +import { NotificationType } from '../enums/notificationType'; + +import { CipherService } from '../abstractions/cipher.service'; +import { CollectionService } from '../abstractions/collection.service'; +import { EnvironmentService } from '../abstractions/environment.service'; +import { FolderService } from '../abstractions/folder.service'; +import { NotificationsService as NotificationsServiceAbstraction } from '../abstractions/notifications.service'; +import { SettingsService } from '../abstractions/settings.service'; +import { SyncService } from '../abstractions/sync.service'; +import { TokenService } from '../abstractions/token.service'; +import { UserService } from '../abstractions/user.service'; + +import { NotificationResponse } from '../models/response/notificationResponse'; + +export class NotificationsService implements NotificationsServiceAbstraction { + private signalrConnection: signalR.HubConnection; + + constructor(private userService: UserService, private tokenService: TokenService, + private syncService: SyncService) { } + + async init(environmentService: EnvironmentService): Promise { + let url = 'https://notifications.bitwarden.com'; + if (environmentService.notificationsUrl != null) { + url = environmentService.notificationsUrl; + } else if (environmentService.baseUrl != null) { + url = environmentService.baseUrl + '/notifications'; + } + + if (this.signalrConnection != null) { + await this.signalrConnection.stop(); + this.signalrConnection = null; + } + + this.signalrConnection = new signalR.HubConnectionBuilder() + .withUrl(url + '/hub', { + accessTokenFactory: () => this.tokenService.getToken(), + }) + .configureLogging(signalR.LogLevel.Information) + .build(); + + this.signalrConnection.on('ReceiveMessage', async (data: any) => { + await this.processNotification(new NotificationResponse(data)); + }); + + this.updateConnection(); + } + + async updateConnection(): Promise { + try { + if (await this.userService.isAuthenticated()) { + await this.signalrConnection.start(); + } else { + await this.signalrConnection.stop(); + } + } catch (e) { + // tslint:disable-next-line + console.error(e.toString()); + } + } + + private async processNotification(notification: NotificationResponse) { + if (notification == null) { + return; + } + + switch (notification.type) { + case NotificationType.SyncCipherCreate: + case NotificationType.SyncCipherDelete: + case NotificationType.SyncCipherUpdate: + case NotificationType.SyncLoginDelete: + this.syncService.fullSync(false); + break; + case NotificationType.SyncFolderCreate: + case NotificationType.SyncFolderDelete: + case NotificationType.SyncFolderUpdate: + this.syncService.fullSync(false); + break; + case NotificationType.SyncVault: + case NotificationType.SyncCiphers: + case NotificationType.SyncOrgKeys: + case NotificationType.SyncSettings: + this.syncService.fullSync(false); + break; + default: + break; + } + } +}