diff --git a/.vscode/launch.json b/.vscode/launch.json index 598ee3d..37ba873 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -9,4 +9,4 @@ "webRoot": "${workspaceFolder}" } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 35c2d53..69bde91 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "arraybuffer", "esnext", "firstseen", + "formik", "keepalive", "motd", "tsdoc" diff --git a/package-lock.json b/package-lock.json index caa9570..34560ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1437,18 +1437,18 @@ } }, "@microsoft/tsdoc": { - "version": "0.12.20", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.20.tgz", - "integrity": "sha512-/b13m37QZYPV6nCOiqkFyvlQjlTNvAcQpgFZ6ZKIqtStJxNdqVo/frULubxMUMWi6p9Uo5f4BRlguv5ViFcL0A==", + "version": "0.12.21", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.12.21.tgz", + "integrity": "sha512-j+9OJ0A0buZZaUn6NxeHUVpoa05tY2PgVs7kXJhJQiKRB0G1zQqbJxer3T7jWtzpqQWP89OBDluyIeyTsMk8Sg==", "dev": true }, "@microsoft/tsdoc-config": { - "version": "0.13.5", - "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.5.tgz", - "integrity": "sha512-KlnIdTRnPSsU9Coz9wzDAkT8JCLopP3ec1sgsgo7trwE6QLMKRpM4hZi2uzVX897SW49Q4f439auGBcQLnZQfA==", + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.13.6.tgz", + "integrity": "sha512-VJjV35PnrNISoX2WMemZjnCIdOUPTRpCz6pu8inISotLd3SgoDSJygGaE7+lOYdCtDl+4c8PWJdZivxxXgOnLw==", "dev": true, "requires": { - "@microsoft/tsdoc": "0.12.20", + "@microsoft/tsdoc": "0.12.21", "ajv": "~6.12.3", "jju": "~1.4.0", "resolve": "~1.12.0" @@ -1831,9 +1831,9 @@ "dev": true }, "@types/invariant": { - "version": "2.2.33", - "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.33.tgz", - "integrity": "sha512-/jUNmS8d4bCKdqslfxW6dg/9Gksfzxz67IYfqApHn+HvHlMVXwYv2zpTDnS/yaK9BB0i0GlBTaYci0EFE62Hmw==" + "version": "2.2.34", + "resolved": "https://registry.npmjs.org/@types/invariant/-/invariant-2.2.34.tgz", + "integrity": "sha512-lYUtmJ9BqUN688fGY1U1HZoWT1/Jrmgigx2loq4ZcJpICECm/Om3V314BxdzypO0u5PORKGMM6x0OXaljV1YFg==" }, "@types/istanbul-lib-coverage": { "version": "2.0.3", @@ -1877,9 +1877,9 @@ "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==" }, "@types/node": { - "version": "12.12.54", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.54.tgz", - "integrity": "sha512-ge4xZ3vSBornVYlDnk7yZ0gK6ChHf/CHB7Gl1I0Jhah8DDnEQqBzgohYG4FX4p81TNirSETOiSyn+y1r9/IR6w==" + "version": "12.12.55", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.12.55.tgz", + "integrity": "sha512-Vd6xQUVvPCTm7Nx1N7XHcpX6t047ltm7TgcsOr4gFHjeYgwZevo+V7I1lfzHnj5BT5frztZ42+RTG4MwYw63dw==" }, "@types/parse-json": { "version": "4.0.0", @@ -1897,9 +1897,9 @@ "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==" }, "@types/react": { - "version": "16.9.46", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.46.tgz", - "integrity": "sha512-dbHzO3aAq1lB3jRQuNpuZ/mnu+CdD3H0WVaaBQA8LTT3S33xhVBUj232T8M3tAhSWJs/D/UqORYUlJNl/8VQZg==", + "version": "16.9.49", + "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.49.tgz", + "integrity": "sha512-DtLFjSj0OYAdVLBbyjhuV9CdGVHCkHn2R+xr3XkBvK2rS1Y1tkc14XSGjYgm5Fjjr90AxH9tiSzc1pCFMGO06g==", "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2094,12 +2094,12 @@ "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==" }, "@typescript-eslint/eslint-plugin": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.9.1.tgz", - "integrity": "sha512-XIr+Mfv7i4paEdBf0JFdIl9/tVxyj+rlilWIfZ97Be0lZ7hPvUbS5iHt9Glc8kRI53dsr0PcAEudbf8rO2wGgg==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.10.1.tgz", + "integrity": "sha512-PQg0emRtzZFWq6PxBcdxRH3QIQiyFO3WCVpRL3fgj5oQS3CDs3AeAKfv4DxNhzn8ITdNJGJ4D3Qw8eAJf3lXeQ==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "3.9.1", + "@typescript-eslint/experimental-utils": "3.10.1", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", @@ -2108,45 +2108,45 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.9.1.tgz", - "integrity": "sha512-lkiZ8iBBaYoyEKhCkkw4SAeatXyBq9Ece5bZXdLe1LWBUwTszGbmbiqmQbwWA8cSYDnjWXp9eDbXpf9Sn0hLAg==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz", + "integrity": "sha512-DewqIgscDzmAfd5nOGe4zm6Bl7PKtMG2Ad0KG8CUZAHlXfAKTF9Ol5PXhiMh39yRL2ChRH1cuuUGOcVyyrhQIw==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/types": "3.9.1", - "@typescript-eslint/typescript-estree": "3.9.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.9.1.tgz", - "integrity": "sha512-y5QvPFUn4Vl4qM40lI+pNWhTcOWtpZAJ8pOEQ21fTTW4xTJkRplMjMRje7LYTXqVKKX9GJhcyweMz2+W1J5bMg==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-3.10.1.tgz", + "integrity": "sha512-Ug1RcWcrJP02hmtaXVS3axPPTTPnZjupqhgj+NnZ6BCkwSImWk/283347+x9wN+lqOdK9Eo3vsyiyDHgsmiEJw==", "dev": true, "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "3.9.1", - "@typescript-eslint/types": "3.9.1", - "@typescript-eslint/typescript-estree": "3.9.1", + "@typescript-eslint/experimental-utils": "3.10.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/typescript-estree": "3.10.1", "eslint-visitor-keys": "^1.1.0" } }, "@typescript-eslint/types": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.9.1.tgz", - "integrity": "sha512-15JcTlNQE1BsYy5NBhctnEhEoctjXOjOK+Q+rk8ugC+WXU9rAcS2BYhoh6X4rOaXJEpIYDl+p7ix+A5U0BqPTw==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-3.10.1.tgz", + "integrity": "sha512-+3+FCUJIahE9q0lDi1WleYzjCwJs5hIsbugIgnbB+dSCYUxl8L6PwmsyOPFZde2hc1DlTo/xnkOgiTLSyAbHiQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.9.1.tgz", - "integrity": "sha512-IqM0gfGxOmIKPhiHW/iyAEXwSVqMmR2wJ9uXHNdFpqVvPaQ3dWg302vW127sBpAiqM9SfHhyS40NKLsoMpN2KA==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz", + "integrity": "sha512-QbcXOuq6WYvnB3XPsZpIwztBoquEYLXh2MtwVU+kO8jgYCiv4G5xrSP/1wg4tkvrEE+esZVquIPX/dxPlePk1w==", "dev": true, "requires": { - "@typescript-eslint/types": "3.9.1", - "@typescript-eslint/visitor-keys": "3.9.1", + "@typescript-eslint/types": "3.10.1", + "@typescript-eslint/visitor-keys": "3.10.1", "debug": "^4.1.1", "glob": "^7.1.6", "is-glob": "^4.0.1", @@ -2156,9 +2156,9 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.9.1.tgz", - "integrity": "sha512-zxdtUjeoSh+prCpogswMwVUJfEFmCOjdzK9rpNjNBfm6EyPt99x3RrJoBOGZO23FCt0WPKUCOL5mb/9D5LjdwQ==", + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz", + "integrity": "sha512-9JgC82AaQeglebjZMgYR5wgmfUdUc+EitGUUMW8u2nDckaeimzW+VsoLV6FoimPv2id3VQzfjwBxEMVz08ameQ==", "dev": true, "requires": { "eslint-visitor-keys": "^1.1.0" @@ -2332,6 +2332,12 @@ "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, "abab": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.4.tgz", @@ -4670,6 +4676,11 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, + "deepmerge": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz", + "integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==" + }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -5684,13 +5695,13 @@ "integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==" }, "eslint-plugin-tsdoc": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.6.tgz", - "integrity": "sha512-pU6/VVEOlC85BrUjsqZGGSRy41N+PHfWXokqjpQRWT1LSpBsAEbRpsueNYSFS+93Sx9CFD0511kjLKVySRbLbg==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.2.7.tgz", + "integrity": "sha512-GAbNpwNfwnolagP6mCQT8wY4usifnAE/iuCz15L3BcEca0xAidctU61h7w40mOuNiSp78DYPUl5gwN89nJ8+8Q==", "dev": true, "requires": { - "@microsoft/tsdoc": "0.12.20", - "@microsoft/tsdoc-config": "0.13.5" + "@microsoft/tsdoc": "0.12.21", + "@microsoft/tsdoc-config": "0.13.6" } }, "eslint-scope": { @@ -6225,6 +6236,29 @@ "locate-path": "^3.0.0" } }, + "find-yarn-workspace-root": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz", + "integrity": "sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==", + "dev": true, + "requires": { + "fs-extra": "^4.0.3", + "micromatch": "^3.1.4" + }, + "dependencies": { + "fs-extra": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", + "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + } + } + }, "flat-cache": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", @@ -6333,6 +6367,32 @@ "mime-types": "^2.1.12" } }, + "formik": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/formik/-/formik-2.1.5.tgz", + "integrity": "sha512-bWpo3PiqVDYslvrRjTq0Isrm0mFXHiO33D8MS6t6dWcqSFGeYF52nlpCM2xwOJ6tRVRznDkL+zz/iHPL4LDuvQ==", + "requires": { + "deepmerge": "^2.1.1", + "hoist-non-react-statics": "^3.3.0", + "lodash": "^4.17.14", + "lodash-es": "^4.17.14", + "react-fast-compare": "^2.0.1", + "scheduler": "^0.18.0", + "tiny-warning": "^1.0.2", + "tslib": "^1.10.0" + }, + "dependencies": { + "scheduler": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz", + "integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + } + } + }, "forwarded": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", @@ -8335,6 +8395,15 @@ "is-buffer": "^1.1.5" } }, + "klaw-sync": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/klaw-sync/-/klaw-sync-6.0.0.tgz", + "integrity": "sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.11" + } + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -9905,6 +9974,45 @@ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, + "patch-package": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-6.2.2.tgz", + "integrity": "sha512-YqScVYkVcClUY0v8fF0kWOjDYopzIM8e3bj/RU1DPeEF14+dCGm6UeOYm4jvCyxqIEQ5/eJzmbWfDWnUleFNMg==", + "dev": true, + "requires": { + "@yarnpkg/lockfile": "^1.1.0", + "chalk": "^2.4.2", + "cross-spawn": "^6.0.5", + "find-yarn-workspace-root": "^1.2.1", + "fs-extra": "^7.0.1", + "is-ci": "^2.0.0", + "klaw-sync": "^6.0.0", + "minimist": "^1.2.0", + "rimraf": "^2.6.3", + "semver": "^5.6.0", + "slash": "^2.0.0", + "tmp": "^0.0.33" + }, + "dependencies": { + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + } + } + }, "path-browserify": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", @@ -10991,9 +11099,9 @@ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=" }, "prettier": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.5.tgz", - "integrity": "sha512-7PtVymN48hGcO4fGjybyBSIWDsLU4H4XlvOHfq91pz9kkGlonzwTfYkaIEwiRg/dAJF9YlbsduBAgtYLi+8cFg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz", + "integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==", "dev": true }, "pretty-bytes": { @@ -11526,6 +11634,11 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", "integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==" }, + "react-fast-compare": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz", + "integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==" + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 55eae45..c9c73a0 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "base64-arraybuffer": "^0.2.0", "bootstrap": "^4.5.2", "debug": "^4.1.1", + "formik": "^2.1.5", "prop-types": "^15.7.2", "react": "^16.13.1", "react-bootstrap": "^1.3.0", @@ -37,7 +38,8 @@ "storybook": "start-storybook -p 9009 -s public", "build-storybook": "build-storybook -s public", "font-install": "fontello-cli install --config src/fontello/config.json --css src/fontello/css --font src/fontello/font", - "font-open": "fontello-cli open --config src/fontello/config.json --css src/fontello/css --font src/fontello/font" + "font-open": "fontello-cli open --config src/fontello/config.json --css src/fontello/css --font src/fontello/font", + "postinstall": "patch-pacakge" }, "eslintConfig": { "extends": "react-app" @@ -60,23 +62,24 @@ "@testing-library/user-event": "^7.2.1", "@types/debug": "^4.1.5", "@types/jest": "^24.9.1", - "@types/node": "^12.12.54", + "@types/node": "^12.12.55", "@types/prop-types": "^15.7.3", - "@types/react": "^16.9.46", + "@types/react": "^16.9.49", "@types/react-dom": "^16.9.8", "@types/react-router-bootstrap": "^0.24.5", "@types/react-router-dom": "^5.1.5", "@types/semver": "^7.3.3", "@types/webpack-env": "^1.15.2", - "@typescript-eslint/eslint-plugin": "^3.9.1", - "@typescript-eslint/parser": "^3.9.1", + "@typescript-eslint/eslint-plugin": "^3.10.1", + "@typescript-eslint/parser": "^3.10.1", "craco-alias": "^2.1.1", "eslint": "^6.8.0", "eslint-config-prettier": "^6.11.0", "eslint-plugin-react": "^7.20.6", - "eslint-plugin-tsdoc": "^0.2.6", + "eslint-plugin-tsdoc": "^0.2.7", "node-sass": "^4.14.1", - "prettier": "^2.0.5" + "patch-package": "^6.2.2", + "prettier": "^2.1.1" }, "stylelint": { "extends": "stylelint-config-recommended", diff --git a/patches/react-bootstrap+1.3.0.patch b/patches/react-bootstrap+1.3.0.patch new file mode 100644 index 0000000..446c14a --- /dev/null +++ b/patches/react-bootstrap+1.3.0.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/react-bootstrap/esm/Form.d.ts b/node_modules/react-bootstrap/esm/Form.d.ts +index e54eb28..a90380f 100644 +--- a/node_modules/react-bootstrap/esm/Form.d.ts ++++ b/node_modules/react-bootstrap/esm/Form.d.ts +@@ -8,7 +8,7 @@ import FormText from './FormText'; + import Switch from './Switch'; + import { BsPrefixProps, BsPrefixRefForwardingComponent } from './helpers'; + declare const FormRow: BsPrefixRefForwardingComponent<"div", unknown>; +-export interface FormProps extends React.HTMLAttributes, BsPrefixProps { ++export interface FormProps extends React.HTMLAttributes, BsPrefixProps { + inline?: boolean; + validated?: boolean; + } diff --git a/src/app/App.tsx b/src/app/App.tsx index 4b97e3c..dc42f6f 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -3,7 +3,7 @@ import { MainLayout } from "../layouts/main"; -import { StorageManager } from "./StorageManager"; +import { WalletManager } from "./WalletManager"; import { kristService } from "@krist/KristConnectionService"; import packageJson from "@/package.json"; @@ -14,6 +14,6 @@ export const App = (): JSX.Element => ( <> - + ); diff --git a/src/app/MasterPasswordDialog.tsx b/src/app/MasterPasswordDialog.tsx index 23429c9..5bb6030 100644 --- a/src/app/MasterPasswordDialog.tsx +++ b/src/app/MasterPasswordDialog.tsx @@ -4,29 +4,100 @@ import Button from "react-bootstrap/Button"; import Form from "react-bootstrap/Form"; -export class MasterPasswordDialog extends Component { +import { Formik, FormikHelpers } from "formik"; + +import { WalletManager } from "./WalletManager"; + +interface MasterPasswordDialogProps { + hasMasterPassword: boolean; + walletManager: WalletManager; +} + +interface FormValues { + password: string; +} + +export class MasterPasswordDialog extends Component { + async onSubmit({ password }: FormValues, helpers: FormikHelpers): Promise { + try { + if (typeof password !== "string" || password.length === 0) + throw new Error("Password is required."); + + const { hasMasterPassword, walletManager } = this.props; + + if (hasMasterPassword) // Attempt login + await walletManager.testMasterPassword(password); + else // Setup a new master password + await walletManager.setMasterPassword(password); + } catch (e) { // Catch any errors (usually 'invalid password') and display + helpers.setSubmitting(false); + helpers.setErrors({ password: e.message || "Unknown error." }); + console.error(e); + } + } + render(): JSX.Element { + const { hasMasterPassword } = this.props; + const body = hasMasterPassword + ?

Enter your master password to access your wallets, or browse + KristWeb as a guest.

+ : <> +

Enter a master password to encrypt your wallets, + or browse KristWeb as a guest.

+

+ Never forget this password. If you forget it, you will have to + create a new one and add all your wallets again. +

+ ; + return ( /* TODO: Animation is disabled for now, because react-bootstrap (or more specifically, react-transition-group) has an incompatibility with strict mode. */ - - Master password - - -

Enter your master password to access your wallets, or browse - KristWeb as a guest.

+ + {({ handleSubmit, handleChange, values, errors, isSubmitting }) => ( +
+ + Master password + + + {/* Embed the body text, which depends on whether or not this is + the first time setting up a master password. */} + {body} - {/* Provide a username field for browser autofill */} - - - - - - + {/* Provide a username field for browser autofill */} +
); } diff --git a/src/app/MasterPasswordSetupDialog.tsx b/src/app/MasterPasswordSetupDialog.tsx deleted file mode 100644 index f2c5c4c..0000000 --- a/src/app/MasterPasswordSetupDialog.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, { Component, RefObject, SyntheticEvent } from "react"; - -import Modal from "react-bootstrap/Modal"; -import Button from "react-bootstrap/Button"; -import Form from "react-bootstrap/Form"; - -import { StorageManager } from "./StorageManager"; - -interface MasterPasswordSetupDialogProps { - storageManager: StorageManager; -} - -export class MasterPasswordSetupDialog extends Component { - passwordInput: RefObject; - - constructor(props: MasterPasswordSetupDialogProps) { - super(props); - - this.passwordInput = React.createRef(); - } - - onSave(event: SyntheticEvent): void { - event.preventDefault(); - - if (!this.passwordInput.current) - throw new Error("passwordInput ref is undefined!"); - - const masterPassword = this.passwordInput.current.value; - this.props.storageManager.setMasterPassword(masterPassword); - } - - render(): JSX.Element { - return ( - /* TODO: Animation is disabled for now, because react-bootstrap (or more - specifically, react-transition-group) has an incompatibility with - strict mode. */ - -
- - Master password - - -

Enter a master password to encrypt your wallets, - or browse KristWeb as a guest.

-

- Never forget this password. If you forget it, you will have to - create a new one and add all your wallets again. -

- - {/* Provide a username field for browser autofill */} -
- - - - -
-
- ); - } -} diff --git a/src/app/StorageManager.tsx b/src/app/StorageManager.tsx deleted file mode 100644 index f2ee3fd..0000000 --- a/src/app/StorageManager.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React, { Component } from "react"; - -import { toHex } from "@utils"; -import { aesGcmEncrypt, aesGcmDecrypt } from "@utils/crypto"; - -import { MasterPasswordDialog } from "./MasterPasswordDialog"; -import { MasterPasswordSetupDialog } from "./MasterPasswordSetupDialog"; - -import Debug from "debug"; -const debug = Debug("kristweb:storage"); - -type StorageManagerData = { - /** Whether or not the user has logged in, either as a guest, or with a - * master password. */ - isLoggedIn: boolean; - - /** Whether or not the user is browsing KristWeb as a guest. */ - isGuest: boolean; - - /** Secure random string that is encrypted with the master password to create - * the "tester" string. */ - salt?: string; - /** The `salt` encrypted with the master password, to test the password is - * correct. */ - tester?: string; - - /** Whether or not the user has configured and saved a master password - * before (whether or not salt+tester are present in local storage). */ - hasMasterPassword: boolean; -} - -export class StorageManager extends Component { - constructor(props: unknown) { - super(props); - - // Check current data stored in local storage. - const salt = localStorage.getItem("salt") || undefined; - const tester = localStorage.getItem("tester") || undefined; - - this.state = { - isLoggedIn: false, - isGuest: true, - - // Salt and tester from local storage (or undefined) - salt, tester, - // There is a master password configured if both `salt` and `tester` exist - hasMasterPassword: !!salt && !!tester - }; - - debug("hasMasterPassword: %b", this.state.hasMasterPassword); - } - - async setMasterPassword(password: string): Promise { - if (!password) throw new Error("Password is required."); - - // Generate the salt (to be encrypted with the master password) - const salt = window.crypto.getRandomValues(new Uint8Array(32)); - - // Generate the encryption tester - const tester = await aesGcmEncrypt(toHex(salt), password); - - debug("master password salt: %x tester: %s", salt, tester); - } - - /** Render the master password login/setup dialog */ - render(): JSX.Element | null { - const { isLoggedIn, hasMasterPassword } = this.state; - if (isLoggedIn) return null; // Don't show the dialog again - - if (hasMasterPassword) // Let the user log in with existing master password - return ; - else // Have the user set a password up first - return ; - } -} diff --git a/src/app/WalletManager.tsx b/src/app/WalletManager.tsx new file mode 100644 index 0000000..5ac2b2c --- /dev/null +++ b/src/app/WalletManager.tsx @@ -0,0 +1,119 @@ +import React, { Component } from "react"; + +import { toHex } from "@utils"; +import { aesGcmEncrypt, aesGcmDecrypt } from "@utils/crypto"; + +import { MasterPasswordDialog } from "./MasterPasswordDialog"; + +import Debug from "debug"; +const debug = Debug("kristweb:walletManager"); + +type WalletManagerData = { + /** Whether or not the user has logged in, either as a guest, or with a + * master password. */ + isLoggedIn: boolean; + + /** Whether or not the user is browsing KristWeb as a guest. */ + isGuest: boolean; + + /** The master password used to encrypt and decrypt local storage data. */ + masterPassword?: string; + + /** Secure random string that is encrypted with the master password to create + * the "tester" string. */ + salt?: string; + /** The `salt` encrypted with the master password, to test the password is + * correct. */ + tester?: string; + + /** Whether or not the user has configured and saved a master password + * before (whether or not salt+tester are present in local storage). */ + hasMasterPassword: boolean; +} + +export class WalletManager extends Component { + constructor(props: unknown) { + super(props); + + // Check current data stored in local storage. + const salt = localStorage.getItem("salt") || undefined; + const tester = localStorage.getItem("tester") || undefined; + + this.state = { + isLoggedIn: false, + isGuest: true, + + // Salt and tester from local storage (or undefined) + salt, tester, + // There is a master password configured if both `salt` and `tester` exist + hasMasterPassword: !!salt && !!tester + }; + + debug("hasMasterPassword: %b", this.state.hasMasterPassword); + } + + async setMasterPassword(password: string): Promise { + if (!password) throw new Error("Password is required."); + + // Generate the salt (to be encrypted with the master password) + const salt = window.crypto.getRandomValues(new Uint8Array(32)); + const saltHex = toHex(salt); + + // Generate the encryption tester + const tester = await aesGcmEncrypt(saltHex, password); + + debug("master password salt: %x tester: %s", salt, tester); + + // Store them in local storage + localStorage.setItem("salt", saltHex); + localStorage.setItem("tester", tester); + + // Set the logged in state + this.setState({ + isLoggedIn: true, + isGuest: false, + masterPassword: password + }); + } + + async testMasterPassword(password: string): Promise { + if (!password) throw new Error("Password is required."); + + // Get the salt and tester from local storage and ensure they exist + const { salt, tester } = this.state; + if (!salt || !tester) throw new Error("Master password has not been set up."); + + try { + // Attempt to decrypt the tester with the given password + const testerDec = await aesGcmDecrypt(tester, password); + + // Verify that the decrypted tester is equal to the salt, if not, the + // provided master password is incorrect. + if (testerDec !== salt) throw new Error("Incorrect password."); + } catch (e) { + // OperationError usually means decryption failure + if (e.name === "OperationError") throw new Error("Incorrect password."); + else throw e; + } + + // Set the logged in state and don't return any errors (login successful) + this.setState({ + isLoggedIn: true, + isGuest: false, + masterPassword: password + }); + } + + /** Render the master password login/setup dialog */ + render(): JSX.Element | null { + const { isLoggedIn, hasMasterPassword } = this.state; + if (isLoggedIn) return null; // Don't show the dialog again + + return ( + + ); + } +}