two-step login pages
|
@ -18,6 +18,9 @@
|
|||
<i class="fa fa-lg" [ngClass]="{'fa-eye': !showPassword, 'fa-eye-slash': showPassword}"></i>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text">
|
||||
<a routerLink="/hint">{{'getMasterPasswordHint' | i18n}}</a>
|
||||
</small>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex">
|
||||
|
@ -32,9 +35,6 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 text-center">
|
||||
<a routerLink="/hint">{{'getMasterPasswordHint' | i18n}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
<header>
|
||||
<div class="left">
|
||||
<a routerLink="/2fa">{{'close' | i18n}}</a>
|
||||
<div class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title">{{'twoStepOptions' | i18n}}</h2>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="center">
|
||||
<span class="title">{{'twoStepOptions' | i18n}}</span>
|
||||
</div>
|
||||
<div class="right"></div>
|
||||
</header>
|
||||
<content>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<a href="#" appStopClick *ngFor="let p of providers" class="box-content-row"
|
||||
(click)="choose(p)">
|
||||
<span class="text">{{p.name}}</span>
|
||||
<span class="detail">{{p.description}}</span>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="#" appStopClick *ngFor="let p of providers" (click)="choose(p)" class="list-group-item list-group-item-action">
|
||||
<img [src]="'../images/two-factor/' + p.type + '.png'" alt="" class="pull-right">
|
||||
<h3>{{p.name}}</h3>
|
||||
{{p.description}}
|
||||
</a>
|
||||
<a href="#" appStopClick class="box-content-row" (click)="recover()">
|
||||
<span class="text">{{'recoveryCodeTitle' | i18n}}</span>
|
||||
<span class="detail">{{'recoveryCodeDesc' | i18n}}</span>
|
||||
<a href="#" appStopClick class="list-group-item list-group-item-action" (click)="recover()">
|
||||
<h3>{{'recoveryCodeTitle' | i18n}}</h3>
|
||||
{{'recoveryCodeDesc' | i18n}}
|
||||
</a>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" data-dismiss="modal">{{'close' | i18n}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</content>
|
||||
|
|
|
@ -22,10 +22,4 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent {
|
|||
i18nService: I18nService, platformUtilsService: PlatformUtilsService) {
|
||||
super(authService, router, analytics, toasterService, i18nService, platformUtilsService, window);
|
||||
}
|
||||
|
||||
choose(p: any) {
|
||||
super.choose(p);
|
||||
this.authService.selectedTwoFactorProviderType = p.type;
|
||||
this.router.navigate(['2fa']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,108 +1,73 @@
|
|||
<form id="two-factor-page" #form (ngSubmit)="submit()" [appApiAction]="formPromise">
|
||||
<header>
|
||||
<div class="left">
|
||||
<a routerLink="/">{{'back' | i18n}}</a>
|
||||
</div>
|
||||
<div class="center">
|
||||
<span class="title">{{title}}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<button type="submit" appBlurClick [disabled]="form.loading"
|
||||
*ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
|
||||
selectedProviderType !== providerType.OrganizationDuo">
|
||||
<span [hidden]="form.loading">{{'continue' | i18n}}</span>
|
||||
<i class="fa fa-spinner fa-lg fa-spin" [hidden]="!form.loading"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
<content>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Authenticator ||
|
||||
selectedProviderType === providerType.Email">
|
||||
<div class="content text-center">
|
||||
<span *ngIf="selectedProviderType === providerType.Authenticator">
|
||||
{{'enterVerificationCodeApp' | i18n}}
|
||||
</span>
|
||||
<span *ngIf="selectedProviderType === providerType.Email">
|
||||
<form #form (ngSubmit)="submit()" [appApiAction]="formPromise" class="container">
|
||||
<div class="row justify-content-md-center mt-5">
|
||||
<div class="col-5">
|
||||
<p class="lead text-center mb-4">{{title}}</p>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Email || selectedProviderType === providerType.Authenticator">
|
||||
<p *ngIf="selectedProviderType === providerType.Authenticator">{{'enterVerificationCodeApp' | i18n}}</p>
|
||||
<p *ngIf="selectedProviderType === providerType.Email">
|
||||
{{'enterVerificationCodeEmail' | i18n : twoFactorEmail}}
|
||||
</span>
|
||||
</div>
|
||||
<div class="box first">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<label for="code">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="text" name="Code" [(ngModel)]="token" required appAutofocus
|
||||
inputmode="tel" appInputVerbatim>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="remember">{{'rememberMe' | i18n}}</label>
|
||||
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember">
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="text" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus>
|
||||
<small class="form-text" *ngIf="selectedProviderType === providerType.Email">
|
||||
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise" *ngIf="selectedProviderType === providerType.Email">
|
||||
{{'sendVerificationCodeEmailAgain' | i18n}}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Yubikey">
|
||||
<div class="content text-center">
|
||||
<p>{{'insertYubiKey' | i18n}}</p>
|
||||
<img src="../../images/two-factor/yubikey.jpg" class="img-rounded img-responsive" alt="">
|
||||
</div>
|
||||
<div class="box first">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row" appBoxRow>
|
||||
<p class="text-center">{{'insertYubiKey' | i18n}}</p>
|
||||
<img src="../../images/yubikey.jpg" class="rounded img-fluid mb-3" alt="">
|
||||
<div class="form-group">
|
||||
<label for="code" class="sr-only">{{'verificationCode' | i18n}}</label>
|
||||
<input id="code" type="password" name="Code" [(ngModel)]="token" required appAutofocus
|
||||
appInputVerbatim>
|
||||
</div>
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="remember">{{'rememberMe' | i18n}}</label>
|
||||
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember">
|
||||
</div>
|
||||
</div>
|
||||
<input id="code" type="password" name="Code" class="form-control" [(ngModel)]="token" required appAutofocus>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.U2f">
|
||||
<div class="content text-center">
|
||||
<span *ngIf="!u2fReady"><i class="fa fa-spinner fa-spin"></i></span>
|
||||
<div *ngIf="u2fReady">
|
||||
<p>{{'insertU2f' | i18n}}</p>
|
||||
<img src="../../images/two-factor/u2fkey.jpg" alt="" class="img-rounded img-responsive" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="box first">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="remember">{{'rememberMe' | i18n}}</label>
|
||||
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-center" *ngIf="!u2fReady">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</p>
|
||||
<ng-container *ngIf="u2fReady">
|
||||
<p class="text-center">{{'insertU2f' | i18n}}</p>
|
||||
<img src="../../images/u2fkey.jpg" alt="" class="rounded img-fluid mb-3">
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="selectedProviderType === providerType.Duo ||
|
||||
selectedProviderType === providerType.OrganizationDuo">
|
||||
<div id="duo-frame" *ngIf="!showNewWindowMessage"><iframe id="duo_iframe"></iframe></div>
|
||||
<div *ngIf="showNewWindowMessage" class="content text-center">{{'twoStepNewWindowMessage' | i18n}}</div>
|
||||
<div class="box">
|
||||
<div class="box-content">
|
||||
<div class="box-content-row box-content-row-checkbox" appBoxRow>
|
||||
<label for="remember">{{'rememberMe' | i18n}}</label>
|
||||
<input id="remember" type="checkbox" name="Remember" [(ngModel)]="remember">
|
||||
</div>
|
||||
</div>
|
||||
<div id="duo-frame">
|
||||
<iframe id="duo_iframe"></iframe>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="content text-center" *ngIf="selectedProviderType == null">
|
||||
<div class="form-check" *ngIf="selectedProviderType != null">
|
||||
<input id="remember" type="checkbox" name="Remember" class="form-check-input" [(ngModel)]="remember">
|
||||
<label for="remember" class="form-check-label">{{'rememberMe' | i18n}}</label>
|
||||
</div>
|
||||
<ng-container *ngIf="selectedProviderType == null">
|
||||
<p>{{'noTwoStepProviders' | i18n}}</p>
|
||||
<p>{{'noTwoStepProviders2' | i18n}}</p>
|
||||
</div>
|
||||
<div class="content no-vpad text-center" *ngIf="selectedProviderType != null">
|
||||
<p>
|
||||
<a href="#" appStopClick (click)="anotherMethod()">{{'useAnotherTwoStepMethod' | i18n}}</a>
|
||||
</p>
|
||||
<p *ngIf="selectedProviderType === providerType.Email">
|
||||
<a href="#" appStopClick (click)="sendEmail(true)" [appApiAction]="emailPromise">
|
||||
{{'sendVerificationCodeEmailAgain' | i18n}}
|
||||
</ng-container>
|
||||
<hr>
|
||||
<div class="d-flex mb-3">
|
||||
<button type="submit" class="btn btn-primary btn-block" [disabled]="form.loading" appBlurClick *ngIf="selectedProviderType != null && selectedProviderType !== providerType.Duo &&
|
||||
selectedProviderType !== providerType.OrganizationDuo && selectedProviderType !== providerType.U2f">
|
||||
<span [hidden]="form.loading">
|
||||
<i class="fa fa-sign-in"></i> {{'continue' | i18n}}</span>
|
||||
<i class="fa fa-spinner fa-spin" [hidden]="!form.loading"></i>
|
||||
</button>
|
||||
<a routerLink="/" class="btn btn-outline-secondary btn-block ml-2 mt-0">
|
||||
{{'cancel' | i18n}}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</content>
|
||||
<div class="text-center">
|
||||
<a href="#" appStopClick (click)="anotherMethod()">{{'useAnotherTwoStepMethod' | i18n}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<iframe id="u2f_iframe" hidden></iframe>
|
||||
<ng-template #twoFactorOptions></ng-template>
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
NgZone,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
ComponentFactoryResolver,
|
||||
ViewChild,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { Router } from '@angular/router';
|
||||
|
@ -11,6 +10,10 @@ import { Router } from '@angular/router';
|
|||
import { ToasterService } from 'angular2-toaster';
|
||||
import { Angulartics2 } from 'angulartics2';
|
||||
|
||||
import { TwoFactorOptionsComponent } from './two-factor-options.component';
|
||||
|
||||
import { ModalComponent } from '../modal.component';
|
||||
|
||||
import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType';
|
||||
|
||||
import { ApiService } from 'jslib/abstractions/api.service';
|
||||
|
@ -20,31 +23,37 @@ import { I18nService } from 'jslib/abstractions/i18n.service';
|
|||
import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service';
|
||||
import { SyncService } from 'jslib/abstractions/sync.service';
|
||||
|
||||
import { BroadcasterService } from 'jslib/angular/services/broadcaster.service';
|
||||
|
||||
import { TwoFactorComponent as BaseTwoFactorComponent } from 'jslib/angular/components/two-factor.component';
|
||||
|
||||
const BroadcasterSubscriptionId = 'TwoFactorComponent';
|
||||
|
||||
@Component({
|
||||
selector: 'app-two-factor',
|
||||
templateUrl: 'two-factor.component.html',
|
||||
})
|
||||
export class TwoFactorComponent extends BaseTwoFactorComponent {
|
||||
showNewWindowMessage = false;
|
||||
@ViewChild('twoFactorOptions', { read: ViewContainerRef }) twoFactorOptionsModal: ViewContainerRef;
|
||||
|
||||
constructor(authService: AuthService, router: Router,
|
||||
analytics: Angulartics2, toasterService: ToasterService,
|
||||
i18nService: I18nService, apiService: ApiService,
|
||||
platformUtilsService: PlatformUtilsService, syncService: SyncService,
|
||||
environmentService: EnvironmentService, private ngZone: NgZone,
|
||||
private broadcasterService: BroadcasterService, private changeDetectorRef: ChangeDetectorRef) {
|
||||
platformUtilsService: PlatformUtilsService, private syncService: SyncService,
|
||||
environmentService: EnvironmentService, private componentFactoryResolver: ComponentFactoryResolver) {
|
||||
super(authService, router, analytics, toasterService, i18nService, apiService,
|
||||
platformUtilsService, window, environmentService);
|
||||
this.successRoute = '/vault';
|
||||
}
|
||||
|
||||
anotherMethod() {
|
||||
this.router.navigate(['2fa-options']);
|
||||
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
|
||||
const modal = this.twoFactorOptionsModal.createComponent(factory).instance;
|
||||
const childComponent = modal.show<TwoFactorOptionsComponent>(TwoFactorOptionsComponent,
|
||||
this.twoFactorOptionsModal);
|
||||
|
||||
childComponent.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => {
|
||||
modal.close();
|
||||
this.selectedProviderType = provider;
|
||||
await this.init();
|
||||
});
|
||||
childComponent.onRecoverSelected.subscribe(() => {
|
||||
modal.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -113,6 +113,7 @@ import { Folder } from 'jslib/models/domain';
|
|||
AttachmentsComponent,
|
||||
FolderAddEditComponent,
|
||||
ModalComponent,
|
||||
TwoFactorOptionsComponent,
|
||||
],
|
||||
providers: [],
|
||||
bootstrap: [AppComponent],
|
||||
|
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.9 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.6 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
@ -559,5 +559,95 @@
|
|||
"example": "1.2.3"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enterVerificationCodeApp": {
|
||||
"message": "Enter the 6 digit verification code from your authenticator app."
|
||||
},
|
||||
"enterVerificationCodeEmail": {
|
||||
"message": "Enter the 6 digit verification code that was emailed to $EMAIL$.",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "example@gmail.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"verificationCodeEmailSent": {
|
||||
"message": "Verification email sent to $EMAIL$.",
|
||||
"placeholders": {
|
||||
"email": {
|
||||
"content": "$1",
|
||||
"example": "example@gmail.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rememberMe": {
|
||||
"message": "Remember me"
|
||||
},
|
||||
"sendVerificationCodeEmailAgain": {
|
||||
"message": "Send verification code email again"
|
||||
},
|
||||
"useAnotherTwoStepMethod": {
|
||||
"message": "Use another two-step login method"
|
||||
},
|
||||
"insertYubiKey": {
|
||||
"message": "Insert your YubiKey into your computer's USB port, then touch its button."
|
||||
},
|
||||
"insertU2f": {
|
||||
"message": "Insert your security key into your computer's USB port. If it has a button, touch it."
|
||||
},
|
||||
"loginUnavailable": {
|
||||
"message": "Login Unavailable"
|
||||
},
|
||||
"noTwoStepProviders": {
|
||||
"message": "This account has two-step login enabled, however, none of the configured two-step providers are supported by this web browser."
|
||||
},
|
||||
"noTwoStepProviders2": {
|
||||
"message": "Please use a supported web browser (such as Chrome) and/or add additional providers that are better supported across web browsers (such as an authenticator app)."
|
||||
},
|
||||
"twoStepOptions": {
|
||||
"message": "Two-step Login Options"
|
||||
},
|
||||
"recoveryCodeDesc": {
|
||||
"message": "Lost access to all of your two-factor providers? Use your recovery code to disable all two-factor providers from your account."
|
||||
},
|
||||
"recoveryCodeTitle": {
|
||||
"message": "Recovery Code"
|
||||
},
|
||||
"authenticatorAppTitle": {
|
||||
"message": "Authenticator App"
|
||||
},
|
||||
"authenticatorAppDesc": {
|
||||
"message": "Use an authenticator app (such as Authy or Google Authenticator) to generate time-based verification codes.",
|
||||
"description": "'Authy' and 'Google Authenticator' are product names and should not be translated."
|
||||
},
|
||||
"yubiKeyTitle": {
|
||||
"message": "YubiKey OTP Security Key"
|
||||
},
|
||||
"yubiKeyDesc": {
|
||||
"message": "Use a YubiKey to access your account. Works with YubiKey 4, 4 Nano, 4C, and NEO devices."
|
||||
},
|
||||
"duoDesc": {
|
||||
"message": "Verify with Duo Security using the Duo Mobile app, SMS, phone call, or U2F security key.",
|
||||
"description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated."
|
||||
},
|
||||
"duoOrganizationDesc": {
|
||||
"message": "Verify with Duo Security for your organization using the Duo Mobile app, SMS, phone call, or U2F security key.",
|
||||
"description": "'Duo Security' and 'Duo Mobile' are product names and should not be translated."
|
||||
},
|
||||
"u2fDesc": {
|
||||
"message": "Use any FIDO U2F enabled security key to access your account."
|
||||
},
|
||||
"u2fTitle": {
|
||||
"message": "FIDO U2F Security Key"
|
||||
},
|
||||
"emailTitle": {
|
||||
"message": "Email"
|
||||
},
|
||||
"emailDesc": {
|
||||
"message": "Verification codes will be emailed to you."
|
||||
},
|
||||
"continue": {
|
||||
"message": "Continue"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -144,6 +144,14 @@ body {
|
|||
color: $text-muted;
|
||||
}
|
||||
}
|
||||
.modal .list-group-flush {
|
||||
:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
justify-content: flex-start;
|
||||
|
@ -151,7 +159,7 @@ body {
|
|||
@include border-radius($modal-content-border-radius);
|
||||
}
|
||||
|
||||
form label {
|
||||
form label:not(.form-check-label) {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,11 @@ export class HtmlStorageService implements StorageService {
|
|||
private localStorageKeys = new Set(['appId', 'anonymousAppId', 'rememberedEmail',
|
||||
ConstantsService.disableFaviconKey, ConstantsService.lockOptionKey, ConstantsService.localeKey,
|
||||
ConstantsService.lockOptionKey]);
|
||||
private localStorageStartsWithKeys = ['twoFactorToken_'];
|
||||
|
||||
get<T>(key: string): Promise<T> {
|
||||
let json: string = null;
|
||||
if (this.localStorageKeys.has(key)) {
|
||||
if (this.isLocalStorage(key)) {
|
||||
json = window.localStorage.getItem(key);
|
||||
} else {
|
||||
json = window.sessionStorage.getItem(key);
|
||||
|
@ -26,7 +27,7 @@ export class HtmlStorageService implements StorageService {
|
|||
}
|
||||
|
||||
const json = JSON.stringify(obj);
|
||||
if (this.localStorageKeys.has(key)) {
|
||||
if (this.isLocalStorage(key)) {
|
||||
window.localStorage.setItem(key, json);
|
||||
} else {
|
||||
window.sessionStorage.setItem(key, json);
|
||||
|
@ -35,11 +36,23 @@ export class HtmlStorageService implements StorageService {
|
|||
}
|
||||
|
||||
remove(key: string): Promise<any> {
|
||||
if (this.localStorageKeys.has(key)) {
|
||||
if (this.isLocalStorage(key)) {
|
||||
window.localStorage.removeItem(key);
|
||||
} else {
|
||||
window.sessionStorage.removeItem(key);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private isLocalStorage(key: string): boolean {
|
||||
if (this.localStorageKeys.has(key)) {
|
||||
return true;
|
||||
}
|
||||
for (const swKey of this.localStorageStartsWithKeys) {
|
||||
if (key.startsWith(swKey)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|