diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 1d811196f0..f45f4f33c8 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -50,6 +50,7 @@ import { UserBillingComponent } from './settings/user-billing.component'; 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 { PasswordGeneratorComponent } from './tools/password-generator.component'; import { ReusedPasswordsReportComponent } from './tools/reused-passwords-report.component'; @@ -166,6 +167,11 @@ const routes: Routes = [ component: WeakPasswordsReportComponent, data: { titleId: 'weakPasswordsReport' }, }, + { + path: 'exposed-passwords-report', + component: ExposedPasswordsReportComponent, + data: { titleId: 'exposedPasswordsReport' }, + }, ], }, ], diff --git a/src/app/app.module.ts b/src/app/app.module.ts index edd41512c0..2a232c323b 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -105,6 +105,7 @@ import { VerifyEmailComponent } from './settings/verify-email.component'; 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 { PasswordGeneratorHistoryComponent } from './tools/password-generator-history.component'; import { PasswordGeneratorComponent } from './tools/password-generator.component'; @@ -222,6 +223,7 @@ registerLocaleData(localeZhCn, 'zh-CN'); DeleteOrganizationComponent, DomainRulesComponent, ExportComponent, + ExposedPasswordsReportComponent, FallbackSrcDirective, FolderAddEditComponent, FooterComponent, diff --git a/src/app/tools/exposed-passwords-report.component.html b/src/app/tools/exposed-passwords-report.component.html new file mode 100644 index 0000000000..3392080873 --- /dev/null +++ b/src/app/tools/exposed-passwords-report.component.html @@ -0,0 +1,40 @@ + +

{{'exposedPasswordsReportDesc' | i18n}}

+ +
+ + {{'noExposedPasswords'}} + + + + {{'exposedPasswordsFoundDesc' | i18n : (ciphers.length | number)}} + + + + + + + + + +
+ + + {{c.name}} + + +
+ {{c.subTitle}} +
+ + {{'exposedXTimes' | i18n : (exposedPasswordMap.get(c.id) | number)}} + +
+
+
+ diff --git a/src/app/tools/exposed-passwords-report.component.ts b/src/app/tools/exposed-passwords-report.component.ts new file mode 100644 index 0000000000..59f0c6bbe5 --- /dev/null +++ b/src/app/tools/exposed-passwords-report.component.ts @@ -0,0 +1,84 @@ +import { + Component, + ComponentFactoryResolver, + ViewChild, + ViewContainerRef, +} from '@angular/core'; + +import { AuditService } from 'jslib/abstractions/audit.service'; +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'; + +@Component({ + selector: 'app-exposed-passwords-report', + templateUrl: 'exposed-passwords-report.component.html', +}) +export class ExposedPasswordsReportComponent { + @ViewChild('cipherAddEdit', { read: ViewContainerRef }) cipherAddEditModalRef: ViewContainerRef; + + loading = false; + hasLoaded = false; + ciphers: CipherView[] = []; + exposedPasswordMap = new Map(); + + private modal: ModalComponent = null; + + constructor(private ciphersService: CipherService, private auditService: AuditService, + private componentFactoryResolver: ComponentFactoryResolver) { } + + async load() { + this.loading = true; + const allCiphers = await this.ciphersService.getAllDecrypted(); + const exposedPasswordCiphers: CipherView[] = []; + const promises: Array> = []; + allCiphers.forEach((c) => { + if (c.type !== CipherType.Login || c.login.password == null || c.login.password === '') { + return; + } + const promise = this.auditService.passwordLeaked(c.login.password).then((exposedCount) => { + if (exposedCount > 0) { + exposedPasswordCiphers.push(c); + this.exposedPasswordMap.set(c.id, exposedCount); + } + }); + promises.push(promise); + }); + await Promise.all(promises); + this.ciphers = exposedPasswordCiphers; + 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; + } +} diff --git a/src/app/tools/tools.component.html b/src/app/tools/tools.component.html index 6459be9b08..d7fc696014 100644 --- a/src/app/tools/tools.component.html +++ b/src/app/tools/tools.component.html @@ -30,6 +30,9 @@ {{'weakPasswordsReport' | i18n}} + + {{'exposedPasswordsReport' | i18n}} + diff --git a/src/locales/en/messages.json b/src/locales/en/messages.json index ab8befa1d0..29b697f6f6 100644 --- a/src/locales/en/messages.json +++ b/src/locales/en/messages.json @@ -1308,6 +1308,39 @@ "noUnsecuredWebsites": { "message": "No items in your vault have unsecured URIs." }, + "exposedPasswordsReport": { + "message": "Exposed Passwords Report" + }, + "exposedPasswordsReportDesc": { + "message": "Exposed passwords have been uncovered in known data breaches." + }, + "exposedPasswordsFound": { + "message": "Exposed Passwords Found" + }, + "exposedPasswordsFoundDesc": { + "message": "We found $COUNT$ items in your vault that have passwords that were exposed in known data breaches. You should change them to use a new password.", + "placeholders": { + "count": { + "content": "$1", + "example": "8" + } + } + }, + "noExposedPasswords": { + "message": "No items in your vault have passwords that have been exposed in known data breaches." + }, + "checkExposedPasswords": { + "message": "Check Exposed Passwords" + }, + "exposedXTimes": { + "message": "Exposed $COUNT$ time(s)", + "placeholders": { + "count": { + "content": "$1", + "example": "52" + } + } + }, "weakPasswordsReport": { "message": "Weak Passwords Report" },