diff --git a/jslib b/jslib index b64757132f..50666a761d 160000 --- a/jslib +++ b/jslib @@ -1 +1 @@ -Subproject commit b64757132faf1ebb5438bec00720c58604fd29f6 +Subproject commit 50666a761dba3d2d7d880f1faf488fd9d719ea50 diff --git a/package-lock.json b/package-lock.json index 67dcc83a06..bebc89fbdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -96,6 +96,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==" + }, "@ngtools/webpack": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-1.10.2.tgz", diff --git a/package.json b/package.json index f6227cd006..df0aff5054 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,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", "bootstrap": "4.1.1", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 7216722e75..5ac09e122c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -31,6 +31,7 @@ import { CryptoService } from 'jslib/abstractions/crypto.service'; import { FolderService } from 'jslib/abstractions/folder.service'; import { I18nService } from 'jslib/abstractions/i18n.service'; import { LockService } from 'jslib/abstractions/lock.service'; +import { NotificationsService } from 'jslib/abstractions/notifications.service'; import { PasswordGenerationService } from 'jslib/abstractions/passwordGeneration.service'; import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; import { SearchService } from 'jslib/abstractions/search.service'; @@ -71,7 +72,8 @@ export class AppComponent implements OnDestroy, OnInit { private platformUtilsService: PlatformUtilsService, private ngZone: NgZone, private lockService: LockService, private storageService: StorageService, private cryptoService: CryptoService, private collectionService: CollectionService, - private routerService: RouterService, private searchService: SearchService) { } + private routerService: RouterService, private searchService: SearchService, + private notificationsService: NotificationsService) { } ngOnInit() { this.ngZone.runOutsideAngular(() => { @@ -87,8 +89,10 @@ export class AppComponent implements OnDestroy, OnInit { this.ngZone.run(async () => { switch (message.command) { case 'loggedIn': - case 'unlocked': case 'loggedOut': + this.notificationsService.updateConnection(); + break; + case 'unlocked': break; case 'logout': this.logOut(!!message.expired); diff --git a/src/app/organizations/settings/organization-billing.component.ts b/src/app/organizations/settings/organization-billing.component.ts index 6769f66395..6ccb5d126d 100644 --- a/src/app/organizations/settings/organization-billing.component.ts +++ b/src/app/organizations/settings/organization-billing.component.ts @@ -209,7 +209,8 @@ export class OrganizationBillingComponent implements OnInit { } get isExpired() { - return this.billing != null && this.billing.expiration != null && this.billing.expiration < new Date(); + return this.billing != null && this.billing.expiration != null && + new Date(this.billing.expiration) < new Date(); } get subscriptionMarkedForCancel() { diff --git a/src/app/organizations/vault/vault.component.ts b/src/app/organizations/vault/vault.component.ts index 2ebf2dc1ae..a6593e4f15 100644 --- a/src/app/organizations/vault/vault.component.ts +++ b/src/app/organizations/vault/vault.component.ts @@ -1,7 +1,10 @@ import { Location } from '@angular/common'; import { + ChangeDetectorRef, Component, ComponentFactoryResolver, + NgZone, + OnDestroy, OnInit, ViewChild, ViewContainerRef, @@ -16,6 +19,8 @@ import { MessagingService } from 'jslib/abstractions/messaging.service'; import { SyncService } from 'jslib/abstractions/sync.service'; import { UserService } from 'jslib/abstractions/user.service'; +import { BroadcasterService } from 'jslib/angular/services/broadcaster.service'; + import { Organization } from 'jslib/models/domain/organization'; import { CipherView } from 'jslib/models/view/cipherView'; @@ -30,11 +35,13 @@ import { CiphersComponent } from './ciphers.component'; import { CollectionsComponent } from './collections.component'; import { GroupingsComponent } from './groupings.component'; +const BroadcasterSubscriptionId = 'OrgVaultComponent'; + @Component({ selector: 'app-org-vault', templateUrl: 'vault.component.html', }) -export class VaultComponent implements OnInit { +export class VaultComponent implements OnInit, OnDestroy { @ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent; @ViewChild(CiphersComponent) ciphersComponent: CiphersComponent; @ViewChild('attachments', { read: ViewContainerRef }) attachmentsModalRef: ViewContainerRef; @@ -52,7 +59,9 @@ export class VaultComponent implements OnInit { constructor(private route: ActivatedRoute, private userService: UserService, private location: Location, private router: Router, private syncService: SyncService, private i18nService: I18nService, - private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService) { } + private componentFactoryResolver: ComponentFactoryResolver, private messagingService: MessagingService, + private broadcasterService: BroadcasterService, private ngZone: NgZone, + private changeDetectorRef: ChangeDetectorRef) { } ngOnInit() { this.route.parent.params.subscribe(async (params) => { @@ -65,6 +74,21 @@ export class VaultComponent implements OnInit { this.ciphersComponent.searchText = this.groupingsComponent.searchText = qParams.search; if (!this.organization.isAdmin) { await this.syncService.fullSync(false); + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case 'syncCompleted': + if (message.successfully) { + await Promise.all([ + this.groupingsComponent.load(), + this.ciphersComponent.refresh(), + ]); + this.changeDetectorRef.detectChanges(); + } + break; + } + }); + }); } await this.groupingsComponent.load(); @@ -95,6 +119,10 @@ export class VaultComponent implements OnInit { }); } + ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + async clearGroupingFilters() { this.ciphersComponent.showAddNew = true; this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault'); diff --git a/src/app/services/services.module.ts b/src/app/services/services.module.ts index dd46704cd3..1cfad1f56f 100644 --- a/src/app/services/services.module.ts +++ b/src/app/services/services.module.ts @@ -38,6 +38,7 @@ import { ExportService } from 'jslib/services/export.service'; import { FolderService } from 'jslib/services/folder.service'; import { ImportService } from 'jslib/services/import.service'; import { LockService } from 'jslib/services/lock.service'; +import { NotificationsService } from 'jslib/services/notifications.service'; import { PasswordGenerationService } from 'jslib/services/passwordGeneration.service'; import { SearchService } from 'jslib/services/search.service'; import { SettingsService } from 'jslib/services/settings.service'; @@ -64,6 +65,7 @@ import { ImportService as ImportServiceAbstraction } from 'jslib/abstractions/im import { LockService as LockServiceAbstraction } from 'jslib/abstractions/lock.service'; import { LogService as LogServiceAbstraction } from 'jslib/abstractions/log.service'; import { MessagingService as MessagingServiceAbstraction } from 'jslib/abstractions/messaging.service'; +import { NotificationsService as NotificationsServiceAbstraction } from 'jslib/abstractions/notifications.service'; import { PasswordGenerationService as PasswordGenerationServiceAbstraction, } from 'jslib/abstractions/passwordGeneration.service'; @@ -92,7 +94,6 @@ const tokenService = new TokenService(storageService); const appIdService = new AppIdService(storageService); const apiService = new ApiService(tokenService, platformUtilsService, async (expired: boolean) => messagingService.send('logout', { expired: expired })); -const environmentService = new EnvironmentService(apiService, storageService); const userService = new UserService(tokenService, storageService); const settingsService = new SettingsService(userService, storageService); export let searchService: SearchService = null; @@ -114,6 +115,9 @@ const authService = new AuthService(cryptoService, apiService, userService, tokenService, appIdService, i18nService, platformUtilsService, messagingService); const exportService = new ExportService(folderService, cipherService, apiService); const importService = new ImportService(cipherService, folderService, apiService, i18nService, collectionService); +const notificationsService = new NotificationsService(userService, tokenService, syncService, appIdService, + apiService); +const environmentService = new EnvironmentService(apiService, storageService, notificationsService); const auditService = new AuditService(cryptoFunctionService, apiService); const analytics = new Analytics(window, () => platformUtilsService.isDev() || platformUtilsService.isSelfHost(), @@ -127,6 +131,7 @@ export function initFactory(): Function { if (!isDev && platformUtilsService.isSelfHost()) { environmentService.baseUrl = window.location.origin; } + environmentService.notificationsUrl = isDev ? 'http://localhost:61840' : null; await apiService.setUrls({ base: isDev ? null : window.location.origin, api: isDev ? 'http://localhost:4000' : null, @@ -139,6 +144,7 @@ export function initFactory(): Function { // api: 'https://api.bitwarden.com', // identity: 'https://identity.bitwarden.com', }); + setTimeout(() => notificationsService.init(environmentService), 3000); lockService.init(true); const locale = await storageService.get(ConstantsService.localeKey); @@ -194,6 +200,7 @@ export function initFactory(): Function { { provide: ExportServiceAbstraction, useValue: exportService }, { provide: SearchServiceAbstraction, useValue: searchService }, { provide: ImportServiceAbstraction, useValue: importService }, + { provide: NotificationsServiceAbstraction, useValue: notificationsService }, { provide: CryptoFunctionServiceAbstraction, useValue: cryptoFunctionService }, { provide: APP_INITIALIZER, diff --git a/src/app/vault/vault.component.ts b/src/app/vault/vault.component.ts index 45ff9f41b8..77f24bdae8 100644 --- a/src/app/vault/vault.component.ts +++ b/src/app/vault/vault.component.ts @@ -1,7 +1,10 @@ import { Location } from '@angular/common'; import { + ChangeDetectorRef, Component, ComponentFactoryResolver, + NgZone, + OnDestroy, OnInit, ViewChild, ViewContainerRef, @@ -40,11 +43,15 @@ import { SyncService } from 'jslib/abstractions/sync.service'; import { TokenService } from 'jslib/abstractions/token.service'; import { UserService } from 'jslib/abstractions/user.service'; +import { BroadcasterService } from 'jslib/angular/services/broadcaster.service'; + +const BroadcasterSubscriptionId = 'VaultComponent'; + @Component({ selector: 'app-vault', templateUrl: 'vault.component.html', }) -export class VaultComponent implements OnInit { +export class VaultComponent implements OnInit, OnDestroy { @ViewChild(GroupingsComponent) groupingsComponent: GroupingsComponent; @ViewChild(CiphersComponent) ciphersComponent: CiphersComponent; @ViewChild(OrganizationsComponent) organizationsComponent: OrganizationsComponent; @@ -74,7 +81,9 @@ export class VaultComponent implements OnInit { private i18nService: I18nService, private componentFactoryResolver: ComponentFactoryResolver, private tokenService: TokenService, private cryptoService: CryptoService, private messagingService: MessagingService, private userService: UserService, - private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService) { } + private platformUtilsService: PlatformUtilsService, private toasterService: ToasterService, + private broadcasterService: BroadcasterService, private ngZone: NgZone, + private changeDetectorRef: ChangeDetectorRef) { } async ngOnInit() { this.showVerifyEmail = !(await this.tokenService.getEmailVerified()); @@ -85,6 +94,23 @@ export class VaultComponent implements OnInit { this.route.queryParams.subscribe(async (params) => { await this.syncService.fullSync(false); + this.broadcasterService.subscribe(BroadcasterSubscriptionId, (message: any) => { + this.ngZone.run(async () => { + switch (message.command) { + case 'syncCompleted': + if (message.successfully) { + await Promise.all([ + this.groupingsComponent.load(), + this.organizationsComponent.load(), + this.ciphersComponent.refresh(), + ]); + this.changeDetectorRef.detectChanges(); + } + break; + } + }); + }); + await Promise.all([ this.groupingsComponent.load(), this.organizationsComponent.load(), @@ -120,6 +146,10 @@ export class VaultComponent implements OnInit { }); } + ngOnDestroy() { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + async clearGroupingFilters() { this.ciphersComponent.showAddNew = true; this.groupingsComponent.searchPlaceholder = this.i18nService.t('searchVault');