Catch unsupported browsers

unspoorted browsers trying to access the login mask will be forwarded to
an info page.
The info page shows that the browser is not suppoted and hints the smallest
supported version of their current browser.
As it works best and might prevent some support calls, I added an hint
for chrome as the favored browser by OpenSlides (debateable)

To update/downgrade the supported versions, simply edit the enum in the
service.

If we cannot detect the browser, we assume it was supported.
This commit is contained in:
Sean 2020-06-11 14:39:14 +02:00
parent 43b13e314e
commit 9387a3f394
10 changed files with 204 additions and 5 deletions

View File

@ -63,6 +63,7 @@
"moment": "^2.24.0", "moment": "^2.24.0",
"ng2-charts": "^2.3.0", "ng2-charts": "^2.3.0",
"ng2-pdf-viewer": "^6.1.2", "ng2-pdf-viewer": "^6.1.2",
"ngx-device-detector": "^1.4.4",
"ngx-file-drop": "^8.0.8", "ngx-file-drop": "^8.0.8",
"ngx-mat-select-search": "^2.1.2", "ngx-mat-select-search": "^2.1.2",
"ngx-material-timepicker": "^5.5.1", "ngx-material-timepicker": "^5.5.1",

View File

@ -7,6 +7,7 @@ import { LoginPrivacyPolicyComponent } from './site/login/components/login-priva
import { LoginWrapperComponent } from './site/login/components/login-wrapper/login-wrapper.component'; 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 { ResetPasswordConfirmComponent } from './site/login/components/reset-password-confirm/reset-password-confirm.component';
import { ResetPasswordComponent } from './site/login/components/reset-password/reset-password.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 * Global app routing
@ -20,7 +21,8 @@ const routes: Routes = [
{ path: 'reset-password', component: ResetPasswordComponent }, { path: 'reset-password', component: ResetPasswordComponent },
{ path: 'reset-password-confirm', component: ResetPasswordConfirmComponent }, { path: 'reset-password-confirm', component: ResetPasswordConfirmComponent },
{ path: 'legalnotice', component: LoginLegalNoticeComponent }, { path: 'legalnotice', component: LoginLegalNoticeComponent },
{ path: 'privacypolicy', component: LoginPrivacyPolicyComponent } { path: 'privacypolicy', component: LoginPrivacyPolicyComponent },
{ path: 'unsupported-browser', component: UnsupportedBrowserComponent }
] ]
}, },
{ {

View File

@ -6,6 +6,7 @@ import { ActivatedRoute, Router } from '@angular/router';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { filter } from 'rxjs/operators';
import { AuthService } from 'app/core/core-services/auth.service'; import { AuthService } from 'app/core/core-services/auth.service';
import { OperatorService } from 'app/core/core-services/operator.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 { UserAuthType } from 'app/shared/models/users/user';
import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher'; import { ParentErrorStateMatcher } from 'app/shared/parent-error-state-matcher';
import { BaseViewComponent } from 'app/site/base/base-view'; import { BaseViewComponent } from 'app/site/base/base-view';
import { BrowserSupportService } from '../../services/browser-support.service';
/** /**
* Login mask component. * Login mask component.
@ -31,6 +33,8 @@ export class LoginMaskComponent extends BaseViewComponent implements OnInit, OnD
*/ */
public hide: boolean; public hide: boolean;
private checkBrowser = true;
/** /**
* Reference to the SnackBarEntry for the installation notice send by the server. * 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 route: ActivatedRoute,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private loginDataService: LoginDataService, private loginDataService: LoginDataService,
private overlayService: OverlayService private overlayService: OverlayService,
private browserSupport: BrowserSupportService
) { ) {
super(title, translate, matSnackBar); super(title, translate, matSnackBar);
// Hide the spinner if the user is at `login-mask` // 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.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(); this.clearOperatorSubscription();
} }
private checkDevice(): void {
if (!this.browserSupport.isBrowserSupported()) {
this.router.navigate(['./unsupported-browser'], { relativeTo: this.route });
}
}
/** /**
* Clears the subscription to the operator. * Clears the subscription to the operator.
*/ */

View File

@ -1,7 +1,9 @@
<!-- The actual form --> <!-- The actual form -->
<header> <header>
<mat-toolbar class="login-logo-bar" color="primary"> <mat-toolbar class="login-logo-bar" color="primary">
<a routerLink="/login"><img src="assets/img/openslides-logo-dark.svg" alt="OpenSlides-logo" /></a> <a routerLink="/login" [queryParams]="{ checkBrowser: false }"
><img src="assets/img/openslides-logo-dark.svg" alt="OpenSlides-logo"
/></a>
</mat-toolbar> </mat-toolbar>
</header> </header>

View File

@ -0,0 +1,37 @@
<main>
<h1 class="center spacer-top-20" *ngIf="!supported">
{{ 'Your browser is not supported by OpenSlides' | translate }}
</h1>
<h1 class="center spacer-top-20" *ngIf="supported">
{{ 'Congratuations! Your browser is supported by OpenSlides' | translate }}
</h1>
<mat-card class="os-card">
<div>
<span>
{{ 'Your browser' | translate }}: {{ currentBrowser }} ({{ 'version' | translate }}:
{{ browserVersion }})
</span>
<br />
<span *ngIf="!!supportedVersion">{{ 'Minimal required version for your browser' | translate }}: {{ supportedVersion }}</span>
</div>
<div class="spacer-top-20" *ngIf="!supported">
<span>{{
'Please update your browser or contact your system administration to have your browser updated for you.'
| translate
}}</span>
</div>
<div class="spacer-top-20">
<span>
{{ 'OpenSlides recommends:' | translate }}
<ul>
<li *ngFor="let browser of recommendedBrowsers">
<a [href]="browser.url">{{ browser.name }}</a>
</li>
</ul>
</span>
</div>
</mat-card>
</main>

View File

@ -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<UnsupportedBrowserComponent>;
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();
});
});

View File

@ -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);
}
}

View File

@ -2,6 +2,8 @@ import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { DeviceDetectorModule } from 'ngx-device-detector';
import { LoginLegalNoticeComponent } from './components/login-legal-notice/login-legal-notice.component'; import { LoginLegalNoticeComponent } from './components/login-legal-notice/login-legal-notice.component';
import { LoginMaskComponent } from './components/login-mask/login-mask.component'; import { LoginMaskComponent } from './components/login-mask/login-mask.component';
import { LoginPrivacyPolicyComponent } from './components/login-privacy-policy/login-privacy-policy.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 { ResetPasswordConfirmComponent } from './components/reset-password-confirm/reset-password-confirm.component';
import { ResetPasswordComponent } from './components/reset-password/reset-password.component'; import { ResetPasswordComponent } from './components/reset-password/reset-password.component';
import { SharedModule } from '../../shared/shared.module'; import { SharedModule } from '../../shared/shared.module';
import { UnsupportedBrowserComponent } from './components/unsupported-browser/unsupported-browser.component';
@NgModule({ @NgModule({
imports: [CommonModule, RouterModule, SharedModule], imports: [CommonModule, RouterModule, SharedModule, DeviceDetectorModule],
declarations: [ declarations: [
LoginWrapperComponent, LoginWrapperComponent,
ResetPasswordComponent, ResetPasswordComponent,
ResetPasswordConfirmComponent, ResetPasswordConfirmComponent,
LoginMaskComponent, LoginMaskComponent,
LoginLegalNoticeComponent, LoginLegalNoticeComponent,
LoginPrivacyPolicyComponent LoginPrivacyPolicyComponent,
UnsupportedBrowserComponent
] ]
}) })
export class LoginModule {} export class LoginModule {}

View File

@ -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();
});
});

View File

@ -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);
}
}
}