diff --git a/client/package.json b/client/package.json index 1b4962f69..9ea16aab0 100644 --- a/client/package.json +++ b/client/package.json @@ -63,6 +63,7 @@ "moment": "^2.24.0", "ng2-charts": "^2.3.0", "ng2-pdf-viewer": "^6.1.2", + "ngx-device-detector": "^1.4.4", "ngx-file-drop": "^8.0.8", "ngx-mat-select-search": "^2.1.2", "ngx-material-timepicker": "^5.5.1", diff --git a/client/src/app/app-routing.module.ts b/client/src/app/app-routing.module.ts index d823ee84b..0a3a3fa89 100644 --- a/client/src/app/app-routing.module.ts +++ b/client/src/app/app-routing.module.ts @@ -7,6 +7,7 @@ import { LoginPrivacyPolicyComponent } from './site/login/components/login-priva import { LoginWrapperComponent } from './site/login/components/login-wrapper/login-wrapper.component'; import { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component'; import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.component'; +import { UnsupportedBrowserComponent } from './site/login/components/unsupported-browser/unsupported-browser.component'; /** * Global app routing @@ -20,7 +21,8 @@ const routes: Routes = [ { path: 'reset-password', component: ResetPasswordComponent }, { path: 'reset-password-confirm', component: ResetPasswordConfirmComponent }, { path: 'legalnotice', component: LoginLegalNoticeComponent }, - { path: 'privacypolicy', component: LoginPrivacyPolicyComponent } + { path: 'privacypolicy', component: LoginPrivacyPolicyComponent }, + { path: 'unsupported-browser', component: UnsupportedBrowserComponent } ] }, { diff --git a/client/src/app/site/login/components/login-mask/login-mask.component.ts b/client/src/app/site/login/components/login-mask/login-mask.component.ts index 0df52349d..bb66fb0bf 100644 --- a/client/src/app/site/login/components/login-mask/login-mask.component.ts +++ b/client/src/app/site/login/components/login-mask/login-mask.component.ts @@ -6,6 +6,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { Subscription } from 'rxjs'; +import { filter } from 'rxjs/operators'; import { AuthService } from 'app/core/core-services/auth.service'; import { OperatorService } from 'app/core/core-services/operator.service'; @@ -14,6 +15,7 @@ import { OverlayService } from 'app/core/ui-services/overlay.service'; import { UserAuthType } from 'app/shared/models/users/user'; import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher'; import { BaseViewComponent } from 'app/site/base/base-view'; +import { BrowserSupportService } from '../../services/browser-support.service'; /** * Login mask component. @@ -31,6 +33,8 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD */ public hide: boolean; + private checkBrowser = true; + /** * Reference to the SnackBarEntry for the installation notice send by the server. */ @@ -82,7 +86,8 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD private route: ActivatedRoute, private formBuilder: FormBuilder, private loginDataService: LoginDataService, - private overlayService: OverlayService + private overlayService: OverlayService, + private browserSupport: BrowserSupportService ) { super(title, translate, matSnackBar); // Hide the spinner if the user is at `login-mask` @@ -113,6 +118,14 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD this.authService.redirectUser(user.id); } }); + + this.route.queryParams.pipe(filter(params => params.checkBrowser)).subscribe(params => { + this.checkBrowser = params.checkBrowser === 'true'; + }); + + if (this.checkBrowser) { + this.checkDevice(); + } } /** @@ -122,6 +135,12 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD this.clearOperatorSubscription(); } + private checkDevice(): void { + if (!this.browserSupport.isBrowserSupported()) { + this.router.navigate(['./unsupported-browser'], { relativeTo: this.route }); + } + } + /** * Clears the subscription to the operator. */ diff --git a/client/src/app/site/login/components/login-wrapper/login-wrapper.component.html b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.html index 4bc549666..6c493032f 100644 --- a/client/src/app/site/login/components/login-wrapper/login-wrapper.component.html +++ b/client/src/app/site/login/components/login-wrapper/login-wrapper.component.html @@ -1,7 +1,9 @@
- OpenSlides-logo + OpenSlides-logo
diff --git a/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.html b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.html new file mode 100644 index 000000000..618bc6ed8 --- /dev/null +++ b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.html @@ -0,0 +1,37 @@ +
+

+ {{ 'Your browser is not supported by OpenSlides' | translate }} +

+

+ {{ 'Congratuations! Your browser is supported by OpenSlides' | translate }} +

+ + +
+ + {{ 'Your browser' | translate }}: {{ currentBrowser }} ({{ 'version' | translate }}: + {{ browserVersion }}) + +
+ {{ 'Minimal required version for your browser' | translate }}: {{ supportedVersion }} +
+ +
+ {{ + 'Please update your browser or contact your system administration to have your browser updated for you.' + | translate + }} +
+ +
+ + {{ 'OpenSlides recommends:' | translate }} + + +
+
+
diff --git a/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.spec.ts b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.spec.ts new file mode 100644 index 000000000..9057c1865 --- /dev/null +++ b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.spec.ts @@ -0,0 +1,27 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { E2EImportsModule } from 'e2e-imports.module'; + +import { UnsupportedBrowserComponent } from './unsupported-browser.component'; + +describe('UnsupportedBrowserComponent', () => { + let component: UnsupportedBrowserComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [E2EImportsModule], + declarations: [UnsupportedBrowserComponent] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(UnsupportedBrowserComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.ts b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.ts new file mode 100644 index 000000000..f4dfb0e11 --- /dev/null +++ b/client/src/app/site/login/components/unsupported-browser/unsupported-browser.component.ts @@ -0,0 +1,28 @@ +import { Component, OnInit } from '@angular/core'; + +import { BrowserRecommendation, BrowserSupportService } from '../../services/browser-support.service'; + +@Component({ + selector: 'os-unsupported-browser', + templateUrl: './unsupported-browser.component.html' +}) +export class UnsupportedBrowserComponent implements OnInit { + public supported: boolean; + public currentBrowser: string; + public browserVersion: string; + public supportedVersion: number; + + public get recommendedBrowsers(): BrowserRecommendation[] { + return this.browserSupprt.recommendedBrowsers; + } + + public constructor(private browserSupprt: BrowserSupportService) {} + + public ngOnInit(): void { + const deviceInfo = this.browserSupprt.getDeviceInfo(); + this.supported = this.browserSupprt.isBrowserSupported(); + this.currentBrowser = deviceInfo.browser; + this.browserVersion = deviceInfo.browser_version; + this.supportedVersion = this.browserSupprt.getSupportedVersion(deviceInfo); + } +} diff --git a/client/src/app/site/login/login.module.ts b/client/src/app/site/login/login.module.ts index 5b101d9ec..1975ec699 100644 --- a/client/src/app/site/login/login.module.ts +++ b/client/src/app/site/login/login.module.ts @@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; +import { DeviceDetectorModule } from 'ngx-device-detector'; + import { LoginLegalNoticeComponent } from './components/login-legal-notice/login-legal-notice.component'; import { LoginMaskComponent } from './components/login-mask/login-mask.component'; import { LoginPrivacyPolicyComponent } from './components/login-privacy-policy/login-privacy-policy.component'; @@ -9,16 +11,18 @@ import { LoginWrapperComponent } from './components/login-wrapper/login-wrapper. import { ResetPasswordConfirmComponent } from './components/reset-password-confirm/reset-password-confirm.component'; import { ResetPasswordComponent } from './components/reset-password/reset-password.component'; import { SharedModule } from '../../shared/shared.module'; +import { UnsupportedBrowserComponent } from './components/unsupported-browser/unsupported-browser.component'; @NgModule({ - imports: [CommonModule, RouterModule, SharedModule], + imports: [CommonModule, RouterModule, SharedModule, DeviceDetectorModule], declarations: [ LoginWrapperComponent, ResetPasswordComponent, ResetPasswordConfirmComponent, LoginMaskComponent, LoginLegalNoticeComponent, - LoginPrivacyPolicyComponent + LoginPrivacyPolicyComponent, + UnsupportedBrowserComponent ] }) export class LoginModule {} diff --git a/client/src/app/site/login/services/browser-support.service.spec.ts b/client/src/app/site/login/services/browser-support.service.spec.ts new file mode 100644 index 000000000..3106528e1 --- /dev/null +++ b/client/src/app/site/login/services/browser-support.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { BrowserSupportService } from './browser-support.service'; + +describe('BrowserSupportService', () => { + let service: BrowserSupportService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(BrowserSupportService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/client/src/app/site/login/services/browser-support.service.ts b/client/src/app/site/login/services/browser-support.service.ts new file mode 100644 index 000000000..b1a394738 --- /dev/null +++ b/client/src/app/site/login/services/browser-support.service.ts @@ -0,0 +1,63 @@ +import { Injectable } from '@angular/core'; + +import { DeviceDetectorService, DeviceInfo } from 'ngx-device-detector'; + +const SmallestSupportedBrowserVersion = { + Chrome: 81, + Safari: 13, + Firefox: 68, + Opera: 66, + 'MS-Edge': 81, + 'MS-Edge-Chromium': 81 +}; + +const BrowserBlacklist = ['IE']; + +export interface BrowserRecommendation { + name: string; + url: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class BrowserSupportService { + public readonly recommendedBrowsers: BrowserRecommendation[] = [ + { + name: 'Google Chrome', + url: 'https://www.google.com/chrome/' + }, + { + name: 'Mozilla Firefox', + url: 'https://www.mozilla.org/firefox/' + } + ]; + + public constructor(private deviceService: DeviceDetectorService) {} + + public getDeviceInfo(): DeviceInfo { + return this.deviceService.getDeviceInfo(); + } + + public getSupportedVersion(info: DeviceInfo): number { + return SmallestSupportedBrowserVersion[info.browser]; + } + + /** + * Detect the browser version and forward to an error page if the browser was too old + */ + public isBrowserSupported(): boolean { + const deviceInfo = this.deviceService.getDeviceInfo(); + const browser = deviceInfo.browser; + const version = parseInt(deviceInfo.browser_version, 10); + + if (BrowserBlacklist.includes(browser)) { + return false; + } else if (!SmallestSupportedBrowserVersion[browser]) { + // if we don't know the browser, let's assume they are working + return true; + } else { + return version >= this.getSupportedVersion(deviceInfo); + } + } +}