diff --git a/package-lock.json b/package-lock.json index 10afb26822..c7cf6b454d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,11 @@ "integrity": "sha512-BZknw3E/z3JmCLqQVANcR17okqVTPZdlxvcIz0fJiJVLUCbSH1hK3zs9r634PVSmrzAxN+n/fxlVRiYoArdOIQ==", "dev": true }, + "@types/lunr": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@types/lunr/-/lunr-2.1.5.tgz", + "integrity": "sha512-esk3CG25hRtHsVHm+LOjiSFYdw8be3uIY653WUwR43Bro914HSimPgPpqgajkhTJ0awK3RQfaIxP7zvbtCpcyg==" + }, "@types/marked": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@types/marked/-/marked-0.3.0.tgz", @@ -297,6 +302,11 @@ "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", "dev": true }, + "lunr": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.1.6.tgz", + "integrity": "sha512-ydJpB8CX8cZ/VE+KMaYaFcZ6+o2LruM6NG76VXdflYTgluvVemz1lW4anE+pyBbLvxJHZdvD1Jy/fOqdzAEJog==" + }, "marked": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/marked/-/marked-0.3.9.tgz", diff --git a/package.json b/package.json index c3142386fe..935e5f8359 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "typescript": "^2.7.1" }, "dependencies": { + "lunr": "2.1.6", "node-forge": "0.7.1", + "@types/lunr": "2.1.5", "@types/node-forge": "0.7.1", "@types/webcrypto": "0.0.28" } diff --git a/src/abstractions/search.service.ts b/src/abstractions/search.service.ts new file mode 100644 index 0000000000..c5983e2427 --- /dev/null +++ b/src/abstractions/search.service.ts @@ -0,0 +1,6 @@ +import { CipherView } from '../models/view/cipherView'; + +export abstract class SearchService { + indexCiphers: () => Promise; + searchCiphers: (query: string) => Promise; +} diff --git a/src/models/view/cipherView.ts b/src/models/view/cipherView.ts index b3f232bcbc..6f00464841 100644 --- a/src/models/view/cipherView.ts +++ b/src/models/view/cipherView.ts @@ -65,4 +65,12 @@ export class CipherView implements View { get hasFields(): boolean { return this.fields && this.fields.length > 0; } + + get login_username(): string { + return this.login != null ? this.login.username : null; + } + + get login_uri(): string { + return this.login != null ? this.login.uri : null; + } } diff --git a/src/services/audit.service.ts b/src/services/audit.service.ts index 8c5479d5b1..60553b834e 100644 --- a/src/services/audit.service.ts +++ b/src/services/audit.service.ts @@ -1,8 +1,9 @@ +import { AuditService as AuditServiceAbstraction } from '../abstractions/audit.service'; import { CryptoService } from '../abstractions/crypto.service'; const PwnedPasswordsApi = 'https://api.pwnedpasswords.com/range/'; -export class AuditService { +export class AuditService implements AuditServiceAbstraction { constructor(private cryptoService: CryptoService) { } diff --git a/src/services/search.service.ts b/src/services/search.service.ts new file mode 100644 index 0000000000..3efe4a2ac4 --- /dev/null +++ b/src/services/search.service.ts @@ -0,0 +1,60 @@ +import * as lunr from 'lunr'; + +import { CipherView } from '../models/view/cipherView'; + +import { CipherService } from '../abstractions/cipher.service'; +import { SearchService as SearchServiceAbstraction } from '../abstractions/search.service'; + +export class SearchService implements SearchServiceAbstraction { + private index: lunr.Index; + + constructor(private cipherService: CipherService) { + } + + async indexCiphers(): Promise { + const builder = new lunr.Builder(); + builder.ref('id'); + builder.field('name'); + builder.field('subTitle'); + builder.field('notes'); + builder.field('login_username'); + builder.field('login_uri'); + + const ciphers = await this.cipherService.getAllDecrypted(); + ciphers.forEach((c) => { + builder.add(c); + }); + + this.index = builder.build(); + } + + async searchCiphers(query: string): Promise { + const results: CipherView[] = []; + if (this.index == null) { + return results; + } + + const ciphers = await this.cipherService.getAllDecrypted(); + const ciphersMap = new Map(); + ciphers.forEach((c) => { + ciphersMap.set(c.id, c); + }); + + query = this.transformQuery(query); + const searchResults = this.index.search(query); + searchResults.forEach((r) => { + if (ciphersMap.has(r.ref)) { + results.push(ciphersMap.get(r.ref)); + } + }); + + return results; + } + + private transformQuery(query: string) { + if (query.indexOf('>') === 0) { + return query.substr(1).trimLeft(); + } + return '*' + query + '*'; + } +}