diff --git a/src/app/accounts/login.component.html b/src/app/accounts/login.component.html index 4454989b19..afac3587ba 100644 --- a/src/app/accounts/login.component.html +++ b/src/app/accounts/login.component.html @@ -18,6 +18,9 @@ + + {{'getMasterPasswordHint' | i18n}} +
@@ -32,9 +35,6 @@
-
- {{'getMasterPasswordHint' | i18n}} -
diff --git a/src/app/accounts/two-factor-options.component.html b/src/app/accounts/two-factor-options.component.html index d2bbd3112a..abbced4fac 100644 --- a/src/app/accounts/two-factor-options.component.html +++ b/src/app/accounts/two-factor-options.component.html @@ -1,24 +1,26 @@ -
-
- {{'close' | i18n}} -
-
- {{'twoStepOptions' | i18n}} -
-
-
- -
-
- - {{p.name}} - {{p.description}} - - - {{'recoveryCodeTitle' | i18n}} - {{'recoveryCodeDesc' | i18n}} - + diff --git a/src/app/accounts/two-factor-options.component.ts b/src/app/accounts/two-factor-options.component.ts index 779d4813c1..6ab0be4faa 100644 --- a/src/app/accounts/two-factor-options.component.ts +++ b/src/app/accounts/two-factor-options.component.ts @@ -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']); - } } diff --git a/src/app/accounts/two-factor.component.html b/src/app/accounts/two-factor.component.html index 404f865e8e..b7f4725d35 100644 --- a/src/app/accounts/two-factor.component.html +++ b/src/app/accounts/two-factor.component.html @@ -1,108 +1,73 @@ -
-
- -
- {{title}} -
-
- -
-
- - -
- - {{'enterVerificationCodeApp' | i18n}} - - - {{'enterVerificationCodeEmail' | i18n : twoFactorEmail}} - -
-
-
-
- - -
-
- - -
-
-
-
- -
-

{{'insertYubiKey' | i18n}}

- -
-
-
-
- - -
-
- - -
-
-
-
- -
- -
-

{{'insertU2f' | i18n}}

- -
-
-
-
-
- - -
-
-
-
- +
+
+

{{title}}

+
+
+ +

{{'enterVerificationCodeApp' | i18n}}

+

+ {{'enterVerificationCodeEmail' | i18n : twoFactorEmail}} +

+
+ + + + + {{'sendVerificationCodeEmailAgain' | i18n}} + + +
+
+ +

{{'insertYubiKey' | i18n}}

+ +
+ + +
+
+ +

+ +

+ +

{{'insertU2f' | i18n}}

+ +
+
+ -
-
{{'twoStepNewWindowMessage' | i18n}}
-
-
-
- - +
+ +
+ +
+ + +
+ +

{{'noTwoStepProviders' | i18n}}

+

{{'noTwoStepProviders2' | i18n}}

+
+
+
+ + + {{'cancel' | i18n}} + +
+
- -
-

{{'noTwoStepProviders' | i18n}}

-

{{'noTwoStepProviders2' | i18n}}

- - +
- + diff --git a/src/app/accounts/two-factor.component.ts b/src/app/accounts/two-factor.component.ts index 3155e52d5c..932a9dd775 100644 --- a/src/app/accounts/two-factor.component.ts +++ b/src/app/accounts/two-factor.component.ts @@ -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, + this.twoFactorOptionsModal); + + childComponent.onProviderSelected.subscribe(async (provider: TwoFactorProviderType) => { + modal.close(); + this.selectedProviderType = provider; + await this.init(); + }); + childComponent.onRecoverSelected.subscribe(() => { + modal.close(); + }); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0124db3882..7ca4e450a0 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -113,6 +113,7 @@ import { Folder } from 'jslib/models/domain'; AttachmentsComponent, FolderAddEditComponent, ModalComponent, + TwoFactorOptionsComponent, ], providers: [], bootstrap: [AppComponent], diff --git a/src/images/two-factor/authapp.png b/src/images/two-factor/0.png similarity index 100% rename from src/images/two-factor/authapp.png rename to src/images/two-factor/0.png diff --git a/src/images/two-factor/gmail.png b/src/images/two-factor/1.png similarity index 100% rename from src/images/two-factor/gmail.png rename to src/images/two-factor/1.png diff --git a/src/images/two-factor/duo.png b/src/images/two-factor/2.png similarity index 100% rename from src/images/two-factor/duo.png rename to src/images/two-factor/2.png diff --git a/src/images/two-factor/yubico.png b/src/images/two-factor/3.png similarity index 100% rename from src/images/two-factor/yubico.png rename to src/images/two-factor/3.png diff --git a/src/images/two-factor/fido.png b/src/images/two-factor/4.png similarity index 100% rename from src/images/two-factor/fido.png rename to src/images/two-factor/4.png diff --git a/src/images/two-factor/6.png b/src/images/two-factor/6.png new file mode 100644 index 0000000000..ab2e434036 Binary files /dev/null and b/src/images/two-factor/6.png differ diff --git a/src/images/two-factor/u2fkey.jpg b/src/images/u2fkey.jpg similarity index 100% rename from src/images/two-factor/u2fkey.jpg rename to src/images/u2fkey.jpg diff --git a/src/images/two-factor/yubikey.jpg b/src/images/yubikey.jpg similarity index 100% rename from src/images/two-factor/yubikey.jpg rename to src/images/yubikey.jpg diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index 8b3c2bbe3b..62dfc5276b 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -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" } } diff --git a/src/scss/styles.scss b/src/scss/styles.scss index 7c1083b567..55f57d0229 100644 --- a/src/scss/styles.scss +++ b/src/scss/styles.scss @@ -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; } diff --git a/src/services/htmlStorage.service.ts b/src/services/htmlStorage.service.ts index a13e10a486..684de60f0d 100644 --- a/src/services/htmlStorage.service.ts +++ b/src/services/htmlStorage.service.ts @@ -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(key: string): Promise { 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 { - 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; + } }