Use classes for vault items and fields to have better control over serialization
This commit is contained in:
parent
1294228918
commit
7bb7839b97
|
@ -1,5 +1,5 @@
|
|||
import { styleMap } from "lit-html/directives/style-map";
|
||||
import { VaultItem, Field, transformedValue } from "@padloc/core/src/item";
|
||||
import { VaultItem, Field } from "@padloc/core/src/item";
|
||||
import { setClipboard } from "@padloc/core/src/platform";
|
||||
import { shared, mixins } from "../styles";
|
||||
import { BaseElement, element, html, css, property } from "./base";
|
||||
|
@ -161,7 +161,7 @@ export class Clipboard extends BaseElement {
|
|||
this.item = item;
|
||||
this.field = field;
|
||||
|
||||
const value = await transformedValue(field);
|
||||
const value = await field.transform();
|
||||
setClipboard(value);
|
||||
|
||||
const tStart = Date.now();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Vault } from "@padloc/core/src/vault";
|
||||
import { VaultItem, ItemTemplate, ITEM_TEMPLATES } from "@padloc/core/src/item";
|
||||
import { VaultItem, Field, ItemTemplate, ITEM_TEMPLATES } from "@padloc/core/src/item";
|
||||
import { translate as $l } from "@padloc/locale/src/translate";
|
||||
import { app, router } from "../globals";
|
||||
import { alert } from "../lib/dialog";
|
||||
|
@ -139,7 +139,7 @@ export class CreateItemDialog extends Dialog<Vault, VaultItem> {
|
|||
const item = await app.createItem(
|
||||
"",
|
||||
vault,
|
||||
this._template.fields.map(f => ({ ...f, value: "" }))
|
||||
this._template.fields.map(f => new Field({ ...f, value: "" }))
|
||||
);
|
||||
this.done(item);
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export class FieldElement extends BaseElement {
|
|||
value: string = "";
|
||||
|
||||
@property()
|
||||
type: FieldType = "note";
|
||||
type: FieldType = FieldType.Note;
|
||||
|
||||
@property()
|
||||
private _masked: boolean = false;
|
||||
|
|
|
@ -469,18 +469,18 @@ export class ItemDialog extends Dialog<string, void> {
|
|||
|
||||
private _getFields() {
|
||||
return [...this._fieldInputs].map((fieldEl: FieldElement) => {
|
||||
return {
|
||||
return new Field({
|
||||
name: fieldEl.name,
|
||||
value: fieldEl.value,
|
||||
type: fieldEl.type
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _itemChanged() {
|
||||
const item = this._item!;
|
||||
this._nameInput.value = item.name;
|
||||
this._fields = item.fields.map(f => ({ ...f }));
|
||||
this._fields = item.fields.map(f => new Field({ ...f }));
|
||||
this._tagsInput.tags = [...item.tags];
|
||||
}
|
||||
|
||||
|
@ -510,7 +510,7 @@ export class ItemDialog extends Dialog<string, void> {
|
|||
return;
|
||||
}
|
||||
|
||||
this._fields.push({ name: fieldDef.name, value: "", type: fieldDef.type });
|
||||
this._fields.push(new Field({ name: fieldDef.name, value: "", type: fieldDef.type }));
|
||||
this.requestUpdate();
|
||||
await this.updateComplete;
|
||||
setTimeout(() => this._fieldInputs[this._fields.length - 1].focus(), 100);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { VaultItem, Field, Tag, FIELD_DEFS } from "@padloc/core/src/item";
|
||||
import { VaultItem, Field, Tag } from "@padloc/core/src/item";
|
||||
import { Vault, VaultID } from "@padloc/core/src/vault";
|
||||
import { translate as $l } from "@padloc/locale/src/translate";
|
||||
import { debounce, wait, escapeRegex } from "@padloc/core/src/util";
|
||||
|
@ -817,7 +817,6 @@ export class ItemsList extends StateMixin(View) {
|
|||
|
||||
<div class="item-fields">
|
||||
${item.fields.map((f: Field, i: number) => {
|
||||
const fieldDef = FIELD_DEFS[f.type] || FIELD_DEFS.text;
|
||||
return html`
|
||||
<div
|
||||
class="item-field tap"
|
||||
|
@ -827,7 +826,7 @@ export class ItemsList extends StateMixin(View) {
|
|||
>
|
||||
<div class="item-field-label">
|
||||
<div class="item-field-name">
|
||||
<pl-icon icon="${fieldDef.icon}"></pl-icon>
|
||||
<pl-icon icon="${f.icon}"></pl-icon>
|
||||
${f.name || $l("Unnamed")}
|
||||
</div>
|
||||
${f.type === "totp"
|
||||
|
@ -836,7 +835,7 @@ export class ItemsList extends StateMixin(View) {
|
|||
`
|
||||
: html`
|
||||
<div class="item-field-value">
|
||||
${fieldDef.format ? fieldDef.format(f.value, true) : f.value}
|
||||
${f.format(true)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { unmarshal, bytesToString } from "@padloc/core/src/encoding";
|
||||
import { PBES2Container } from "@padloc/core/src/container";
|
||||
import { validateLegacyContainer, parseLegacyContainer } from "@padloc/core/src/legacy";
|
||||
import { VaultItem, Field, createVaultItem, guessFieldType } from "@padloc/core/src/item";
|
||||
import { VaultItem, Field, createVaultItem, FieldType } from "@padloc/core/src/item";
|
||||
import { Err, ErrorCode } from "@padloc/core/src/error";
|
||||
import { uuid } from "@padloc/core/src/util";
|
||||
import { translate as $l } from "@padloc/locale/src/translate";
|
||||
|
@ -80,11 +80,12 @@ export async function fromTable(data: string[][], nameColIndex?: number, tagsCol
|
|||
if (i != nameColIndex && i != tagsColIndex && row[i]) {
|
||||
const name = colNames[i];
|
||||
const value = row[i];
|
||||
fields.push({
|
||||
name,
|
||||
value,
|
||||
type: guessFieldType({ name, value })
|
||||
});
|
||||
fields.push(
|
||||
new Field().fromRaw({
|
||||
name,
|
||||
value
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,7 +133,7 @@ export async function importLegacyContainer(container: PBES2Container) {
|
|||
const items = records
|
||||
.filter(({ removed }) => !removed)
|
||||
.map(async ({ name = "Unnamed", fields = [], tags, category, updated, lastUsed }) => {
|
||||
return {
|
||||
return new VaultItem({
|
||||
id: await uuid(),
|
||||
name,
|
||||
fields,
|
||||
|
@ -142,7 +143,7 @@ export async function importLegacyContainer(container: PBES2Container) {
|
|||
updatedBy: "",
|
||||
attachments: [],
|
||||
favorited: []
|
||||
} as VaultItem;
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(items);
|
||||
|
@ -210,9 +211,9 @@ async function lpParseRow(row: string[]): Promise<VaultItem> {
|
|||
const notesIndex = 3;
|
||||
|
||||
let fields: Field[] = [
|
||||
{ name: $l("Username"), value: row[usernameIndex], type: "username" },
|
||||
{ name: $l("Password"), value: row[passwordIndex], type: "password" },
|
||||
{ name: $l("URL"), value: row[urlIndex], type: "url" }
|
||||
new Field({ name: $l("Username"), value: row[usernameIndex], type: FieldType.Username }),
|
||||
new Field({ name: $l("Password"), value: row[passwordIndex], type: FieldType.Password }),
|
||||
new Field({ name: $l("URL"), value: row[urlIndex], type: FieldType.Url })
|
||||
];
|
||||
let notes = row[notesIndex];
|
||||
|
||||
|
@ -224,7 +225,7 @@ async function lpParseRow(row: string[]): Promise<VaultItem> {
|
|||
fields = fields.filter(f => f.name != "url" && f.name != "NoteType");
|
||||
} else {
|
||||
// We've got a regular 'site' item, so the 'extra' column simply contains notes
|
||||
fields.push({ name: $l("Notes"), value: notes, type: "note" });
|
||||
fields.push(new Field({ name: $l("Notes"), value: notes, type: FieldType.Note }));
|
||||
}
|
||||
|
||||
const dir = row[categoryIndex];
|
||||
|
|
|
@ -24,7 +24,6 @@ import {
|
|||
} from "./api";
|
||||
import { Client } from "./client";
|
||||
import { Sender } from "./transport";
|
||||
import { translate as $l } from "@padloc/locale/src/translate";
|
||||
import {
|
||||
DeviceInfo,
|
||||
getDeviceInfo,
|
||||
|
@ -1196,11 +1195,6 @@ export class App {
|
|||
|
||||
/** Creates a new [[VaultItem]] */
|
||||
async createItem(name: string, vault: { id: VaultID }, fields?: Field[], tags?: Tag[]): Promise<VaultItem> {
|
||||
fields = fields || [
|
||||
{ name: $l("Username"), value: "", type: "username" },
|
||||
{ name: $l("Password"), value: "", type: "password" },
|
||||
{ name: $l("URL"), value: "", type: "url" }
|
||||
];
|
||||
const item = await createVaultItem(name || "", fields, tags);
|
||||
if (this.account) {
|
||||
item.updatedBy = this.account.id;
|
||||
|
@ -1233,7 +1227,7 @@ export class App {
|
|||
upd.favorite ? favorited.add(account.id) : favorited.delete(account.id);
|
||||
}
|
||||
|
||||
vault.items.update({ ...item, ...upd, updatedBy: this.account!.id, favorited: [...favorited] });
|
||||
vault.items.update(new VaultItem({ ...item, ...upd, updatedBy: this.account!.id, favorited: [...favorited] }));
|
||||
await this.saveVault(vault);
|
||||
await this.syncVault(vault);
|
||||
}
|
||||
|
@ -1279,7 +1273,7 @@ export class App {
|
|||
if (items.some(item => !!item.attachments.length)) {
|
||||
throw "Items with attachments cannot be moved!";
|
||||
}
|
||||
const newItems = await Promise.all(items.map(async item => ({ ...item, id: await uuid() })));
|
||||
const newItems = await Promise.all(items.map(async item => new VaultItem({ ...item, id: await uuid() })));
|
||||
await this.addItems(newItems, target);
|
||||
await this.deleteItems(items);
|
||||
return newItems;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { bytesToBase64, base64ToBytes } from "./encoding";
|
||||
import { Serializable, AsBytes } from "./encoding";
|
||||
import { SimpleContainer } from "./container";
|
||||
import { VaultID } from "./vault";
|
||||
import { AESKeyParams } from "./crypto";
|
||||
|
@ -60,13 +60,20 @@ function readFileAsDataURL(blob: File): Promise<string> {
|
|||
|
||||
export type AttachmentID = string;
|
||||
|
||||
export interface AttachmentInfo {
|
||||
id: AttachmentID;
|
||||
vault: VaultID;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
key: string;
|
||||
export class AttachmentInfo extends Serializable {
|
||||
constructor(vals: Partial<AttachmentInfo> = {}) {
|
||||
super();
|
||||
Object.assign(this, vals);
|
||||
}
|
||||
|
||||
id: AttachmentID = "";
|
||||
vault: VaultID = "";
|
||||
name: string = "";
|
||||
size: number = 0;
|
||||
type: string = "";
|
||||
|
||||
@AsBytes()
|
||||
key!: Uint8Array;
|
||||
}
|
||||
|
||||
export class Attachment extends SimpleContainer {
|
||||
|
@ -78,29 +85,23 @@ export class Attachment extends SimpleContainer {
|
|||
uploadProgress?: RequestProgress;
|
||||
downloadProgress?: RequestProgress;
|
||||
|
||||
constructor(info?: Partial<AttachmentInfo>) {
|
||||
constructor({ key, ...info }: Partial<AttachmentInfo> = {}) {
|
||||
super();
|
||||
if (info) {
|
||||
this.id = info.id || "";
|
||||
this.vault = info.vault || "";
|
||||
this.name = info.name || "";
|
||||
this.size = info.size || 0;
|
||||
this.type = info.type || "";
|
||||
if (info.key) {
|
||||
this._key = base64ToBytes(info.key);
|
||||
}
|
||||
}
|
||||
Object.assign(this, {
|
||||
_key: key,
|
||||
...info
|
||||
});
|
||||
}
|
||||
|
||||
get info(): AttachmentInfo {
|
||||
return {
|
||||
return new AttachmentInfo({
|
||||
id: this.id,
|
||||
vault: this.vault,
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
size: this.size,
|
||||
key: this._key ? bytesToBase64(this._key) : ""
|
||||
};
|
||||
key: this._key
|
||||
});
|
||||
}
|
||||
|
||||
get loaded(): boolean {
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
import { Serializable } from "./encoding";
|
||||
|
||||
export interface CollectionItem {
|
||||
id: string;
|
||||
updated?: Date;
|
||||
}
|
||||
import { VaultItem } from "./item";
|
||||
|
||||
/**
|
||||
* A collection of items, used for consolidating changes made independently
|
||||
* A collection of vault items items, used for consolidating changes made independently
|
||||
* across multiple instances through "merging".
|
||||
*/
|
||||
export class Collection<T extends CollectionItem> extends Serializable implements Iterable<T> {
|
||||
/** Number of items in this Collection */
|
||||
export class VaultItemCollection extends Serializable implements Iterable<VaultItem> {
|
||||
/** Number of items in this VaultItemCollection */
|
||||
get size() {
|
||||
return this._items.size;
|
||||
}
|
||||
|
@ -19,12 +15,23 @@ export class Collection<T extends CollectionItem> extends Serializable implement
|
|||
return !!this._changes.size;
|
||||
}
|
||||
|
||||
private _items: Map<string, T>;
|
||||
/** Aggregated list of tags assigned to the items in this collection */
|
||||
get tags(): string[] {
|
||||
const tags = new Set<string>();
|
||||
for (const r of this) {
|
||||
for (const t of r.tags) {
|
||||
tags.add(t);
|
||||
}
|
||||
}
|
||||
return [...tags];
|
||||
}
|
||||
|
||||
private _items: Map<string, VaultItem>;
|
||||
private _changes = new Map<string, Date>();
|
||||
|
||||
constructor(items: T[] = []) {
|
||||
constructor(items: VaultItem[] = []) {
|
||||
super();
|
||||
this._items = new Map(items.map(item => [item.id, item] as [string, T]));
|
||||
this._items = new Map(items.map(item => [item.id, item] as [string, VaultItem]));
|
||||
}
|
||||
|
||||
/** Get an item with a given `id` */
|
||||
|
@ -36,7 +43,7 @@ export class Collection<T extends CollectionItem> extends Serializable implement
|
|||
* Updates one or more items based on their id. If no item with the same id
|
||||
* exists, the item will be added to the collection
|
||||
*/
|
||||
update(...items: T[]) {
|
||||
update(...items: VaultItem[]) {
|
||||
for (const item of items) {
|
||||
item.updated = new Date();
|
||||
this._items.set(item.id, item);
|
||||
|
@ -47,7 +54,7 @@ export class Collection<T extends CollectionItem> extends Serializable implement
|
|||
/**
|
||||
* Removes one or more items based on their id.
|
||||
*/
|
||||
remove(...items: T[]) {
|
||||
remove(...items: VaultItem[]) {
|
||||
for (const item of items) {
|
||||
this._items.delete(item.id);
|
||||
this._changes.set(item.id, new Date());
|
||||
|
@ -55,9 +62,9 @@ export class Collection<T extends CollectionItem> extends Serializable implement
|
|||
}
|
||||
|
||||
/**
|
||||
* Merges in changes from another [[Collection]] instance.
|
||||
* Merges in changes from another [[VaultItemCollection]] instance.
|
||||
*/
|
||||
merge(coll: Collection<T>) {
|
||||
merge(coll: VaultItemCollection) {
|
||||
// Delete any items from this collection that don't
|
||||
// exist in the other collection and haven't been changed recently
|
||||
for (const item of this) {
|
||||
|
@ -82,20 +89,17 @@ export class Collection<T extends CollectionItem> extends Serializable implement
|
|||
}
|
||||
}
|
||||
|
||||
protected _toRaw() {
|
||||
protected _toRaw(version: string) {
|
||||
return {
|
||||
items: Array.from(this),
|
||||
items: Array.from(this).map(item => item.toRaw(version)),
|
||||
changes: [...this._changes]
|
||||
};
|
||||
}
|
||||
|
||||
protected _fromRaw({ items, changes }: any) {
|
||||
for (const item of items) {
|
||||
if (!(item.updated instanceof Date)) {
|
||||
item.updated = new Date(item.updated);
|
||||
}
|
||||
}
|
||||
this._items = new Map(items.map((item: any) => [item.id, item] as [string, T]));
|
||||
this._items = new Map(
|
||||
items.map((item: any) => [item.id, new VaultItem().fromRaw(item)] as [string, VaultItem])
|
||||
);
|
||||
this._changes = new Map<string, Date>(
|
||||
changes && changes.map(([id, date]: [string, string]) => [id, new Date(date)])
|
||||
);
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { translate as $l } from "@padloc/locale/src/translate";
|
||||
import { base32ToBytes } from "./encoding";
|
||||
import { base32ToBytes, Serializable, AsSerializable, AsDate } from "./encoding";
|
||||
import { totp } from "./otp";
|
||||
import { uuid } from "./util";
|
||||
import { Collection, CollectionItem } from "./collection";
|
||||
import { AccountID } from "./account";
|
||||
import { AttachmentInfo } from "./attachment";
|
||||
|
||||
|
@ -12,19 +11,20 @@ export type Tag = string;
|
|||
/** Unique identifier for [[VaultItem]]s */
|
||||
export type VaultItemID = string;
|
||||
|
||||
export type FieldType =
|
||||
| "username"
|
||||
| "password"
|
||||
| "url"
|
||||
| "email"
|
||||
| "date"
|
||||
| "month"
|
||||
| "credit"
|
||||
| "phone"
|
||||
| "pin"
|
||||
| "totp"
|
||||
| "note"
|
||||
| "text";
|
||||
export enum FieldType {
|
||||
Username = "username",
|
||||
Password = "password",
|
||||
Url = "url",
|
||||
Email = "email",
|
||||
Date = "date",
|
||||
Month = "month",
|
||||
Credit = "credit",
|
||||
Phone = "phone",
|
||||
Pin = "pin",
|
||||
Totp = "totp",
|
||||
Note = "note",
|
||||
Text = "text"
|
||||
}
|
||||
|
||||
/**
|
||||
* Field definition containing meta data for a certain field type
|
||||
|
@ -50,8 +50,8 @@ export interface FieldDef {
|
|||
|
||||
/** Available field types and respective meta data */
|
||||
export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
||||
username: {
|
||||
type: "username",
|
||||
[FieldType.Username]: {
|
||||
type: FieldType.Username,
|
||||
pattern: ".*",
|
||||
mask: false,
|
||||
multiline: false,
|
||||
|
@ -60,8 +60,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return $l("Username");
|
||||
}
|
||||
},
|
||||
password: {
|
||||
type: "password",
|
||||
[FieldType.Password]: {
|
||||
type: FieldType.Password,
|
||||
pattern: ".*",
|
||||
mask: true,
|
||||
multiline: true,
|
||||
|
@ -73,8 +73,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return masked ? value.replace(/./g, "\u2022") : value;
|
||||
}
|
||||
},
|
||||
url: {
|
||||
type: "url",
|
||||
[FieldType.Url]: {
|
||||
type: FieldType.Url,
|
||||
pattern: ".*",
|
||||
mask: false,
|
||||
multiline: false,
|
||||
|
@ -83,8 +83,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return $l("URL");
|
||||
}
|
||||
},
|
||||
email: {
|
||||
type: "email",
|
||||
[FieldType.Email]: {
|
||||
type: FieldType.Email,
|
||||
pattern: ".*",
|
||||
mask: false,
|
||||
multiline: false,
|
||||
|
@ -93,8 +93,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return $l("Email Address");
|
||||
}
|
||||
},
|
||||
date: {
|
||||
type: "date",
|
||||
[FieldType.Date]: {
|
||||
type: FieldType.Date,
|
||||
pattern: "\\d\\d\\d\\d-\\d\\d-\\d\\d",
|
||||
mask: false,
|
||||
multiline: false,
|
||||
|
@ -106,8 +106,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return new Date(value).toLocaleDateString();
|
||||
}
|
||||
},
|
||||
month: {
|
||||
type: "month",
|
||||
[FieldType.Month]: {
|
||||
type: FieldType.Month,
|
||||
pattern: "\\d\\d\\d\\d-\\d\\d",
|
||||
mask: false,
|
||||
multiline: false,
|
||||
|
@ -116,8 +116,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return $l("Month");
|
||||
}
|
||||
},
|
||||
credit: {
|
||||
type: "credit",
|
||||
[FieldType.Credit]: {
|
||||
type: FieldType.Credit,
|
||||
pattern: "\\d*",
|
||||
mask: true,
|
||||
multiline: false,
|
||||
|
@ -136,8 +136,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return parts.join(" ");
|
||||
}
|
||||
},
|
||||
phone: {
|
||||
type: "phone",
|
||||
[FieldType.Phone]: {
|
||||
type: FieldType.Phone,
|
||||
pattern: ".*",
|
||||
mask: false,
|
||||
multiline: false,
|
||||
|
@ -146,8 +146,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return $l("Phone Number");
|
||||
}
|
||||
},
|
||||
pin: {
|
||||
type: "pin",
|
||||
[FieldType.Pin]: {
|
||||
type: FieldType.Pin,
|
||||
pattern: "\\d*",
|
||||
mask: true,
|
||||
multiline: false,
|
||||
|
@ -159,8 +159,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return masked ? value.replace(/./g, "\u2022") : value;
|
||||
}
|
||||
},
|
||||
totp: {
|
||||
type: "totp",
|
||||
[FieldType.Totp]: {
|
||||
type: FieldType.Totp,
|
||||
pattern: ".*",
|
||||
mask: false,
|
||||
multiline: false,
|
||||
|
@ -172,8 +172,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return await totp(base32ToBytes(value));
|
||||
}
|
||||
},
|
||||
note: {
|
||||
type: "note",
|
||||
[FieldType.Note]: {
|
||||
type: FieldType.Note,
|
||||
pattern: ".*",
|
||||
mask: false,
|
||||
multiline: true,
|
||||
|
@ -182,8 +182,8 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
return $l("Note");
|
||||
}
|
||||
},
|
||||
text: {
|
||||
type: "text",
|
||||
[FieldType.Text]: {
|
||||
type: FieldType.Text,
|
||||
pattern: ".*",
|
||||
mask: false,
|
||||
multiline: false,
|
||||
|
@ -194,16 +194,44 @@ export const FIELD_DEFS: { [t in FieldType]: FieldDef } = {
|
|||
}
|
||||
};
|
||||
|
||||
export interface Field {
|
||||
/** field name */
|
||||
name: string;
|
||||
/** field content */
|
||||
value: string;
|
||||
export class Field extends Serializable {
|
||||
constructor(vals: Partial<Field> = {}) {
|
||||
super();
|
||||
Object.assign(this, vals);
|
||||
}
|
||||
|
||||
/**
|
||||
* field type, determining meta data via the corresponding field definition
|
||||
* in [[FIELD_DEFS]]
|
||||
*/
|
||||
type: FieldType;
|
||||
type: FieldType = FieldType.Text;
|
||||
/** field name */
|
||||
name: string = "";
|
||||
/** field content */
|
||||
value: string = "";
|
||||
|
||||
get def(): FieldDef {
|
||||
return FIELD_DEFS[this.type];
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this.def.icon;
|
||||
}
|
||||
|
||||
async transform() {
|
||||
return this.def.transform ? await this.def.transform(this.value) : this.value;
|
||||
}
|
||||
|
||||
format(masked: boolean) {
|
||||
return this.def.format ? this.def.format(this.value, masked) : this.value;
|
||||
}
|
||||
|
||||
protected _fromRaw(raw: any) {
|
||||
if (!raw.type) {
|
||||
raw.type = guessFieldType(raw);
|
||||
}
|
||||
return super._fromRaw(raw);
|
||||
}
|
||||
}
|
||||
|
||||
/** Normalizes a tag value by removing invalid characters */
|
||||
|
@ -212,38 +240,52 @@ export function normalizeTag(tag: string): Tag {
|
|||
}
|
||||
|
||||
/** Represents an entry within a vault */
|
||||
export interface VaultItem extends CollectionItem {
|
||||
export class VaultItem extends Serializable {
|
||||
constructor(vals: Partial<VaultItem> = {}) {
|
||||
super();
|
||||
Object.assign(this, vals);
|
||||
}
|
||||
|
||||
/** unique identfier */
|
||||
id: VaultItemID;
|
||||
id: VaultItemID = "";
|
||||
|
||||
/** item name */
|
||||
name: string;
|
||||
name: string = "";
|
||||
|
||||
/** item fields */
|
||||
fields: Field[];
|
||||
@AsSerializable(Field)
|
||||
fields: Field[] = [];
|
||||
|
||||
/** array of tags assigned with this item */
|
||||
tags: Tag[];
|
||||
tags: Tag[] = [];
|
||||
|
||||
/** Date and time of last update */
|
||||
@AsDate()
|
||||
updated: Date = new Date();
|
||||
|
||||
/** [[Account]] the item was last updated by */
|
||||
updatedBy: AccountID;
|
||||
updatedBy: AccountID = "";
|
||||
|
||||
/** Last time the item was interacted with */
|
||||
lastUsed: Date;
|
||||
/** attachments associated with this item */
|
||||
attachments: AttachmentInfo[];
|
||||
@AsDate()
|
||||
lastUsed: Date = new Date(0);
|
||||
|
||||
/** Accounts that have favorited this item */
|
||||
favorited: AccountID[];
|
||||
favorited: AccountID[] = [];
|
||||
|
||||
/** attachments associated with this item */
|
||||
@AsSerializable(AttachmentInfo)
|
||||
attachments: AttachmentInfo[] = [];
|
||||
}
|
||||
|
||||
/** Creates a new vault item */
|
||||
export async function createVaultItem(name: string, fields?: Field[], tags?: Tag[]): Promise<VaultItem> {
|
||||
return {
|
||||
id: await uuid(),
|
||||
name: name,
|
||||
fields: fields || [],
|
||||
tags: tags || [],
|
||||
updated: new Date(),
|
||||
updatedBy: "",
|
||||
lastUsed: new Date(0),
|
||||
attachments: [],
|
||||
favorited: []
|
||||
};
|
||||
return new VaultItem({
|
||||
name,
|
||||
fields,
|
||||
tags,
|
||||
id: await uuid()
|
||||
});
|
||||
}
|
||||
|
||||
const matchUsername = /username/i;
|
||||
|
@ -254,14 +296,14 @@ const matchNote = /\n/;
|
|||
/** Guesses the most appropriate field type based on field name and value */
|
||||
export function guessFieldType({ name = "", value = "", masked }: any): FieldType {
|
||||
return masked || name.match(matchPassword)
|
||||
? "password"
|
||||
? FieldType.Password
|
||||
: name.match(matchUsername)
|
||||
? "username"
|
||||
? FieldType.Username
|
||||
: name.match(matchUrl)
|
||||
? "url"
|
||||
? FieldType.Url
|
||||
: value.match(matchNote)
|
||||
? "note"
|
||||
: "text";
|
||||
? FieldType.Note
|
||||
: FieldType.Text;
|
||||
}
|
||||
|
||||
export interface ItemTemplate {
|
||||
|
@ -280,19 +322,19 @@ export const ITEM_TEMPLATES: ItemTemplate[] = [
|
|||
get name() {
|
||||
return $l("Username");
|
||||
},
|
||||
type: "username"
|
||||
type: FieldType.Username
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Password");
|
||||
},
|
||||
type: "password"
|
||||
type: FieldType.Password
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("URL");
|
||||
},
|
||||
type: "url"
|
||||
type: FieldType.Url
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -304,31 +346,31 @@ export const ITEM_TEMPLATES: ItemTemplate[] = [
|
|||
get name() {
|
||||
return $l("Card Number");
|
||||
},
|
||||
type: "credit"
|
||||
type: FieldType.Credit
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Card Owner");
|
||||
},
|
||||
type: "text"
|
||||
type: FieldType.Text
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Valid Until");
|
||||
},
|
||||
type: "month"
|
||||
type: FieldType.Month
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("CVC");
|
||||
},
|
||||
type: "pin"
|
||||
type: FieldType.Pin
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("PIN");
|
||||
},
|
||||
type: "pin"
|
||||
type: FieldType.Pin
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -340,25 +382,25 @@ export const ITEM_TEMPLATES: ItemTemplate[] = [
|
|||
get name() {
|
||||
return $l("Account Owner");
|
||||
},
|
||||
type: "text"
|
||||
type: FieldType.Text
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("IBAN");
|
||||
},
|
||||
type: "text"
|
||||
type: FieldType.Text
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("BIC");
|
||||
},
|
||||
type: "text"
|
||||
type: FieldType.Text
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Card PIN");
|
||||
},
|
||||
type: "pin"
|
||||
type: FieldType.Pin
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -370,13 +412,13 @@ export const ITEM_TEMPLATES: ItemTemplate[] = [
|
|||
get name() {
|
||||
return $l("Name");
|
||||
},
|
||||
type: "text"
|
||||
type: FieldType.Text
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Password");
|
||||
},
|
||||
type: "password"
|
||||
type: FieldType.Password
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -388,43 +430,43 @@ export const ITEM_TEMPLATES: ItemTemplate[] = [
|
|||
get name() {
|
||||
return $l("Full Name");
|
||||
},
|
||||
type: "text"
|
||||
type: FieldType.Text
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Passport Number");
|
||||
},
|
||||
type: "text"
|
||||
type: FieldType.Text
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Country");
|
||||
},
|
||||
type: "text"
|
||||
type: FieldType.Text
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Birthdate");
|
||||
},
|
||||
type: "date"
|
||||
type: FieldType.Date
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Birthplace");
|
||||
},
|
||||
type: "text"
|
||||
type: FieldType.Text
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Issued On");
|
||||
},
|
||||
type: "date"
|
||||
type: FieldType.Date
|
||||
},
|
||||
{
|
||||
get name() {
|
||||
return $l("Expires");
|
||||
},
|
||||
type: "date"
|
||||
type: FieldType.Date
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -436,7 +478,7 @@ export const ITEM_TEMPLATES: ItemTemplate[] = [
|
|||
get name() {
|
||||
return $l("Note");
|
||||
},
|
||||
type: "note"
|
||||
type: FieldType.Note
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@ -452,40 +494,3 @@ export const ITEM_TEMPLATES: ItemTemplate[] = [
|
|||
fields: []
|
||||
}
|
||||
];
|
||||
|
||||
/** A collection of [[VaultItem]]s */
|
||||
export class VaultItemCollection extends Collection<VaultItem> {
|
||||
/** Aggregated list of tags assigned to the items in this collection */
|
||||
get tags(): string[] {
|
||||
const tags = new Set<string>();
|
||||
for (const r of this) {
|
||||
for (const t of r.tags) {
|
||||
tags.add(t);
|
||||
}
|
||||
}
|
||||
return [...tags];
|
||||
}
|
||||
|
||||
protected _fromRaw(raw: any) {
|
||||
return super._fromRaw({
|
||||
...raw,
|
||||
items: raw.items.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
lastUsed: new Date(item.lastUsed),
|
||||
attachments: item.attachments || [],
|
||||
fields: item.fields.map(({ name = "", value = "", masked, type }: any) => ({
|
||||
name: name,
|
||||
value: value,
|
||||
type: type || guessFieldType({ name, value, masked })
|
||||
}))
|
||||
};
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function transformedValue(field: Field) {
|
||||
const type = FIELD_DEFS[field.type] || FIELD_DEFS.text;
|
||||
return type.transform ? await type.transform(field.value) : field.value;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SharedContainer } from "./container";
|
||||
import { Storable } from "./storage";
|
||||
import { VaultItemCollection } from "./item";
|
||||
import { VaultItemCollection } from "./collection";
|
||||
import { Account, AccountID } from "./account";
|
||||
import { OrgID } from "./org";
|
||||
import { Exclude, AsDate } from "./encoding";
|
||||
|
|
|
@ -120,19 +120,6 @@ export class ExtensionApp extends App {
|
|||
await this.app.storage.save(new RouterState({ path: this.router.path, params }));
|
||||
}
|
||||
|
||||
// private async _fieldClicked({ detail: { item, index } }: CustomEvent<{ item: VaultItem; index: number }>) {
|
||||
// const field = item.fields[index];
|
||||
// const value = await transformedValue(field);
|
||||
// const filled = await messageTab({
|
||||
// type: "fillActive",
|
||||
// value
|
||||
// });
|
||||
//
|
||||
// if (filled) {
|
||||
// window.close();
|
||||
// }
|
||||
// }
|
||||
|
||||
protected async _fieldDragged(e: CustomEvent<{ item: VaultItem; index: number; event: DragEvent }>) {
|
||||
super._fieldDragged(e);
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ import { App } from "@padloc/core/src/app";
|
|||
import { bytesToBase64, base64ToBytes } from "@padloc/core/src/encoding";
|
||||
import { AjaxSender } from "@padloc/app/src/lib/ajax";
|
||||
import { debounce } from "@padloc/core/src/util";
|
||||
import { transformedValue } from "@padloc/core/src/item";
|
||||
import { ExtensionPlatform } from "./platform";
|
||||
import { Message, messageTab } from "./message";
|
||||
|
||||
|
@ -82,7 +81,7 @@ class ExtensionBackground {
|
|||
}
|
||||
|
||||
const field = item.item.fields[index];
|
||||
const value = await transformedValue(field);
|
||||
const value = await field.transform()
|
||||
await messageTab({
|
||||
type: "fillActive",
|
||||
value
|
||||
|
|
Loading…
Reference in New Issue