diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts
index f45f4f33c8..c624f52bfe 100644
--- a/src/app/app-routing.module.ts
+++ b/src/app/app-routing.module.ts
@@ -52,6 +52,7 @@ import { BreachReportComponent } from './tools/breach-report.component';
import { ExportComponent } from './tools/export.component';
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
import { ImportComponent } from './tools/import.component';
+import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
import { PasswordGeneratorComponent } from './tools/password-generator.component';
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
import { ToolsComponent } from './tools/tools.component';
@@ -172,6 +173,11 @@ const routes: Routes = [
component: ExposedPasswordsReportComponent,
data: { titleId: 'exposedPasswordsReport' },
},
+ {
+ path: 'inactive-two-factor-report',
+ component: InactiveTwoFactorReportComponent,
+ data: { titleId: 'exposedPasswordsReport' },
+ },
],
},
],
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 2a232c323b..0042298cd4 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -107,6 +107,7 @@ import { BreachReportComponent } from './tools/breach-report.component';
import { ExportComponent } from './tools/export.component';
import { ExposedPasswordsReportComponent } from './tools/exposed-passwords-report.component';
import { ImportComponent } from './tools/import.component';
+import { InactiveTwoFactorReportComponent } from './tools/inactive-two-factor-report.component';
import { PasswordGeneratorHistoryComponent } from './tools/password-generator-history.component';
import { PasswordGeneratorComponent } from './tools/password-generator.component';
import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component';
@@ -230,9 +231,10 @@ registerLocaleData(localeZhCn, 'zh-CN');
FrontendLayoutComponent,
GroupingsComponent,
HintComponent,
- IconComponent,
I18nPipe,
+ IconComponent,
ImportComponent,
+ InactiveTwoFactorReportComponent,
InputVerbatimDirective,
LockComponent,
LoginComponent,
diff --git a/src/app/tools/inactive-two-factor-report.component.html b/src/app/tools/inactive-two-factor-report.component.html
new file mode 100644
index 0000000000..acf4931497
--- /dev/null
+++ b/src/app/tools/inactive-two-factor-report.component.html
@@ -0,0 +1,44 @@
+
+{{'inactive2faReportDesc' | i18n}}
+
+
+
+
+
+ {{'noInactive2fa'}}
+
+
+
+ {{'inactive2faFoundDesc' | i18n : (ciphers.length | number)}}
+
+
+
+
+
diff --git a/src/app/tools/inactive-two-factor-report.component.ts b/src/app/tools/inactive-two-factor-report.component.ts
new file mode 100644
index 0000000000..f1dbe67256
--- /dev/null
+++ b/src/app/tools/inactive-two-factor-report.component.ts
@@ -0,0 +1,130 @@
+import {
+ Component,
+ ComponentFactoryResolver,
+ OnInit,
+ ViewChild,
+ ViewContainerRef,
+} from '@angular/core';
+
+import { CipherService } from 'jslib/abstractions/cipher.service';
+
+import { CipherView } from 'jslib/models/view/cipherView';
+
+import { CipherType } from 'jslib/enums/cipherType';
+
+import { ModalComponent } from '../modal.component';
+import { AddEditComponent } from '../vault/add-edit.component';
+
+import { Utils } from 'jslib/misc/utils';
+
+@Component({
+ selector: 'app-inactive-two-factor-report',
+ templateUrl: 'inactive-two-factor-report.component.html',
+})
+export class InactiveTwoFactorReportComponent implements OnInit {
+ @ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef;
+
+ loading = false;
+ hasLoaded = false;
+ services = new Map();
+ cipherDocs = new Map();
+ ciphers: CipherView[] = [];
+
+ private modal: ModalComponent = null;
+
+ constructor(private ciphersService: CipherService, private componentFactoryResolver: ComponentFactoryResolver) { }
+
+ async ngOnInit() {
+ await this.load();
+ }
+
+ async load() {
+ this.loading = true;
+ try {
+ await this.load2fa();
+ } catch { }
+ if (this.services.size > 0) {
+ const allCiphers = await this.ciphersService.getAllDecrypted();
+ const inactive2faCiphers: CipherView[] = [];
+ const promises: Array> = [];
+ const docs = new Map();
+ allCiphers.forEach((c) => {
+ if (c.type !== CipherType.Login || (c.login.totp != null && c.login.totp !== '') || !c.login.hasUris) {
+ return;
+ }
+ for (let i = 0; i < c.login.uris.length; i++) {
+ const u = c.login.uris[i];
+ if (u.uri != null && u.uri !== '') {
+ const hostname = Utils.getHostname(u.uri);
+ if (hostname != null && this.services.has(hostname)) {
+ if (this.services.get(hostname) != null) {
+ docs.set(c.id, this.services.get(hostname));
+ }
+ inactive2faCiphers.push(c);
+ break;
+ }
+ }
+ }
+ });
+ await Promise.all(promises);
+ this.ciphers = inactive2faCiphers;
+ this.cipherDocs = docs;
+ }
+ this.loading = false;
+ this.hasLoaded = true;
+ }
+
+ selectCipher(cipher: CipherView) {
+ if (this.modal != null) {
+ this.modal.close();
+ }
+
+ const factory = this.componentFactoryResolver.resolveComponentFactory(ModalComponent);
+ this.modal = this.cipherAddEditModalRef.createComponent(factory).instance;
+ const childComponent = this.modal.show(
+ AddEditComponent, this.cipherAddEditModalRef);
+
+ childComponent.cipherId = cipher == null ? null : cipher.id;
+ childComponent.onSavedCipher.subscribe(async (c: CipherView) => {
+ this.modal.close();
+ await this.load();
+ });
+ childComponent.onDeletedCipher.subscribe(async (c: CipherView) => {
+ this.modal.close();
+ await this.load();
+ });
+
+ this.modal.onClosed.subscribe(() => {
+ this.modal = null;
+ });
+
+ return childComponent;
+ }
+
+ private async load2fa() {
+ if (this.services.size > 0) {
+ return;
+ }
+ const response = await fetch(new Request('https://twofactorauth.org/data.json'));
+ if (response.status !== 200) {
+ throw new Error();
+ }
+ const responseJson = await response.json();
+ for (const categoryName in responseJson) {
+ if (responseJson.hasOwnProperty(categoryName)) {
+ const category = responseJson[categoryName];
+ for (const serviceName in category) {
+ if (category.hasOwnProperty(serviceName)) {
+ const service = category[serviceName];
+ if (service.tfa && service.url != null) {
+ const hostname = Utils.getHostname(service.url);
+ if (hostname != null) {
+ this.services.set(hostname, service.doc);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/app/tools/tools.component.html b/src/app/tools/tools.component.html
index d7fc696014..18fbc988a8 100644
--- a/src/app/tools/tools.component.html
+++ b/src/app/tools/tools.component.html
@@ -33,6 +33,9 @@
{{'exposedPasswordsReport' | i18n}}
+
+ {{'inactive2faReport' | i18n}}
+
diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json
index 29b697f6f6..0b39f9f3e1 100644
--- a/src/locales/en/messages.json
+++ b/src/locales/en/messages.json
@@ -1308,6 +1308,30 @@
"noUnsecuredWebsites": {
"message": "No items in your vault have unsecured URIs."
},
+ "inactive2faReport": {
+ "message": "Inactive 2FA Report"
+ },
+ "inactive2faReportDesc": {
+ "message": "Two-factor authentication (2FA) is an important security setting that helps secure your accounts. If the website offers it, you should always enable two-factor authentication."
+ },
+ "inactive2faFound": {
+ "message": "Logins Without 2FA Found"
+ },
+ "inactive2faFoundDesc": {
+ "message": "We found $COUNT$ website(s) in your vault that may not be configured with two-factor authentication (according to twofactorauth.org). To further protect your accounts, you should always use two-factor authentication.",
+ "placeholders": {
+ "count": {
+ "content": "$1",
+ "example": "8"
+ }
+ }
+ },
+ "noInactive2fa": {
+ "message": "No websites were found in your vault with a missing two-factor authentication configuration."
+ },
+ "2faInstructions": {
+ "message": "2FA Instructions"
+ },
"exposedPasswordsReport": {
"message": "Exposed Passwords Report"
},