Create new pwa package to separate webpack builds from ui package
This commit is contained in:
parent
e61fc47f99
commit
b5b8b6603f
|
@ -18,3 +18,4 @@ packages/electron/build
|
|||
packages/electron/app
|
||||
packages/electron/dist
|
||||
*.env
|
||||
packages/pwa/dist
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"version": "0.0.0"
|
||||
"version": "3.0.5"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "padlock",
|
||||
"name": "padloc",
|
||||
"private": true,
|
||||
"version": "3.0.4",
|
||||
"description": "A minimalist password manager",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,6 @@
|
|||
{
|
||||
"name": "@padloc/app",
|
||||
"version": "3.0.5",
|
||||
"main": "src/elements/app.js",
|
||||
"author": "Martin Kleinschrodt <martin@maklesoft.com>",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
|
@ -30,29 +29,8 @@
|
|||
"@types/workbox-window": "^4.3.1",
|
||||
"@types/zxcvbn": "^4.4.0",
|
||||
"chai": "^4.2.0",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^3.0.0",
|
||||
"favicons-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "^4.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"mocha": "^5.2.0",
|
||||
"reflect-metadata": "^0.1.12",
|
||||
"style-loader": "^1.0.0",
|
||||
"ts-loader": "^6.0.4",
|
||||
"ts-node": "^7.0.1",
|
||||
"typescript": "^3.5.2",
|
||||
"wct-mocha": "^1.0.0",
|
||||
"webpack": "^4.35.3",
|
||||
"webpack-cli": "^3.3.5",
|
||||
"webpack-dev-server": "^3.7.2",
|
||||
"webpack-pwa-manifest": "^4.0.0",
|
||||
"workbox-cli": "^4.3.1",
|
||||
"workbox-webpack-plugin": "^5.0.0-alpha.0"
|
||||
"reflect-metadata": "^0.1.12"
|
||||
},
|
||||
"description": "Padloc UI",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"start": "http-server dist -s -p $PL_CLIENT_PORT --proxy $PL_CLIENT_URL?",
|
||||
"dev": "webpack-dev-server"
|
||||
}
|
||||
"description": "Padloc Web-Based UI package"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import { App } from "@padloc/core/src/app";
|
||||
import { Router } from "./lib/route";
|
||||
import { AjaxSender } from "./lib/ajax";
|
||||
import { LocalStorage } from "./lib/storage";
|
||||
|
||||
const sender = new AjaxSender(process.env.PL_SERVER_URL!);
|
||||
const billingEnabled = process.env.PL_BILLING_ENABLED === "true";
|
||||
const disablePayment = process.env.PL_BILLING_DISABLE_PAYMENT === "true";
|
||||
const stripePublicKey = process.env.PL_BILLING_STRIPE_PUBLIC_KEY || "";
|
||||
|
||||
if (billingEnabled && !disablePayment && !stripePublicKey) {
|
||||
throw "Billing enabled but no stripe public key provided!";
|
||||
}
|
||||
|
||||
const billingConfig = billingEnabled
|
||||
? {
|
||||
disablePayment,
|
||||
stripePublicKey
|
||||
}
|
||||
: undefined;
|
||||
export const app = (window.app = new App(new LocalStorage(), sender, billingConfig));
|
||||
export const router = (window.router = new Router());
|
|
@ -1,5 +0,0 @@
|
|||
import { setPlatform } from "@padloc/core/src/platform";
|
||||
import { WebPlatform } from "./lib/platform";
|
||||
import "./elements/app";
|
||||
|
||||
setPlatform(new WebPlatform());
|
|
@ -0,0 +1,61 @@
|
|||
import { Err, ErrorCode } from "@padloc/core/src/error";
|
||||
import { marshal, unmarshal } from "@padloc/core/src/encoding";
|
||||
import { Request, Response, Sender, RequestProgress } from "@padloc/core/src/transport";
|
||||
|
||||
export type Method = "GET" | "POST" | "PUT" | "DELETE";
|
||||
|
||||
export async function request(
|
||||
method: Method,
|
||||
url: string,
|
||||
body?: string,
|
||||
headers?: Map<string, string>,
|
||||
progress?: RequestProgress
|
||||
): Promise<XMLHttpRequest> {
|
||||
let req = new XMLHttpRequest();
|
||||
|
||||
return new Promise<XMLHttpRequest>((resolve, reject) => {
|
||||
req.onreadystatechange = () => {
|
||||
if (req.readyState === 4) {
|
||||
if (!req.status) {
|
||||
reject(new Err(ErrorCode.FAILED_CONNECTION));
|
||||
} else {
|
||||
resolve(req);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
req.open(method, url, true);
|
||||
if (headers) {
|
||||
headers.forEach((value, key) => req.setRequestHeader(key, value));
|
||||
}
|
||||
if (progress) {
|
||||
req.onprogress = (pg: { loaded: number; total: number }) => (progress.downloadProgress = pg);
|
||||
req.upload.onprogress = (pg: { loaded: number; total: number }) => (progress.uploadProgress = pg);
|
||||
}
|
||||
req.send(body);
|
||||
} catch (e) {
|
||||
reject(new Err(ErrorCode.FAILED_CONNECTION));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export class AjaxSender implements Sender {
|
||||
constructor(public url: string) {}
|
||||
|
||||
async send(req: Request, progress?: RequestProgress): Promise<Response> {
|
||||
const body = marshal(req.toRaw());
|
||||
const res = await request(
|
||||
"POST",
|
||||
this.url,
|
||||
body,
|
||||
new Map<string, string>([["Content-Type", "application/json"], ["Accept", "application/json"]]),
|
||||
progress
|
||||
);
|
||||
try {
|
||||
return new Response().fromRaw(unmarshal(res.responseText));
|
||||
} catch (e) {
|
||||
throw new Err(ErrorCode.SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
const defaults = {
|
||||
animation: "slideIn",
|
||||
duration: 500,
|
||||
easing: "ease",
|
||||
delay: 0,
|
||||
fill: "backwards",
|
||||
initialDelay: 0,
|
||||
fullDuration: 1000,
|
||||
clear: false,
|
||||
direction: "normal"
|
||||
};
|
||||
|
||||
const clearAnimation = new Map<HTMLElement, number>();
|
||||
|
||||
export function animateElement(el: HTMLElement, opts = {}) {
|
||||
const { animation, duration, direction, easing, delay, fill, clear } = Object.assign({}, defaults, opts);
|
||||
clearTimeout(clearAnimation.get(el));
|
||||
el.style.animation = "";
|
||||
el.offsetLeft;
|
||||
el.style.animation = `${animation} ${direction} ${duration}ms ${easing} ${delay}ms ${fill}`;
|
||||
if (clear) {
|
||||
const clearDelay = typeof clear === "number" ? clear : 0;
|
||||
clearAnimation.set(el, window.setTimeout(() => (el.style.animation = ""), delay + duration + clearDelay));
|
||||
}
|
||||
|
||||
return new Promise(resolve => setTimeout(resolve, delay + duration));
|
||||
}
|
||||
|
||||
export function animateCascade(nodes: Iterable<Node | Element>, opts = {}) {
|
||||
const els = Array.from(nodes);
|
||||
const { fullDuration, duration, initialDelay } = Object.assign({}, defaults, opts);
|
||||
const dt = Math.max(30, Math.floor((fullDuration - duration) / els.length));
|
||||
|
||||
const promises = [];
|
||||
for (const [i, e] of els.entries()) {
|
||||
promises.push(animateElement(e as HTMLElement, Object.assign(opts, { delay: initialDelay + i * dt })));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import { VaultItem, Field } from "@padloc/core/src/item";
|
||||
import "../elements/clipboard";
|
||||
import { Clipboard } from "../elements/clipboard";
|
||||
import { getSingleton } from "./singleton"
|
||||
|
||||
export async function setClipboard(item: VaultItem, field: Field, duration?: number) {
|
||||
const singleton = getSingleton("pl-clipboard") as Clipboard;
|
||||
return singleton.set(item, field, duration);
|
||||
}
|
||||
|
||||
export function clearClipboard() {
|
||||
const singleton = getSingleton("pl-clipboard") as Clipboard;
|
||||
singleton.clear();
|
||||
}
|
|
@ -0,0 +1,272 @@
|
|||
import {
|
||||
CryptoProvider,
|
||||
PBKDF2Params,
|
||||
AESKey,
|
||||
RSAPublicKey,
|
||||
RSAPrivateKey,
|
||||
HMACKey,
|
||||
SymmetricKey,
|
||||
AESKeyParams,
|
||||
RSAKeyParams,
|
||||
HMACParams,
|
||||
HMACKeyParams,
|
||||
AESEncryptionParams,
|
||||
RSAEncryptionParams,
|
||||
HashParams,
|
||||
RSASigningParams
|
||||
} from "@padloc/core/src/crypto";
|
||||
import { Err, ErrorCode } from "@padloc/core/src/error";
|
||||
import SJCLProvider from "@padloc/core/src/sjcl";
|
||||
|
||||
const webCrypto = window.crypto && window.crypto.subtle;
|
||||
|
||||
export class WebCryptoProvider implements CryptoProvider {
|
||||
async randomBytes(n: number): Promise<Uint8Array> {
|
||||
const bytes = window.crypto.getRandomValues(new Uint8Array(n));
|
||||
return bytes;
|
||||
}
|
||||
|
||||
async hash(input: Uint8Array, params: HashParams): Promise<Uint8Array> {
|
||||
const bytes = await webCrypto.digest({ name: params.algorithm }, input);
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
generateKey(params: AESKeyParams): Promise<AESKey>;
|
||||
generateKey(params: RSAKeyParams): Promise<{ privateKey: RSAPrivateKey; publicKey: RSAPublicKey }>;
|
||||
generateKey(params: HMACKeyParams): Promise<HMACKey>;
|
||||
async generateKey(params: AESKeyParams | RSAKeyParams | HMACKeyParams) {
|
||||
switch (params.algorithm) {
|
||||
case "AES":
|
||||
case "HMAC":
|
||||
return this.randomBytes(params.keySize / 8);
|
||||
case "RSA":
|
||||
const keyPair = await webCrypto.generateKey(Object.assign(params, { name: "RSA-OAEP" }), true, [
|
||||
"encrypt",
|
||||
"decrypt"
|
||||
]);
|
||||
|
||||
const privateKey = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
|
||||
const publicKey = await crypto.subtle.exportKey("spki", keyPair.publicKey);
|
||||
|
||||
return {
|
||||
privateKey: new Uint8Array(privateKey),
|
||||
publicKey: new Uint8Array(publicKey)
|
||||
};
|
||||
// case "HMAC":
|
||||
// const key = await webCrypto.generateKey(Object.assign({}, params, { name: params.algorithm }), true, [
|
||||
// "sign",
|
||||
// "verify"
|
||||
// ]);
|
||||
// const raw = await webCrypto.exportKey("raw", key);
|
||||
// return new Uint8Array(raw));
|
||||
}
|
||||
}
|
||||
|
||||
async deriveKey(password: Uint8Array, params: PBKDF2Params): Promise<SymmetricKey> {
|
||||
const baseKey = await webCrypto.importKey("raw", password, params.algorithm, false, ["deriveBits"]);
|
||||
|
||||
const key = await webCrypto.deriveBits(
|
||||
{
|
||||
name: params.algorithm,
|
||||
salt: params.salt!,
|
||||
iterations: params.iterations,
|
||||
hash: params.hash
|
||||
},
|
||||
baseKey,
|
||||
params.keySize
|
||||
);
|
||||
|
||||
return new Uint8Array(key);
|
||||
}
|
||||
|
||||
encrypt(key: AESKey, data: Uint8Array, params: AESEncryptionParams): Promise<Uint8Array>;
|
||||
encrypt(publicKey: RSAPublicKey, data: Uint8Array, params: RSAEncryptionParams): Promise<Uint8Array>;
|
||||
async encrypt(
|
||||
key: AESKey | RSAPublicKey,
|
||||
data: Uint8Array,
|
||||
params: AESEncryptionParams | RSAEncryptionParams
|
||||
): Promise<Uint8Array> {
|
||||
switch (params.algorithm) {
|
||||
case "AES-GCM":
|
||||
case "AES-CCM":
|
||||
return this._encryptAES(key, data, params);
|
||||
case "RSA-OAEP":
|
||||
return this._encryptRSA(key, data, params);
|
||||
default:
|
||||
throw new Err(ErrorCode.INVALID_ENCRYPTION_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
decrypt(key: AESKey, data: Uint8Array, params: AESEncryptionParams): Promise<Uint8Array>;
|
||||
decrypt(publicKey: RSAPublicKey, data: Uint8Array, params: RSAEncryptionParams): Promise<Uint8Array>;
|
||||
async decrypt(
|
||||
key: AESKey | RSAPublicKey,
|
||||
data: Uint8Array,
|
||||
params: AESEncryptionParams | RSAEncryptionParams
|
||||
): Promise<Uint8Array> {
|
||||
switch (params.algorithm) {
|
||||
case "AES-GCM":
|
||||
case "AES-CCM":
|
||||
return this._decryptAES(key, data, params);
|
||||
case "RSA-OAEP":
|
||||
return this._decryptRSA(key, data, params);
|
||||
default:
|
||||
throw new Err(ErrorCode.INVALID_ENCRYPTION_PARAMS);
|
||||
}
|
||||
}
|
||||
|
||||
private async _encryptAES(key: AESKey, data: Uint8Array, params: AESEncryptionParams): Promise<Uint8Array> {
|
||||
if (params.algorithm === "AES-CCM") {
|
||||
return SJCLProvider.encrypt(key, data, params);
|
||||
}
|
||||
|
||||
const k = await webCrypto.importKey("raw", key, params.algorithm, false, ["encrypt"]);
|
||||
|
||||
try {
|
||||
const buf = await webCrypto.encrypt(
|
||||
{
|
||||
name: params.algorithm,
|
||||
iv: params.iv,
|
||||
additionalData: params.additionalData,
|
||||
tagLength: params.tagSize
|
||||
},
|
||||
k,
|
||||
data
|
||||
);
|
||||
|
||||
return new Uint8Array(buf);
|
||||
} catch (e) {
|
||||
throw new Err(ErrorCode.ENCRYPTION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
private async _decryptAES(key: AESKey, data: Uint8Array, params: AESEncryptionParams): Promise<Uint8Array> {
|
||||
if (params.algorithm === "AES-CCM") {
|
||||
return SJCLProvider.decrypt(key, data, params);
|
||||
}
|
||||
|
||||
const k = await webCrypto.importKey("raw", key, params.algorithm, false, ["decrypt"]);
|
||||
|
||||
try {
|
||||
const buf = await webCrypto.decrypt(
|
||||
{
|
||||
name: params.algorithm,
|
||||
iv: params.iv!,
|
||||
additionalData: params.additionalData!,
|
||||
tagLength: params.tagSize
|
||||
},
|
||||
k,
|
||||
data
|
||||
);
|
||||
|
||||
return new Uint8Array(buf);
|
||||
} catch (e) {
|
||||
throw new Err(ErrorCode.DECRYPTION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
async _encryptRSA(publicKey: RSAPublicKey, key: AESKey, params: RSAEncryptionParams) {
|
||||
const p = Object.assign({}, params, { name: params.algorithm });
|
||||
const k = await webCrypto.importKey("spki", publicKey, p, false, ["encrypt"]);
|
||||
try {
|
||||
const buf = await webCrypto.encrypt(p, k, key);
|
||||
return new Uint8Array(buf);
|
||||
} catch (e) {
|
||||
throw new Err(ErrorCode.DECRYPTION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
async _decryptRSA(privateKey: RSAPrivateKey, key: AESKey, params: RSAEncryptionParams) {
|
||||
const p = Object.assign({}, params, { name: params.algorithm });
|
||||
const k = await webCrypto.importKey("pkcs8", privateKey, p, false, ["decrypt"]);
|
||||
try {
|
||||
const buf = await webCrypto.decrypt(p, k, key);
|
||||
return new Uint8Array(buf);
|
||||
} catch (e) {
|
||||
throw new Err(ErrorCode.DECRYPTION_FAILED);
|
||||
}
|
||||
}
|
||||
|
||||
async fingerprint(key: RSAPublicKey): Promise<Uint8Array> {
|
||||
const bytes = await webCrypto.digest("SHA-256", key);
|
||||
return new Uint8Array(bytes);
|
||||
}
|
||||
|
||||
async sign(key: HMACKey, data: Uint8Array, params: HMACParams): Promise<Uint8Array>;
|
||||
async sign(key: RSAPrivateKey, data: Uint8Array, params: RSASigningParams): Promise<Uint8Array>;
|
||||
async sign(
|
||||
key: HMACKey | RSAPrivateKey,
|
||||
data: Uint8Array,
|
||||
params: HMACParams | RSASigningParams
|
||||
): Promise<Uint8Array> {
|
||||
switch (params.algorithm) {
|
||||
case "HMAC":
|
||||
return this._signHMAC(key, data, params);
|
||||
case "RSA-PSS":
|
||||
return this._signRSA(key, data, params);
|
||||
default:
|
||||
throw new Err(ErrorCode.NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
async verify(key: HMACKey, signature: Uint8Array, data: Uint8Array, params: HMACParams): Promise<boolean>;
|
||||
async verify(
|
||||
key: RSAPrivateKey,
|
||||
signature: Uint8Array,
|
||||
data: Uint8Array,
|
||||
params: RSASigningParams
|
||||
): Promise<boolean>;
|
||||
async verify(
|
||||
key: HMACKey | RSAPrivateKey,
|
||||
signature: Uint8Array,
|
||||
data: Uint8Array,
|
||||
params: HMACParams | RSASigningParams
|
||||
): Promise<boolean> {
|
||||
switch (params.algorithm) {
|
||||
case "HMAC":
|
||||
return this._verifyHMAC(key, signature, data, params);
|
||||
case "RSA-PSS":
|
||||
return this._verifyRSA(key, signature, data, params);
|
||||
default:
|
||||
throw new Err(ErrorCode.NOT_SUPPORTED);
|
||||
}
|
||||
}
|
||||
|
||||
private async _signHMAC(key: HMACKey, data: Uint8Array, params: HMACParams): Promise<Uint8Array> {
|
||||
const p = Object.assign({}, params, { name: params.algorithm, length: params.keySize });
|
||||
const k = await webCrypto.importKey("raw", key, p, false, ["sign"]);
|
||||
const signature = await webCrypto.sign(p, k, data);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
|
||||
private async _verifyHMAC(
|
||||
key: HMACKey,
|
||||
signature: Uint8Array,
|
||||
data: Uint8Array,
|
||||
params: HMACParams
|
||||
): Promise<boolean> {
|
||||
const p = Object.assign({}, params, { name: params.algorithm, length: params.keySize });
|
||||
const k = await webCrypto.importKey("raw", key, p, false, ["verify"]);
|
||||
return await webCrypto.verify(p, k, signature, data);
|
||||
}
|
||||
|
||||
private async _signRSA(key: RSAPrivateKey, data: Uint8Array, params: RSASigningParams): Promise<Uint8Array> {
|
||||
const p = Object.assign({}, params, { name: params.algorithm });
|
||||
const k = await webCrypto.importKey("pkcs8", key, p, false, ["sign"]);
|
||||
const signature = await webCrypto.sign(p, k, data);
|
||||
return new Uint8Array(signature);
|
||||
}
|
||||
|
||||
private async _verifyRSA(
|
||||
key: RSAPublicKey,
|
||||
signature: Uint8Array,
|
||||
data: Uint8Array,
|
||||
params: RSASigningParams
|
||||
): Promise<boolean> {
|
||||
const p = Object.assign({}, params, { name: params.algorithm });
|
||||
const k = await webCrypto.importKey("spki", key, p, false, ["verify"]);
|
||||
return await webCrypto.verify(p, k, signature, data);
|
||||
}
|
||||
}
|
||||
|
||||
export default WebCryptoProvider;
|
|
@ -0,0 +1,87 @@
|
|||
import { translate as $l } from "@padloc/locale/src/translate";
|
||||
import { BaseElement } from "../elements/base";
|
||||
import "../elements/generator";
|
||||
import "../elements/alert-dialog";
|
||||
import "../elements/prompt-dialog";
|
||||
import "../elements/export-dialog";
|
||||
import { AlertDialog, AlertOptions } from "../elements/alert-dialog";
|
||||
import { PromptDialog, PromptOptions } from "../elements/prompt-dialog";
|
||||
import { getSingleton } from "./singleton";
|
||||
|
||||
let lastDialogPromise = Promise.resolve();
|
||||
let currentDialog: any;
|
||||
|
||||
export const getDialog = getSingleton;
|
||||
|
||||
export function lineUpDialog(d: string | any, fn: (d: any) => Promise<any>): Promise<any> {
|
||||
const dialog = typeof d === "string" ? getSingleton(d) : d;
|
||||
const promise = lastDialogPromise.then(() => {
|
||||
currentDialog = dialog;
|
||||
return fn(dialog);
|
||||
});
|
||||
|
||||
lastDialogPromise = promise;
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
export function alert(message: string, options?: AlertOptions, instant = false): Promise<number> {
|
||||
options = options || {};
|
||||
options.message = message;
|
||||
return instant
|
||||
? getDialog("pl-alert-dialog").show(options)
|
||||
: lineUpDialog("pl-alert-dialog", (dialog: AlertDialog) => dialog.show(options));
|
||||
}
|
||||
|
||||
export function confirm(
|
||||
message: string,
|
||||
confirmLabel = $l("Confirm"),
|
||||
cancelLabel = $l("Cancel"),
|
||||
options: any = {},
|
||||
instant?: boolean
|
||||
) {
|
||||
options.options = [confirmLabel, cancelLabel];
|
||||
options.type = options.type || "question";
|
||||
options.horizontal = typeof options.horizontal !== "undefined" ? options.horizontal : true;
|
||||
return alert(message, options, instant).then(choice => choice === 0);
|
||||
}
|
||||
|
||||
export function prompt(message: string, opts: PromptOptions = {}, instant = false) {
|
||||
opts.message = message;
|
||||
return instant
|
||||
? getDialog("pl-prompt-dialog").show(opts)
|
||||
: lineUpDialog("pl-prompt-dialog", (dialog: PromptDialog) => dialog.show(opts));
|
||||
}
|
||||
|
||||
export function choose(message: string, options: string[], opts: AlertOptions = {}): Promise<number> {
|
||||
opts.options = options;
|
||||
return alert(message, {
|
||||
...opts,
|
||||
options,
|
||||
type: "choice",
|
||||
vertical: true
|
||||
});
|
||||
}
|
||||
|
||||
export function generate() {
|
||||
return lineUpDialog("pl-generator", dialog => dialog.show());
|
||||
}
|
||||
|
||||
export function clearDialogs() {
|
||||
if (currentDialog) {
|
||||
currentDialog.open = false;
|
||||
}
|
||||
lastDialogPromise = Promise.resolve();
|
||||
}
|
||||
|
||||
export function dialog(name: string) {
|
||||
return (prototype: BaseElement, propertyName: string) => {
|
||||
Object.defineProperty(prototype, propertyName, {
|
||||
get() {
|
||||
return getDialog(name);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
import { VaultItem } from "@padloc/core/src/item";
|
||||
import { PBES2Container } from "@padloc/core/src/container";
|
||||
import { marshal, stringToBytes } from "@padloc/core/src/encoding";
|
||||
import { loadPapa, ImportFormat, CSV, PBES2 } from "./import";
|
||||
|
||||
export const supportedFormats: ImportFormat[] = [CSV, PBES2];
|
||||
export { CSV, PBES2 } from "./import";
|
||||
|
||||
function itemsToTable(items: VaultItem[]) {
|
||||
// Array of column names
|
||||
let cols = ["name", "tags"];
|
||||
// Column indizes associated with field/column names
|
||||
let colInds = {};
|
||||
// Two dimensional array, starting with column names
|
||||
let table = [cols];
|
||||
|
||||
// Fill up columns array with distinct field names
|
||||
for (let item of items) {
|
||||
for (let field of item.fields) {
|
||||
if (!colInds[field.name]) {
|
||||
colInds[field.name] = cols.length;
|
||||
cols.push(field.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Creates an array of empty strings with the length of the `cols` array
|
||||
function emptyRow() {
|
||||
var l = cols.length;
|
||||
var row: string[] = [];
|
||||
while (l--) {
|
||||
row.push("");
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
// Add a row for each item
|
||||
items.forEach(function(item) {
|
||||
// Create an empty row to be filled with item name, category and field values
|
||||
var row = emptyRow();
|
||||
// VaultItem name and category are always the first and second column respectively
|
||||
row[0] = item.name;
|
||||
row[1] = item.tags.join(",");
|
||||
|
||||
// Fill up columns with corrensponding field values if the fields exist on the item. All
|
||||
// other columns remain empty
|
||||
item.fields.forEach(function(item) {
|
||||
row[colInds[item.name]] = item.value;
|
||||
});
|
||||
|
||||
// Add row to table
|
||||
table.push(row);
|
||||
});
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
export async function asCSV(items: VaultItem[]): Promise<string> {
|
||||
const papa = await loadPapa();
|
||||
return papa.unparse(itemsToTable(items));
|
||||
}
|
||||
|
||||
export async function asPBES2Container(items: VaultItem[], password: string): Promise<string> {
|
||||
const container = new PBES2Container();
|
||||
await container.unlock(password);
|
||||
await container.setData(stringToBytes(marshal({ items })));
|
||||
return container.toJSON();
|
||||
}
|
|
@ -0,0 +1,262 @@
|
|||
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 { Err, ErrorCode } from "@padloc/core/src/error";
|
||||
import { uuid } from "@padloc/core/src/util";
|
||||
import { translate as $l } from "@padloc/locale/src/translate";
|
||||
|
||||
export interface ImportFormat {
|
||||
format: "csv" | "padlock-legacy" | "lastpass" | "padloc";
|
||||
toString(): string;
|
||||
}
|
||||
|
||||
export const CSV: ImportFormat = {
|
||||
format: "csv",
|
||||
toString() {
|
||||
return "CSV";
|
||||
}
|
||||
};
|
||||
|
||||
export const PADLOCK_LEGACY: ImportFormat = {
|
||||
format: "padlock-legacy",
|
||||
toString() {
|
||||
return "Padlock (v2)";
|
||||
}
|
||||
};
|
||||
|
||||
export const LASTPASS: ImportFormat = {
|
||||
format: "lastpass",
|
||||
toString() {
|
||||
return "LastPass";
|
||||
}
|
||||
};
|
||||
|
||||
export const PBES2: ImportFormat = {
|
||||
format: "padloc",
|
||||
toString() {
|
||||
return "Encrypted Container";
|
||||
}
|
||||
};
|
||||
|
||||
export const supportedFormats: ImportFormat[] = [CSV, PADLOCK_LEGACY, LASTPASS, PBES2];
|
||||
|
||||
export function loadPapa(): Promise<any> {
|
||||
return import(/* webpackChunkName: "papaparse" */ "papaparse");
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a data table (represented by a two-dimensional array) and converts it
|
||||
* into an array of items
|
||||
* @param Array data Two-dimensional array containing tabular item data; The first 'row'
|
||||
* should contain field names. All other rows represent items, containing
|
||||
* the item name, field values and optionally a list of tags.
|
||||
* @param Integer nameColIndex Index of the column containing the item names. Defaults to 0
|
||||
* @param Integer tagsColIndex Index of the column containing the item categories. If left empty
|
||||
* no categories will be used
|
||||
*/
|
||||
export async function fromTable(data: string[][], nameColIndex?: number, tagsColIndex?: number): Promise<VaultItem[]> {
|
||||
// Use first row for column names
|
||||
const colNames = data[0];
|
||||
|
||||
if (nameColIndex === undefined) {
|
||||
const i = colNames.indexOf("name");
|
||||
nameColIndex = i !== -1 ? i : 0;
|
||||
}
|
||||
|
||||
if (tagsColIndex === undefined) {
|
||||
tagsColIndex = colNames.indexOf("tags");
|
||||
if (tagsColIndex === -1) {
|
||||
tagsColIndex = colNames.indexOf("category");
|
||||
}
|
||||
}
|
||||
|
||||
// All subsequent rows should contain values
|
||||
let items = data.slice(1).map(function(row) {
|
||||
// Construct an array of field object from column names and values
|
||||
let fields: Field[] = [];
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
// Skip name column, category column (if any) and empty fields
|
||||
if (i != nameColIndex && i != tagsColIndex && row[i]) {
|
||||
const name = colNames[i];
|
||||
const value = row[i];
|
||||
fields.push({
|
||||
name,
|
||||
value,
|
||||
type: guessFieldType({ name, value })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const name = row[nameColIndex!];
|
||||
const tags = row[tagsColIndex!];
|
||||
return createVaultItem(name, fields, (tags && tags.split(",")) || []);
|
||||
});
|
||||
|
||||
return Promise.all(items);
|
||||
}
|
||||
|
||||
export async function isCSV(data: string): Promise<Boolean> {
|
||||
const papa = await loadPapa();
|
||||
return papa.parse(data).errors.length === 0;
|
||||
}
|
||||
|
||||
export async function asCSV(data: string, nameColIndex?: number, tagsColIndex?: number): Promise<VaultItem[]> {
|
||||
const papa = await loadPapa();
|
||||
const parsed = papa.parse(data);
|
||||
if (parsed.errors.length) {
|
||||
throw new Err(ErrorCode.INVALID_CSV);
|
||||
}
|
||||
return fromTable(parsed.data, nameColIndex, tagsColIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given string represents a Padlock enrypted backup
|
||||
*/
|
||||
export function isPadlockV1(data: string): boolean {
|
||||
try {
|
||||
return validateLegacyContainer(unmarshal(data));
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function asPadlockLegacy(data: string, password: string): Promise<VaultItem[]> {
|
||||
const container = parseLegacyContainer(unmarshal(data));
|
||||
await container.unlock(password);
|
||||
|
||||
const records = unmarshal(bytesToString(await container.getData())) as any[];
|
||||
const items = records
|
||||
.filter(({ removed }) => !removed)
|
||||
.map(async record => {
|
||||
return {
|
||||
id: await uuid(),
|
||||
name: record.name,
|
||||
fields: record.fields,
|
||||
tags: record.tags || [record.category],
|
||||
updated: new Date(record.updated),
|
||||
lastUsed: new Date(record.lastUsed),
|
||||
updatedBy: "",
|
||||
attachments: [],
|
||||
favorited: []
|
||||
};
|
||||
});
|
||||
|
||||
return Promise.all(items);
|
||||
}
|
||||
|
||||
export function isPBES2Container(data: string) {
|
||||
try {
|
||||
new PBES2Container().fromRaw(unmarshal(data));
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function asPBES2Container(data: string, password: string): Promise<VaultItem[]> {
|
||||
const container = new PBES2Container().fromRaw(unmarshal(data));
|
||||
await container.unlock(password);
|
||||
|
||||
const raw = unmarshal(bytesToString(await container.getData())) as any;
|
||||
const items = raw.items.map((item: any) => {
|
||||
return {
|
||||
...item,
|
||||
updated: new Date(item.updated),
|
||||
lastUsed: new Date(item.lastUsed)
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
/*
|
||||
* Lastpass secure notes are exported by putting non-standard fields into the 'extra' column. Every line
|
||||
* represents a field in the following format:
|
||||
*
|
||||
* field_name:data
|
||||
*
|
||||
* We're parsing that information to retrieve the individual fields
|
||||
*/
|
||||
function lpParseNotes(str: string): Field[] {
|
||||
let lines = str.split("\n");
|
||||
let fields = lines
|
||||
.filter(line => !!line)
|
||||
.map(line => {
|
||||
let split = line.indexOf(":");
|
||||
return {
|
||||
name: line.substring(0, split),
|
||||
value: line.substring(split + 1),
|
||||
type: "text"
|
||||
} as Field;
|
||||
});
|
||||
return fields;
|
||||
}
|
||||
|
||||
/*
|
||||
* Parses a single row in a LastPass CSV file. Apart from extracting the default fields, we also parse
|
||||
* the 'extra' column for 'special notes' and remove any special fields that are not needed outside of
|
||||
* LastPass
|
||||
*/
|
||||
async function lpParseRow(row: string[]): Promise<VaultItem> {
|
||||
const nameIndex = 4;
|
||||
const categoryIndex = 5;
|
||||
const urlIndex = 0;
|
||||
const usernameIndex = 1;
|
||||
const passwordIndex = 2;
|
||||
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" }
|
||||
];
|
||||
let notes = row[notesIndex];
|
||||
|
||||
if (row[urlIndex] === "http://sn") {
|
||||
// The 'http://sn' url indicates that this line represents a 'secure note', which means
|
||||
// we'll have to parse the 'extra' column to retrieve the individual fields
|
||||
fields.push(...lpParseNotes(notes));
|
||||
// In case of 'secure notes' we don't want the url and NoteType field
|
||||
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" });
|
||||
}
|
||||
|
||||
const dir = row[categoryIndex];
|
||||
// Create a basic item using the standard fields
|
||||
return createVaultItem(row[nameIndex], fields, dir ? [dir] : []);
|
||||
}
|
||||
|
||||
export async function asLastPass(data: string): Promise<VaultItem[]> {
|
||||
const papa = await loadPapa();
|
||||
let items = papa
|
||||
.parse(data)
|
||||
.data // Remove first row as it only contains field names
|
||||
.slice(1)
|
||||
// Filter out empty rows
|
||||
.filter((row: string[]) => row.length > 1)
|
||||
.map(lpParseRow);
|
||||
|
||||
return Promise.all(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a given string represents a LastPass CSV file
|
||||
*/
|
||||
export function isLastPass(data: string): boolean {
|
||||
return data.split("\n")[0] === "url,username,password,extra,name,grouping,fav";
|
||||
}
|
||||
|
||||
export function guessFormat(data: string): ImportFormat | null {
|
||||
return isPBES2Container(data)
|
||||
? PBES2
|
||||
: isPadlockV1(data)
|
||||
? PADLOCK_LEGACY
|
||||
: isLastPass(data)
|
||||
? LASTPASS
|
||||
: isCSV(data)
|
||||
? CSV
|
||||
: null;
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
import { Platform, StubPlatform, DeviceInfo } from "@padloc/core/src/platform";
|
||||
import { WebCryptoProvider } from "./crypto";
|
||||
|
||||
const browserInfo = (async () => {
|
||||
const { UAParser } = await import(/* webpackChunkName: "ua-parser" */ "ua-parser-js");
|
||||
return new UAParser(navigator.userAgent).getResult();
|
||||
})();
|
||||
|
||||
export class WebPlatform extends StubPlatform implements Platform {
|
||||
private _clipboardTextArea: HTMLTextAreaElement;
|
||||
private _qrVideo: HTMLVideoElement;
|
||||
private _qrCanvas: HTMLCanvasElement;
|
||||
|
||||
crypto = new WebCryptoProvider();
|
||||
|
||||
// Set clipboard text using `document.execCommand("cut")`.
|
||||
// NOTE: This only works in certain environments like Google Chrome apps with the appropriate permissions set
|
||||
async setClipboard(text: string): Promise<void> {
|
||||
this._clipboardTextArea = this._clipboardTextArea || document.createElement("textarea");
|
||||
this._clipboardTextArea.contentEditable = "true";
|
||||
this._clipboardTextArea.readOnly = false;
|
||||
this._clipboardTextArea.value = text;
|
||||
document.body.appendChild(this._clipboardTextArea);
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(this._clipboardTextArea);
|
||||
|
||||
const s = window.getSelection();
|
||||
s!.removeAllRanges();
|
||||
s!.addRange(range);
|
||||
this._clipboardTextArea.select();
|
||||
|
||||
this._clipboardTextArea.setSelectionRange(0, this._clipboardTextArea.value.length); // A big number, to cover anything that could be inside the element.
|
||||
|
||||
document.execCommand("cut");
|
||||
document.body.removeChild(this._clipboardTextArea);
|
||||
}
|
||||
|
||||
// Get clipboard text using `document.execCommand("paste")`
|
||||
// NOTE: This only works in certain environments like Google Chrome apps with the appropriate permissions set
|
||||
async getClipboard(): Promise<string> {
|
||||
this._clipboardTextArea = this._clipboardTextArea || document.createElement("textarea");
|
||||
document.body.appendChild(this._clipboardTextArea);
|
||||
this._clipboardTextArea.value = "";
|
||||
this._clipboardTextArea.select();
|
||||
document.execCommand("paste");
|
||||
document.body.removeChild(this._clipboardTextArea);
|
||||
return this._clipboardTextArea.value;
|
||||
}
|
||||
|
||||
async getDeviceInfo() {
|
||||
const { os, browser } = await browserInfo;
|
||||
return new DeviceInfo({
|
||||
platform: (os.name && os.name.replace(" ", "")) || "",
|
||||
osVersion: (os.version && os.version.replace(" ", "")) || "",
|
||||
id: "",
|
||||
appVersion: process.env.PL_VERSION || "",
|
||||
manufacturer: "",
|
||||
model: "",
|
||||
browser: browser.name || "",
|
||||
userAgent: navigator.userAgent,
|
||||
locale: navigator.language || "en"
|
||||
});
|
||||
}
|
||||
|
||||
async scanQR() {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const tick = async () => {
|
||||
if (this._qrVideo.readyState !== this._qrVideo.HAVE_ENOUGH_DATA) {
|
||||
requestAnimationFrame(() => tick());
|
||||
return;
|
||||
}
|
||||
|
||||
const { default: jsQR } = await import(/* webpackChunkName: "jsqr" */ "jsqr");
|
||||
|
||||
const canvas = this._qrCanvas.getContext("2d")!;
|
||||
this._qrCanvas.height = this._qrVideo.videoHeight;
|
||||
this._qrCanvas.width = this._qrVideo.videoWidth;
|
||||
canvas.drawImage(this._qrVideo, 0, 0, this._qrCanvas.width, this._qrCanvas.height);
|
||||
const imageData = canvas.getImageData(0, 0, this._qrCanvas.width, this._qrCanvas.height);
|
||||
const code = jsQR(imageData.data, imageData.width, imageData.height, {
|
||||
inversionAttempts: "dontInvert"
|
||||
});
|
||||
if (code) {
|
||||
resolve(code.data);
|
||||
}
|
||||
requestAnimationFrame(() => tick());
|
||||
};
|
||||
|
||||
if (!this._qrVideo) {
|
||||
this._qrVideo = document.createElement("video");
|
||||
this._qrVideo.setAttribute("playsinline", "");
|
||||
this._qrVideo.setAttribute("muted", "");
|
||||
this._qrVideo.setAttribute("autoplay", "");
|
||||
}
|
||||
|
||||
if (!this._qrCanvas) {
|
||||
this._qrCanvas = document.createElement("canvas");
|
||||
Object.assign(this._qrCanvas.style, {
|
||||
position: "absolute",
|
||||
top: "0",
|
||||
left: "0",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
zIndex: "-1"
|
||||
});
|
||||
document.body.appendChild(this._qrCanvas);
|
||||
}
|
||||
|
||||
this._qrCanvas.style.display = "block";
|
||||
|
||||
navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: "environment" } }).then(stream => {
|
||||
// Use facingMode: environment to attemt to get the front camera on phones
|
||||
this._qrVideo.srcObject = stream;
|
||||
this._qrVideo.play();
|
||||
requestAnimationFrame(() => tick());
|
||||
}, reject);
|
||||
});
|
||||
}
|
||||
|
||||
async stopScanQR() {
|
||||
const stream: MediaStream | null = this._qrVideo && (this._qrVideo.srcObject as MediaStream);
|
||||
if (stream) {
|
||||
for (const track of stream.getTracks()) {
|
||||
track.stop();
|
||||
}
|
||||
}
|
||||
|
||||
this._qrVideo && (this._qrVideo.srcObject = null);
|
||||
this._qrCanvas.style.display = "none";
|
||||
}
|
||||
|
||||
async composeEmail(addr: string, subj: string, msg: string) {
|
||||
window.open(`mailto:${addr}?subject=${encodeURIComponent(subj)}&body=${encodeURIComponent(msg)}`, "_system");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
import { EventEmitter } from "@padloc/core/src/event-target";
|
||||
|
||||
export class Router extends EventEmitter {
|
||||
history: string[] = [];
|
||||
|
||||
constructor(public basePath = "/") {
|
||||
super();
|
||||
window.addEventListener("popstate", () => {
|
||||
this._pathChanged();
|
||||
});
|
||||
this._pathChanged();
|
||||
}
|
||||
|
||||
private _pathChanged() {
|
||||
const index = (history.state && history.state.historyIndex) || 0;
|
||||
const path = this.path;
|
||||
const direction = this.history.length - 1 < index ? "forward" : "backward";
|
||||
|
||||
if (this.history.length === index) {
|
||||
this.history.push(path);
|
||||
} else
|
||||
while (this.history.length - 1 > index) {
|
||||
this.history.pop();
|
||||
}
|
||||
|
||||
this.dispatch("route-changed", { path, direction });
|
||||
}
|
||||
|
||||
get path() {
|
||||
return window.location.pathname.replace(new RegExp("^" + this.basePath), "");
|
||||
}
|
||||
|
||||
get params() {
|
||||
const params = {};
|
||||
for (const [key, value] of new URLSearchParams(window.location.search)) {
|
||||
params[key] = value;
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
set params(params: { [prop: string]: string }) {
|
||||
history.pushState(
|
||||
{ historyIndex: this.history.length - 1 },
|
||||
"",
|
||||
this.basePath + this.path + "?" + new URLSearchParams(params).toString()
|
||||
);
|
||||
}
|
||||
|
||||
get canGoBack() {
|
||||
return this.history.length > 1;
|
||||
}
|
||||
|
||||
go(path: string, params?: { [prop: string]: string }) {
|
||||
const queryString = new URLSearchParams(params || this.params).toString();
|
||||
|
||||
if (path !== this.path || queryString !== window.location.search) {
|
||||
let url = this.basePath + path;
|
||||
if (queryString) {
|
||||
url += "?" + queryString;
|
||||
}
|
||||
|
||||
history.pushState({ historyIndex: this.history.length }, "", url);
|
||||
this._pathChanged();
|
||||
}
|
||||
}
|
||||
|
||||
forward() {
|
||||
history.forward();
|
||||
}
|
||||
|
||||
back(alternate = "") {
|
||||
if (this.canGoBack) {
|
||||
history.back();
|
||||
} else {
|
||||
this.go(alternate);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
const singletons = {};
|
||||
let container: HTMLElement;
|
||||
|
||||
export function getSingleton(elName: string) {
|
||||
if (!container) {
|
||||
container = document.querySelector("pl-app") as HTMLElement;
|
||||
}
|
||||
|
||||
let el = singletons[elName];
|
||||
|
||||
if (!el) {
|
||||
singletons[elName] = el = document.createElement(elName);
|
||||
container.appendChild(el);
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { Storage, Storable, StorableConstructor } from "@padloc/core/src/storage";
|
||||
import { Err, ErrorCode } from "@padloc/core/src/error";
|
||||
// @ts-ignore
|
||||
import localStorage from "localforage/src/localforage";
|
||||
|
||||
export class LocalStorage implements Storage {
|
||||
async save(s: Storable) {
|
||||
await localStorage.setItem(`${s.kind}_${s.id}`, s.toRaw());
|
||||
}
|
||||
|
||||
async get<T extends Storable>(cls: T | StorableConstructor<T>, id: string) {
|
||||
const s = cls instanceof Storable ? cls : new cls();
|
||||
const data = await localStorage.getItem(`${s.kind}_${id}`);
|
||||
if (!data) {
|
||||
throw new Err(ErrorCode.NOT_FOUND);
|
||||
}
|
||||
return s.fromRaw(data);
|
||||
}
|
||||
|
||||
async delete(s: Storable) {
|
||||
await localStorage.removeItem(`${s.kind}_${s.id}`);
|
||||
}
|
||||
|
||||
async clear() {
|
||||
await localStorage.clear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
const loaded: Map<string, Promise<any>> = new Map<string, Promise<any>>();
|
||||
export function loadScript(src: string, global?: string): Promise<any> {
|
||||
if (loaded.has(src)) {
|
||||
return loaded.get(src)!;
|
||||
}
|
||||
|
||||
const s = document.createElement("script");
|
||||
s.src = src;
|
||||
s.type = "text/javascript";
|
||||
const p = new Promise((resolve, reject) => {
|
||||
s.onload = () => resolve(global ? window[global] : undefined);
|
||||
s.onerror = e => reject(e);
|
||||
document.head.appendChild(s);
|
||||
});
|
||||
|
||||
loaded.set(src, p);
|
||||
return p;
|
||||
}
|
||||
|
||||
export async function formatDateFromNow(date: Date | string | number, addSuffix = true) {
|
||||
const { distanceInWordsToNow } = await import(/* webpackChunkName: "date-fns" */ "date-fns");
|
||||
return distanceInWordsToNow(date, { addSuffix });
|
||||
}
|
||||
|
||||
export async function passwordStrength(pwd: string): Promise<{ score: number }> {
|
||||
// @ts-ignore
|
||||
const { default: zxcvbn } = await import(/* webpackChunkName: "zxcvbn" */ "zxcvbn");
|
||||
return zxcvbn(pwd);
|
||||
}
|
||||
|
||||
export function toggleAttribute(el: Element, attr: string, on: boolean) {
|
||||
if (on) {
|
||||
el.setAttribute(attr, "");
|
||||
} else {
|
||||
el.removeAttribute(attr);
|
||||
}
|
||||
}
|
||||
|
||||
export function mediaType(mimeType: string) {
|
||||
const match = mimeType.match(/(.*)\/(.*)/);
|
||||
const [, type, subtype] = match || ["", "", ""];
|
||||
|
||||
switch (type) {
|
||||
case "video":
|
||||
return "video";
|
||||
case "audio":
|
||||
return "audio";
|
||||
case "image":
|
||||
return "image";
|
||||
case "text":
|
||||
switch (subtype) {
|
||||
case "csv":
|
||||
// return "csv";
|
||||
case "plain":
|
||||
return "text";
|
||||
default:
|
||||
return "code";
|
||||
}
|
||||
case "application":
|
||||
switch (subtype) {
|
||||
case "pdf":
|
||||
return "pdf";
|
||||
case "json":
|
||||
return "code";
|
||||
case "pkcs8":
|
||||
case "pkcs10":
|
||||
case "pkix-cert":
|
||||
case "pkix-crl":
|
||||
case "pkcs7-mime":
|
||||
case "x-x509-ca-cert":
|
||||
case "x-x509-user-cert":
|
||||
case "x-pkcs12":
|
||||
case "x-pkcs7-certificates":
|
||||
case "x-pkcs7-mime":
|
||||
case "x-pkcs7-crl":
|
||||
case "x-pem-file":
|
||||
case "x-pkcs12":
|
||||
case "x-pkcs7-certreqresp":
|
||||
return "certificate";
|
||||
case "zip":
|
||||
case "x-7z-compressed":
|
||||
case "x-freearc":
|
||||
case "x-bzip":
|
||||
case "x-bzip2":
|
||||
case "java-archive":
|
||||
case "x-rar-compressed":
|
||||
case "x-tar":
|
||||
return "archive";
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function fileIcon(mimeType: string) {
|
||||
const mType = mediaType(mimeType);
|
||||
return mType ? `file-${mType}` : "file";
|
||||
}
|
||||
|
||||
export function fileSize(size: number = 0) {
|
||||
return size < 1e6 ? Math.ceil(size / 10) / 100 + " KB" : Math.ceil(size / 10000) / 100 + " MB";
|
||||
}
|
||||
|
||||
export function mask(value: string): string {
|
||||
return value && value.replace(/[^\n]/g, "\u2022");
|
||||
}
|
||||
|
||||
export function isTouch(): boolean {
|
||||
return window.matchMedia("(hover: none)").matches;
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
importScripts("https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js");
|
||||
|
||||
declare const workbox: typeof import("workbox-sw");
|
||||
declare const __WB_MANIFEST: any;
|
||||
|
||||
workbox.precaching.precacheAndRoute(__WB_MANIFEST);
|
||||
// @ts-ignore
|
||||
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST);
|
||||
|
||||
workbox.routing.registerNavigationRoute(workbox.precaching.getCacheKeyForURL("index.html"));
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@padloc/core",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.5",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@padloc/locale",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.5",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 73 KiB |
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "@padloc/pwa",
|
||||
"version": "3.0.5",
|
||||
"author": "Martin Kleinschrodt <martin@maklesoft.com>",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@padloc/core": "^3.0.0",
|
||||
"@padloc/app": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"css-loader": "^3.0.0",
|
||||
"favicons-webpack-plugin": "^1.0.1",
|
||||
"file-loader": "^4.0.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-server": "^0.11.1",
|
||||
"style-loader": "^1.0.0",
|
||||
"ts-loader": "^6.0.4",
|
||||
"ts-node": "^7.0.1",
|
||||
"typescript": "^3.5.2",
|
||||
"webpack": "^4.35.3",
|
||||
"webpack-cli": "^3.3.5",
|
||||
"webpack-dev-server": "^3.7.2",
|
||||
"webpack-pwa-manifest": "^4.0.0",
|
||||
"workbox-cli": "^4.3.1",
|
||||
"workbox-webpack-plugin": "^5.0.0-alpha.0"
|
||||
},
|
||||
"description": "Padloc Progressive Web App",
|
||||
"scripts": {
|
||||
"build": "webpack",
|
||||
"dev": "webpack-dev-server",
|
||||
"start": "http-server $PL_PWA_DIR -s -p $PL_PWA_PORT --proxy $PL_PWA_URL?",
|
||||
"build_and_start": "npm run build && npm start"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
import { setPlatform } from "@padloc/core/src/platform";
|
||||
import { WebPlatform } from "@padloc/app/src/lib/platform";
|
||||
import "@padloc/app/src/elements/app";
|
||||
|
||||
setPlatform(new WebPlatform());
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"extends": "../app/tsconfig.json"
|
||||
}
|
|
@ -7,10 +7,12 @@ const WebpackPwaManifest = require("webpack-pwa-manifest");
|
|||
const FaviconsWebpackPlugin = require("favicons-webpack-plugin");
|
||||
const { version } = require("./package.json");
|
||||
|
||||
const out = process.env.PL_PWA_DIR || path.resolve(__dirname, "dist");
|
||||
|
||||
module.exports = {
|
||||
entry: path.resolve(__dirname, "src/index.ts"),
|
||||
output: {
|
||||
path: path.resolve(__dirname, "dist"),
|
||||
path: out,
|
||||
filename: "[name].js",
|
||||
chunkFilename: "[name].chunk.js",
|
||||
publicPath: "/"
|
||||
|
@ -49,7 +51,7 @@ module.exports = {
|
|||
new CleanWebpackPlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
title: "Padloc",
|
||||
template: path.resolve(__dirname, "index.html"),
|
||||
template: path.resolve(__dirname, "src/index.html"),
|
||||
meta: {
|
||||
"Content-Security-Policy": {
|
||||
"http-equiv": "Content-Security-Policy",
|
||||
|
@ -61,7 +63,7 @@ module.exports = {
|
|||
}
|
||||
}
|
||||
}),
|
||||
new FaviconsWebpackPlugin(path.resolve(__dirname, "assets/icons/512.png")),
|
||||
new FaviconsWebpackPlugin(path.resolve(__dirname, "assets/icon-512.png")),
|
||||
new WebpackPwaManifest({
|
||||
name: "Padloc Password Manager",
|
||||
short_name: "Padloc",
|
||||
|
@ -69,13 +71,13 @@ module.exports = {
|
|||
theme: "#59c6ff",
|
||||
icons: [
|
||||
{
|
||||
src: path.resolve(__dirname, "assets/icons/512.png"),
|
||||
src: path.resolve(__dirname, "assets/icon-512.png"),
|
||||
sizes: [96, 128, 192, 256, 384, 512]
|
||||
}
|
||||
]
|
||||
}),
|
||||
new InjectManifest({
|
||||
swSrc: path.resolve(__dirname, "src/sw.ts"),
|
||||
swSrc: path.resolve(__dirname, "../app/src/sw.ts"),
|
||||
swDest: "sw.js",
|
||||
exclude: [/favicon\.png$/, /\.map$/]
|
||||
})
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@padloc/server",
|
||||
"version": "3.0.0",
|
||||
"version": "3.0.5",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
|
|
@ -12,7 +12,7 @@ async function init() {
|
|||
setPlatform(new NodePlatform());
|
||||
|
||||
const config = new ServerConfig({
|
||||
clientUrl: process.env.PL_CLIENT_URL || "https://localhost:8081",
|
||||
clientUrl: process.env.PL_PWA_PORT || "https://localhost:8081",
|
||||
reportErrors: process.env.PL_REPORT_ERRORS || "",
|
||||
mfa: (process.env.PL_MFA as ("email" | "none")) || "email",
|
||||
accountQuota: {
|
||||
|
|
Loading…
Reference in New Issue