From 45ba62937114689ff981818d384b391195d62ec6 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Thu, 5 Apr 2018 22:21:18 -0400 Subject: [PATCH] move vault components to jslib --- src/angular/components/add-edit.component.ts | 277 ++++++++++++++++++ .../components/attachments.component.ts | 125 ++++++++ .../password-generator-history.component.ts | 33 +++ .../password-generator.component.ts | 128 ++++++++ src/angular/components/view.component.ts | 194 ++++++++++++ 5 files changed, 757 insertions(+) create mode 100644 src/angular/components/add-edit.component.ts create mode 100644 src/angular/components/attachments.component.ts create mode 100644 src/angular/components/password-generator-history.component.ts create mode 100644 src/angular/components/password-generator.component.ts create mode 100644 src/angular/components/view.component.ts diff --git a/src/angular/components/add-edit.component.ts b/src/angular/components/add-edit.component.ts new file mode 100644 index 0000000000..cc05dc6789 --- /dev/null +++ b/src/angular/components/add-edit.component.ts @@ -0,0 +1,277 @@ +import { + EventEmitter, + Input, + OnChanges, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { CipherType } from '../../enums/cipherType'; +import { FieldType } from '../../enums/fieldType'; +import { SecureNoteType } from '../../enums/secureNoteType'; +import { UriMatchType } from '../../enums/uriMatchType'; + +import { AuditService } from '../../abstractions/audit.service'; +import { CipherService } from '../../abstractions/cipher.service'; +import { FolderService } from '../../abstractions/folder.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { CardView } from '../../models/view/cardView'; +import { CipherView } from '../../models/view/cipherView'; +import { FieldView } from '../../models/view/fieldView'; +import { FolderView } from '../../models/view/folderView'; +import { IdentityView } from '../../models/view/identityView'; +import { LoginUriView } from '../../models/view/loginUriView'; +import { LoginView } from '../../models/view/loginView'; +import { SecureNoteView } from '../../models/view/secureNoteView'; + +export class AddEditComponent implements OnChanges { + @Input() folderId: string; + @Input() cipherId: string; + @Input() type: CipherType; + @Output() onSavedCipher = new EventEmitter(); + @Output() onDeletedCipher = new EventEmitter(); + @Output() onCancelled = new EventEmitter(); + @Output() onEditAttachments = new EventEmitter(); + @Output() onGeneratePassword = new EventEmitter(); + + editMode: boolean = false; + cipher: CipherView; + folders: FolderView[]; + title: string; + formPromise: Promise; + deletePromise: Promise; + checkPasswordPromise: Promise; + showPassword: boolean = false; + cipherType = CipherType; + fieldType = FieldType; + addFieldType: FieldType = FieldType.Text; + typeOptions: any[]; + cardBrandOptions: any[]; + cardExpMonthOptions: any[]; + identityTitleOptions: any[]; + addFieldTypeOptions: any[]; + uriMatchOptions: any[]; + + constructor(protected cipherService: CipherService, protected folderService: FolderService, + protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, + protected analytics: Angulartics2, protected toasterService: ToasterService, + protected auditService: AuditService) { + this.typeOptions = [ + { name: i18nService.t('typeLogin'), value: CipherType.Login }, + { name: i18nService.t('typeCard'), value: CipherType.Card }, + { name: i18nService.t('typeIdentity'), value: CipherType.Identity }, + { name: i18nService.t('typeSecureNote'), value: CipherType.SecureNote }, + ]; + this.cardBrandOptions = [ + { name: '-- ' + i18nService.t('select') + ' --', value: null }, + { name: 'Visa', value: 'Visa' }, + { name: 'Mastercard', value: 'Mastercard' }, + { name: 'American Express', value: 'Amex' }, + { name: 'Discover', value: 'Discover' }, + { name: 'Diners Club', value: 'Diners Club' }, + { name: 'JCB', value: 'JCB' }, + { name: 'Maestro', value: 'Maestro' }, + { name: 'UnionPay', value: 'UnionPay' }, + { name: i18nService.t('other'), value: 'Other' }, + ]; + this.cardExpMonthOptions = [ + { name: '-- ' + i18nService.t('select') + ' --', value: null }, + { name: '01 - ' + i18nService.t('january'), value: '1' }, + { name: '02 - ' + i18nService.t('february'), value: '2' }, + { name: '03 - ' + i18nService.t('march'), value: '3' }, + { name: '04 - ' + i18nService.t('april'), value: '4' }, + { name: '05 - ' + i18nService.t('may'), value: '5' }, + { name: '06 - ' + i18nService.t('june'), value: '6' }, + { name: '07 - ' + i18nService.t('july'), value: '7' }, + { name: '08 - ' + i18nService.t('august'), value: '8' }, + { name: '09 - ' + i18nService.t('september'), value: '9' }, + { name: '10 - ' + i18nService.t('october'), value: '10' }, + { name: '11 - ' + i18nService.t('november'), value: '11' }, + { name: '12 - ' + i18nService.t('december'), value: '12' }, + ]; + this.identityTitleOptions = [ + { name: '-- ' + i18nService.t('select') + ' --', value: null }, + { name: i18nService.t('mr'), value: i18nService.t('mr') }, + { name: i18nService.t('mrs'), value: i18nService.t('mrs') }, + { name: i18nService.t('ms'), value: i18nService.t('ms') }, + { name: i18nService.t('dr'), value: i18nService.t('dr') }, + ]; + this.addFieldTypeOptions = [ + { name: i18nService.t('cfTypeText'), value: FieldType.Text }, + { name: i18nService.t('cfTypeHidden'), value: FieldType.Hidden }, + { name: i18nService.t('cfTypeBoolean'), value: FieldType.Boolean }, + ]; + this.uriMatchOptions = [ + { name: i18nService.t('defaultMatchDetection'), value: null }, + { name: i18nService.t('baseDomain'), value: UriMatchType.Domain }, + { name: i18nService.t('host'), value: UriMatchType.Host }, + { name: i18nService.t('startsWith'), value: UriMatchType.StartsWith }, + { name: i18nService.t('regEx'), value: UriMatchType.RegularExpression }, + { name: i18nService.t('exact'), value: UriMatchType.Exact }, + { name: i18nService.t('never'), value: UriMatchType.Never }, + ]; + } + + async ngOnChanges() { + this.editMode = this.cipherId != null; + + if (this.editMode) { + this.editMode = true; + this.title = this.i18nService.t('editItem'); + const cipher = await this.cipherService.get(this.cipherId); + this.cipher = await cipher.decrypt(); + } else { + this.title = this.i18nService.t('addItem'); + this.cipher = new CipherView(); + this.cipher.folderId = this.folderId; + this.cipher.type = this.type == null ? CipherType.Login : this.type; + this.cipher.login = new LoginView(); + this.cipher.login.uris = [new LoginUriView()]; + this.cipher.card = new CardView(); + this.cipher.identity = new IdentityView(); + this.cipher.secureNote = new SecureNoteView(); + this.cipher.secureNote.type = SecureNoteType.Generic; + } + + this.folders = await this.folderService.getAllDecrypted(); + } + + async submit() { + if (this.cipher.name == null || this.cipher.name === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('nameRequired')); + return; + } + + const cipher = await this.cipherService.encrypt(this.cipher); + + try { + this.formPromise = this.cipherService.saveWithServer(cipher); + await this.formPromise; + this.cipher.id = cipher.id; + this.analytics.eventTrack.next({ action: this.editMode ? 'Edited Cipher' : 'Added Cipher' }); + this.toasterService.popAsync('success', null, + this.i18nService.t(this.editMode ? 'editedItem' : 'addedItem')); + this.onSavedCipher.emit(this.cipher); + } catch { } + } + + addUri() { + if (this.cipher.type !== CipherType.Login) { + return; + } + + if (this.cipher.login.uris == null) { + this.cipher.login.uris = []; + } + + this.cipher.login.uris.push(new LoginUriView()); + } + + removeUri(uri: LoginUriView) { + if (this.cipher.type !== CipherType.Login || this.cipher.login.uris == null) { + return; + } + + const i = this.cipher.login.uris.indexOf(uri); + if (i > -1) { + this.cipher.login.uris.splice(i, 1); + } + } + + addField() { + if (this.cipher.fields == null) { + this.cipher.fields = []; + } + + const f = new FieldView(); + f.type = this.addFieldType; + this.cipher.fields.push(f); + } + + removeField(field: FieldView) { + const i = this.cipher.fields.indexOf(field); + if (i > -1) { + this.cipher.fields.splice(i, 1); + } + } + + cancel() { + this.onCancelled.emit(this.cipher); + } + + attachments() { + this.onEditAttachments.emit(this.cipher); + } + + async delete() { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteItemConfirmation'), this.i18nService.t('deleteItem'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return; + } + + try { + this.deletePromise = this.cipherService.deleteWithServer(this.cipher.id); + await this.deletePromise; + this.analytics.eventTrack.next({ action: 'Deleted Cipher' }); + this.toasterService.popAsync('success', null, this.i18nService.t('deletedItem')); + this.onDeletedCipher.emit(this.cipher); + } catch { } + } + + async generatePassword() { + if (this.cipher.login != null && this.cipher.login.password != null && this.cipher.login.password.length) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('overwritePasswordConfirmation'), this.i18nService.t('overwritePassword'), + this.i18nService.t('yes'), this.i18nService.t('no')); + if (!confirmed) { + return; + } + } + + this.onGeneratePassword.emit(); + } + + togglePassword() { + this.analytics.eventTrack.next({ action: 'Toggled Password on Edit' }); + this.showPassword = !this.showPassword; + document.getElementById('loginPassword').focus(); + } + + toggleFieldValue(field: FieldView) { + const f = (field as any); + f.showValue = !f.showValue; + } + + toggleUriOptions(uri: LoginUriView) { + const u = (uri as any); + u.showOptions = u.showOptions == null && uri.match != null ? false : !u.showOptions; + } + + loginUriMatchChanged(uri: LoginUriView) { + const u = (uri as any); + u.showOptions = u.showOptions == null ? true : u.showOptions; + } + + async checkPassword() { + if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') { + return; + } + + this.analytics.eventTrack.next({ action: 'Check Password' }); + this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); + const matches = await this.checkPasswordPromise; + + if (matches > 0) { + this.toasterService.popAsync('warning', null, this.i18nService.t('passwordExposed', matches.toString())); + } else { + this.toasterService.popAsync('success', null, this.i18nService.t('passwordSafe')); + } + } +} diff --git a/src/angular/components/attachments.component.ts b/src/angular/components/attachments.component.ts new file mode 100644 index 0000000000..6a02a7c259 --- /dev/null +++ b/src/angular/components/attachments.component.ts @@ -0,0 +1,125 @@ +import { + EventEmitter, + Input, + OnInit, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { CipherService } from '../../abstractions/cipher.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { TokenService } from '../../abstractions/token.service'; + +import { Cipher } from '../../models/domain/cipher'; + +import { AttachmentView } from '../../models/view/attachmentView'; +import { CipherView } from '../../models/view/cipherView'; + +export class AttachmentsComponent implements OnInit { + @Input() cipherId: string; + + cipher: CipherView; + cipherDomain: Cipher; + hasUpdatedKey: boolean; + canAccessAttachments: boolean; + formPromise: Promise; + deletePromises: { [id: string]: Promise; } = {}; + + constructor(protected cipherService: CipherService, protected analytics: Angulartics2, + protected toasterService: ToasterService, protected i18nService: I18nService, + protected cryptoService: CryptoService, protected tokenService: TokenService, + protected platformUtilsService: PlatformUtilsService) { } + + async ngOnInit() { + this.cipherDomain = await this.cipherService.get(this.cipherId); + this.cipher = await this.cipherDomain.decrypt(); + + const key = await this.cryptoService.getEncKey(); + this.hasUpdatedKey = key != null; + const isPremium = this.tokenService.getPremium(); + this.canAccessAttachments = isPremium || this.cipher.organizationId != null; + + if (!this.canAccessAttachments) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('premiumRequiredDesc'), this.i18nService.t('premiumRequired'), + this.i18nService.t('learnMore'), this.i18nService.t('cancel')); + if (confirmed) { + this.platformUtilsService.launchUri('https://vault.bitwarden.com/#/?premium=purchase'); + } + } else if (!this.hasUpdatedKey) { + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('updateKey'), this.i18nService.t('featureUnavailable'), + this.i18nService.t('learnMore'), this.i18nService.t('cancel'), 'warning'); + if (confirmed) { + this.platformUtilsService.launchUri('https://help.bitwarden.com/article/update-encryption-key/'); + } + } + } + + async submit() { + if (!this.hasUpdatedKey) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('updateKey')); + return; + } + + const fileEl = document.getElementById('file') as HTMLInputElement; + const files = fileEl.files; + if (files == null || files.length === 0) { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('selectFile')); + return; + } + + if (files[0].size > 104857600) { // 100 MB + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('maxFileSize')); + return; + } + + try { + this.formPromise = this.cipherService.saveAttachmentWithServer(this.cipherDomain, files[0]); + this.cipherDomain = await this.formPromise; + this.cipher = await this.cipherDomain.decrypt(); + this.analytics.eventTrack.next({ action: 'Added Attachment' }); + this.toasterService.popAsync('success', null, this.i18nService.t('attachmentSaved')); + } catch { } + + // reset file input + // ref: https://stackoverflow.com/a/20552042 + fileEl.type = ''; + fileEl.type = 'file'; + fileEl.value = ''; + } + + async delete(attachment: AttachmentView) { + if (this.deletePromises[attachment.id] != null) { + return; + } + + const confirmed = await this.platformUtilsService.showDialog( + this.i18nService.t('deleteAttachmentConfirmation'), this.i18nService.t('deleteAttachment'), + this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return; + } + + try { + this.deletePromises[attachment.id] = this.cipherService.deleteAttachmentWithServer( + this.cipher.id, attachment.id); + await this.deletePromises[attachment.id]; + this.analytics.eventTrack.next({ action: 'Deleted Attachment' }); + this.toasterService.popAsync('success', null, this.i18nService.t('deletedAttachment')); + const i = this.cipher.attachments.indexOf(attachment); + if (i > -1) { + this.cipher.attachments.splice(i, 1); + } + } catch { } + + this.deletePromises[attachment.id] = null; + } +} diff --git a/src/angular/components/password-generator-history.component.ts b/src/angular/components/password-generator-history.component.ts new file mode 100644 index 0000000000..fd2a7dc372 --- /dev/null +++ b/src/angular/components/password-generator-history.component.ts @@ -0,0 +1,33 @@ +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { OnInit } from '@angular/core'; + +import { I18nService } from '../../abstractions/i18n.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +import { PasswordHistory } from '../../models/domain/passwordHistory'; + +export class PasswordGeneratorHistoryComponent implements OnInit { + history: PasswordHistory[] = []; + + constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, + protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, + protected toasterService: ToasterService) { } + + async ngOnInit() { + this.history = await this.passwordGenerationService.getHistory(); + } + + clear() { + this.history = []; + this.passwordGenerationService.clear(); + } + + copy(password: string) { + this.analytics.eventTrack.next({ action: 'Copied Historical Password' }); + this.platformUtilsService.copyToClipboard(password); + this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); + } +} diff --git a/src/angular/components/password-generator.component.ts b/src/angular/components/password-generator.component.ts new file mode 100644 index 0000000000..10b318888a --- /dev/null +++ b/src/angular/components/password-generator.component.ts @@ -0,0 +1,128 @@ +import * as template from './password-generator.component.html'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { + ChangeDetectorRef, + EventEmitter, + Input, + NgZone, + OnInit, + Output, +} from '@angular/core'; + +import { I18nService } from '../../abstractions/i18n.service'; +import { PasswordGenerationService } from '../../abstractions/passwordGeneration.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; + +export class PasswordGeneratorComponent implements OnInit { + @Input() showSelect: boolean = false; + @Output() onSelected = new EventEmitter(); + + options: any = {}; + password: string = '-'; + showOptions = false; + avoidAmbiguous = false; + + constructor(protected passwordGenerationService: PasswordGenerationService, protected analytics: Angulartics2, + protected platformUtilsService: PlatformUtilsService, protected i18nService: I18nService, + protected toasterService: ToasterService, protected ngZone: NgZone, + protected changeDetectorRef: ChangeDetectorRef) { } + + async ngOnInit() { + this.options = await this.passwordGenerationService.getOptions(); + this.avoidAmbiguous = !this.options.ambiguous; + this.password = this.passwordGenerationService.generatePassword(this.options); + this.analytics.eventTrack.next({ action: 'Generated Password' }); + await this.passwordGenerationService.addHistory(this.password); + } + + async sliderChanged() { + this.saveOptions(false); + await this.passwordGenerationService.addHistory(this.password); + this.analytics.eventTrack.next({ action: 'Regenerated Password' }); + } + + async sliderInput() { + this.normalizeOptions(); + this.password = this.passwordGenerationService.generatePassword(this.options); + } + + async saveOptions(regenerate: boolean = true) { + this.normalizeOptions(); + await this.passwordGenerationService.saveOptions(this.options); + + if (regenerate) { + await this.regenerate(); + } + } + + async regenerate() { + this.password = this.passwordGenerationService.generatePassword(this.options); + await this.passwordGenerationService.addHistory(this.password); + this.analytics.eventTrack.next({ action: 'Regenerated Password' }); + } + + copy() { + this.analytics.eventTrack.next({ action: 'Copied Generated Password' }); + this.platformUtilsService.copyToClipboard(this.password); + this.toasterService.popAsync('info', null, this.i18nService.t('valueCopied', this.i18nService.t('password'))); + } + + select() { + this.analytics.eventTrack.next({ action: 'Selected Generated Password' }); + this.onSelected.emit(this.password); + } + + toggleOptions() { + this.showOptions = !this.showOptions; + } + + private normalizeOptions() { + this.options.minLowercase = 0; + this.options.minUppercase = 0; + this.options.ambiguous = !this.avoidAmbiguous; + + if (!this.options.uppercase && !this.options.lowercase && !this.options.number && !this.options.special) { + this.options.lowercase = true; + const lowercase = document.querySelector('#lowercase') as HTMLInputElement; + if (lowercase) { + lowercase.checked = true; + } + } + + if (!this.options.length) { + this.options.length = 5; + } else if (this.options.length > 128) { + this.options.length = 128; + } + + if (!this.options.minNumber) { + this.options.minNumber = 0; + } else if (this.options.minNumber > this.options.length) { + this.options.minNumber = this.options.length; + } else if (this.options.minNumber > 9) { + this.options.minNumber = 9; + } + + if (!this.options.minSpecial) { + this.options.minSpecial = 0; + } else if (this.options.minSpecial > this.options.length) { + this.options.minSpecial = this.options.length; + } else if (this.options.minSpecial > 9) { + this.options.minSpecial = 9; + } + + if (this.options.minSpecial + this.options.minNumber > this.options.length) { + this.options.minSpecial = this.options.length - this.options.minNumber; + } + } + + private functionWithChangeDetection(func: Function) { + this.ngZone.run(async () => { + func(); + this.changeDetectorRef.detectChanges(); + }); + } +} diff --git a/src/angular/components/view.component.ts b/src/angular/components/view.component.ts new file mode 100644 index 0000000000..ef9d52f346 --- /dev/null +++ b/src/angular/components/view.component.ts @@ -0,0 +1,194 @@ +import { + EventEmitter, + Input, + OnChanges, + OnDestroy, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { CipherType } from '../../enums/cipherType'; +import { FieldType } from '../../enums/fieldType'; + +import { AuditService } from '../../abstractions/audit.service'; +import { CipherService } from '../../abstractions/cipher.service'; +import { CryptoService } from '../../abstractions/crypto.service'; +import { I18nService } from '../../abstractions/i18n.service'; +import { PlatformUtilsService } from '../../abstractions/platformUtils.service'; +import { TokenService } from '../../abstractions/token.service'; +import { TotpService } from '../../abstractions/totp.service'; + +import { AttachmentView } from '../../models/view/attachmentView'; +import { CipherView } from '../../models/view/cipherView'; +import { FieldView } from '../../models/view/fieldView'; +import { LoginUriView } from '../../models/view/loginUriView'; + +export class ViewComponent implements OnChanges, OnDestroy { + @Input() cipherId: string; + @Output() onEditCipher = new EventEmitter(); + + cipher: CipherView; + showPassword: boolean; + isPremium: boolean; + totpCode: string; + totpCodeFormatted: string; + totpDash: number; + totpSec: number; + totpLow: boolean; + fieldType = FieldType; + checkPasswordPromise: Promise; + + private totpInterval: any; + + constructor(protected cipherService: CipherService, protected totpService: TotpService, + protected tokenService: TokenService, protected toasterService: ToasterService, + protected cryptoService: CryptoService, protected platformUtilsService: PlatformUtilsService, + protected i18nService: I18nService, protected analytics: Angulartics2, + protected auditService: AuditService) { } + + async ngOnChanges() { + this.cleanUp(); + + const cipher = await this.cipherService.get(this.cipherId); + this.cipher = await cipher.decrypt(); + + this.isPremium = this.tokenService.getPremium(); + + if (this.cipher.type === CipherType.Login && this.cipher.login.totp && + (cipher.organizationUseTotp || this.isPremium)) { + await this.totpUpdateCode(); + await this.totpTick(); + + this.totpInterval = setInterval(async () => { + await this.totpTick(); + }, 1000); + } + } + + ngOnDestroy() { + this.cleanUp(); + } + + edit() { + this.onEditCipher.emit(this.cipher); + } + + togglePassword() { + this.analytics.eventTrack.next({ action: 'Toggled Password' }); + this.showPassword = !this.showPassword; + } + + async checkPassword() { + if (this.cipher.login == null || this.cipher.login.password == null || this.cipher.login.password === '') { + return; + } + + this.analytics.eventTrack.next({ action: 'Check Password' }); + this.checkPasswordPromise = this.auditService.passwordLeaked(this.cipher.login.password); + const matches = await this.checkPasswordPromise; + + if (matches > 0) { + this.toasterService.popAsync('warning', null, this.i18nService.t('passwordExposed', matches.toString())); + } else { + this.toasterService.popAsync('success', null, this.i18nService.t('passwordSafe')); + } + } + + toggleFieldValue(field: FieldView) { + const f = (field as any); + f.showValue = !f.showValue; + } + + launch(uri: LoginUriView) { + if (!uri.canLaunch) { + return; + } + + this.analytics.eventTrack.next({ action: 'Launched Login URI' }); + this.platformUtilsService.launchUri(uri.uri); + } + + copy(value: string, typeI18nKey: string, aType: string) { + if (value == null) { + return; + } + + this.analytics.eventTrack.next({ action: 'Copied ' + aType }); + this.platformUtilsService.copyToClipboard(value); + this.toasterService.popAsync('info', null, + this.i18nService.t('valueCopied', this.i18nService.t(typeI18nKey))); + } + + async downloadAttachment(attachment: AttachmentView) { + const a = (attachment as any); + if (a.downloading) { + return; + } + + if (this.cipher.organizationId == null && !this.isPremium) { + this.toasterService.popAsync('error', this.i18nService.t('premiumRequired'), + this.i18nService.t('premiumRequiredDesc')); + return; + } + + a.downloading = true; + const response = await fetch(new Request(attachment.url, { cache: 'no-cache' })); + if (response.status !== 200) { + this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + a.downloading = false; + return; + } + + try { + const buf = await response.arrayBuffer(); + const key = await this.cryptoService.getOrgKey(this.cipher.organizationId); + const decBuf = await this.cryptoService.decryptFromBytes(buf, key); + this.platformUtilsService.saveFile(window, decBuf, null, attachment.fileName); + } catch (e) { + this.toasterService.popAsync('error', null, this.i18nService.t('errorOccurred')); + } + + a.downloading = false; + } + + private cleanUp() { + this.cipher = null; + this.showPassword = false; + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + } + + private async totpUpdateCode() { + if (this.cipher == null || this.cipher.type !== CipherType.Login || this.cipher.login.totp == null) { + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + return; + } + + this.totpCode = await this.totpService.getCode(this.cipher.login.totp); + if (this.totpCode != null) { + this.totpCodeFormatted = this.totpCode.substring(0, 3) + ' ' + this.totpCode.substring(3); + } else { + this.totpCodeFormatted = null; + if (this.totpInterval) { + clearInterval(this.totpInterval); + } + } + } + + private async totpTick() { + const epoch = Math.round(new Date().getTime() / 1000.0); + const mod = epoch % 30; + + this.totpSec = 30 - mod; + this.totpDash = +(Math.round(((2.62 * mod) + 'e+2') as any) + 'e-2'); + this.totpLow = this.totpSec <= 7; + if (mod === 0) { + await this.totpUpdateCode(); + } + } +}