diff --git a/src/app/app.module.ts b/src/app/app.module.ts index bf08d3ad56..943973a0db 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -44,6 +44,7 @@ import { PurgeVaultComponent } from './settings/purge-vault.component'; import { SettingsComponent } from './settings/settings.component'; import { TwoFactorAuthenticatorComponent } from './settings/two-factor-authenticator.component'; import { TwoFactorDuoComponent } from './settings/two-factor-duo.component'; +import { TwoFactorEmailComponent } from './settings/two-factor-email.component'; import { TwoFactorSetupComponent } from './settings/two-factor-setup.component'; import { TwoFactorYubiKeyComponent } from './settings/two-factor-yubikey.component'; @@ -149,6 +150,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe'; TwoFactorAuthenticatorComponent, TwoFactorComponent, TwoFactorDuoComponent, + TwoFactorEmailComponent, TwoFactorOptionsComponent, TwoFactorYubiKeyComponent, TwoFactorSetupComponent, @@ -171,6 +173,7 @@ import { SearchCiphersPipe } from 'jslib/angular/pipes/search-ciphers.pipe'; ShareComponent, TwoFactorAuthenticatorComponent, TwoFactorDuoComponent, + TwoFactorEmailComponent, TwoFactorOptionsComponent, TwoFactorYubiKeyComponent, ], diff --git a/src/app/settings/two-factor-email.component.html b/src/app/settings/two-factor-email.component.html new file mode 100644 index 0000000000..fc503669c1 --- /dev/null +++ b/src/app/settings/two-factor-email.component.html @@ -0,0 +1,72 @@ + diff --git a/src/app/settings/two-factor-email.component.ts b/src/app/settings/two-factor-email.component.ts new file mode 100644 index 0000000000..637876ab94 --- /dev/null +++ b/src/app/settings/two-factor-email.component.ts @@ -0,0 +1,126 @@ +import { + Component, + EventEmitter, + Output, +} from '@angular/core'; + +import { ToasterService } from 'angular2-toaster'; +import { Angulartics2 } from 'angulartics2'; + +import { ApiService } from 'jslib/abstractions/api.service'; +import { CryptoService } from 'jslib/abstractions/crypto.service'; +import { I18nService } from 'jslib/abstractions/i18n.service'; +import { PlatformUtilsService } from 'jslib/abstractions/platformUtils.service'; +import { UserService } from 'jslib/abstractions/user.service'; + +import { PasswordVerificationRequest } from 'jslib/models/request/passwordVerificationRequest'; +import { TwoFactorProviderRequest } from 'jslib/models/request/twoFactorProviderRequest'; + +import { TwoFactorProviderType } from 'jslib/enums/twoFactorProviderType'; +import { UpdateTwoFactorEmailRequest } from 'jslib/models/request/updateTwoFactorEmailRequest'; +import { TwoFactorEmailResponse } from 'jslib/models/response/twoFactorEmailResponse'; +import { TwoFactorEmailRequest } from 'jslib/models/request'; + +@Component({ + selector: 'app-two-factor-email', + templateUrl: 'two-factor-email.component.html', +}) +export class TwoFactorEmailComponent { + @Output() onUpdated = new EventEmitter(); + + enabled = false; + authed = false; + email: string; + token: string; + masterPassword: string; + sentEmail: string; + + authPromise: Promise; + formPromise: Promise; + emailPromise: Promise; + + private masterPasswordHash: string; + + constructor(private apiService: ApiService, private i18nService: I18nService, + private analytics: Angulartics2, private toasterService: ToasterService, + private cryptoService: CryptoService, private platformUtilsService: PlatformUtilsService, + private userService: UserService) { } + + async auth() { + if (this.masterPassword == null || this.masterPassword === '') { + this.toasterService.popAsync('error', this.i18nService.t('errorOccurred'), + this.i18nService.t('masterPassRequired')); + return; + } + + const request = new PasswordVerificationRequest(); + request.masterPasswordHash = this.masterPasswordHash = + await this.cryptoService.hashPassword(this.masterPassword, null); + try { + this.authPromise = this.apiService.getTwoFactorEmail(request); + const response = await this.authPromise; + this.authed = true; + await this.processResponse(response); + } catch { } + } + + async submit() { + if (this.enabled) { + this.disable(); + } else { + this.enable(); + } + } + + async sendEmail() { + try { + const request = new TwoFactorEmailRequest(this.email, this.masterPasswordHash); + this.emailPromise = this.apiService.postTwoFactorEmailSetup(request); + await this.emailPromise; + this.sentEmail = this.email; + } catch { } + } + + private async enable() { + const request = new UpdateTwoFactorEmailRequest(); + request.masterPasswordHash = this.masterPasswordHash; + request.email = this.email; + request.token = this.token; + try { + this.formPromise = this.apiService.putTwoFactorEmail(request); + const response = await this.formPromise; + await this.processResponse(response); + this.analytics.eventTrack.next({ action: 'Enabled Two-step Email' }); + this.onUpdated.emit(true); + } catch { } + } + + private async disable() { + const confirmed = await this.platformUtilsService.showDialog(this.i18nService.t('twoStepDisableDesc'), + this.i18nService.t('disable'), this.i18nService.t('yes'), this.i18nService.t('no'), 'warning'); + if (!confirmed) { + return; + } + + try { + const request = new TwoFactorProviderRequest(); + request.masterPasswordHash = this.masterPasswordHash; + request.type = TwoFactorProviderType.Email; + this.formPromise = this.apiService.putTwoFactorDisable(request); + await this.formPromise; + this.enabled = false; + this.analytics.eventTrack.next({ action: 'Disabled Two-step Email' }); + this.toasterService.popAsync('success', null, this.i18nService.t('twoStepDisabled')); + this.onUpdated.emit(false); + } catch { } + } + + private async processResponse(response: TwoFactorEmailResponse) { + this.token = null; + this.email = response.email; + this.enabled = response.enabled; + if (!this.enabled && (this.email == null || this.email === '')) { + this.email = await this.userService.getEmail(); + } + } +} diff --git a/src/app/settings/two-factor-setup.component.ts b/src/app/settings/two-factor-setup.component.ts index 9d13c3720f..aa3e1cc4ec 100644 --- a/src/app/settings/two-factor-setup.component.ts +++ b/src/app/settings/two-factor-setup.component.ts @@ -18,6 +18,7 @@ import { ModalComponent } from '../modal.component'; import { TwoFactorAuthenticatorComponent } from './two-factor-authenticator.component'; import { TwoFactorDuoComponent } from './two-factor-duo.component'; +import { TwoFactorEmailComponent } from './two-factor-email.component'; import { TwoFactorYubiKeyComponent } from './two-factor-yubikey.component'; @Component({ @@ -30,6 +31,7 @@ export class TwoFactorSetupComponent implements OnInit { @ViewChild('yubikeyTemplate', { read: ViewContainerRef }) yubikeyModalRef: ViewContainerRef; @ViewChild('u2fTemplate', { read: ViewContainerRef }) u2fModalRef: ViewContainerRef; @ViewChild('duoTemplate', { read: ViewContainerRef }) duoModalRef: ViewContainerRef; + @ViewChild('emailTemplate', { read: ViewContainerRef }) emailModalRef: ViewContainerRef; providers: any[] = []; premium: boolean; @@ -100,6 +102,12 @@ export class TwoFactorSetupComponent implements OnInit { this.updateStatus(enabled, TwoFactorProviderType.Duo); }); break; + case TwoFactorProviderType.Email: + const emailComp = this.openModal(this.emailModalRef, TwoFactorEmailComponent); + emailComp.onUpdated.subscribe((enabled: boolean) => { + this.updateStatus(enabled, TwoFactorProviderType.Email); + }); + break; default: break; } diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index f88f75dfce..7e8920eb64 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -1086,5 +1086,17 @@ }, "twoFactorDuoApiHostname": { "message": "API Hostname" + }, + "twoFactorEmailDesc": { + "message": "Follow these steps to set up two-step login with email:" + }, + "twoFactorEmailEnterEmail": { + "message": "Enter the email that you wish to receive verification codes" + }, + "twoFactorEmailEnterCode": { + "message": "Enter the resulting 6 digit verification code from the email" + }, + "sendEmail": { + "message": "Send Email" } }