From f855a8272c7dc3c3099643cf34e49d3089e6575e Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 4 Apr 2018 09:47:43 -0400 Subject: [PATCH] share components with jslib --- .../components/environment.component.ts | 59 +++++++ src/angular/components/hint.component.ts | 41 +++++ src/angular/components/login.component.ts | 6 +- src/angular/components/register.component.ts | 78 ++++++++++ .../two-factor-options.component.ts | 66 ++++++++ .../components/two-factor.component.ts | 147 ++++++++++++++++++ 6 files changed, 392 insertions(+), 5 deletions(-) create mode 100644 src/angular/components/environment.component.ts create mode 100644 src/angular/components/hint.component.ts create mode 100644 src/angular/components/register.component.ts create mode 100644 src/angular/components/two-factor-options.component.ts create mode 100644 src/angular/components/two-factor.component.ts diff --git a/src/angular/components/environment.component.ts b/src/angular/components/environment.component.ts new file mode 100644 index 0000000000..3bc31043f1 --- /dev/null +++ b/src/angular/components/environment.component.ts @@ -0,0 +1,59 @@ +import { + EventEmitter, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { EnvironmentService } from '../../abstractions/environment.service'; +import { I18nService } from '../../abstractions/i18n.service'; + +export class EnvironmentComponent { + @Output() onSaved = new EventEmitter(); + + iconsUrl: string; + identityUrl: string; + apiUrl: string; + webVaultUrl: string; + baseUrl: string; + showCustom = false; + + constructor(protected analytics: Angulartics2, protected toasterService: ToasterService, + protected environmentService: EnvironmentService, protected i18nService: I18nService) { + this.baseUrl = environmentService.baseUrl || ''; + this.webVaultUrl = environmentService.webVaultUrl || ''; + this.apiUrl = environmentService.apiUrl || ''; + this.identityUrl = environmentService.identityUrl || ''; + this.iconsUrl = environmentService.iconsUrl || ''; + } + + async submit() { + const resUrls = await this.environmentService.setUrls({ + base: this.baseUrl, + api: this.apiUrl, + identity: this.identityUrl, + webVault: this.webVaultUrl, + icons: this.iconsUrl, + }); + + // re-set urls since service can change them, ex: prefixing https:// + this.baseUrl = resUrls.base; + this.apiUrl = resUrls.api; + this.identityUrl = resUrls.identity; + this.webVaultUrl = resUrls.webVault; + this.iconsUrl = resUrls.icons; + + this.analytics.eventTrack.next({ action: 'Set Environment URLs' }); + this.toasterService.popAsync('success', null, this.i18nService.t('environmentSaved')); + this.saved(); + } + + toggleCustom() { + this.showCustom = !this.showCustom; + } + + protected saved() { + this.onSaved.emit(); + } +} diff --git a/src/angular/components/hint.component.ts b/src/angular/components/hint.component.ts new file mode 100644 index 0000000000..be8a8ec8e4 --- /dev/null +++ b/src/angular/components/hint.component.ts @@ -0,0 +1,41 @@ +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { PasswordHintRequest } from '../../models/request/passwordHintRequest'; + +import { ApiService } from '../../abstractions/api.service'; +import { I18nService } from '../../abstractions/i18n.service'; + +export class HintComponent { + email: string = ''; + formPromise: Promise; + + protected successRoute = 'login'; + + constructor(protected router: Router, protected analytics: Angulartics2, + protected toasterService: ToasterService, protected i18nService: I18nService, + protected apiService: ApiService) { } + + async submit() { + if (this.email == null || this.email === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('emailRequired')); + return; + } + if (this.email.indexOf('@') === -1) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidEmail')); + return; + } + + try { + this.formPromise = this.apiService.postPasswordHint(new PasswordHintRequest(this.email)); + await this.formPromise; + this.analytics.eventTrack.next({ action: 'Requested Hint' }); + this.toasterService.popAsync('success', null, this.i18nService.t('masterPassSent')); + this.router.navigate([this.successRoute]); + } catch { } + } +} diff --git a/src/angular/components/login.component.ts b/src/angular/components/login.component.ts index 951490e79f..0da933fa28 100644 --- a/src/angular/components/login.component.ts +++ b/src/angular/components/login.component.ts @@ -1,8 +1,4 @@ -import { - Component, - EventEmitter, - Input, -} from '@angular/core'; +import { Input } from '@angular/core'; import { Router } from '@angular/router'; import { ToasterService } from 'angular2-toaster'; diff --git a/src/angular/components/register.component.ts b/src/angular/components/register.component.ts new file mode 100644 index 0000000000..eb9c39f0da --- /dev/null +++ b/src/angular/components/register.component.ts @@ -0,0 +1,78 @@ +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { RegisterRequest } from '../../models/request/registerRequest'; + +import { ApiService } from '../../abstractions/api.service'; +import { AuthService } from '../../abstractions/auth.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { I18nService } from '../../abstractions/i18n.service'; + +export class RegisterComponent { + email: string = ''; + masterPassword: string = ''; + confirmMasterPassword: string = ''; + hint: string = ''; + showPassword: boolean = false; + formPromise: Promise; + + protected successRoute = 'login'; + + constructor(protected authService: AuthService, protected router: Router, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected i18nService: I18nService, protected cryptoService: CryptoService, + protected apiService: ApiService) { } + + async submit() { + if (this.email == null || this.email === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('emailRequired')); + return; + } + if (this.email.indexOf('@') === -1) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('invalidEmail')); + return; + } + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + if (this.masterPassword.length < 8) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassLength')); + return; + } + if (this.masterPassword !== this.confirmMasterPassword) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassDoesntMatch')); + return; + } + + try { + this.formPromise = this.register(); + await this.formPromise; + this.analytics.eventTrack.next({ action: 'Registered' }); + this.toasterService.popAsync('success', null, this.i18nService.t('newAccountCreated')); + this.router.navigate([this.successRoute]); + } catch { } + } + + togglePassword(confirmField: boolean) { + this.analytics.eventTrack.next({ action: 'Toggled Master Password on Register' }); + this.showPassword = !this.showPassword; + document.getElementById(confirmField ? 'masterPasswordRetype' : 'masterPassword').focus(); + } + + private async register() { + this.email = this.email.toLowerCase(); + const key = this.cryptoService.makeKey(this.masterPassword, this.email); + const encKey = await this.cryptoService.makeEncKey(key); + const hashedPassword = await this.cryptoService.hashPassword(this.masterPassword, key); + const request = new RegisterRequest(this.email, hashedPassword, this.hint, encKey.encryptedString); + await this.apiService.postRegister(request); + } +} diff --git a/src/angular/components/two-factor-options.component.ts b/src/angular/components/two-factor-options.component.ts new file mode 100644 index 0000000000..cad2734cf6 --- /dev/null +++ b/src/angular/components/two-factor-options.component.ts @@ -0,0 +1,66 @@ +import { + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +import { AuthService } from '../../abstractions/auth.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { TwoFactorProviders } from '../../services/auth.service'; + +export class TwoFactorOptionsComponent implements OnInit { + @Output() onProviderSelected = new EventEmitter(); + @Output() onRecoverSelected = new EventEmitter(); + + providers: any[] = []; + + constructor(protected authService: AuthService, protected router: Router, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService) { } + + ngOnInit() { + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.OrganizationDuo)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.OrganizationDuo]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Authenticator)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Authenticator]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Yubikey)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Yubikey]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Duo)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Duo]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.U2f) && + this.platformUtilsService.supportsU2f(window)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.U2f]); + } + + if (this.authService.twoFactorProviders.has(TwoFactorProviderType.Email)) { + this.providers.push(TwoFactorProviders[TwoFactorProviderType.Email]); + } + } + + choose(p: any) { + this.onProviderSelected.emit(p.type); + } + + recover() { + this.analytics.eventTrack.next({ action: 'Selected Recover' }); + this.platformUtilsService.launchUri('https://help.bitwarden.com/article/lost-two-step-device/'); + this.onRecoverSelected.emit(); + } +} diff --git a/src/angular/components/two-factor.component.ts b/src/angular/components/two-factor.component.ts new file mode 100644 index 0000000000..2005bcac49 --- /dev/null +++ b/src/angular/components/two-factor.component.ts @@ -0,0 +1,147 @@ +import { OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { TwoFactorOptionsComponent } from './two-factor-options.component'; + +import { TwoFactorProviderType } from '../../enums/twoFactorProviderType'; + +import { TwoFactorEmailRequest } from '../../models/request/twoFactorEmailRequest'; + +import { ApiService } from '../../abstractions/api.service'; +import { AuthService } from '../../abstractions/auth.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { SyncService } from '../../abstractions/sync.service'; + +import { TwoFactorProviders } from '../../services/auth.service'; + +export class TwoFactorComponent implements OnInit { + token: string = ''; + remember: boolean = false; + u2fReady: boolean = false; + providers = TwoFactorProviders; + providerType = TwoFactorProviderType; + selectedProviderType: TwoFactorProviderType = TwoFactorProviderType.Authenticator; + u2fSupported: boolean = false; + u2f: any = null; + title: string = ''; + twoFactorEmail: string = null; + formPromise: Promise; + emailPromise: Promise; + + protected successRoute = 'vault'; + + constructor(protected authService: AuthService, protected router: Router, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected i18nService: I18nService, protected apiService: ApiService, + protected platformUtilsService: PlatformUtilsService, protected syncService: SyncService) { + this.u2fSupported = this.platformUtilsService.supportsU2f(window); + } + + async ngOnInit() { + if (this.authService.email == null || this.authService.masterPasswordHash == null || + this.authService.twoFactorProviders == null) { + this.router.navigate(['login']); + return; + } + + this.selectedProviderType = this.authService.getDefaultTwoFactorProvider(this.u2fSupported); + await this.init(); + } + + async init() { + if (this.selectedProviderType == null) { + this.title = this.i18nService.t('loginUnavailable'); + return; + } + + this.title = (TwoFactorProviders as any)[this.selectedProviderType].name; + const params = this.authService.twoFactorProviders.get(this.selectedProviderType); + switch (this.selectedProviderType) { + case TwoFactorProviderType.U2f: + if (!this.u2fSupported) { + break; + } + + const challenges = JSON.parse(params.Challenges); + // TODO: init u2f + break; + case TwoFactorProviderType.Duo: + case TwoFactorProviderType.OrganizationDuo: + setTimeout(() => { + (window as any).Duo.init({ + host: params.Host, + sig_request: params.Signature, + submit_callback: async (f: HTMLFormElement) => { + const sig = f.querySelector('input[name="sig_response"]') as HTMLInputElement; + if (sig != null) { + this.token = sig.value; + await this.submit(); + } + }, + }); + }); + break; + case TwoFactorProviderType.Email: + this.twoFactorEmail = params.Email; + if (this.authService.twoFactorProviders.size > 1) { + await this.sendEmail(false); + } + break; + default: + break; + } + } + + async submit() { + if (this.token == null || this.token === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('verificationCodeRequired')); + return; + } + + if (this.selectedProviderType === TwoFactorProviderType.U2f) { + // TODO: stop U2f + } else if (this.selectedProviderType === TwoFactorProviderType.Email || + this.selectedProviderType === TwoFactorProviderType.Authenticator) { + this.token = this.token.replace(' ', '').trim(); + } + + try { + this.formPromise = this.authService.logInTwoFactor(this.selectedProviderType, this.token, this.remember); + await this.formPromise; + this.syncService.fullSync(true); + this.analytics.eventTrack.next({ action: 'Logged In From Two-step' }); + this.router.navigate([this.successRoute]); + } catch { + if (this.selectedProviderType === TwoFactorProviderType.U2f) { + // TODO: start U2F again + } + } + } + + async sendEmail(doToast: boolean) { + if (this.selectedProviderType !== TwoFactorProviderType.Email) { + return; + } + + if (this.emailPromise != null) { + return; + } + + try { + const request = new TwoFactorEmailRequest(this.authService.email, this.authService.masterPasswordHash); + this.emailPromise = this.apiService.postTwoFactorEmail(request); + await this.emailPromise; + if (doToast) { + this.toasterService.popAsync('success', null, + this.i18nService.t('verificationCodeEmailSent', this.twoFactorEmail)); + } + } catch { } + + this.emailPromise = null; + } +}