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):