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:
parent
43b13e314e
commit
9387a3f394
@ -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",
|
||||||
|
@ -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 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -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.
|
||||||
*/
|
*/
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
@ -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>
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 {}
|
||||||
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user