mirror of
https://github.com/bitwarden/browser
synced 2025-12-15 15:53:27 +00:00
[SG-998] and [SG-999] Vault and Autofill team refactor (#4542)
* Move DeprecatedVaultFilterService to vault folder * [libs] move VaultItemsComponent * [libs] move AddEditComponent * [libs] move AddEditCustomFields * [libs] move attachmentsComponent * [libs] folderAddEditComponent * [libs] IconComponent * [libs] PasswordRepormptComponent * [libs] PremiumComponent * [libs] ViewCustomFieldsComponent * [libs] ViewComponent * [libs] PasswordRepromptService * [libs] Move FolderService and FolderApiService abstractions * [libs] FolderService imports * [libs] PasswordHistoryComponent * [libs] move Sync and SyncNotifier abstractions * [libs] SyncService imports * [libs] fix file casing for passwordReprompt abstraction * [libs] SyncNotifier import fix * [libs] CipherServiceAbstraction * [libs] PasswordRepromptService abstraction * [libs] Fix file casing for angular passwordReprompt service * [libs] fix file casing for SyncNotifierService * [libs] CipherRepromptType * [libs] rename CipherRepromptType * [libs] CipherType * [libs] Rename CipherType * [libs] CipherData * [libs] FolderData * [libs] PasswordHistoryData * [libs] AttachmentData * [libs] CardData * [libs] FieldData * [libs] IdentityData * [libs] LocalData * [libs] LoginData * [libs] SecureNoteData * [libs] LoginUriData * [libs] Domain classes * [libs] SecureNote * [libs] Request models * [libs] Response models * [libs] View part 1 * [libs] Views part 2 * [libs] Move folder services * [libs] Views fixes * [libs] Move sync services * [libs] cipher service * [libs] Types * [libs] Sync file casing * [libs] Fix folder service import * [libs] Move spec files * [libs] casing fixes on spec files * [browser] Autofill background, clipboard, commands * [browser] Fix ContextMenusBackground casing * [browser] Rename fix * [browser] Autofill content * [browser] autofill.js * [libs] enpass importer spec fix * [browser] autofill models * [browser] autofill manifest path updates * [browser] Autofill notification files * [browser] autofill services * [browser] Fix file casing * [browser] Vault popup loose components * [browser] Vault components * [browser] Manifest fixes * [browser] Vault services * [cli] vault commands and models * [browser] File capitilization fixes * [desktop] Vault components and services * [web] vault loose components * [web] Vault components * [browser] Fix misc-utils import * [libs] Fix psono spec imports * [fix] Add comments to address lint rules
This commit is contained in:
22
libs/common/src/vault/models/data/attachment.data.ts
Normal file
22
libs/common/src/vault/models/data/attachment.data.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { AttachmentResponse } from "../response/attachment.response";
|
||||
|
||||
export class AttachmentData {
|
||||
id: string;
|
||||
url: string;
|
||||
fileName: string;
|
||||
key: string;
|
||||
size: string;
|
||||
sizeName: string;
|
||||
|
||||
constructor(response?: AttachmentResponse) {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
this.id = response.id;
|
||||
this.url = response.url;
|
||||
this.fileName = response.fileName;
|
||||
this.key = response.key;
|
||||
this.size = response.size;
|
||||
this.sizeName = response.sizeName;
|
||||
}
|
||||
}
|
||||
23
libs/common/src/vault/models/data/card.data.ts
Normal file
23
libs/common/src/vault/models/data/card.data.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { CardApi } from "../../../models/api/card.api";
|
||||
|
||||
export class CardData {
|
||||
cardholderName: string;
|
||||
brand: string;
|
||||
number: string;
|
||||
expMonth: string;
|
||||
expYear: string;
|
||||
code: string;
|
||||
|
||||
constructor(data?: CardApi) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cardholderName = data.cardholderName;
|
||||
this.brand = data.brand;
|
||||
this.number = data.number;
|
||||
this.expMonth = data.expMonth;
|
||||
this.expYear = data.expYear;
|
||||
this.code = data.code;
|
||||
}
|
||||
}
|
||||
85
libs/common/src/vault/models/data/cipher.data.ts
Normal file
85
libs/common/src/vault/models/data/cipher.data.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
import { CipherType } from "../../enums/cipher-type";
|
||||
import { CipherResponse } from "../response/cipher.response";
|
||||
|
||||
import { AttachmentData } from "./attachment.data";
|
||||
import { CardData } from "./card.data";
|
||||
import { FieldData } from "./field.data";
|
||||
import { IdentityData } from "./identity.data";
|
||||
import { LoginData } from "./login.data";
|
||||
import { PasswordHistoryData } from "./password-history.data";
|
||||
import { SecureNoteData } from "./secure-note.data";
|
||||
|
||||
export class CipherData {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
folderId: string;
|
||||
edit: boolean;
|
||||
viewPassword: boolean;
|
||||
organizationUseTotp: boolean;
|
||||
favorite: boolean;
|
||||
revisionDate: string;
|
||||
type: CipherType;
|
||||
name: string;
|
||||
notes: string;
|
||||
login?: LoginData;
|
||||
secureNote?: SecureNoteData;
|
||||
card?: CardData;
|
||||
identity?: IdentityData;
|
||||
fields?: FieldData[];
|
||||
attachments?: AttachmentData[];
|
||||
passwordHistory?: PasswordHistoryData[];
|
||||
collectionIds?: string[];
|
||||
creationDate: string;
|
||||
deletedDate: string;
|
||||
reprompt: CipherRepromptType;
|
||||
|
||||
constructor(response?: CipherResponse, collectionIds?: string[]) {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = response.id;
|
||||
this.organizationId = response.organizationId;
|
||||
this.folderId = response.folderId;
|
||||
this.edit = response.edit;
|
||||
this.viewPassword = response.viewPassword;
|
||||
this.organizationUseTotp = response.organizationUseTotp;
|
||||
this.favorite = response.favorite;
|
||||
this.revisionDate = response.revisionDate;
|
||||
this.type = response.type;
|
||||
this.name = response.name;
|
||||
this.notes = response.notes;
|
||||
this.collectionIds = collectionIds != null ? collectionIds : response.collectionIds;
|
||||
this.creationDate = response.creationDate;
|
||||
this.deletedDate = response.deletedDate;
|
||||
this.reprompt = response.reprompt;
|
||||
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
this.login = new LoginData(response.login);
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.secureNote = new SecureNoteData(response.secureNote);
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.card = new CardData(response.card);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.identity = new IdentityData(response.identity);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (response.fields != null) {
|
||||
this.fields = response.fields.map((f) => new FieldData(f));
|
||||
}
|
||||
if (response.attachments != null) {
|
||||
this.attachments = response.attachments.map((a) => new AttachmentData(a));
|
||||
}
|
||||
if (response.passwordHistory != null) {
|
||||
this.passwordHistory = response.passwordHistory.map((ph) => new PasswordHistoryData(ph));
|
||||
}
|
||||
}
|
||||
}
|
||||
20
libs/common/src/vault/models/data/field.data.ts
Normal file
20
libs/common/src/vault/models/data/field.data.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { FieldType } from "../../../enums/fieldType";
|
||||
import { LinkedIdType } from "../../../enums/linkedIdType";
|
||||
import { FieldApi } from "../../../models/api/field.api";
|
||||
|
||||
export class FieldData {
|
||||
type: FieldType;
|
||||
name: string;
|
||||
value: string;
|
||||
linkedId: LinkedIdType;
|
||||
|
||||
constructor(response?: FieldApi) {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
this.type = response.type;
|
||||
this.name = response.name;
|
||||
this.value = response.value;
|
||||
this.linkedId = response.linkedId;
|
||||
}
|
||||
}
|
||||
13
libs/common/src/vault/models/data/folder.data.ts
Normal file
13
libs/common/src/vault/models/data/folder.data.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { FolderResponse } from "../response/folder.response";
|
||||
|
||||
export class FolderData {
|
||||
id: string;
|
||||
name: string;
|
||||
revisionDate: string;
|
||||
|
||||
constructor(response: FolderResponse) {
|
||||
this.name = response.name;
|
||||
this.id = response.id;
|
||||
this.revisionDate = response.revisionDate;
|
||||
}
|
||||
}
|
||||
47
libs/common/src/vault/models/data/identity.data.ts
Normal file
47
libs/common/src/vault/models/data/identity.data.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { IdentityApi } from "../../../models/api/identity.api";
|
||||
|
||||
export class IdentityData {
|
||||
title: string;
|
||||
firstName: string;
|
||||
middleName: string;
|
||||
lastName: string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
address3: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postalCode: string;
|
||||
country: string;
|
||||
company: string;
|
||||
email: string;
|
||||
phone: string;
|
||||
ssn: string;
|
||||
username: string;
|
||||
passportNumber: string;
|
||||
licenseNumber: string;
|
||||
|
||||
constructor(data?: IdentityApi) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.title = data.title;
|
||||
this.firstName = data.firstName;
|
||||
this.middleName = data.middleName;
|
||||
this.lastName = data.lastName;
|
||||
this.address1 = data.address1;
|
||||
this.address2 = data.address2;
|
||||
this.address3 = data.address3;
|
||||
this.city = data.city;
|
||||
this.state = data.state;
|
||||
this.postalCode = data.postalCode;
|
||||
this.country = data.country;
|
||||
this.company = data.company;
|
||||
this.email = data.email;
|
||||
this.phone = data.phone;
|
||||
this.ssn = data.ssn;
|
||||
this.username = data.username;
|
||||
this.passportNumber = data.passportNumber;
|
||||
this.licenseNumber = data.licenseNumber;
|
||||
}
|
||||
}
|
||||
4
libs/common/src/vault/models/data/local.data.ts
Normal file
4
libs/common/src/vault/models/data/local.data.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export type LocalData = {
|
||||
lastUsedDate?: number;
|
||||
lastLaunched?: number;
|
||||
};
|
||||
15
libs/common/src/vault/models/data/login-uri.data.ts
Normal file
15
libs/common/src/vault/models/data/login-uri.data.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { UriMatchType } from "../../../enums/uriMatchType";
|
||||
import { LoginUriApi } from "../../../models/api/login-uri.api";
|
||||
|
||||
export class LoginUriData {
|
||||
uri: string;
|
||||
match: UriMatchType = null;
|
||||
|
||||
constructor(data?: LoginUriApi) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
this.uri = data.uri;
|
||||
this.match = data.match;
|
||||
}
|
||||
}
|
||||
28
libs/common/src/vault/models/data/login.data.ts
Normal file
28
libs/common/src/vault/models/data/login.data.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { LoginApi } from "../../../models/api/login.api";
|
||||
|
||||
import { LoginUriData } from "./login-uri.data";
|
||||
|
||||
export class LoginData {
|
||||
uris: LoginUriData[];
|
||||
username: string;
|
||||
password: string;
|
||||
passwordRevisionDate: string;
|
||||
totp: string;
|
||||
autofillOnPageLoad: boolean;
|
||||
|
||||
constructor(data?: LoginApi) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.username = data.username;
|
||||
this.password = data.password;
|
||||
this.passwordRevisionDate = data.passwordRevisionDate;
|
||||
this.totp = data.totp;
|
||||
this.autofillOnPageLoad = data.autofillOnPageLoad;
|
||||
|
||||
if (data.uris) {
|
||||
this.uris = data.uris.map((u) => new LoginUriData(u));
|
||||
}
|
||||
}
|
||||
}
|
||||
15
libs/common/src/vault/models/data/password-history.data.ts
Normal file
15
libs/common/src/vault/models/data/password-history.data.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { PasswordHistoryResponse } from "../response/password-history.response";
|
||||
|
||||
export class PasswordHistoryData {
|
||||
password: string;
|
||||
lastUsedDate: string;
|
||||
|
||||
constructor(response?: PasswordHistoryResponse) {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.password = response.password;
|
||||
this.lastUsedDate = response.lastUsedDate;
|
||||
}
|
||||
}
|
||||
14
libs/common/src/vault/models/data/secure-note.data.ts
Normal file
14
libs/common/src/vault/models/data/secure-note.data.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { SecureNoteType } from "../../../enums/secureNoteType";
|
||||
import { SecureNoteApi } from "../../../models/api/secure-note.api";
|
||||
|
||||
export class SecureNoteData {
|
||||
type: SecureNoteType;
|
||||
|
||||
constructor(data?: SecureNoteApi) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.type = data.type;
|
||||
}
|
||||
}
|
||||
154
libs/common/src/vault/models/domain/attachment.spec.ts
Normal file
154
libs/common/src/vault/models/domain/attachment.spec.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { CryptoService } from "../../../abstractions/crypto.service";
|
||||
import { EncryptService } from "../../../abstractions/encrypt.service";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { ContainerService } from "../../../services/container.service";
|
||||
import { AttachmentData } from "../../models/data/attachment.data";
|
||||
import { Attachment } from "../../models/domain/attachment";
|
||||
|
||||
describe("Attachment", () => {
|
||||
let data: AttachmentData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
id: "id",
|
||||
url: "url",
|
||||
fileName: "fileName",
|
||||
key: "key",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new AttachmentData();
|
||||
const attachment = new Attachment(data);
|
||||
|
||||
expect(attachment).toEqual({
|
||||
id: null,
|
||||
url: null,
|
||||
size: undefined,
|
||||
sizeName: null,
|
||||
key: null,
|
||||
fileName: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const attachment = new Attachment(data);
|
||||
|
||||
expect(attachment).toEqual({
|
||||
size: "1100",
|
||||
id: "id",
|
||||
url: "url",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: { encryptedString: "fileName", encryptionType: 0 },
|
||||
key: { encryptedString: "key", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("toAttachmentData", () => {
|
||||
const attachment = new Attachment(data);
|
||||
expect(attachment.toAttachmentData()).toEqual(data);
|
||||
});
|
||||
|
||||
describe("decrypt", () => {
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoService = mock<CryptoService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
|
||||
(window as any).bitwardenContainerService = new ContainerService(
|
||||
cryptoService,
|
||||
encryptService
|
||||
);
|
||||
});
|
||||
|
||||
it("expected output", async () => {
|
||||
const attachment = new Attachment();
|
||||
attachment.id = "id";
|
||||
attachment.url = "url";
|
||||
attachment.size = "1100";
|
||||
attachment.sizeName = "1.1 KB";
|
||||
attachment.key = mockEnc("key");
|
||||
attachment.fileName = mockEnc("fileName");
|
||||
|
||||
encryptService.decryptToBytes.mockResolvedValue(makeStaticByteArray(32));
|
||||
|
||||
const view = await attachment.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
id: "id",
|
||||
url: "url",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: "fileName",
|
||||
key: expect.any(SymmetricCryptoKey),
|
||||
});
|
||||
});
|
||||
|
||||
describe("decrypts attachment.key", () => {
|
||||
let attachment: Attachment;
|
||||
|
||||
beforeEach(() => {
|
||||
attachment = new Attachment();
|
||||
attachment.key = mock<EncString>();
|
||||
});
|
||||
|
||||
it("uses the provided key without depending on CryptoService", async () => {
|
||||
const providedKey = mock<SymmetricCryptoKey>();
|
||||
|
||||
await attachment.decrypt(null, providedKey);
|
||||
|
||||
expect(cryptoService.getKeyForUserEncryption).not.toHaveBeenCalled();
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, providedKey);
|
||||
});
|
||||
|
||||
it("gets an organization key if required", async () => {
|
||||
const orgKey = mock<SymmetricCryptoKey>();
|
||||
cryptoService.getOrgKey.calledWith("orgId").mockResolvedValue(orgKey);
|
||||
|
||||
await attachment.decrypt("orgId", null);
|
||||
|
||||
expect(cryptoService.getOrgKey).toHaveBeenCalledWith("orgId");
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, orgKey);
|
||||
});
|
||||
|
||||
it("gets the user's decryption key if required", async () => {
|
||||
const userKey = mock<SymmetricCryptoKey>();
|
||||
cryptoService.getKeyForUserEncryption.mockResolvedValue(userKey);
|
||||
|
||||
await attachment.decrypt(null, null);
|
||||
|
||||
expect(cryptoService.getKeyForUserEncryption).toHaveBeenCalled();
|
||||
expect(encryptService.decryptToBytes).toHaveBeenCalledWith(attachment.key, userKey);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const actual = Attachment.fromJSON({
|
||||
key: "myKey",
|
||||
fileName: "myFileName",
|
||||
});
|
||||
|
||||
expect(actual).toEqual({
|
||||
key: "myKey_fromJSON",
|
||||
fileName: "myFileName_fromJSON",
|
||||
});
|
||||
expect(actual).toBeInstanceOf(Attachment);
|
||||
});
|
||||
|
||||
it("returns null if object is null", () => {
|
||||
expect(Attachment.fromJSON(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
108
libs/common/src/vault/models/domain/attachment.ts
Normal file
108
libs/common/src/vault/models/domain/attachment.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Utils } from "../../../misc/utils";
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { AttachmentData } from "../data/attachment.data";
|
||||
import { AttachmentView } from "../view/attachment.view";
|
||||
|
||||
export class Attachment extends Domain {
|
||||
id: string;
|
||||
url: string;
|
||||
size: string;
|
||||
sizeName: string; // Readable size, ex: "4.2 KB" or "1.43 GB"
|
||||
key: EncString;
|
||||
fileName: EncString;
|
||||
|
||||
constructor(obj?: AttachmentData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.size = obj.size;
|
||||
this.buildDomainModel(
|
||||
this,
|
||||
obj,
|
||||
{
|
||||
id: null,
|
||||
url: null,
|
||||
sizeName: null,
|
||||
fileName: null,
|
||||
key: null,
|
||||
},
|
||||
["id", "url", "sizeName"]
|
||||
);
|
||||
}
|
||||
|
||||
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<AttachmentView> {
|
||||
const view = await this.decryptObj(
|
||||
new AttachmentView(this),
|
||||
{
|
||||
fileName: null,
|
||||
},
|
||||
orgId,
|
||||
encKey
|
||||
);
|
||||
|
||||
if (this.key != null) {
|
||||
view.key = await this.decryptAttachmentKey(orgId, encKey);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private async decryptAttachmentKey(orgId: string, encKey?: SymmetricCryptoKey) {
|
||||
try {
|
||||
if (encKey == null) {
|
||||
encKey = await this.getKeyForDecryption(orgId);
|
||||
}
|
||||
|
||||
const encryptService = Utils.getContainerService().getEncryptService();
|
||||
const decValue = await encryptService.decryptToBytes(this.key, encKey);
|
||||
return new SymmetricCryptoKey(decValue);
|
||||
} catch (e) {
|
||||
// TODO: error?
|
||||
}
|
||||
}
|
||||
|
||||
private async getKeyForDecryption(orgId: string) {
|
||||
const cryptoService = Utils.getContainerService().getCryptoService();
|
||||
return orgId != null
|
||||
? await cryptoService.getOrgKey(orgId)
|
||||
: await cryptoService.getKeyForUserEncryption();
|
||||
}
|
||||
|
||||
toAttachmentData(): AttachmentData {
|
||||
const a = new AttachmentData();
|
||||
a.size = this.size;
|
||||
this.buildDataModel(
|
||||
this,
|
||||
a,
|
||||
{
|
||||
id: null,
|
||||
url: null,
|
||||
sizeName: null,
|
||||
fileName: null,
|
||||
key: null,
|
||||
},
|
||||
["id", "url", "sizeName"]
|
||||
);
|
||||
return a;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<Attachment>>): Attachment {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const key = EncString.fromJSON(obj.key);
|
||||
const fileName = EncString.fromJSON(obj.fileName);
|
||||
|
||||
return Object.assign(new Attachment(), obj, {
|
||||
key,
|
||||
fileName,
|
||||
});
|
||||
}
|
||||
}
|
||||
102
libs/common/src/vault/models/domain/card.spec.ts
Normal file
102
libs/common/src/vault/models/domain/card.spec.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { CardData } from "../../../vault/models/data/card.data";
|
||||
import { Card } from "../../models/domain/card";
|
||||
|
||||
describe("Card", () => {
|
||||
let data: CardData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
cardholderName: "encHolder",
|
||||
brand: "encBrand",
|
||||
number: "encNumber",
|
||||
expMonth: "encMonth",
|
||||
expYear: "encYear",
|
||||
code: "encCode",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new CardData();
|
||||
const card = new Card(data);
|
||||
|
||||
expect(card).toEqual({
|
||||
cardholderName: null,
|
||||
brand: null,
|
||||
number: null,
|
||||
expMonth: null,
|
||||
expYear: null,
|
||||
code: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const card = new Card(data);
|
||||
|
||||
expect(card).toEqual({
|
||||
cardholderName: { encryptedString: "encHolder", encryptionType: 0 },
|
||||
brand: { encryptedString: "encBrand", encryptionType: 0 },
|
||||
number: { encryptedString: "encNumber", encryptionType: 0 },
|
||||
expMonth: { encryptedString: "encMonth", encryptionType: 0 },
|
||||
expYear: { encryptedString: "encYear", encryptionType: 0 },
|
||||
code: { encryptedString: "encCode", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("toCardData", () => {
|
||||
const card = new Card(data);
|
||||
expect(card.toCardData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const card = new Card();
|
||||
card.cardholderName = mockEnc("cardHolder");
|
||||
card.brand = mockEnc("brand");
|
||||
card.number = mockEnc("number");
|
||||
card.expMonth = mockEnc("expMonth");
|
||||
card.expYear = mockEnc("expYear");
|
||||
card.code = mockEnc("code");
|
||||
|
||||
const view = await card.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
_brand: "brand",
|
||||
_number: "number",
|
||||
_subTitle: null,
|
||||
cardholderName: "cardHolder",
|
||||
code: "code",
|
||||
expMonth: "expMonth",
|
||||
expYear: "expYear",
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const actual = Card.fromJSON({
|
||||
cardholderName: "mockCardHolder",
|
||||
brand: "mockBrand",
|
||||
number: "mockNumber",
|
||||
expMonth: "mockExpMonth",
|
||||
expYear: "mockExpYear",
|
||||
code: "mockCode",
|
||||
});
|
||||
|
||||
expect(actual).toEqual({
|
||||
cardholderName: "mockCardHolder_fromJSON",
|
||||
brand: "mockBrand_fromJSON",
|
||||
number: "mockNumber_fromJSON",
|
||||
expMonth: "mockExpMonth_fromJSON",
|
||||
expYear: "mockExpYear_fromJSON",
|
||||
code: "mockCode_fromJSON",
|
||||
});
|
||||
expect(actual).toBeInstanceOf(Card);
|
||||
});
|
||||
|
||||
it("returns null if object is null", () => {
|
||||
expect(Card.fromJSON(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
87
libs/common/src/vault/models/domain/card.ts
Normal file
87
libs/common/src/vault/models/domain/card.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { CardData } from "../data/card.data";
|
||||
import { CardView } from "../view/card.view";
|
||||
|
||||
export class Card extends Domain {
|
||||
cardholderName: EncString;
|
||||
brand: EncString;
|
||||
number: EncString;
|
||||
expMonth: EncString;
|
||||
expYear: EncString;
|
||||
code: EncString;
|
||||
|
||||
constructor(obj?: CardData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildDomainModel(
|
||||
this,
|
||||
obj,
|
||||
{
|
||||
cardholderName: null,
|
||||
brand: null,
|
||||
number: null,
|
||||
expMonth: null,
|
||||
expYear: null,
|
||||
code: null,
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<CardView> {
|
||||
return this.decryptObj(
|
||||
new CardView(),
|
||||
{
|
||||
cardholderName: null,
|
||||
brand: null,
|
||||
number: null,
|
||||
expMonth: null,
|
||||
expYear: null,
|
||||
code: null,
|
||||
},
|
||||
orgId,
|
||||
encKey
|
||||
);
|
||||
}
|
||||
|
||||
toCardData(): CardData {
|
||||
const c = new CardData();
|
||||
this.buildDataModel(this, c, {
|
||||
cardholderName: null,
|
||||
brand: null,
|
||||
number: null,
|
||||
expMonth: null,
|
||||
expYear: null,
|
||||
code: null,
|
||||
});
|
||||
return c;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<Card>>): Card {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cardholderName = EncString.fromJSON(obj.cardholderName);
|
||||
const brand = EncString.fromJSON(obj.brand);
|
||||
const number = EncString.fromJSON(obj.number);
|
||||
const expMonth = EncString.fromJSON(obj.expMonth);
|
||||
const expYear = EncString.fromJSON(obj.expYear);
|
||||
const code = EncString.fromJSON(obj.code);
|
||||
return Object.assign(new Card(), obj, {
|
||||
cardholderName,
|
||||
brand,
|
||||
number,
|
||||
expMonth,
|
||||
expYear,
|
||||
code,
|
||||
});
|
||||
}
|
||||
}
|
||||
677
libs/common/src/vault/models/domain/cipher.spec.ts
Normal file
677
libs/common/src/vault/models/domain/cipher.spec.ts
Normal file
@@ -0,0 +1,677 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { FieldType } from "../../../enums/fieldType";
|
||||
import { SecureNoteType } from "../../../enums/secureNoteType";
|
||||
import { UriMatchType } from "../../../enums/uriMatchType";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { InitializerKey } from "../../../services/cryptography/initializer-key";
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
import { CipherType } from "../../enums/cipher-type";
|
||||
import { CipherData } from "../../models/data/cipher.data";
|
||||
import { Attachment } from "../../models/domain/attachment";
|
||||
import { Card } from "../../models/domain/card";
|
||||
import { Cipher } from "../../models/domain/cipher";
|
||||
import { Field } from "../../models/domain/field";
|
||||
import { Identity } from "../../models/domain/identity";
|
||||
import { Login } from "../../models/domain/login";
|
||||
import { Password } from "../../models/domain/password";
|
||||
import { SecureNote } from "../../models/domain/secure-note";
|
||||
import { CardView } from "../../models/view/card.view";
|
||||
import { IdentityView } from "../../models/view/identity.view";
|
||||
import { LoginView } from "../../models/view/login.view";
|
||||
|
||||
describe("Cipher DTO", () => {
|
||||
it("Convert from empty CipherData", () => {
|
||||
const data = new CipherData();
|
||||
const cipher = new Cipher(data);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
initializerKey: InitializerKey.Cipher,
|
||||
id: null,
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: null,
|
||||
notes: null,
|
||||
type: undefined,
|
||||
favorite: undefined,
|
||||
organizationUseTotp: undefined,
|
||||
edit: undefined,
|
||||
viewPassword: true,
|
||||
revisionDate: null,
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
creationDate: null,
|
||||
deletedDate: null,
|
||||
reprompt: undefined,
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
});
|
||||
});
|
||||
|
||||
describe("LoginCipher", () => {
|
||||
let cipherData: CipherData;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherData = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
organizationUseTotp: true,
|
||||
favorite: false,
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
type: CipherType.Login,
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
creationDate: "2022-01-01T12:00:00.000Z",
|
||||
deletedDate: null,
|
||||
reprompt: CipherRepromptType.None,
|
||||
login: {
|
||||
uris: [{ uri: "EncryptedString", match: UriMatchType.Domain }],
|
||||
username: "EncryptedString",
|
||||
password: "EncryptedString",
|
||||
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||
totp: "EncryptedString",
|
||||
autofillOnPageLoad: false,
|
||||
},
|
||||
passwordHistory: [
|
||||
{ password: "EncryptedString", lastUsedDate: "2022-01-31T12:00:00.000Z" },
|
||||
],
|
||||
attachments: [
|
||||
{
|
||||
id: "a1",
|
||||
url: "url",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: "file",
|
||||
key: "EncKey",
|
||||
},
|
||||
{
|
||||
id: "a2",
|
||||
url: "url",
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
fileName: "file",
|
||||
key: "EncKey",
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
name: "EncryptedString",
|
||||
value: "EncryptedString",
|
||||
type: FieldType.Text,
|
||||
linkedId: null,
|
||||
},
|
||||
{
|
||||
name: "EncryptedString",
|
||||
value: "EncryptedString",
|
||||
type: FieldType.Hidden,
|
||||
linkedId: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
initializerKey: InitializerKey.Cipher,
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 1,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
login: {
|
||||
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
autofillOnPageLoad: false,
|
||||
username: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
password: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
totp: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
uris: [{ match: 0, uri: { encryptedString: "EncryptedString", encryptionType: 0 } }],
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
fileName: { encryptedString: "file", encryptionType: 0 },
|
||||
id: "a1",
|
||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
url: "url",
|
||||
},
|
||||
{
|
||||
fileName: { encryptedString: "file", encryptionType: 0 },
|
||||
id: "a2",
|
||||
key: { encryptedString: "EncKey", encryptionType: 0 },
|
||||
size: "1100",
|
||||
sizeName: "1.1 KB",
|
||||
url: "url",
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
linkedId: null,
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 0,
|
||||
value: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
{
|
||||
linkedId: null,
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 1,
|
||||
value: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
],
|
||||
passwordHistory: [
|
||||
{
|
||||
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
password: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("toCipherData", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
expect(cipher.toCipherData()).toEqual(cipherData);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const cipher = new Cipher();
|
||||
cipher.id = "id";
|
||||
cipher.organizationId = "orgId";
|
||||
cipher.folderId = "folderId";
|
||||
cipher.edit = true;
|
||||
cipher.viewPassword = true;
|
||||
cipher.organizationUseTotp = true;
|
||||
cipher.favorite = false;
|
||||
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
cipher.type = CipherType.Login;
|
||||
cipher.name = mockEnc("EncryptedString");
|
||||
cipher.notes = mockEnc("EncryptedString");
|
||||
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
|
||||
const loginView = new LoginView();
|
||||
loginView.username = "username";
|
||||
loginView.password = "password";
|
||||
|
||||
const login = Substitute.for<Login>();
|
||||
login.decrypt(Arg.any(), Arg.any()).resolves(loginView);
|
||||
cipher.login = login;
|
||||
|
||||
const cipherView = await cipher.decrypt();
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
type: 1,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
login: loginView,
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
collectionIds: undefined,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("SecureNoteCipher", () => {
|
||||
let cipherData: CipherData;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherData = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
organizationUseTotp: true,
|
||||
favorite: false,
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
type: CipherType.SecureNote,
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
creationDate: "2022-01-01T12:00:00.000Z",
|
||||
deletedDate: null,
|
||||
reprompt: CipherRepromptType.None,
|
||||
secureNote: {
|
||||
type: SecureNoteType.Generic,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
initializerKey: InitializerKey.Cipher,
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 2,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
secureNote: { type: SecureNoteType.Generic },
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("toCipherData", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
expect(cipher.toCipherData()).toEqual(cipherData);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const cipher = new Cipher();
|
||||
cipher.id = "id";
|
||||
cipher.organizationId = "orgId";
|
||||
cipher.folderId = "folderId";
|
||||
cipher.edit = true;
|
||||
cipher.viewPassword = true;
|
||||
cipher.organizationUseTotp = true;
|
||||
cipher.favorite = false;
|
||||
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
cipher.type = CipherType.SecureNote;
|
||||
cipher.name = mockEnc("EncryptedString");
|
||||
cipher.notes = mockEnc("EncryptedString");
|
||||
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
cipher.secureNote = new SecureNote();
|
||||
cipher.secureNote.type = SecureNoteType.Generic;
|
||||
|
||||
const cipherView = await cipher.decrypt();
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
type: 2,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
secureNote: { type: 0 },
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
collectionIds: undefined,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("CardCipher", () => {
|
||||
let cipherData: CipherData;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherData = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
organizationUseTotp: true,
|
||||
favorite: false,
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
type: CipherType.Card,
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
creationDate: "2022-01-01T12:00:00.000Z",
|
||||
deletedDate: null,
|
||||
reprompt: CipherRepromptType.None,
|
||||
card: {
|
||||
cardholderName: "EncryptedString",
|
||||
brand: "EncryptedString",
|
||||
number: "EncryptedString",
|
||||
expMonth: "EncryptedString",
|
||||
expYear: "EncryptedString",
|
||||
code: "EncryptedString",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
initializerKey: InitializerKey.Cipher,
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 3,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
card: {
|
||||
cardholderName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
brand: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
number: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
expMonth: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
expYear: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
code: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("toCipherData", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
expect(cipher.toCipherData()).toEqual(cipherData);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const cipher = new Cipher();
|
||||
cipher.id = "id";
|
||||
cipher.organizationId = "orgId";
|
||||
cipher.folderId = "folderId";
|
||||
cipher.edit = true;
|
||||
cipher.viewPassword = true;
|
||||
cipher.organizationUseTotp = true;
|
||||
cipher.favorite = false;
|
||||
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
cipher.type = CipherType.Card;
|
||||
cipher.name = mockEnc("EncryptedString");
|
||||
cipher.notes = mockEnc("EncryptedString");
|
||||
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
|
||||
const cardView = new CardView();
|
||||
cardView.cardholderName = "cardholderName";
|
||||
cardView.number = "4111111111111111";
|
||||
|
||||
const card = Substitute.for<Card>();
|
||||
card.decrypt(Arg.any(), Arg.any()).resolves(cardView);
|
||||
cipher.card = card;
|
||||
|
||||
const cipherView = await cipher.decrypt();
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
type: 3,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
card: cardView,
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
collectionIds: undefined,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("IdentityCipher", () => {
|
||||
let cipherData: CipherData;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherData = {
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
organizationUseTotp: true,
|
||||
favorite: false,
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
type: CipherType.Identity,
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
creationDate: "2022-01-01T12:00:00.000Z",
|
||||
deletedDate: null,
|
||||
reprompt: CipherRepromptType.None,
|
||||
identity: {
|
||||
title: "EncryptedString",
|
||||
firstName: "EncryptedString",
|
||||
middleName: "EncryptedString",
|
||||
lastName: "EncryptedString",
|
||||
address1: "EncryptedString",
|
||||
address2: "EncryptedString",
|
||||
address3: "EncryptedString",
|
||||
city: "EncryptedString",
|
||||
state: "EncryptedString",
|
||||
postalCode: "EncryptedString",
|
||||
country: "EncryptedString",
|
||||
company: "EncryptedString",
|
||||
email: "EncryptedString",
|
||||
phone: "EncryptedString",
|
||||
ssn: "EncryptedString",
|
||||
username: "EncryptedString",
|
||||
passportNumber: "EncryptedString",
|
||||
licenseNumber: "EncryptedString",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
|
||||
expect(cipher).toEqual({
|
||||
initializerKey: InitializerKey.Cipher,
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
notes: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
type: 4,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
collectionIds: undefined,
|
||||
localData: null,
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
identity: {
|
||||
title: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
firstName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
middleName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
lastName: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
address1: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
address2: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
address3: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
city: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
state: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
postalCode: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
country: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
company: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
email: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
phone: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
ssn: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
username: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
passportNumber: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
licenseNumber: { encryptedString: "EncryptedString", encryptionType: 0 },
|
||||
},
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("toCipherData", () => {
|
||||
const cipher = new Cipher(cipherData);
|
||||
expect(cipher.toCipherData()).toEqual(cipherData);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const cipher = new Cipher();
|
||||
cipher.id = "id";
|
||||
cipher.organizationId = "orgId";
|
||||
cipher.folderId = "folderId";
|
||||
cipher.edit = true;
|
||||
cipher.viewPassword = true;
|
||||
cipher.organizationUseTotp = true;
|
||||
cipher.favorite = false;
|
||||
cipher.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
cipher.type = CipherType.Identity;
|
||||
cipher.name = mockEnc("EncryptedString");
|
||||
cipher.notes = mockEnc("EncryptedString");
|
||||
cipher.creationDate = new Date("2022-01-01T12:00:00.000Z");
|
||||
cipher.deletedDate = null;
|
||||
cipher.reprompt = CipherRepromptType.None;
|
||||
|
||||
const identityView = new IdentityView();
|
||||
identityView.firstName = "firstName";
|
||||
identityView.lastName = "lastName";
|
||||
|
||||
const identity = Substitute.for<Identity>();
|
||||
identity.decrypt(Arg.any(), Arg.any()).resolves(identityView);
|
||||
cipher.identity = identity;
|
||||
|
||||
const cipherView = await cipher.decrypt();
|
||||
|
||||
expect(cipherView).toMatchObject({
|
||||
id: "id",
|
||||
organizationId: "orgId",
|
||||
folderId: "folderId",
|
||||
name: "EncryptedString",
|
||||
notes: "EncryptedString",
|
||||
type: 4,
|
||||
favorite: false,
|
||||
organizationUseTotp: true,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
identity: identityView,
|
||||
attachments: null,
|
||||
fields: null,
|
||||
passwordHistory: null,
|
||||
collectionIds: undefined,
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
creationDate: new Date("2022-01-01T12:00:00.000Z"),
|
||||
deletedDate: null,
|
||||
reprompt: 0,
|
||||
localData: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
jest.spyOn(Attachment, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(Field, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(Password, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
|
||||
const deletedDate = new Date("2022-09-04T01:06:40.441Z");
|
||||
const actual = Cipher.fromJSON({
|
||||
name: "myName",
|
||||
notes: "myNotes",
|
||||
revisionDate: revisionDate.toISOString(),
|
||||
attachments: ["attachment1", "attachment2"] as any,
|
||||
fields: ["field1", "field2"] as any,
|
||||
passwordHistory: ["ph1", "ph2"] as any,
|
||||
deletedDate: deletedDate.toISOString(),
|
||||
} as Jsonify<Cipher>);
|
||||
|
||||
expect(actual).toMatchObject({
|
||||
name: "myName_fromJSON",
|
||||
notes: "myNotes_fromJSON",
|
||||
revisionDate: revisionDate,
|
||||
attachments: ["attachment1_fromJSON", "attachment2_fromJSON"],
|
||||
fields: ["field1_fromJSON", "field2_fromJSON"],
|
||||
passwordHistory: ["ph1_fromJSON", "ph2_fromJSON"],
|
||||
deletedDate: deletedDate,
|
||||
});
|
||||
expect(actual).toBeInstanceOf(Cipher);
|
||||
});
|
||||
|
||||
test.each([
|
||||
// Test description, CipherType, expected output
|
||||
["LoginView", CipherType.Login, { login: "myLogin_fromJSON" }],
|
||||
["CardView", CipherType.Card, { card: "myCard_fromJSON" }],
|
||||
["IdentityView", CipherType.Identity, { identity: "myIdentity_fromJSON" }],
|
||||
["Secure Note", CipherType.SecureNote, { secureNote: "mySecureNote_fromJSON" }],
|
||||
])("initializes %s", (description: string, cipherType: CipherType, expected: any) => {
|
||||
jest.spyOn(Login, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(Identity, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(Card, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(SecureNote, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const actual = Cipher.fromJSON({
|
||||
login: "myLogin",
|
||||
card: "myCard",
|
||||
identity: "myIdentity",
|
||||
secureNote: "mySecureNote",
|
||||
type: cipherType,
|
||||
} as any);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
|
||||
it("returns null if object is null", () => {
|
||||
expect(Cipher.fromJSON(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
290
libs/common/src/vault/models/domain/cipher.ts
Normal file
290
libs/common/src/vault/models/domain/cipher.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { Decryptable } from "../../../interfaces/decryptable.interface";
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { InitializerKey } from "../../../services/cryptography/initializer-key";
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
import { CipherType } from "../../enums/cipher-type";
|
||||
import { CipherData } from "../data/cipher.data";
|
||||
import { LocalData } from "../data/local.data";
|
||||
import { CipherView } from "../view/cipher.view";
|
||||
|
||||
import { Attachment } from "./attachment";
|
||||
import { Card } from "./card";
|
||||
import { Field } from "./field";
|
||||
import { Identity } from "./identity";
|
||||
import { Login } from "./login";
|
||||
import { Password } from "./password";
|
||||
import { SecureNote } from "./secure-note";
|
||||
|
||||
export class Cipher extends Domain implements Decryptable<CipherView> {
|
||||
readonly initializerKey = InitializerKey.Cipher;
|
||||
|
||||
id: string;
|
||||
organizationId: string;
|
||||
folderId: string;
|
||||
name: EncString;
|
||||
notes: EncString;
|
||||
type: CipherType;
|
||||
favorite: boolean;
|
||||
organizationUseTotp: boolean;
|
||||
edit: boolean;
|
||||
viewPassword: boolean;
|
||||
revisionDate: Date;
|
||||
localData: LocalData;
|
||||
login: Login;
|
||||
identity: Identity;
|
||||
card: Card;
|
||||
secureNote: SecureNote;
|
||||
attachments: Attachment[];
|
||||
fields: Field[];
|
||||
passwordHistory: Password[];
|
||||
collectionIds: string[];
|
||||
creationDate: Date;
|
||||
deletedDate: Date;
|
||||
reprompt: CipherRepromptType;
|
||||
|
||||
constructor(obj?: CipherData, localData: LocalData = null) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildDomainModel(
|
||||
this,
|
||||
obj,
|
||||
{
|
||||
id: null,
|
||||
organizationId: null,
|
||||
folderId: null,
|
||||
name: null,
|
||||
notes: null,
|
||||
},
|
||||
["id", "organizationId", "folderId"]
|
||||
);
|
||||
|
||||
this.type = obj.type;
|
||||
this.favorite = obj.favorite;
|
||||
this.organizationUseTotp = obj.organizationUseTotp;
|
||||
this.edit = obj.edit;
|
||||
if (obj.viewPassword != null) {
|
||||
this.viewPassword = obj.viewPassword;
|
||||
} else {
|
||||
this.viewPassword = true; // Default for already synced Ciphers without viewPassword
|
||||
}
|
||||
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
|
||||
this.collectionIds = obj.collectionIds;
|
||||
this.localData = localData;
|
||||
this.creationDate = obj.creationDate != null ? new Date(obj.creationDate) : null;
|
||||
this.deletedDate = obj.deletedDate != null ? new Date(obj.deletedDate) : null;
|
||||
this.reprompt = obj.reprompt;
|
||||
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
this.login = new Login(obj.login);
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.secureNote = new SecureNote(obj.secureNote);
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.card = new Card(obj.card);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.identity = new Identity(obj.identity);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (obj.attachments != null) {
|
||||
this.attachments = obj.attachments.map((a) => new Attachment(a));
|
||||
} else {
|
||||
this.attachments = null;
|
||||
}
|
||||
|
||||
if (obj.fields != null) {
|
||||
this.fields = obj.fields.map((f) => new Field(f));
|
||||
} else {
|
||||
this.fields = null;
|
||||
}
|
||||
|
||||
if (obj.passwordHistory != null) {
|
||||
this.passwordHistory = obj.passwordHistory.map((ph) => new Password(ph));
|
||||
} else {
|
||||
this.passwordHistory = null;
|
||||
}
|
||||
}
|
||||
|
||||
async decrypt(encKey?: SymmetricCryptoKey): Promise<CipherView> {
|
||||
const model = new CipherView(this);
|
||||
|
||||
await this.decryptObj(
|
||||
model,
|
||||
{
|
||||
name: null,
|
||||
notes: null,
|
||||
},
|
||||
this.organizationId,
|
||||
encKey
|
||||
);
|
||||
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
model.login = await this.login.decrypt(this.organizationId, encKey);
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
model.secureNote = await this.secureNote.decrypt(this.organizationId, encKey);
|
||||
break;
|
||||
case CipherType.Card:
|
||||
model.card = await this.card.decrypt(this.organizationId, encKey);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
model.identity = await this.identity.decrypt(this.organizationId, encKey);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const orgId = this.organizationId;
|
||||
|
||||
if (this.attachments != null && this.attachments.length > 0) {
|
||||
const attachments: any[] = [];
|
||||
await this.attachments.reduce((promise, attachment) => {
|
||||
return promise
|
||||
.then(() => {
|
||||
return attachment.decrypt(orgId, encKey);
|
||||
})
|
||||
.then((decAttachment) => {
|
||||
attachments.push(decAttachment);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
model.attachments = attachments;
|
||||
}
|
||||
|
||||
if (this.fields != null && this.fields.length > 0) {
|
||||
const fields: any[] = [];
|
||||
await this.fields.reduce((promise, field) => {
|
||||
return promise
|
||||
.then(() => {
|
||||
return field.decrypt(orgId, encKey);
|
||||
})
|
||||
.then((decField) => {
|
||||
fields.push(decField);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
model.fields = fields;
|
||||
}
|
||||
|
||||
if (this.passwordHistory != null && this.passwordHistory.length > 0) {
|
||||
const passwordHistory: any[] = [];
|
||||
await this.passwordHistory.reduce((promise, ph) => {
|
||||
return promise
|
||||
.then(() => {
|
||||
return ph.decrypt(orgId, encKey);
|
||||
})
|
||||
.then((decPh) => {
|
||||
passwordHistory.push(decPh);
|
||||
});
|
||||
}, Promise.resolve());
|
||||
model.passwordHistory = passwordHistory;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
toCipherData(): CipherData {
|
||||
const c = new CipherData();
|
||||
c.id = this.id;
|
||||
c.organizationId = this.organizationId;
|
||||
c.folderId = this.folderId;
|
||||
c.edit = this.edit;
|
||||
c.viewPassword = this.viewPassword;
|
||||
c.organizationUseTotp = this.organizationUseTotp;
|
||||
c.favorite = this.favorite;
|
||||
c.revisionDate = this.revisionDate != null ? this.revisionDate.toISOString() : null;
|
||||
c.type = this.type;
|
||||
c.collectionIds = this.collectionIds;
|
||||
c.creationDate = this.creationDate != null ? this.creationDate.toISOString() : null;
|
||||
c.deletedDate = this.deletedDate != null ? this.deletedDate.toISOString() : null;
|
||||
c.reprompt = this.reprompt;
|
||||
|
||||
this.buildDataModel(this, c, {
|
||||
name: null,
|
||||
notes: null,
|
||||
});
|
||||
|
||||
switch (c.type) {
|
||||
case CipherType.Login:
|
||||
c.login = this.login.toLoginData();
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
c.secureNote = this.secureNote.toSecureNoteData();
|
||||
break;
|
||||
case CipherType.Card:
|
||||
c.card = this.card.toCardData();
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
c.identity = this.identity.toIdentityData();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (this.fields != null) {
|
||||
c.fields = this.fields.map((f) => f.toFieldData());
|
||||
}
|
||||
if (this.attachments != null) {
|
||||
c.attachments = this.attachments.map((a) => a.toAttachmentData());
|
||||
}
|
||||
if (this.passwordHistory != null) {
|
||||
c.passwordHistory = this.passwordHistory.map((ph) => ph.toPasswordHistoryData());
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<Cipher>) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const domain = new Cipher();
|
||||
const name = EncString.fromJSON(obj.name);
|
||||
const notes = EncString.fromJSON(obj.notes);
|
||||
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||
const attachments = obj.attachments?.map((a: any) => Attachment.fromJSON(a));
|
||||
const fields = obj.fields?.map((f: any) => Field.fromJSON(f));
|
||||
const passwordHistory = obj.passwordHistory?.map((ph: any) => Password.fromJSON(ph));
|
||||
|
||||
Object.assign(domain, obj, {
|
||||
name,
|
||||
notes,
|
||||
revisionDate,
|
||||
deletedDate,
|
||||
attachments,
|
||||
fields,
|
||||
passwordHistory,
|
||||
});
|
||||
|
||||
switch (obj.type) {
|
||||
case CipherType.Card:
|
||||
domain.card = Card.fromJSON(obj.card);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
domain.identity = Identity.fromJSON(obj.identity);
|
||||
break;
|
||||
case CipherType.Login:
|
||||
domain.login = Login.fromJSON(obj.login);
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
domain.secureNote = SecureNote.fromJSON(obj.secureNote);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return domain;
|
||||
}
|
||||
}
|
||||
85
libs/common/src/vault/models/domain/field.spec.ts
Normal file
85
libs/common/src/vault/models/domain/field.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import { mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { FieldType } from "../../../enums/fieldType";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { FieldData } from "../../models/data/field.data";
|
||||
import { Field } from "../../models/domain/field";
|
||||
|
||||
describe("Field", () => {
|
||||
let data: FieldData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
type: FieldType.Text,
|
||||
name: "encName",
|
||||
value: "encValue",
|
||||
linkedId: null,
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new FieldData();
|
||||
const field = new Field(data);
|
||||
|
||||
expect(field).toEqual({
|
||||
type: undefined,
|
||||
name: null,
|
||||
value: null,
|
||||
linkedId: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const field = new Field(data);
|
||||
|
||||
expect(field).toEqual({
|
||||
type: FieldType.Text,
|
||||
name: { encryptedString: "encName", encryptionType: 0 },
|
||||
value: { encryptedString: "encValue", encryptionType: 0 },
|
||||
linkedId: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("toFieldData", () => {
|
||||
const field = new Field(data);
|
||||
expect(field.toFieldData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const field = new Field();
|
||||
field.type = FieldType.Text;
|
||||
field.name = mockEnc("encName");
|
||||
field.value = mockEnc("encValue");
|
||||
|
||||
const view = await field.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
type: 0,
|
||||
name: "encName",
|
||||
value: "encValue",
|
||||
newField: false,
|
||||
showCount: false,
|
||||
showValue: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const actual = Field.fromJSON({
|
||||
name: "myName",
|
||||
value: "myValue",
|
||||
});
|
||||
|
||||
expect(actual).toEqual({
|
||||
name: "myName_fromJSON",
|
||||
value: "myValue_fromJSON",
|
||||
});
|
||||
expect(actual).toBeInstanceOf(Field);
|
||||
});
|
||||
|
||||
it("returns null if object is null", () => {
|
||||
expect(Field.fromJSON(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
77
libs/common/src/vault/models/domain/field.ts
Normal file
77
libs/common/src/vault/models/domain/field.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { FieldType } from "../../../enums/fieldType";
|
||||
import { LinkedIdType } from "../../../enums/linkedIdType";
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { FieldData } from "../data/field.data";
|
||||
import { FieldView } from "../view/field.view";
|
||||
|
||||
export class Field extends Domain {
|
||||
name: EncString;
|
||||
value: EncString;
|
||||
type: FieldType;
|
||||
linkedId: LinkedIdType;
|
||||
|
||||
constructor(obj?: FieldData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.type = obj.type;
|
||||
this.linkedId = obj.linkedId;
|
||||
this.buildDomainModel(
|
||||
this,
|
||||
obj,
|
||||
{
|
||||
name: null,
|
||||
value: null,
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<FieldView> {
|
||||
return this.decryptObj(
|
||||
new FieldView(this),
|
||||
{
|
||||
name: null,
|
||||
value: null,
|
||||
},
|
||||
orgId,
|
||||
encKey
|
||||
);
|
||||
}
|
||||
|
||||
toFieldData(): FieldData {
|
||||
const f = new FieldData();
|
||||
this.buildDataModel(
|
||||
this,
|
||||
f,
|
||||
{
|
||||
name: null,
|
||||
value: null,
|
||||
type: null,
|
||||
linkedId: null,
|
||||
},
|
||||
["type", "linkedId"]
|
||||
);
|
||||
return f;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<Field>>): Field {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = EncString.fromJSON(obj.name);
|
||||
const value = EncString.fromJSON(obj.value);
|
||||
|
||||
return Object.assign(new Field(), obj, {
|
||||
name,
|
||||
value,
|
||||
});
|
||||
}
|
||||
}
|
||||
63
libs/common/src/vault/models/domain/folder.spec.ts
Normal file
63
libs/common/src/vault/models/domain/folder.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { FolderData } from "../../models/data/folder.data";
|
||||
import { Folder } from "../../models/domain/folder";
|
||||
|
||||
describe("Folder", () => {
|
||||
let data: FolderData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
id: "id",
|
||||
name: "encName",
|
||||
revisionDate: "2022-01-31T12:00:00.000Z",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const field = new Folder(data);
|
||||
|
||||
expect(field).toEqual({
|
||||
id: "id",
|
||||
name: { encryptedString: "encName", encryptionType: 0 },
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const folder = new Folder();
|
||||
folder.id = "id";
|
||||
folder.name = mockEnc("encName");
|
||||
folder.revisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
|
||||
const view = await folder.decrypt();
|
||||
|
||||
expect(view).toEqual({
|
||||
id: "id",
|
||||
name: "encName",
|
||||
revisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
jest.mock("../../../models/domain/enc-string");
|
||||
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
it("initializes nested objects", () => {
|
||||
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
|
||||
const actual = Folder.fromJSON({
|
||||
revisionDate: revisionDate.toISOString(),
|
||||
name: "name",
|
||||
id: "id",
|
||||
});
|
||||
|
||||
const expected = {
|
||||
revisionDate: revisionDate,
|
||||
name: "name_fromJSON",
|
||||
id: "id",
|
||||
};
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
46
libs/common/src/vault/models/domain/folder.ts
Normal file
46
libs/common/src/vault/models/domain/folder.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { FolderData } from "../data/folder.data";
|
||||
import { FolderView } from "../view/folder.view";
|
||||
|
||||
export class Folder extends Domain {
|
||||
id: string;
|
||||
name: EncString;
|
||||
revisionDate: Date;
|
||||
|
||||
constructor(obj?: FolderData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildDomainModel(
|
||||
this,
|
||||
obj,
|
||||
{
|
||||
id: null,
|
||||
name: null,
|
||||
},
|
||||
["id"]
|
||||
);
|
||||
|
||||
this.revisionDate = obj.revisionDate != null ? new Date(obj.revisionDate) : null;
|
||||
}
|
||||
|
||||
decrypt(): Promise<FolderView> {
|
||||
return this.decryptObj(
|
||||
new FolderView(this),
|
||||
{
|
||||
name: null,
|
||||
},
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<Folder>) {
|
||||
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||
return Object.assign(new Folder(), obj, { name: EncString.fromJSON(obj.name), revisionDate });
|
||||
}
|
||||
}
|
||||
187
libs/common/src/vault/models/domain/identity.spec.ts
Normal file
187
libs/common/src/vault/models/domain/identity.spec.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { IdentityData } from "../../models/data/identity.data";
|
||||
import { Identity } from "../../models/domain/identity";
|
||||
|
||||
describe("Identity", () => {
|
||||
let data: IdentityData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
title: "enctitle",
|
||||
firstName: "encfirstName",
|
||||
middleName: "encmiddleName",
|
||||
lastName: "enclastName",
|
||||
address1: "encaddress1",
|
||||
address2: "encaddress2",
|
||||
address3: "encaddress3",
|
||||
city: "enccity",
|
||||
state: "encstate",
|
||||
postalCode: "encpostalCode",
|
||||
country: "enccountry",
|
||||
company: "enccompany",
|
||||
email: "encemail",
|
||||
phone: "encphone",
|
||||
ssn: "encssn",
|
||||
username: "encusername",
|
||||
passportNumber: "encpassportNumber",
|
||||
licenseNumber: "enclicenseNumber",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new IdentityData();
|
||||
const identity = new Identity(data);
|
||||
|
||||
expect(identity).toEqual({
|
||||
address1: null,
|
||||
address2: null,
|
||||
address3: null,
|
||||
city: null,
|
||||
company: null,
|
||||
country: null,
|
||||
email: null,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
licenseNumber: null,
|
||||
middleName: null,
|
||||
passportNumber: null,
|
||||
phone: null,
|
||||
postalCode: null,
|
||||
ssn: null,
|
||||
state: null,
|
||||
title: null,
|
||||
username: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const identity = new Identity(data);
|
||||
|
||||
expect(identity).toEqual({
|
||||
title: { encryptedString: "enctitle", encryptionType: 0 },
|
||||
firstName: { encryptedString: "encfirstName", encryptionType: 0 },
|
||||
middleName: { encryptedString: "encmiddleName", encryptionType: 0 },
|
||||
lastName: { encryptedString: "enclastName", encryptionType: 0 },
|
||||
address1: { encryptedString: "encaddress1", encryptionType: 0 },
|
||||
address2: { encryptedString: "encaddress2", encryptionType: 0 },
|
||||
address3: { encryptedString: "encaddress3", encryptionType: 0 },
|
||||
city: { encryptedString: "enccity", encryptionType: 0 },
|
||||
state: { encryptedString: "encstate", encryptionType: 0 },
|
||||
postalCode: { encryptedString: "encpostalCode", encryptionType: 0 },
|
||||
country: { encryptedString: "enccountry", encryptionType: 0 },
|
||||
company: { encryptedString: "enccompany", encryptionType: 0 },
|
||||
email: { encryptedString: "encemail", encryptionType: 0 },
|
||||
phone: { encryptedString: "encphone", encryptionType: 0 },
|
||||
ssn: { encryptedString: "encssn", encryptionType: 0 },
|
||||
username: { encryptedString: "encusername", encryptionType: 0 },
|
||||
passportNumber: { encryptedString: "encpassportNumber", encryptionType: 0 },
|
||||
licenseNumber: { encryptedString: "enclicenseNumber", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("toIdentityData", () => {
|
||||
const identity = new Identity(data);
|
||||
expect(identity.toIdentityData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const identity = new Identity();
|
||||
|
||||
identity.title = mockEnc("mockTitle");
|
||||
identity.firstName = mockEnc("mockFirstName");
|
||||
identity.middleName = mockEnc("mockMiddleName");
|
||||
identity.lastName = mockEnc("mockLastName");
|
||||
identity.address1 = mockEnc("mockAddress1");
|
||||
identity.address2 = mockEnc("mockAddress2");
|
||||
identity.address3 = mockEnc("mockAddress3");
|
||||
identity.city = mockEnc("mockCity");
|
||||
identity.state = mockEnc("mockState");
|
||||
identity.postalCode = mockEnc("mockPostalCode");
|
||||
identity.country = mockEnc("mockCountry");
|
||||
identity.company = mockEnc("mockCompany");
|
||||
identity.email = mockEnc("mockEmail");
|
||||
identity.phone = mockEnc("mockPhone");
|
||||
identity.ssn = mockEnc("mockSsn");
|
||||
identity.username = mockEnc("mockUsername");
|
||||
identity.passportNumber = mockEnc("mockPassportNumber");
|
||||
identity.licenseNumber = mockEnc("mockLicenseNumber");
|
||||
|
||||
const view = await identity.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
_firstName: "mockFirstName",
|
||||
_lastName: "mockLastName",
|
||||
_subTitle: null,
|
||||
address1: "mockAddress1",
|
||||
address2: "mockAddress2",
|
||||
address3: "mockAddress3",
|
||||
city: "mockCity",
|
||||
company: "mockCompany",
|
||||
country: "mockCountry",
|
||||
email: "mockEmail",
|
||||
licenseNumber: "mockLicenseNumber",
|
||||
middleName: "mockMiddleName",
|
||||
passportNumber: "mockPassportNumber",
|
||||
phone: "mockPhone",
|
||||
postalCode: "mockPostalCode",
|
||||
ssn: "mockSsn",
|
||||
state: "mockState",
|
||||
title: "mockTitle",
|
||||
username: "mockUsername",
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const actual = Identity.fromJSON({
|
||||
firstName: "mockFirstName",
|
||||
lastName: "mockLastName",
|
||||
address1: "mockAddress1",
|
||||
address2: "mockAddress2",
|
||||
address3: "mockAddress3",
|
||||
city: "mockCity",
|
||||
company: "mockCompany",
|
||||
country: "mockCountry",
|
||||
email: "mockEmail",
|
||||
licenseNumber: "mockLicenseNumber",
|
||||
middleName: "mockMiddleName",
|
||||
passportNumber: "mockPassportNumber",
|
||||
phone: "mockPhone",
|
||||
postalCode: "mockPostalCode",
|
||||
ssn: "mockSsn",
|
||||
state: "mockState",
|
||||
title: "mockTitle",
|
||||
username: "mockUsername",
|
||||
});
|
||||
|
||||
expect(actual).toEqual({
|
||||
firstName: "mockFirstName_fromJSON",
|
||||
lastName: "mockLastName_fromJSON",
|
||||
address1: "mockAddress1_fromJSON",
|
||||
address2: "mockAddress2_fromJSON",
|
||||
address3: "mockAddress3_fromJSON",
|
||||
city: "mockCity_fromJSON",
|
||||
company: "mockCompany_fromJSON",
|
||||
country: "mockCountry_fromJSON",
|
||||
email: "mockEmail_fromJSON",
|
||||
licenseNumber: "mockLicenseNumber_fromJSON",
|
||||
middleName: "mockMiddleName_fromJSON",
|
||||
passportNumber: "mockPassportNumber_fromJSON",
|
||||
phone: "mockPhone_fromJSON",
|
||||
postalCode: "mockPostalCode_fromJSON",
|
||||
ssn: "mockSsn_fromJSON",
|
||||
state: "mockState_fromJSON",
|
||||
title: "mockTitle_fromJSON",
|
||||
username: "mockUsername_fromJSON",
|
||||
});
|
||||
expect(actual).toBeInstanceOf(Identity);
|
||||
});
|
||||
|
||||
it("returns null if object is null", () => {
|
||||
expect(Identity.fromJSON(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
160
libs/common/src/vault/models/domain/identity.ts
Normal file
160
libs/common/src/vault/models/domain/identity.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { IdentityData } from "../data/identity.data";
|
||||
import { IdentityView } from "../view/identity.view";
|
||||
|
||||
export class Identity extends Domain {
|
||||
title: EncString;
|
||||
firstName: EncString;
|
||||
middleName: EncString;
|
||||
lastName: EncString;
|
||||
address1: EncString;
|
||||
address2: EncString;
|
||||
address3: EncString;
|
||||
city: EncString;
|
||||
state: EncString;
|
||||
postalCode: EncString;
|
||||
country: EncString;
|
||||
company: EncString;
|
||||
email: EncString;
|
||||
phone: EncString;
|
||||
ssn: EncString;
|
||||
username: EncString;
|
||||
passportNumber: EncString;
|
||||
licenseNumber: EncString;
|
||||
|
||||
constructor(obj?: IdentityData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildDomainModel(
|
||||
this,
|
||||
obj,
|
||||
{
|
||||
title: null,
|
||||
firstName: null,
|
||||
middleName: null,
|
||||
lastName: null,
|
||||
address1: null,
|
||||
address2: null,
|
||||
address3: null,
|
||||
city: null,
|
||||
state: null,
|
||||
postalCode: null,
|
||||
country: null,
|
||||
company: null,
|
||||
email: null,
|
||||
phone: null,
|
||||
ssn: null,
|
||||
username: null,
|
||||
passportNumber: null,
|
||||
licenseNumber: null,
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<IdentityView> {
|
||||
return this.decryptObj(
|
||||
new IdentityView(),
|
||||
{
|
||||
title: null,
|
||||
firstName: null,
|
||||
middleName: null,
|
||||
lastName: null,
|
||||
address1: null,
|
||||
address2: null,
|
||||
address3: null,
|
||||
city: null,
|
||||
state: null,
|
||||
postalCode: null,
|
||||
country: null,
|
||||
company: null,
|
||||
email: null,
|
||||
phone: null,
|
||||
ssn: null,
|
||||
username: null,
|
||||
passportNumber: null,
|
||||
licenseNumber: null,
|
||||
},
|
||||
orgId,
|
||||
encKey
|
||||
);
|
||||
}
|
||||
|
||||
toIdentityData(): IdentityData {
|
||||
const i = new IdentityData();
|
||||
this.buildDataModel(this, i, {
|
||||
title: null,
|
||||
firstName: null,
|
||||
middleName: null,
|
||||
lastName: null,
|
||||
address1: null,
|
||||
address2: null,
|
||||
address3: null,
|
||||
city: null,
|
||||
state: null,
|
||||
postalCode: null,
|
||||
country: null,
|
||||
company: null,
|
||||
email: null,
|
||||
phone: null,
|
||||
ssn: null,
|
||||
username: null,
|
||||
passportNumber: null,
|
||||
licenseNumber: null,
|
||||
});
|
||||
return i;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<Identity>): Identity {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = EncString.fromJSON(obj.title);
|
||||
const firstName = EncString.fromJSON(obj.firstName);
|
||||
const middleName = EncString.fromJSON(obj.middleName);
|
||||
const lastName = EncString.fromJSON(obj.lastName);
|
||||
const address1 = EncString.fromJSON(obj.address1);
|
||||
const address2 = EncString.fromJSON(obj.address2);
|
||||
const address3 = EncString.fromJSON(obj.address3);
|
||||
const city = EncString.fromJSON(obj.city);
|
||||
const state = EncString.fromJSON(obj.state);
|
||||
const postalCode = EncString.fromJSON(obj.postalCode);
|
||||
const country = EncString.fromJSON(obj.country);
|
||||
const company = EncString.fromJSON(obj.company);
|
||||
const email = EncString.fromJSON(obj.email);
|
||||
const phone = EncString.fromJSON(obj.phone);
|
||||
const ssn = EncString.fromJSON(obj.ssn);
|
||||
const username = EncString.fromJSON(obj.username);
|
||||
const passportNumber = EncString.fromJSON(obj.passportNumber);
|
||||
const licenseNumber = EncString.fromJSON(obj.licenseNumber);
|
||||
|
||||
return Object.assign(new Identity(), obj, {
|
||||
title,
|
||||
firstName,
|
||||
middleName,
|
||||
lastName,
|
||||
address1,
|
||||
address2,
|
||||
address3,
|
||||
city,
|
||||
state,
|
||||
postalCode,
|
||||
country,
|
||||
company,
|
||||
email,
|
||||
phone,
|
||||
ssn,
|
||||
username,
|
||||
passportNumber,
|
||||
licenseNumber,
|
||||
});
|
||||
}
|
||||
}
|
||||
79
libs/common/src/vault/models/domain/login-uri.spec.ts
Normal file
79
libs/common/src/vault/models/domain/login-uri.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { UriMatchType } from "../../../enums/uriMatchType";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { LoginUriData } from "../data/login-uri.data";
|
||||
|
||||
import { LoginUri } from "./login-uri";
|
||||
|
||||
describe("LoginUri", () => {
|
||||
let data: LoginUriData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
uri: "encUri",
|
||||
match: UriMatchType.Domain,
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new LoginUriData();
|
||||
const loginUri = new LoginUri(data);
|
||||
|
||||
expect(loginUri).toEqual({
|
||||
match: null,
|
||||
uri: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const loginUri = new LoginUri(data);
|
||||
|
||||
expect(loginUri).toEqual({
|
||||
match: 0,
|
||||
uri: { encryptedString: "encUri", encryptionType: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("toLoginUriData", () => {
|
||||
const loginUri = new LoginUri(data);
|
||||
expect(loginUri.toLoginUriData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const loginUri = new LoginUri();
|
||||
loginUri.match = UriMatchType.Exact;
|
||||
loginUri.uri = mockEnc("uri");
|
||||
|
||||
const view = await loginUri.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
_canLaunch: null,
|
||||
_domain: null,
|
||||
_host: null,
|
||||
_hostname: null,
|
||||
_uri: "uri",
|
||||
match: 3,
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const actual = LoginUri.fromJSON({
|
||||
uri: "myUri",
|
||||
} as Jsonify<LoginUri>);
|
||||
|
||||
expect(actual).toEqual({
|
||||
uri: "myUri_fromJSON",
|
||||
});
|
||||
expect(actual).toBeInstanceOf(LoginUri);
|
||||
});
|
||||
|
||||
it("returns null if object is null", () => {
|
||||
expect(LoginUri.fromJSON(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
66
libs/common/src/vault/models/domain/login-uri.ts
Normal file
66
libs/common/src/vault/models/domain/login-uri.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { UriMatchType } from "../../../enums/uriMatchType";
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { LoginUriData } from "../data/login-uri.data";
|
||||
import { LoginUriView } from "../view/login-uri.view";
|
||||
|
||||
export class LoginUri extends Domain {
|
||||
uri: EncString;
|
||||
match: UriMatchType;
|
||||
|
||||
constructor(obj?: LoginUriData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.match = obj.match;
|
||||
this.buildDomainModel(
|
||||
this,
|
||||
obj,
|
||||
{
|
||||
uri: null,
|
||||
},
|
||||
[]
|
||||
);
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<LoginUriView> {
|
||||
return this.decryptObj(
|
||||
new LoginUriView(this),
|
||||
{
|
||||
uri: null,
|
||||
},
|
||||
orgId,
|
||||
encKey
|
||||
);
|
||||
}
|
||||
|
||||
toLoginUriData(): LoginUriData {
|
||||
const u = new LoginUriData();
|
||||
this.buildDataModel(
|
||||
this,
|
||||
u,
|
||||
{
|
||||
uri: null,
|
||||
match: null,
|
||||
},
|
||||
["match"]
|
||||
);
|
||||
return u;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<LoginUri>): LoginUri {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const uri = EncString.fromJSON(obj.uri);
|
||||
return Object.assign(new LoginUri(), obj, {
|
||||
uri,
|
||||
});
|
||||
}
|
||||
}
|
||||
131
libs/common/src/vault/models/domain/login.spec.ts
Normal file
131
libs/common/src/vault/models/domain/login.spec.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Substitute, Arg } from "@fluffy-spoon/substitute";
|
||||
|
||||
import { mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { UriMatchType } from "../../../enums/uriMatchType";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { LoginData } from "../../models/data/login.data";
|
||||
import { Login } from "../../models/domain/login";
|
||||
import { LoginUri } from "../../models/domain/login-uri";
|
||||
import { LoginUriView } from "../../models/view/login-uri.view";
|
||||
|
||||
describe("Login DTO", () => {
|
||||
it("Convert from empty LoginData", () => {
|
||||
const data = new LoginData();
|
||||
const login = new Login(data);
|
||||
|
||||
expect(login).toEqual({
|
||||
passwordRevisionDate: null,
|
||||
autofillOnPageLoad: undefined,
|
||||
username: null,
|
||||
password: null,
|
||||
totp: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert from full LoginData", () => {
|
||||
const data: LoginData = {
|
||||
uris: [{ uri: "uri", match: UriMatchType.Domain }],
|
||||
username: "username",
|
||||
password: "password",
|
||||
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||
totp: "123",
|
||||
autofillOnPageLoad: false,
|
||||
};
|
||||
const login = new Login(data);
|
||||
|
||||
expect(login).toEqual({
|
||||
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
autofillOnPageLoad: false,
|
||||
username: { encryptedString: "username", encryptionType: 0 },
|
||||
password: { encryptedString: "password", encryptionType: 0 },
|
||||
totp: { encryptedString: "123", encryptionType: 0 },
|
||||
uris: [{ match: 0, uri: { encryptedString: "uri", encryptionType: 0 } }],
|
||||
});
|
||||
});
|
||||
|
||||
it("Initialize without LoginData", () => {
|
||||
const login = new Login();
|
||||
|
||||
expect(login).toEqual({});
|
||||
});
|
||||
|
||||
it("Decrypts correctly", async () => {
|
||||
const loginUri = Substitute.for<LoginUri>();
|
||||
const loginUriView = new LoginUriView();
|
||||
loginUriView.uri = "decrypted uri";
|
||||
loginUri.decrypt(Arg.any()).resolves(loginUriView);
|
||||
|
||||
const login = new Login();
|
||||
login.uris = [loginUri];
|
||||
login.username = mockEnc("encrypted username");
|
||||
login.password = mockEnc("encrypted password");
|
||||
login.passwordRevisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
login.totp = mockEnc("encrypted totp");
|
||||
login.autofillOnPageLoad = true;
|
||||
|
||||
const loginView = await login.decrypt(null);
|
||||
expect(loginView).toEqual({
|
||||
username: "encrypted username",
|
||||
password: "encrypted password",
|
||||
passwordRevisionDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
totp: "encrypted totp",
|
||||
uris: [
|
||||
{
|
||||
match: null,
|
||||
_uri: "decrypted uri",
|
||||
_domain: null,
|
||||
_hostname: null,
|
||||
_host: null,
|
||||
_canLaunch: null,
|
||||
},
|
||||
],
|
||||
autofillOnPageLoad: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("Converts from LoginData and back", () => {
|
||||
const data: LoginData = {
|
||||
uris: [{ uri: "uri", match: UriMatchType.Domain }],
|
||||
username: "username",
|
||||
password: "password",
|
||||
passwordRevisionDate: "2022-01-31T12:00:00.000Z",
|
||||
totp: "123",
|
||||
autofillOnPageLoad: false,
|
||||
};
|
||||
const login = new Login(data);
|
||||
|
||||
const loginData = login.toLoginData();
|
||||
|
||||
expect(loginData).toEqual(data);
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(LoginUri, "fromJSON").mockImplementation(mockFromJson);
|
||||
const passwordRevisionDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
|
||||
const actual = Login.fromJSON({
|
||||
uris: ["loginUri1", "loginUri2"] as any,
|
||||
username: "myUsername",
|
||||
password: "myPassword",
|
||||
passwordRevisionDate: passwordRevisionDate.toISOString(),
|
||||
totp: "myTotp",
|
||||
});
|
||||
|
||||
expect(actual).toEqual({
|
||||
uris: ["loginUri1_fromJSON", "loginUri2_fromJSON"] as any,
|
||||
username: "myUsername_fromJSON",
|
||||
password: "myPassword_fromJSON",
|
||||
passwordRevisionDate: passwordRevisionDate,
|
||||
totp: "myTotp_fromJSON",
|
||||
});
|
||||
expect(actual).toBeInstanceOf(Login);
|
||||
});
|
||||
|
||||
it("returns null if object is null", () => {
|
||||
expect(Login.fromJSON(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
111
libs/common/src/vault/models/domain/login.ts
Normal file
111
libs/common/src/vault/models/domain/login.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { LoginData } from "../data/login.data";
|
||||
import { LoginView } from "../view/login.view";
|
||||
|
||||
import { LoginUri } from "./login-uri";
|
||||
|
||||
export class Login extends Domain {
|
||||
uris: LoginUri[];
|
||||
username: EncString;
|
||||
password: EncString;
|
||||
passwordRevisionDate?: Date;
|
||||
totp: EncString;
|
||||
autofillOnPageLoad: boolean;
|
||||
|
||||
constructor(obj?: LoginData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.passwordRevisionDate =
|
||||
obj.passwordRevisionDate != null ? new Date(obj.passwordRevisionDate) : null;
|
||||
this.autofillOnPageLoad = obj.autofillOnPageLoad;
|
||||
this.buildDomainModel(
|
||||
this,
|
||||
obj,
|
||||
{
|
||||
username: null,
|
||||
password: null,
|
||||
totp: null,
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (obj.uris) {
|
||||
this.uris = [];
|
||||
obj.uris.forEach((u) => {
|
||||
this.uris.push(new LoginUri(u));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<LoginView> {
|
||||
const view = await this.decryptObj(
|
||||
new LoginView(this),
|
||||
{
|
||||
username: null,
|
||||
password: null,
|
||||
totp: null,
|
||||
},
|
||||
orgId,
|
||||
encKey
|
||||
);
|
||||
|
||||
if (this.uris != null) {
|
||||
view.uris = [];
|
||||
for (let i = 0; i < this.uris.length; i++) {
|
||||
const uri = await this.uris[i].decrypt(orgId, encKey);
|
||||
view.uris.push(uri);
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
toLoginData(): LoginData {
|
||||
const l = new LoginData();
|
||||
l.passwordRevisionDate =
|
||||
this.passwordRevisionDate != null ? this.passwordRevisionDate.toISOString() : null;
|
||||
l.autofillOnPageLoad = this.autofillOnPageLoad;
|
||||
this.buildDataModel(this, l, {
|
||||
username: null,
|
||||
password: null,
|
||||
totp: null,
|
||||
});
|
||||
|
||||
if (this.uris != null && this.uris.length > 0) {
|
||||
l.uris = [];
|
||||
this.uris.forEach((u) => {
|
||||
l.uris.push(u.toLoginUriData());
|
||||
});
|
||||
}
|
||||
|
||||
return l;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<Login>>): Login {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const username = EncString.fromJSON(obj.username);
|
||||
const password = EncString.fromJSON(obj.password);
|
||||
const totp = EncString.fromJSON(obj.totp);
|
||||
const passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||
const uris = obj.uris?.map((uri: any) => LoginUri.fromJSON(uri));
|
||||
|
||||
return Object.assign(new Login(), obj, {
|
||||
username,
|
||||
password,
|
||||
totp,
|
||||
passwordRevisionDate: passwordRevisionDate,
|
||||
uris: uris,
|
||||
});
|
||||
}
|
||||
}
|
||||
73
libs/common/src/vault/models/domain/password.spec.ts
Normal file
73
libs/common/src/vault/models/domain/password.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { mockEnc, mockFromJson } from "../../../../spec/utils";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { PasswordHistoryData } from "../../models/data/password-history.data";
|
||||
import { Password } from "../../models/domain/password";
|
||||
|
||||
describe("Password", () => {
|
||||
let data: PasswordHistoryData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
password: "encPassword",
|
||||
lastUsedDate: "2022-01-31T12:00:00.000Z",
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new PasswordHistoryData();
|
||||
const password = new Password(data);
|
||||
|
||||
expect(password).toMatchObject({
|
||||
password: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const password = new Password(data);
|
||||
|
||||
expect(password).toEqual({
|
||||
password: { encryptedString: "encPassword", encryptionType: 0 },
|
||||
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
|
||||
it("toPasswordHistoryData", () => {
|
||||
const password = new Password(data);
|
||||
expect(password.toPasswordHistoryData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const password = new Password();
|
||||
password.password = mockEnc("password");
|
||||
password.lastUsedDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
|
||||
const view = await password.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
password: "password",
|
||||
lastUsedDate: new Date("2022-01-31T12:00:00.000Z"),
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
jest.spyOn(EncString, "fromJSON").mockImplementation(mockFromJson);
|
||||
const lastUsedDate = new Date("2022-01-31T12:00:00.000Z");
|
||||
|
||||
const actual = Password.fromJSON({
|
||||
password: "myPassword",
|
||||
lastUsedDate: lastUsedDate.toISOString(),
|
||||
});
|
||||
|
||||
expect(actual).toEqual({
|
||||
password: "myPassword_fromJSON",
|
||||
lastUsedDate: lastUsedDate,
|
||||
});
|
||||
expect(actual).toBeInstanceOf(Password);
|
||||
});
|
||||
|
||||
it("returns null if object is null", () => {
|
||||
expect(Password.fromJSON(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
58
libs/common/src/vault/models/domain/password.ts
Normal file
58
libs/common/src/vault/models/domain/password.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { EncString } from "../../../models/domain/enc-string";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { PasswordHistoryData } from "../data/password-history.data";
|
||||
import { PasswordHistoryView } from "../view/password-history.view";
|
||||
|
||||
export class Password extends Domain {
|
||||
password: EncString;
|
||||
lastUsedDate: Date;
|
||||
|
||||
constructor(obj?: PasswordHistoryData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.buildDomainModel(this, obj, {
|
||||
password: null,
|
||||
});
|
||||
this.lastUsedDate = new Date(obj.lastUsedDate);
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<PasswordHistoryView> {
|
||||
return this.decryptObj(
|
||||
new PasswordHistoryView(this),
|
||||
{
|
||||
password: null,
|
||||
},
|
||||
orgId,
|
||||
encKey
|
||||
);
|
||||
}
|
||||
|
||||
toPasswordHistoryData(): PasswordHistoryData {
|
||||
const ph = new PasswordHistoryData();
|
||||
ph.lastUsedDate = this.lastUsedDate.toISOString();
|
||||
this.buildDataModel(this, ph, {
|
||||
password: null,
|
||||
});
|
||||
return ph;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<Password>>): Password {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const password = EncString.fromJSON(obj.password);
|
||||
const lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate);
|
||||
|
||||
return Object.assign(new Password(), obj, {
|
||||
password,
|
||||
lastUsedDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
53
libs/common/src/vault/models/domain/secure-note.spec.ts
Normal file
53
libs/common/src/vault/models/domain/secure-note.spec.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { SecureNoteType } from "../../../enums/secureNoteType";
|
||||
import { SecureNoteData } from "../data/secure-note.data";
|
||||
|
||||
import { SecureNote } from "./secure-note";
|
||||
|
||||
describe("SecureNote", () => {
|
||||
let data: SecureNoteData;
|
||||
|
||||
beforeEach(() => {
|
||||
data = {
|
||||
type: SecureNoteType.Generic,
|
||||
};
|
||||
});
|
||||
|
||||
it("Convert from empty", () => {
|
||||
const data = new SecureNoteData();
|
||||
const secureNote = new SecureNote(data);
|
||||
|
||||
expect(secureNote).toEqual({
|
||||
type: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("Convert", () => {
|
||||
const secureNote = new SecureNote(data);
|
||||
|
||||
expect(secureNote).toEqual({
|
||||
type: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("toSecureNoteData", () => {
|
||||
const secureNote = new SecureNote(data);
|
||||
expect(secureNote.toSecureNoteData()).toEqual(data);
|
||||
});
|
||||
|
||||
it("Decrypt", async () => {
|
||||
const secureNote = new SecureNote();
|
||||
secureNote.type = SecureNoteType.Generic;
|
||||
|
||||
const view = await secureNote.decrypt(null);
|
||||
|
||||
expect(view).toEqual({
|
||||
type: 0,
|
||||
});
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("returns null if object is null", () => {
|
||||
expect(SecureNote.fromJSON(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
38
libs/common/src/vault/models/domain/secure-note.ts
Normal file
38
libs/common/src/vault/models/domain/secure-note.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SecureNoteType } from "../../../enums/secureNoteType";
|
||||
import Domain from "../../../models/domain/domain-base";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { SecureNoteData } from "../data/secure-note.data";
|
||||
import { SecureNoteView } from "../view/secure-note.view";
|
||||
|
||||
export class SecureNote extends Domain {
|
||||
type: SecureNoteType;
|
||||
|
||||
constructor(obj?: SecureNoteData) {
|
||||
super();
|
||||
if (obj == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.type = obj.type;
|
||||
}
|
||||
|
||||
decrypt(orgId: string, encKey?: SymmetricCryptoKey): Promise<SecureNoteView> {
|
||||
return Promise.resolve(new SecureNoteView(this));
|
||||
}
|
||||
|
||||
toSecureNoteData(): SecureNoteData {
|
||||
const n = new SecureNoteData();
|
||||
n.type = this.type;
|
||||
return n;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<SecureNote>): SecureNote {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Object.assign(new SecureNote(), obj);
|
||||
}
|
||||
}
|
||||
87
libs/common/src/vault/models/domain/sorted-ciphers-cache.ts
Normal file
87
libs/common/src/vault/models/domain/sorted-ciphers-cache.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { CipherView } from "../view/cipher.view";
|
||||
|
||||
const CacheTTL = 3000;
|
||||
|
||||
export class SortedCiphersCache {
|
||||
private readonly sortedCiphersByUrl: Map<string, Ciphers> = new Map<string, Ciphers>();
|
||||
private readonly timeouts: Map<string, any> = new Map<string, any>();
|
||||
|
||||
constructor(private readonly comparator: (a: CipherView, b: CipherView) => number) {}
|
||||
|
||||
isCached(url: string) {
|
||||
return this.sortedCiphersByUrl.has(url);
|
||||
}
|
||||
|
||||
addCiphers(url: string, ciphers: CipherView[]) {
|
||||
ciphers.sort(this.comparator);
|
||||
this.sortedCiphersByUrl.set(url, new Ciphers(ciphers));
|
||||
this.resetTimer(url);
|
||||
}
|
||||
|
||||
getLastUsed(url: string) {
|
||||
this.resetTimer(url);
|
||||
return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getLastUsed() : null;
|
||||
}
|
||||
|
||||
getLastLaunched(url: string) {
|
||||
return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getLastLaunched() : null;
|
||||
}
|
||||
|
||||
getNext(url: string) {
|
||||
this.resetTimer(url);
|
||||
return this.isCached(url) ? this.sortedCiphersByUrl.get(url).getNext() : null;
|
||||
}
|
||||
|
||||
updateLastUsedIndex(url: string) {
|
||||
if (this.isCached(url)) {
|
||||
this.sortedCiphersByUrl.get(url).updateLastUsedIndex();
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.sortedCiphersByUrl.clear();
|
||||
this.timeouts.clear();
|
||||
}
|
||||
|
||||
private resetTimer(url: string) {
|
||||
clearTimeout(this.timeouts.get(url));
|
||||
this.timeouts.set(
|
||||
url,
|
||||
setTimeout(() => {
|
||||
this.sortedCiphersByUrl.delete(url);
|
||||
this.timeouts.delete(url);
|
||||
}, CacheTTL)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Ciphers {
|
||||
lastUsedIndex = -1;
|
||||
|
||||
constructor(private readonly ciphers: CipherView[]) {}
|
||||
|
||||
getLastUsed() {
|
||||
this.lastUsedIndex = Math.max(this.lastUsedIndex, 0);
|
||||
return this.ciphers[this.lastUsedIndex];
|
||||
}
|
||||
|
||||
getLastLaunched() {
|
||||
const usedCiphers = this.ciphers.filter((cipher) => cipher.localData?.lastLaunched);
|
||||
const sortedCiphers = usedCiphers.sort(
|
||||
(x, y) => y.localData.lastLaunched.valueOf() - x.localData.lastLaunched.valueOf()
|
||||
);
|
||||
return sortedCiphers[0];
|
||||
}
|
||||
|
||||
getNextIndex() {
|
||||
return (this.lastUsedIndex + 1) % this.ciphers.length;
|
||||
}
|
||||
|
||||
getNext() {
|
||||
return this.ciphers[this.getNextIndex()];
|
||||
}
|
||||
|
||||
updateLastUsedIndex() {
|
||||
this.lastUsedIndex = this.getNextIndex();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export class AttachmentRequest {
|
||||
fileName: string;
|
||||
key: string;
|
||||
fileSize: number;
|
||||
adminRequest: boolean;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class CipherBulkDeleteRequest {
|
||||
ids: string[];
|
||||
organizationId: string;
|
||||
|
||||
constructor(ids: string[], organizationId?: string) {
|
||||
this.ids = ids == null ? [] : ids;
|
||||
this.organizationId = organizationId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export class CipherBulkMoveRequest {
|
||||
ids: string[];
|
||||
folderId: string;
|
||||
|
||||
constructor(ids: string[], folderId: string) {
|
||||
this.ids = ids == null ? [] : ids;
|
||||
this.folderId = folderId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class CipherBulkRestoreRequest {
|
||||
ids: string[];
|
||||
|
||||
constructor(ids: string[]) {
|
||||
this.ids = ids == null ? [] : ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
import { CipherWithIdRequest } from "./cipher-with-id.request";
|
||||
|
||||
export class CipherBulkShareRequest {
|
||||
ciphers: CipherWithIdRequest[];
|
||||
collectionIds: string[];
|
||||
|
||||
constructor(ciphers: Cipher[], collectionIds: string[]) {
|
||||
if (ciphers != null) {
|
||||
this.ciphers = [];
|
||||
ciphers.forEach((c) => {
|
||||
this.ciphers.push(new CipherWithIdRequest(c));
|
||||
});
|
||||
}
|
||||
this.collectionIds = collectionIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export class CipherCollectionsRequest {
|
||||
collectionIds: string[];
|
||||
|
||||
constructor(collectionIds: string[]) {
|
||||
this.collectionIds = collectionIds == null ? [] : collectionIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
import { CipherRequest } from "./cipher.request";
|
||||
|
||||
export class CipherCreateRequest {
|
||||
cipher: CipherRequest;
|
||||
collectionIds: string[];
|
||||
|
||||
constructor(cipher: Cipher) {
|
||||
this.cipher = new CipherRequest(cipher);
|
||||
this.collectionIds = cipher.collectionIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
export class CipherPartialRequest {
|
||||
folderId: string;
|
||||
favorite: boolean;
|
||||
|
||||
constructor(cipher: Cipher) {
|
||||
this.folderId = cipher.folderId;
|
||||
this.favorite = cipher.favorite;
|
||||
}
|
||||
}
|
||||
13
libs/common/src/vault/models/request/cipher-share.request.ts
Normal file
13
libs/common/src/vault/models/request/cipher-share.request.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
import { CipherRequest } from "./cipher.request";
|
||||
|
||||
export class CipherShareRequest {
|
||||
cipher: CipherRequest;
|
||||
collectionIds: string[];
|
||||
|
||||
constructor(cipher: Cipher) {
|
||||
this.cipher = new CipherRequest(cipher);
|
||||
this.collectionIds = cipher.collectionIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
import { CipherRequest } from "./cipher.request";
|
||||
|
||||
export class CipherWithIdRequest extends CipherRequest {
|
||||
id: string;
|
||||
|
||||
constructor(cipher: Cipher) {
|
||||
super(cipher);
|
||||
this.id = cipher.id;
|
||||
}
|
||||
}
|
||||
164
libs/common/src/vault/models/request/cipher.request.ts
Normal file
164
libs/common/src/vault/models/request/cipher.request.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { CardApi } from "../../../models/api/card.api";
|
||||
import { FieldApi } from "../../../models/api/field.api";
|
||||
import { IdentityApi } from "../../../models/api/identity.api";
|
||||
import { LoginUriApi } from "../../../models/api/login-uri.api";
|
||||
import { LoginApi } from "../../../models/api/login.api";
|
||||
import { SecureNoteApi } from "../../../models/api/secure-note.api";
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
import { CipherType } from "../../enums/cipher-type";
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
import { AttachmentRequest } from "./attachment.request";
|
||||
import { PasswordHistoryRequest } from "./password-history.request";
|
||||
|
||||
export class CipherRequest {
|
||||
type: CipherType;
|
||||
folderId: string;
|
||||
organizationId: string;
|
||||
name: string;
|
||||
notes: string;
|
||||
favorite: boolean;
|
||||
login: LoginApi;
|
||||
secureNote: SecureNoteApi;
|
||||
card: CardApi;
|
||||
identity: IdentityApi;
|
||||
fields: FieldApi[];
|
||||
passwordHistory: PasswordHistoryRequest[];
|
||||
// Deprecated, remove at some point and rename attachments2 to attachments
|
||||
attachments: { [id: string]: string };
|
||||
attachments2: { [id: string]: AttachmentRequest };
|
||||
lastKnownRevisionDate: Date;
|
||||
reprompt: CipherRepromptType;
|
||||
|
||||
constructor(cipher: Cipher) {
|
||||
this.type = cipher.type;
|
||||
this.folderId = cipher.folderId;
|
||||
this.organizationId = cipher.organizationId;
|
||||
this.name = cipher.name ? cipher.name.encryptedString : null;
|
||||
this.notes = cipher.notes ? cipher.notes.encryptedString : null;
|
||||
this.favorite = cipher.favorite;
|
||||
this.lastKnownRevisionDate = cipher.revisionDate;
|
||||
this.reprompt = cipher.reprompt;
|
||||
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
this.login = new LoginApi();
|
||||
this.login.uris = null;
|
||||
this.login.username = cipher.login.username ? cipher.login.username.encryptedString : null;
|
||||
this.login.password = cipher.login.password ? cipher.login.password.encryptedString : null;
|
||||
this.login.passwordRevisionDate =
|
||||
cipher.login.passwordRevisionDate != null
|
||||
? cipher.login.passwordRevisionDate.toISOString()
|
||||
: null;
|
||||
this.login.totp = cipher.login.totp ? cipher.login.totp.encryptedString : null;
|
||||
this.login.autofillOnPageLoad = cipher.login.autofillOnPageLoad;
|
||||
|
||||
if (cipher.login.uris != null) {
|
||||
this.login.uris = cipher.login.uris.map((u) => {
|
||||
const uri = new LoginUriApi();
|
||||
uri.uri = u.uri != null ? u.uri.encryptedString : null;
|
||||
uri.match = u.match != null ? u.match : null;
|
||||
return uri;
|
||||
});
|
||||
}
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
this.secureNote = new SecureNoteApi();
|
||||
this.secureNote.type = cipher.secureNote.type;
|
||||
break;
|
||||
case CipherType.Card:
|
||||
this.card = new CardApi();
|
||||
this.card.cardholderName =
|
||||
cipher.card.cardholderName != null ? cipher.card.cardholderName.encryptedString : null;
|
||||
this.card.brand = cipher.card.brand != null ? cipher.card.brand.encryptedString : null;
|
||||
this.card.number = cipher.card.number != null ? cipher.card.number.encryptedString : null;
|
||||
this.card.expMonth =
|
||||
cipher.card.expMonth != null ? cipher.card.expMonth.encryptedString : null;
|
||||
this.card.expYear =
|
||||
cipher.card.expYear != null ? cipher.card.expYear.encryptedString : null;
|
||||
this.card.code = cipher.card.code != null ? cipher.card.code.encryptedString : null;
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
this.identity = new IdentityApi();
|
||||
this.identity.title =
|
||||
cipher.identity.title != null ? cipher.identity.title.encryptedString : null;
|
||||
this.identity.firstName =
|
||||
cipher.identity.firstName != null ? cipher.identity.firstName.encryptedString : null;
|
||||
this.identity.middleName =
|
||||
cipher.identity.middleName != null ? cipher.identity.middleName.encryptedString : null;
|
||||
this.identity.lastName =
|
||||
cipher.identity.lastName != null ? cipher.identity.lastName.encryptedString : null;
|
||||
this.identity.address1 =
|
||||
cipher.identity.address1 != null ? cipher.identity.address1.encryptedString : null;
|
||||
this.identity.address2 =
|
||||
cipher.identity.address2 != null ? cipher.identity.address2.encryptedString : null;
|
||||
this.identity.address3 =
|
||||
cipher.identity.address3 != null ? cipher.identity.address3.encryptedString : null;
|
||||
this.identity.city =
|
||||
cipher.identity.city != null ? cipher.identity.city.encryptedString : null;
|
||||
this.identity.state =
|
||||
cipher.identity.state != null ? cipher.identity.state.encryptedString : null;
|
||||
this.identity.postalCode =
|
||||
cipher.identity.postalCode != null ? cipher.identity.postalCode.encryptedString : null;
|
||||
this.identity.country =
|
||||
cipher.identity.country != null ? cipher.identity.country.encryptedString : null;
|
||||
this.identity.company =
|
||||
cipher.identity.company != null ? cipher.identity.company.encryptedString : null;
|
||||
this.identity.email =
|
||||
cipher.identity.email != null ? cipher.identity.email.encryptedString : null;
|
||||
this.identity.phone =
|
||||
cipher.identity.phone != null ? cipher.identity.phone.encryptedString : null;
|
||||
this.identity.ssn =
|
||||
cipher.identity.ssn != null ? cipher.identity.ssn.encryptedString : null;
|
||||
this.identity.username =
|
||||
cipher.identity.username != null ? cipher.identity.username.encryptedString : null;
|
||||
this.identity.passportNumber =
|
||||
cipher.identity.passportNumber != null
|
||||
? cipher.identity.passportNumber.encryptedString
|
||||
: null;
|
||||
this.identity.licenseNumber =
|
||||
cipher.identity.licenseNumber != null
|
||||
? cipher.identity.licenseNumber.encryptedString
|
||||
: null;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (cipher.fields != null) {
|
||||
this.fields = cipher.fields.map((f) => {
|
||||
const field = new FieldApi();
|
||||
field.type = f.type;
|
||||
field.name = f.name ? f.name.encryptedString : null;
|
||||
field.value = f.value ? f.value.encryptedString : null;
|
||||
field.linkedId = f.linkedId;
|
||||
return field;
|
||||
});
|
||||
}
|
||||
|
||||
if (cipher.passwordHistory != null) {
|
||||
this.passwordHistory = [];
|
||||
cipher.passwordHistory.forEach((ph) => {
|
||||
this.passwordHistory.push({
|
||||
lastUsedDate: ph.lastUsedDate,
|
||||
password: ph.password ? ph.password.encryptedString : null,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (cipher.attachments != null) {
|
||||
this.attachments = {};
|
||||
this.attachments2 = {};
|
||||
cipher.attachments.forEach((attachment) => {
|
||||
const fileName = attachment.fileName ? attachment.fileName.encryptedString : null;
|
||||
this.attachments[attachment.id] = fileName;
|
||||
const attachmentRequest = new AttachmentRequest();
|
||||
attachmentRequest.fileName = fileName;
|
||||
if (attachment.key != null) {
|
||||
attachmentRequest.key = attachment.key.encryptedString;
|
||||
}
|
||||
this.attachments2[attachment.id] = attachmentRequest;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Folder } from "../domain/folder";
|
||||
|
||||
import { FolderRequest } from "./folder.request";
|
||||
|
||||
export class FolderWithIdRequest extends FolderRequest {
|
||||
id: string;
|
||||
|
||||
constructor(folder: Folder) {
|
||||
super(folder);
|
||||
this.id = folder.id;
|
||||
}
|
||||
}
|
||||
9
libs/common/src/vault/models/request/folder.request.ts
Normal file
9
libs/common/src/vault/models/request/folder.request.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Folder } from "../domain/folder";
|
||||
|
||||
export class FolderRequest {
|
||||
name: string;
|
||||
|
||||
constructor(folder: Folder) {
|
||||
this.name = folder.name ? folder.name.encryptedString : null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class PasswordHistoryRequest {
|
||||
password: string;
|
||||
lastUsedDate: Date;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { FileUploadType } from "../../../enums/fileUploadType";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
import { CipherResponse } from "./cipher.response";
|
||||
|
||||
export class AttachmentUploadDataResponse extends BaseResponse {
|
||||
attachmentId: string;
|
||||
fileUploadType: FileUploadType;
|
||||
cipherResponse: CipherResponse;
|
||||
cipherMiniResponse: CipherResponse;
|
||||
url: string = null;
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.attachmentId = this.getResponseProperty("AttachmentId");
|
||||
this.fileUploadType = this.getResponseProperty("FileUploadType");
|
||||
const cipherResponse = this.getResponseProperty("CipherResponse");
|
||||
const cipherMiniResponse = this.getResponseProperty("CipherMiniResponse");
|
||||
this.cipherResponse = cipherResponse == null ? null : new CipherResponse(cipherResponse);
|
||||
this.cipherMiniResponse =
|
||||
cipherMiniResponse == null ? null : new CipherResponse(cipherMiniResponse);
|
||||
this.url = this.getResponseProperty("Url");
|
||||
}
|
||||
}
|
||||
20
libs/common/src/vault/models/response/attachment.response.ts
Normal file
20
libs/common/src/vault/models/response/attachment.response.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class AttachmentResponse extends BaseResponse {
|
||||
id: string;
|
||||
url: string;
|
||||
fileName: string;
|
||||
key: string;
|
||||
size: string;
|
||||
sizeName: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.url = this.getResponseProperty("Url");
|
||||
this.fileName = this.getResponseProperty("FileName");
|
||||
this.key = this.getResponseProperty("Key");
|
||||
this.size = this.getResponseProperty("Size");
|
||||
this.sizeName = this.getResponseProperty("SizeName");
|
||||
}
|
||||
}
|
||||
94
libs/common/src/vault/models/response/cipher.response.ts
Normal file
94
libs/common/src/vault/models/response/cipher.response.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { CardApi } from "../../../models/api/card.api";
|
||||
import { FieldApi } from "../../../models/api/field.api";
|
||||
import { IdentityApi } from "../../../models/api/identity.api";
|
||||
import { LoginApi } from "../../../models/api/login.api";
|
||||
import { SecureNoteApi } from "../../../models/api/secure-note.api";
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
|
||||
import { AttachmentResponse } from "./attachment.response";
|
||||
import { PasswordHistoryResponse } from "./password-history.response";
|
||||
|
||||
export class CipherResponse extends BaseResponse {
|
||||
id: string;
|
||||
organizationId: string;
|
||||
folderId: string;
|
||||
type: number;
|
||||
name: string;
|
||||
notes: string;
|
||||
fields: FieldApi[];
|
||||
login: LoginApi;
|
||||
card: CardApi;
|
||||
identity: IdentityApi;
|
||||
secureNote: SecureNoteApi;
|
||||
favorite: boolean;
|
||||
edit: boolean;
|
||||
viewPassword: boolean;
|
||||
organizationUseTotp: boolean;
|
||||
revisionDate: string;
|
||||
attachments: AttachmentResponse[];
|
||||
passwordHistory: PasswordHistoryResponse[];
|
||||
collectionIds: string[];
|
||||
creationDate: string;
|
||||
deletedDate: string;
|
||||
reprompt: CipherRepromptType;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.organizationId = this.getResponseProperty("OrganizationId");
|
||||
this.folderId = this.getResponseProperty("FolderId") || null;
|
||||
this.type = this.getResponseProperty("Type");
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.notes = this.getResponseProperty("Notes");
|
||||
this.favorite = this.getResponseProperty("Favorite") || false;
|
||||
this.edit = !!this.getResponseProperty("Edit");
|
||||
if (this.getResponseProperty("ViewPassword") == null) {
|
||||
this.viewPassword = true;
|
||||
} else {
|
||||
this.viewPassword = this.getResponseProperty("ViewPassword");
|
||||
}
|
||||
this.organizationUseTotp = this.getResponseProperty("OrganizationUseTotp");
|
||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||
this.collectionIds = this.getResponseProperty("CollectionIds");
|
||||
this.creationDate = this.getResponseProperty("CreationDate");
|
||||
this.deletedDate = this.getResponseProperty("DeletedDate");
|
||||
|
||||
const login = this.getResponseProperty("Login");
|
||||
if (login != null) {
|
||||
this.login = new LoginApi(login);
|
||||
}
|
||||
|
||||
const card = this.getResponseProperty("Card");
|
||||
if (card != null) {
|
||||
this.card = new CardApi(card);
|
||||
}
|
||||
|
||||
const identity = this.getResponseProperty("Identity");
|
||||
if (identity != null) {
|
||||
this.identity = new IdentityApi(identity);
|
||||
}
|
||||
|
||||
const secureNote = this.getResponseProperty("SecureNote");
|
||||
if (secureNote != null) {
|
||||
this.secureNote = new SecureNoteApi(secureNote);
|
||||
}
|
||||
|
||||
const fields = this.getResponseProperty("Fields");
|
||||
if (fields != null) {
|
||||
this.fields = fields.map((f: any) => new FieldApi(f));
|
||||
}
|
||||
|
||||
const attachments = this.getResponseProperty("Attachments");
|
||||
if (attachments != null) {
|
||||
this.attachments = attachments.map((a: any) => new AttachmentResponse(a));
|
||||
}
|
||||
|
||||
const passwordHistory = this.getResponseProperty("PasswordHistory");
|
||||
if (passwordHistory != null) {
|
||||
this.passwordHistory = passwordHistory.map((h: any) => new PasswordHistoryResponse(h));
|
||||
}
|
||||
|
||||
this.reprompt = this.getResponseProperty("Reprompt") || CipherRepromptType.None;
|
||||
}
|
||||
}
|
||||
14
libs/common/src/vault/models/response/folder.response.ts
Normal file
14
libs/common/src/vault/models/response/folder.response.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class FolderResponse extends BaseResponse {
|
||||
id: string;
|
||||
name: string;
|
||||
revisionDate: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.id = this.getResponseProperty("Id");
|
||||
this.name = this.getResponseProperty("Name");
|
||||
this.revisionDate = this.getResponseProperty("RevisionDate");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
|
||||
export class PasswordHistoryResponse extends BaseResponse {
|
||||
password: string;
|
||||
lastUsedDate: string;
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
this.password = this.getResponseProperty("Password");
|
||||
this.lastUsedDate = this.getResponseProperty("LastUsedDate");
|
||||
}
|
||||
}
|
||||
58
libs/common/src/vault/models/response/sync.response.ts
Normal file
58
libs/common/src/vault/models/response/sync.response.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { BaseResponse } from "../../../models/response/base.response";
|
||||
import { CollectionDetailsResponse } from "../../../models/response/collection.response";
|
||||
import { DomainsResponse } from "../../../models/response/domains.response";
|
||||
import { PolicyResponse } from "../../../models/response/policy.response";
|
||||
import { ProfileResponse } from "../../../models/response/profile.response";
|
||||
import { SendResponse } from "../../../models/response/send.response";
|
||||
|
||||
import { CipherResponse } from "./cipher.response";
|
||||
import { FolderResponse } from "./folder.response";
|
||||
|
||||
export class SyncResponse extends BaseResponse {
|
||||
profile?: ProfileResponse;
|
||||
folders: FolderResponse[] = [];
|
||||
collections: CollectionDetailsResponse[] = [];
|
||||
ciphers: CipherResponse[] = [];
|
||||
domains?: DomainsResponse;
|
||||
policies?: PolicyResponse[] = [];
|
||||
sends: SendResponse[] = [];
|
||||
|
||||
constructor(response: any) {
|
||||
super(response);
|
||||
|
||||
const profile = this.getResponseProperty("Profile");
|
||||
if (profile != null) {
|
||||
this.profile = new ProfileResponse(profile);
|
||||
}
|
||||
|
||||
const folders = this.getResponseProperty("Folders");
|
||||
if (folders != null) {
|
||||
this.folders = folders.map((f: any) => new FolderResponse(f));
|
||||
}
|
||||
|
||||
const collections = this.getResponseProperty("Collections");
|
||||
if (collections != null) {
|
||||
this.collections = collections.map((c: any) => new CollectionDetailsResponse(c));
|
||||
}
|
||||
|
||||
const ciphers = this.getResponseProperty("Ciphers");
|
||||
if (ciphers != null) {
|
||||
this.ciphers = ciphers.map((c: any) => new CipherResponse(c));
|
||||
}
|
||||
|
||||
const domains = this.getResponseProperty("Domains");
|
||||
if (domains != null) {
|
||||
this.domains = new DomainsResponse(domains);
|
||||
}
|
||||
|
||||
const policies = this.getResponseProperty("Policies");
|
||||
if (policies != null) {
|
||||
this.policies = policies.map((p: any) => new PolicyResponse(p));
|
||||
}
|
||||
|
||||
const sends = this.getResponseProperty("Sends");
|
||||
if (sends != null) {
|
||||
this.sends = sends.map((s: any) => new SendResponse(s));
|
||||
}
|
||||
}
|
||||
}
|
||||
18
libs/common/src/vault/models/view/attachment.view.spec.ts
Normal file
18
libs/common/src/vault/models/view/attachment.view.spec.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { mockFromJson } from "../../../../spec/utils";
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
|
||||
import { AttachmentView } from "./attachment.view";
|
||||
|
||||
jest.mock("../../../models/domain/symmetric-crypto-key");
|
||||
|
||||
describe("AttachmentView", () => {
|
||||
it("fromJSON initializes nested objects", () => {
|
||||
jest.spyOn(SymmetricCryptoKey, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const actual = AttachmentView.fromJSON({
|
||||
key: "encKeyB64" as any,
|
||||
});
|
||||
|
||||
expect(actual.key).toEqual("encKeyB64_fromJSON");
|
||||
});
|
||||
});
|
||||
41
libs/common/src/vault/models/view/attachment.view.ts
Normal file
41
libs/common/src/vault/models/view/attachment.view.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SymmetricCryptoKey } from "../../../models/domain/symmetric-crypto-key";
|
||||
import { View } from "../../../models/view/view";
|
||||
import { Attachment } from "../domain/attachment";
|
||||
|
||||
export class AttachmentView implements View {
|
||||
id: string = null;
|
||||
url: string = null;
|
||||
size: string = null;
|
||||
sizeName: string = null;
|
||||
fileName: string = null;
|
||||
key: SymmetricCryptoKey = null;
|
||||
|
||||
constructor(a?: Attachment) {
|
||||
if (!a) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = a.id;
|
||||
this.url = a.url;
|
||||
this.size = a.size;
|
||||
this.sizeName = a.sizeName;
|
||||
}
|
||||
|
||||
get fileSize(): number {
|
||||
try {
|
||||
if (this.size != null) {
|
||||
return parseInt(this.size, null);
|
||||
}
|
||||
} catch {
|
||||
// Invalid file size.
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<AttachmentView>>): AttachmentView {
|
||||
const key = obj.key == null ? null : SymmetricCryptoKey.fromJSON(obj.key);
|
||||
return Object.assign(new AttachmentView(), obj, { key: key });
|
||||
}
|
||||
}
|
||||
84
libs/common/src/vault/models/view/card.view.ts
Normal file
84
libs/common/src/vault/models/view/card.view.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { CardLinkedId as LinkedId } from "../../../enums/linkedIdType";
|
||||
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
|
||||
|
||||
import { ItemView } from "./item.view";
|
||||
|
||||
export class CardView extends ItemView {
|
||||
@linkedFieldOption(LinkedId.CardholderName)
|
||||
cardholderName: string = null;
|
||||
@linkedFieldOption(LinkedId.ExpMonth, "expirationMonth")
|
||||
expMonth: string = null;
|
||||
@linkedFieldOption(LinkedId.ExpYear, "expirationYear")
|
||||
expYear: string = null;
|
||||
@linkedFieldOption(LinkedId.Code, "securityCode")
|
||||
code: string = null;
|
||||
|
||||
private _brand: string = null;
|
||||
private _number: string = null;
|
||||
private _subTitle: string = null;
|
||||
|
||||
get maskedCode(): string {
|
||||
return this.code != null ? "•".repeat(this.code.length) : null;
|
||||
}
|
||||
|
||||
get maskedNumber(): string {
|
||||
return this.number != null ? "•".repeat(this.number.length) : null;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.Brand)
|
||||
get brand(): string {
|
||||
return this._brand;
|
||||
}
|
||||
set brand(value: string) {
|
||||
this._brand = value;
|
||||
this._subTitle = null;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.Number)
|
||||
get number(): string {
|
||||
return this._number;
|
||||
}
|
||||
set number(value: string) {
|
||||
this._number = value;
|
||||
this._subTitle = null;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
if (this._subTitle == null) {
|
||||
this._subTitle = this.brand;
|
||||
if (this.number != null && this.number.length >= 4) {
|
||||
if (this._subTitle != null && this._subTitle !== "") {
|
||||
this._subTitle += ", ";
|
||||
} else {
|
||||
this._subTitle = "";
|
||||
}
|
||||
|
||||
// Show last 5 on amex, last 4 for all others
|
||||
const count =
|
||||
this.number.length >= 5 && this.number.match(new RegExp("^3[47]")) != null ? 5 : 4;
|
||||
this._subTitle += "*" + this.number.substr(this.number.length - count);
|
||||
}
|
||||
}
|
||||
return this._subTitle;
|
||||
}
|
||||
|
||||
get expiration(): string {
|
||||
if (!this.expMonth && !this.expYear) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let exp = this.expMonth != null ? ("0" + this.expMonth).slice(-2) : "__";
|
||||
exp += " / " + (this.expYear != null ? this.formatYear(this.expYear) : "____");
|
||||
return exp;
|
||||
}
|
||||
|
||||
private formatYear(year: string): string {
|
||||
return year.length === 2 ? "20" + year : year;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<CardView>>): CardView {
|
||||
return Object.assign(new CardView(), obj);
|
||||
}
|
||||
}
|
||||
76
libs/common/src/vault/models/view/cipher.view.spec.ts
Normal file
76
libs/common/src/vault/models/view/cipher.view.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { mockFromJson } from "../../../../spec/utils";
|
||||
import { CipherType } from "../../enums/cipher-type";
|
||||
|
||||
import { AttachmentView } from "./attachment.view";
|
||||
import { CardView } from "./card.view";
|
||||
import { CipherView } from "./cipher.view";
|
||||
import { FieldView } from "./field.view";
|
||||
import { IdentityView } from "./identity.view";
|
||||
import { LoginView } from "./login.view";
|
||||
import { PasswordHistoryView } from "./password-history.view";
|
||||
import { SecureNoteView } from "./secure-note.view";
|
||||
|
||||
jest.mock("../../models/view/login.view");
|
||||
jest.mock("../../models/view/attachment.view");
|
||||
jest.mock("../../models/view/field.view");
|
||||
jest.mock("../../models/view/password-history.view");
|
||||
|
||||
describe("CipherView", () => {
|
||||
beforeEach(() => {
|
||||
(LoginView as any).mockClear();
|
||||
(AttachmentView as any).mockClear();
|
||||
(FieldView as any).mockClear();
|
||||
(PasswordHistoryView as any).mockClear();
|
||||
});
|
||||
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
jest.spyOn(AttachmentView, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(FieldView, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(PasswordHistoryView, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
|
||||
const deletedDate = new Date("2022-09-04T01:06:40.441Z");
|
||||
const actual = CipherView.fromJSON({
|
||||
revisionDate: revisionDate.toISOString(),
|
||||
deletedDate: deletedDate.toISOString(),
|
||||
attachments: ["attachment1", "attachment2"] as any,
|
||||
fields: ["field1", "field2"] as any,
|
||||
passwordHistory: ["ph1", "ph2", "ph3"] as any,
|
||||
});
|
||||
|
||||
const expected = {
|
||||
revisionDate: revisionDate,
|
||||
deletedDate: deletedDate,
|
||||
attachments: ["attachment1_fromJSON", "attachment2_fromJSON"],
|
||||
fields: ["field1_fromJSON", "field2_fromJSON"],
|
||||
passwordHistory: ["ph1_fromJSON", "ph2_fromJSON", "ph3_fromJSON"],
|
||||
};
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
|
||||
test.each([
|
||||
// Test description, CipherType, expected output
|
||||
["LoginView", CipherType.Login, { login: "myLogin_fromJSON" }],
|
||||
["CardView", CipherType.Card, { card: "myCard_fromJSON" }],
|
||||
["IdentityView", CipherType.Identity, { identity: "myIdentity_fromJSON" }],
|
||||
["Secure Note", CipherType.SecureNote, { secureNote: "mySecureNote_fromJSON" }],
|
||||
])("initializes %s", (description: string, cipherType: CipherType, expected: any) => {
|
||||
jest.spyOn(LoginView, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(IdentityView, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(CardView, "fromJSON").mockImplementation(mockFromJson);
|
||||
jest.spyOn(SecureNoteView, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const actual = CipherView.fromJSON({
|
||||
login: "myLogin",
|
||||
card: "myCard",
|
||||
identity: "myIdentity",
|
||||
secureNote: "mySecureNote",
|
||||
type: cipherType,
|
||||
} as any);
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
179
libs/common/src/vault/models/view/cipher.view.ts
Normal file
179
libs/common/src/vault/models/view/cipher.view.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { LinkedIdType } from "../../../enums/linkedIdType";
|
||||
import { InitializerMetadata } from "../../../interfaces/initializer-metadata.interface";
|
||||
import { View } from "../../../models/view/view";
|
||||
import { InitializerKey } from "../../../services/cryptography/initializer-key";
|
||||
import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
|
||||
import { CipherType } from "../../enums/cipher-type";
|
||||
import { LocalData } from "../data/local.data";
|
||||
import { Cipher } from "../domain/cipher";
|
||||
|
||||
import { AttachmentView } from "./attachment.view";
|
||||
import { CardView } from "./card.view";
|
||||
import { FieldView } from "./field.view";
|
||||
import { IdentityView } from "./identity.view";
|
||||
import { LoginView } from "./login.view";
|
||||
import { PasswordHistoryView } from "./password-history.view";
|
||||
import { SecureNoteView } from "./secure-note.view";
|
||||
|
||||
export class CipherView implements View, InitializerMetadata {
|
||||
readonly initializerKey = InitializerKey.CipherView;
|
||||
|
||||
id: string = null;
|
||||
organizationId: string = null;
|
||||
folderId: string = null;
|
||||
name: string = null;
|
||||
notes: string = null;
|
||||
type: CipherType = null;
|
||||
favorite = false;
|
||||
organizationUseTotp = false;
|
||||
edit = false;
|
||||
viewPassword = true;
|
||||
localData: LocalData;
|
||||
login = new LoginView();
|
||||
identity = new IdentityView();
|
||||
card = new CardView();
|
||||
secureNote = new SecureNoteView();
|
||||
attachments: AttachmentView[] = null;
|
||||
fields: FieldView[] = null;
|
||||
passwordHistory: PasswordHistoryView[] = null;
|
||||
collectionIds: string[] = null;
|
||||
revisionDate: Date = null;
|
||||
creationDate: Date = null;
|
||||
deletedDate: Date = null;
|
||||
reprompt: CipherRepromptType = CipherRepromptType.None;
|
||||
|
||||
constructor(c?: Cipher) {
|
||||
if (!c) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = c.id;
|
||||
this.organizationId = c.organizationId;
|
||||
this.folderId = c.folderId;
|
||||
this.favorite = c.favorite;
|
||||
this.organizationUseTotp = c.organizationUseTotp;
|
||||
this.edit = c.edit;
|
||||
this.viewPassword = c.viewPassword;
|
||||
this.type = c.type;
|
||||
this.localData = c.localData;
|
||||
this.collectionIds = c.collectionIds;
|
||||
this.revisionDate = c.revisionDate;
|
||||
this.creationDate = c.creationDate;
|
||||
this.deletedDate = c.deletedDate;
|
||||
// Old locally stored ciphers might have reprompt == null. If so set it to None.
|
||||
this.reprompt = c.reprompt ?? CipherRepromptType.None;
|
||||
}
|
||||
|
||||
private get item() {
|
||||
switch (this.type) {
|
||||
case CipherType.Login:
|
||||
return this.login;
|
||||
case CipherType.SecureNote:
|
||||
return this.secureNote;
|
||||
case CipherType.Card:
|
||||
return this.card;
|
||||
case CipherType.Identity:
|
||||
return this.identity;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
return this.item.subTitle;
|
||||
}
|
||||
|
||||
get hasPasswordHistory(): boolean {
|
||||
return this.passwordHistory && this.passwordHistory.length > 0;
|
||||
}
|
||||
|
||||
get hasAttachments(): boolean {
|
||||
return this.attachments && this.attachments.length > 0;
|
||||
}
|
||||
|
||||
get hasOldAttachments(): boolean {
|
||||
if (this.hasAttachments) {
|
||||
for (let i = 0; i < this.attachments.length; i++) {
|
||||
if (this.attachments[i].key == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
get hasFields(): boolean {
|
||||
return this.fields && this.fields.length > 0;
|
||||
}
|
||||
|
||||
get passwordRevisionDisplayDate(): Date {
|
||||
if (this.type !== CipherType.Login || this.login == null) {
|
||||
return null;
|
||||
} else if (this.login.password == null || this.login.password === "") {
|
||||
return null;
|
||||
}
|
||||
return this.login.passwordRevisionDate;
|
||||
}
|
||||
|
||||
get isDeleted(): boolean {
|
||||
return this.deletedDate != null;
|
||||
}
|
||||
|
||||
get linkedFieldOptions() {
|
||||
return this.item.linkedFieldOptions;
|
||||
}
|
||||
|
||||
linkedFieldValue(id: LinkedIdType) {
|
||||
const linkedFieldOption = this.linkedFieldOptions?.get(id);
|
||||
if (linkedFieldOption == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = this.item;
|
||||
return this.item[linkedFieldOption.propertyKey as keyof typeof item];
|
||||
}
|
||||
|
||||
linkedFieldI18nKey(id: LinkedIdType): string {
|
||||
return this.linkedFieldOptions.get(id)?.i18nKey;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<CipherView>>): CipherView {
|
||||
const view = new CipherView();
|
||||
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||
const deletedDate = obj.deletedDate == null ? null : new Date(obj.deletedDate);
|
||||
const attachments = obj.attachments?.map((a: any) => AttachmentView.fromJSON(a));
|
||||
const fields = obj.fields?.map((f: any) => FieldView.fromJSON(f));
|
||||
const passwordHistory = obj.passwordHistory?.map((ph: any) => PasswordHistoryView.fromJSON(ph));
|
||||
|
||||
Object.assign(view, obj, {
|
||||
revisionDate: revisionDate,
|
||||
deletedDate: deletedDate,
|
||||
attachments: attachments,
|
||||
fields: fields,
|
||||
passwordHistory: passwordHistory,
|
||||
});
|
||||
|
||||
switch (obj.type) {
|
||||
case CipherType.Card:
|
||||
view.card = CardView.fromJSON(obj.card);
|
||||
break;
|
||||
case CipherType.Identity:
|
||||
view.identity = IdentityView.fromJSON(obj.identity);
|
||||
break;
|
||||
case CipherType.Login:
|
||||
view.login = LoginView.fromJSON(obj.login);
|
||||
break;
|
||||
case CipherType.SecureNote:
|
||||
view.secureNote = SecureNoteView.fromJSON(obj.secureNote);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
}
|
||||
33
libs/common/src/vault/models/view/field.view.ts
Normal file
33
libs/common/src/vault/models/view/field.view.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { FieldType } from "../../../enums/fieldType";
|
||||
import { LinkedIdType } from "../../../enums/linkedIdType";
|
||||
import { View } from "../../../models/view/view";
|
||||
import { Field } from "../domain/field";
|
||||
|
||||
export class FieldView implements View {
|
||||
name: string = null;
|
||||
value: string = null;
|
||||
type: FieldType = null;
|
||||
newField = false; // Marks if the field is new and hasn't been saved
|
||||
showValue = false;
|
||||
showCount = false;
|
||||
linkedId: LinkedIdType = null;
|
||||
|
||||
constructor(f?: Field) {
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.type = f.type;
|
||||
this.linkedId = f.linkedId;
|
||||
}
|
||||
|
||||
get maskedValue(): string {
|
||||
return this.value != null ? "••••••••" : null;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<FieldView>>): FieldView {
|
||||
return Object.assign(new FieldView(), obj);
|
||||
}
|
||||
}
|
||||
22
libs/common/src/vault/models/view/folder.view.spec.ts
Normal file
22
libs/common/src/vault/models/view/folder.view.spec.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { FolderView } from "./folder.view";
|
||||
|
||||
describe("FolderView", () => {
|
||||
describe("fromJSON", () => {
|
||||
it("initializes nested objects", () => {
|
||||
const revisionDate = new Date("2022-08-04T01:06:40.441Z");
|
||||
const actual = FolderView.fromJSON({
|
||||
revisionDate: revisionDate.toISOString(),
|
||||
name: "name",
|
||||
id: "id",
|
||||
});
|
||||
|
||||
const expected = {
|
||||
revisionDate: revisionDate,
|
||||
name: "name",
|
||||
id: "id",
|
||||
};
|
||||
|
||||
expect(actual).toMatchObject(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
25
libs/common/src/vault/models/view/folder.view.ts
Normal file
25
libs/common/src/vault/models/view/folder.view.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { ITreeNodeObject } from "../../../models/domain/tree-node";
|
||||
import { View } from "../../../models/view/view";
|
||||
import { Folder } from "../domain/folder";
|
||||
|
||||
export class FolderView implements View, ITreeNodeObject {
|
||||
id: string = null;
|
||||
name: string = null;
|
||||
revisionDate: Date = null;
|
||||
|
||||
constructor(f?: Folder) {
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.id = f.id;
|
||||
this.revisionDate = f.revisionDate;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Jsonify<FolderView>) {
|
||||
const revisionDate = obj.revisionDate == null ? null : new Date(obj.revisionDate);
|
||||
return Object.assign(new FolderView(), obj, { revisionDate });
|
||||
}
|
||||
}
|
||||
148
libs/common/src/vault/models/view/identity.view.ts
Normal file
148
libs/common/src/vault/models/view/identity.view.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { IdentityLinkedId as LinkedId } from "../../../enums/linkedIdType";
|
||||
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
|
||||
import { Utils } from "../../../misc/utils";
|
||||
|
||||
import { ItemView } from "./item.view";
|
||||
|
||||
export class IdentityView extends ItemView {
|
||||
@linkedFieldOption(LinkedId.Title)
|
||||
title: string = null;
|
||||
@linkedFieldOption(LinkedId.MiddleName)
|
||||
middleName: string = null;
|
||||
@linkedFieldOption(LinkedId.Address1)
|
||||
address1: string = null;
|
||||
@linkedFieldOption(LinkedId.Address2)
|
||||
address2: string = null;
|
||||
@linkedFieldOption(LinkedId.Address3)
|
||||
address3: string = null;
|
||||
@linkedFieldOption(LinkedId.City, "cityTown")
|
||||
city: string = null;
|
||||
@linkedFieldOption(LinkedId.State, "stateProvince")
|
||||
state: string = null;
|
||||
@linkedFieldOption(LinkedId.PostalCode, "zipPostalCode")
|
||||
postalCode: string = null;
|
||||
@linkedFieldOption(LinkedId.Country)
|
||||
country: string = null;
|
||||
@linkedFieldOption(LinkedId.Company)
|
||||
company: string = null;
|
||||
@linkedFieldOption(LinkedId.Email)
|
||||
email: string = null;
|
||||
@linkedFieldOption(LinkedId.Phone)
|
||||
phone: string = null;
|
||||
@linkedFieldOption(LinkedId.Ssn)
|
||||
ssn: string = null;
|
||||
@linkedFieldOption(LinkedId.Username)
|
||||
username: string = null;
|
||||
@linkedFieldOption(LinkedId.PassportNumber)
|
||||
passportNumber: string = null;
|
||||
@linkedFieldOption(LinkedId.LicenseNumber)
|
||||
licenseNumber: string = null;
|
||||
|
||||
private _firstName: string = null;
|
||||
private _lastName: string = null;
|
||||
private _subTitle: string = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.FirstName)
|
||||
get firstName(): string {
|
||||
return this._firstName;
|
||||
}
|
||||
set firstName(value: string) {
|
||||
this._firstName = value;
|
||||
this._subTitle = null;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.LastName)
|
||||
get lastName(): string {
|
||||
return this._lastName;
|
||||
}
|
||||
set lastName(value: string) {
|
||||
this._lastName = value;
|
||||
this._subTitle = null;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
if (this._subTitle == null && (this.firstName != null || this.lastName != null)) {
|
||||
this._subTitle = "";
|
||||
if (this.firstName != null) {
|
||||
this._subTitle = this.firstName;
|
||||
}
|
||||
if (this.lastName != null) {
|
||||
if (this._subTitle !== "") {
|
||||
this._subTitle += " ";
|
||||
}
|
||||
this._subTitle += this.lastName;
|
||||
}
|
||||
}
|
||||
|
||||
return this._subTitle;
|
||||
}
|
||||
|
||||
@linkedFieldOption(LinkedId.FullName)
|
||||
get fullName(): string {
|
||||
if (
|
||||
this.title != null ||
|
||||
this.firstName != null ||
|
||||
this.middleName != null ||
|
||||
this.lastName != null
|
||||
) {
|
||||
let name = "";
|
||||
if (this.title != null) {
|
||||
name += this.title + " ";
|
||||
}
|
||||
if (this.firstName != null) {
|
||||
name += this.firstName + " ";
|
||||
}
|
||||
if (this.middleName != null) {
|
||||
name += this.middleName + " ";
|
||||
}
|
||||
if (this.lastName != null) {
|
||||
name += this.lastName;
|
||||
}
|
||||
return name.trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
get fullAddress(): string {
|
||||
let address = this.address1;
|
||||
if (!Utils.isNullOrWhitespace(this.address2)) {
|
||||
if (!Utils.isNullOrWhitespace(address)) {
|
||||
address += ", ";
|
||||
}
|
||||
address += this.address2;
|
||||
}
|
||||
if (!Utils.isNullOrWhitespace(this.address3)) {
|
||||
if (!Utils.isNullOrWhitespace(address)) {
|
||||
address += ", ";
|
||||
}
|
||||
address += this.address3;
|
||||
}
|
||||
return address;
|
||||
}
|
||||
|
||||
get fullAddressPart2(): string {
|
||||
if (this.city == null && this.state == null && this.postalCode == null) {
|
||||
return null;
|
||||
}
|
||||
const city = this.city || "-";
|
||||
const state = this.state;
|
||||
const postalCode = this.postalCode || "-";
|
||||
let addressPart2 = city;
|
||||
if (!Utils.isNullOrWhitespace(state)) {
|
||||
addressPart2 += ", " + state;
|
||||
}
|
||||
addressPart2 += ", " + postalCode;
|
||||
return addressPart2;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<IdentityView>>): IdentityView {
|
||||
return Object.assign(new IdentityView(), obj);
|
||||
}
|
||||
}
|
||||
7
libs/common/src/vault/models/view/item.view.ts
Normal file
7
libs/common/src/vault/models/view/item.view.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { LinkedMetadata } from "../../../misc/linkedFieldOption.decorator";
|
||||
import { View } from "../../../models/view/view";
|
||||
|
||||
export abstract class ItemView implements View {
|
||||
linkedFieldOptions: Map<number, LinkedMetadata>;
|
||||
abstract get subTitle(): string;
|
||||
}
|
||||
66
libs/common/src/vault/models/view/login-uri-view.spec.ts
Normal file
66
libs/common/src/vault/models/view/login-uri-view.spec.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { UriMatchType } from "../../../enums/uriMatchType";
|
||||
|
||||
import { LoginUriView } from "./login-uri.view";
|
||||
|
||||
const testData = [
|
||||
{
|
||||
match: UriMatchType.Host,
|
||||
uri: "http://example.com/login",
|
||||
expected: "http://example.com/login",
|
||||
},
|
||||
{
|
||||
match: UriMatchType.Host,
|
||||
uri: "bitwarden.com",
|
||||
expected: "http://bitwarden.com",
|
||||
},
|
||||
{
|
||||
match: UriMatchType.Host,
|
||||
uri: "bitwarden.de",
|
||||
expected: "http://bitwarden.de",
|
||||
},
|
||||
{
|
||||
match: UriMatchType.Host,
|
||||
uri: "bitwarden.br",
|
||||
expected: "http://bitwarden.br",
|
||||
},
|
||||
];
|
||||
|
||||
describe("LoginUriView", () => {
|
||||
it("isWebsite() given an invalid domain should return false", async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: UriMatchType.Host, uri: "bit!:_&ward.com" });
|
||||
expect(uri.isWebsite).toBe(false);
|
||||
});
|
||||
|
||||
testData.forEach((data) => {
|
||||
it(`isWebsite() given ${data.uri} should return true`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: data.match, uri: data.uri });
|
||||
expect(uri.isWebsite).toBe(true);
|
||||
});
|
||||
|
||||
it(`launchUri() given ${data.uri} should return ${data.expected}`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: data.match, uri: data.uri });
|
||||
expect(uri.launchUri).toBe(data.expected);
|
||||
});
|
||||
|
||||
it(`canLaunch() given ${data.uri} should return true`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: data.match, uri: data.uri });
|
||||
expect(uri.canLaunch).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it(`canLaunch should return false when MatchDetection is set to Regex`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: UriMatchType.RegularExpression, uri: "bitwarden.com" });
|
||||
expect(uri.canLaunch).toBe(false);
|
||||
});
|
||||
|
||||
it(`canLaunch() should return false when the given protocol does not match CanLaunchWhiteList`, async () => {
|
||||
const uri = new LoginUriView();
|
||||
Object.assign(uri, { match: UriMatchType.Host, uri: "someprotocol://bitwarden.com" });
|
||||
expect(uri.canLaunch).toBe(false);
|
||||
});
|
||||
});
|
||||
132
libs/common/src/vault/models/view/login-uri.view.ts
Normal file
132
libs/common/src/vault/models/view/login-uri.view.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { UriMatchType } from "../../../enums/uriMatchType";
|
||||
import { Utils } from "../../../misc/utils";
|
||||
import { View } from "../../../models/view/view";
|
||||
import { LoginUri } from "../domain/login-uri";
|
||||
|
||||
const CanLaunchWhitelist = [
|
||||
"https://",
|
||||
"http://",
|
||||
"ssh://",
|
||||
"ftp://",
|
||||
"sftp://",
|
||||
"irc://",
|
||||
"vnc://",
|
||||
// https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/clients/remote-desktop-uri
|
||||
"rdp://", // Legacy RDP URI scheme
|
||||
"ms-rd:", // Preferred RDP URI scheme
|
||||
"chrome://",
|
||||
"iosapp://",
|
||||
"androidapp://",
|
||||
];
|
||||
|
||||
export class LoginUriView implements View {
|
||||
match: UriMatchType = null;
|
||||
|
||||
private _uri: string = null;
|
||||
private _domain: string = null;
|
||||
private _hostname: string = null;
|
||||
private _host: string = null;
|
||||
private _canLaunch: boolean = null;
|
||||
|
||||
constructor(u?: LoginUri) {
|
||||
if (!u) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.match = u.match;
|
||||
}
|
||||
|
||||
get uri(): string {
|
||||
return this._uri;
|
||||
}
|
||||
set uri(value: string) {
|
||||
this._uri = value;
|
||||
this._domain = null;
|
||||
this._canLaunch = null;
|
||||
}
|
||||
|
||||
get domain(): string {
|
||||
if (this._domain == null && this.uri != null) {
|
||||
this._domain = Utils.getDomain(this.uri);
|
||||
if (this._domain === "") {
|
||||
this._domain = null;
|
||||
}
|
||||
}
|
||||
|
||||
return this._domain;
|
||||
}
|
||||
|
||||
get hostname(): string {
|
||||
if (this.match === UriMatchType.RegularExpression) {
|
||||
return null;
|
||||
}
|
||||
if (this._hostname == null && this.uri != null) {
|
||||
this._hostname = Utils.getHostname(this.uri);
|
||||
if (this._hostname === "") {
|
||||
this._hostname = null;
|
||||
}
|
||||
}
|
||||
|
||||
return this._hostname;
|
||||
}
|
||||
|
||||
get host(): string {
|
||||
if (this.match === UriMatchType.RegularExpression) {
|
||||
return null;
|
||||
}
|
||||
if (this._host == null && this.uri != null) {
|
||||
this._host = Utils.getHost(this.uri);
|
||||
if (this._host === "") {
|
||||
this._host = null;
|
||||
}
|
||||
}
|
||||
|
||||
return this._host;
|
||||
}
|
||||
|
||||
get hostnameOrUri(): string {
|
||||
return this.hostname != null ? this.hostname : this.uri;
|
||||
}
|
||||
|
||||
get hostOrUri(): string {
|
||||
return this.host != null ? this.host : this.uri;
|
||||
}
|
||||
|
||||
get isWebsite(): boolean {
|
||||
return (
|
||||
this.uri != null &&
|
||||
(this.uri.indexOf("http://") === 0 ||
|
||||
this.uri.indexOf("https://") === 0 ||
|
||||
(this.uri.indexOf("://") < 0 && !Utils.isNullOrWhitespace(Utils.getDomain(this.uri))))
|
||||
);
|
||||
}
|
||||
|
||||
get canLaunch(): boolean {
|
||||
if (this._canLaunch != null) {
|
||||
return this._canLaunch;
|
||||
}
|
||||
if (this.uri != null && this.match !== UriMatchType.RegularExpression) {
|
||||
const uri = this.launchUri;
|
||||
for (let i = 0; i < CanLaunchWhitelist.length; i++) {
|
||||
if (uri.indexOf(CanLaunchWhitelist[i]) === 0) {
|
||||
this._canLaunch = true;
|
||||
return this._canLaunch;
|
||||
}
|
||||
}
|
||||
}
|
||||
this._canLaunch = false;
|
||||
return this._canLaunch;
|
||||
}
|
||||
|
||||
get launchUri(): string {
|
||||
return this.uri.indexOf("://") < 0 && !Utils.isNullOrWhitespace(Utils.getDomain(this.uri))
|
||||
? "http://" + this.uri
|
||||
: this.uri;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<LoginUriView>>): LoginUriView {
|
||||
return Object.assign(new LoginUriView(), obj);
|
||||
}
|
||||
}
|
||||
28
libs/common/src/vault/models/view/login.view.spec.ts
Normal file
28
libs/common/src/vault/models/view/login.view.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { mockFromJson } from "../../../../spec/utils";
|
||||
|
||||
import { LoginUriView } from "./login-uri.view";
|
||||
import { LoginView } from "./login.view";
|
||||
|
||||
jest.mock("../../models/view/login-uri.view");
|
||||
|
||||
describe("LoginView", () => {
|
||||
beforeEach(() => {
|
||||
(LoginUriView as any).mockClear();
|
||||
});
|
||||
|
||||
it("fromJSON initializes nested objects", () => {
|
||||
jest.spyOn(LoginUriView, "fromJSON").mockImplementation(mockFromJson);
|
||||
|
||||
const passwordRevisionDate = new Date();
|
||||
|
||||
const actual = LoginView.fromJSON({
|
||||
passwordRevisionDate: passwordRevisionDate.toISOString(),
|
||||
uris: ["uri1", "uri2", "uri3"] as any,
|
||||
});
|
||||
|
||||
expect(actual).toMatchObject({
|
||||
passwordRevisionDate: passwordRevisionDate,
|
||||
uris: ["uri1_fromJSON", "uri2_fromJSON", "uri3_fromJSON"],
|
||||
});
|
||||
});
|
||||
});
|
||||
76
libs/common/src/vault/models/view/login.view.ts
Normal file
76
libs/common/src/vault/models/view/login.view.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { LoginLinkedId as LinkedId } from "../../../enums/linkedIdType";
|
||||
import { linkedFieldOption } from "../../../misc/linkedFieldOption.decorator";
|
||||
import { Utils } from "../../../misc/utils";
|
||||
import { Login } from "../domain/login";
|
||||
|
||||
import { ItemView } from "./item.view";
|
||||
import { LoginUriView } from "./login-uri.view";
|
||||
|
||||
export class LoginView extends ItemView {
|
||||
@linkedFieldOption(LinkedId.Username)
|
||||
username: string = null;
|
||||
@linkedFieldOption(LinkedId.Password)
|
||||
password: string = null;
|
||||
|
||||
passwordRevisionDate?: Date = null;
|
||||
totp: string = null;
|
||||
uris: LoginUriView[] = null;
|
||||
autofillOnPageLoad: boolean = null;
|
||||
|
||||
constructor(l?: Login) {
|
||||
super();
|
||||
if (!l) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.passwordRevisionDate = l.passwordRevisionDate;
|
||||
this.autofillOnPageLoad = l.autofillOnPageLoad;
|
||||
}
|
||||
|
||||
get uri(): string {
|
||||
return this.hasUris ? this.uris[0].uri : null;
|
||||
}
|
||||
|
||||
get maskedPassword(): string {
|
||||
return this.password != null ? "••••••••" : null;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
return this.username;
|
||||
}
|
||||
|
||||
get canLaunch(): boolean {
|
||||
return this.hasUris && this.uris.some((u) => u.canLaunch);
|
||||
}
|
||||
|
||||
get hasTotp(): boolean {
|
||||
return !Utils.isNullOrWhitespace(this.totp);
|
||||
}
|
||||
|
||||
get launchUri(): string {
|
||||
if (this.hasUris) {
|
||||
const uri = this.uris.find((u) => u.canLaunch);
|
||||
if (uri != null) {
|
||||
return uri.launchUri;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
get hasUris(): boolean {
|
||||
return this.uris != null && this.uris.length > 0;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<LoginView>>): LoginView {
|
||||
const passwordRevisionDate =
|
||||
obj.passwordRevisionDate == null ? null : new Date(obj.passwordRevisionDate);
|
||||
const uris = obj.uris?.map((uri: any) => LoginUriView.fromJSON(uri));
|
||||
|
||||
return Object.assign(new LoginView(), obj, {
|
||||
passwordRevisionDate: passwordRevisionDate,
|
||||
uris: uris,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PasswordHistoryView } from "./password-history.view";
|
||||
|
||||
describe("PasswordHistoryView", () => {
|
||||
it("fromJSON initializes nested objects", () => {
|
||||
const lastUsedDate = new Date();
|
||||
|
||||
const actual = PasswordHistoryView.fromJSON({
|
||||
lastUsedDate: lastUsedDate.toISOString(),
|
||||
});
|
||||
|
||||
expect(actual.lastUsedDate).toEqual(lastUsedDate);
|
||||
});
|
||||
});
|
||||
25
libs/common/src/vault/models/view/password-history.view.ts
Normal file
25
libs/common/src/vault/models/view/password-history.view.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { View } from "../../../models/view/view";
|
||||
import { Password } from "../domain/password";
|
||||
|
||||
export class PasswordHistoryView implements View {
|
||||
password: string = null;
|
||||
lastUsedDate: Date = null;
|
||||
|
||||
constructor(ph?: Password) {
|
||||
if (!ph) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.lastUsedDate = ph.lastUsedDate;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<PasswordHistoryView>>): PasswordHistoryView {
|
||||
const lastUsedDate = obj.lastUsedDate == null ? null : new Date(obj.lastUsedDate);
|
||||
|
||||
return Object.assign(new PasswordHistoryView(), obj, {
|
||||
lastUsedDate: lastUsedDate,
|
||||
});
|
||||
}
|
||||
}
|
||||
27
libs/common/src/vault/models/view/secure-note.view.ts
Normal file
27
libs/common/src/vault/models/view/secure-note.view.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Jsonify } from "type-fest";
|
||||
|
||||
import { SecureNoteType } from "../../../enums/secureNoteType";
|
||||
import { SecureNote } from "../domain/secure-note";
|
||||
|
||||
import { ItemView } from "./item.view";
|
||||
|
||||
export class SecureNoteView extends ItemView {
|
||||
type: SecureNoteType = null;
|
||||
|
||||
constructor(n?: SecureNote) {
|
||||
super();
|
||||
if (!n) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.type = n.type;
|
||||
}
|
||||
|
||||
get subTitle(): string {
|
||||
return null;
|
||||
}
|
||||
|
||||
static fromJSON(obj: Partial<Jsonify<SecureNoteView>>): SecureNoteView {
|
||||
return Object.assign(new SecureNoteView(), obj);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user