From e4d3e119d3adc8e97447283e70370fbde1441f24 Mon Sep 17 00:00:00 2001 From: Sean Engelhardt Date: Fri, 3 May 2019 17:39:48 +0200 Subject: [PATCH] Service Worker Updates Introdcues a new update service. Listens to service-worker updates and shows a snack-bar to inform about updates. Provides a function to manually check for updates. The service worker tries to be consistent in it's own version and updates in the background. Some manuall trigger will be required to update, which is either a reload or the execution of the provded check function with help from @FinnStutzenstein --- client/ngsw-config.json | 42 ++++++------- client/src/app/app.component.ts | 6 +- .../core/ui-services/update.service.spec.ts | 18 ++++++ .../app/core/ui-services/update.service.ts | 61 +++++++++++++++++++ .../legal-notice/legal-notice.component.html | 28 +++++++-- .../legal-notice/legal-notice.component.ts | 11 +++- openslides/core/websocket.py | 36 ++++++++--- 7 files changed, 162 insertions(+), 40 deletions(-) create mode 100644 client/src/app/core/ui-services/update.service.spec.ts create mode 100644 client/src/app/core/ui-services/update.service.ts diff --git a/client/ngsw-config.json b/client/ngsw-config.json index 818b90e44..49b210599 100644 --- a/client/ngsw-config.json +++ b/client/ngsw-config.json @@ -1,26 +1,20 @@ { - "index": "/index.html", - "assetGroups": [ - { - "name": "app", - "installMode": "prefetch", - "resources": { - "files": [ - "/favicon.png", - "/index.html", - "/*.css", - "/*.js" - ] - } - }, { - "name": "assets", - "installMode": "lazy", - "updateMode": "prefetch", - "resources": { - "files": [ - "/assets/**" - ] - } - } - ] + "index": "/index.html", + "assetGroups": [ + { + "name": "app", + "installMode": "prefetch", + "resources": { + "files": ["/favicon.png", "/index.html", "/*.css", "/*.js"] + } + }, + { + "name": "assets", + "installMode": "lazy", + "updateMode": "prefetch", + "resources": { + "files": ["/assets/**"] + } + } + ] } diff --git a/client/src/app/app.component.ts b/client/src/app/app.component.ts index f0e6dc613..b76f759d6 100644 --- a/client/src/app/app.component.ts +++ b/client/src/app/app.component.ts @@ -12,6 +12,7 @@ import { OperatorService } from './core/core-services/operator.service'; import { ServertimeService } from './core/core-services/servertime.service'; import { ThemeService } from './core/ui-services/theme.service'; import { DataStoreUpgradeService } from './core/core-services/data-store-upgrade.service'; +import { UpdateService } from './core/ui-services/update.service'; /** * Angular's global App Component @@ -38,6 +39,8 @@ export class AppComponent { * @param countUsersService to call the constructor of the CountUserService * @param configService to call the constructor of the ConfigService * @param loadFontService to call the constructor of the LoadFontService + * @param dataStoreUpgradeService + * @param update Service Worker Updates */ public constructor( translate: TranslateService, @@ -50,7 +53,8 @@ export class AppComponent { countUsersService: CountUsersService, // Needed to register itself. configService: ConfigService, loadFontService: LoadFontService, - dataStoreUpgradeService: DataStoreUpgradeService // to start it. + dataStoreUpgradeService: DataStoreUpgradeService, // to start it. + update: UpdateService ) { // manually add the supported languages translate.addLangs(['en', 'de', 'cs']); diff --git a/client/src/app/core/ui-services/update.service.spec.ts b/client/src/app/core/ui-services/update.service.spec.ts new file mode 100644 index 000000000..556f4ff6e --- /dev/null +++ b/client/src/app/core/ui-services/update.service.spec.ts @@ -0,0 +1,18 @@ +import { TestBed } from '@angular/core/testing'; + +import { UpdateService } from './update.service'; +import { E2EImportsModule } from 'e2e-imports.module'; + +describe('UpdateService', () => { + beforeEach(() => + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + providers: [UpdateService] + }) + ); + + it('should be created', () => { + const service: UpdateService = TestBed.get(UpdateService); + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/core/ui-services/update.service.ts b/client/src/app/core/ui-services/update.service.ts new file mode 100644 index 000000000..b1448e447 --- /dev/null +++ b/client/src/app/core/ui-services/update.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; + +import { SwUpdate } from '@angular/service-worker'; +import { MatSnackBar } from '@angular/material'; +import { NotifyService } from '../core-services/notify.service'; + +/** + * Handle Service Worker updates using the SwUpdate service form angular. + */ +@Injectable({ + providedIn: 'root' +}) +export class UpdateService { + private static NOTIFY_NAME = 'swCheckForUpdate'; + + /** + * Constructor. + * Listens to available updates + * + * @param swUpdate Service Worker update service + * @param matSnackBar Currently to show that an update is available + */ + public constructor(private swUpdate: SwUpdate, matSnackBar: MatSnackBar, private notify: NotifyService) { + swUpdate.available.subscribe(() => { + // TODO: Find a better solution OR make an update-bar like for history mode + const ref = matSnackBar.open('A new update is available!', 'Refresh', { + duration: 0 + }); + + // Enforces an update + ref.onAction().subscribe(() => { + this.swUpdate.activateUpdate().then(() => { + document.location.reload(); + }); + }); + }); + + // Listen on requests from other users to check for updates. + this.notify.getMessageObservable(UpdateService.NOTIFY_NAME).subscribe(() => { + this.checkForUpdate(); + }); + } + + /** + * Trigger that to manually check for updates + */ + public checkForUpdate(): void { + if (this.swUpdate.isEnabled) { + this.swUpdate.checkForUpdate(); + } + } + + /** + * Emits a message to all clients initiating to check for updates. This method + * can only be called by users with 'users.can_manage'. This will be checked by + * the server. + */ + public initiateUpdateCheckForAllClients(): void { + this.notify.sendToAllUsers(UpdateService.NOTIFY_NAME, {}); + } +} diff --git a/client/src/app/site/common/components/legal-notice/legal-notice.component.html b/client/src/app/site/common/components/legal-notice/legal-notice.component.html index c4f4158c7..75914712b 100644 --- a/client/src/app/site/common/components/legal-notice/legal-notice.component.html +++ b/client/src/app/site/common/components/legal-notice/legal-notice.component.html @@ -1,4 +1,4 @@ - +

Legal notice

@@ -7,9 +7,29 @@ - +
+ +
+ +
+ +
+ +
+ +
diff --git a/client/src/app/site/common/components/legal-notice/legal-notice.component.ts b/client/src/app/site/common/components/legal-notice/legal-notice.component.ts index 6408145ab..240596add 100644 --- a/client/src/app/site/common/components/legal-notice/legal-notice.component.ts +++ b/client/src/app/site/common/components/legal-notice/legal-notice.component.ts @@ -1,14 +1,23 @@ import { Component } from '@angular/core'; import { OpenSlidesService } from 'app/core/core-services/openslides.service'; +import { UpdateService } from 'app/core/ui-services/update.service'; @Component({ selector: 'os-legal-notice', templateUrl: './legal-notice.component.html' }) export class LegalNoticeComponent { - public constructor(private openSlidesService: OpenSlidesService) {} + public constructor(private openSlidesService: OpenSlidesService, private update: UpdateService) {} public resetCache(): void { this.openSlidesService.reset(); } + + public checkForUpdate(): void { + this.update.checkForUpdate(); + } + + public initiateUpdateCheckForAllClients(): void { + this.update.initiateUpdateCheckForAllClients(); + } } diff --git a/openslides/core/websocket.py b/openslides/core/websocket.py index 2f6fd4151..47118bfa2 100644 --- a/openslides/core/websocket.py +++ b/openslides/core/websocket.py @@ -1,5 +1,6 @@ -from typing import Any +from typing import Any, Dict +from ..utils.auth import async_has_perm from ..utils.constants import get_constants from ..utils.projector import get_projector_data from ..utils.websocket import ( @@ -44,19 +45,34 @@ class NotifyWebsocketClientMessage(BaseWebsocketClientMessage): }, "required": ["name", "content"], } + # Define a required permission for a notify message here. If the emitting user does not + # have this permission, he will get an error message in response. + notify_permissions: Dict[str, str] = {"swCheckForUpdate": "users.can_manage"} async def receive_content( self, consumer: "ProtocollAsyncJsonWebsocketConsumer", content: Any, id: str ) -> None: - await consumer.channel_layer.group_send( - "site", - { - "type": "send_notify", - "incomming": content, - "senderChannelName": consumer.channel_name, - "senderUserId": consumer.scope["user"]["id"], - }, - ) + # Check if the user is allowed to send this notify message + perm = self.notify_permissions.get(content["name"]) + if perm is not None and not await async_has_perm( + consumer.scope["user"]["id"], perm + ): + await consumer.send_json( + type="error", + content=f"You need '{perm}' to send this message.", + in_response=id, + ) + else: + # Forward to all other active site consumers to handle the notify message. + await consumer.channel_layer.group_send( + "site", + { + "type": "send_notify", + "incomming": content, + "senderChannelName": consumer.channel_name, + "senderUserId": consumer.scope["user"]["id"], + }, + ) class ConstantsWebsocketClientMessage(BaseWebsocketClientMessage):