diff --git a/.vscode/settings.json b/.vscode/settings.json index f5a7cba..0b5b87c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -24,8 +24,10 @@ "pnpm", "privatekeys", "readonly", + "serialised", "singleline", "submenu", + "testid", "tsdoc", "typeahead" ], diff --git a/languages.json b/languages.json deleted file mode 100644 index 209c45d..0000000 --- a/languages.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "en": { - "name": "English (GB)", - "country": "GB", - "contributors": [] - }, - "de": { - "name": "German", - "nativeName": "Deutsch", - "country": "de", - "contributors": [ - { - "name": "Lignum", - "url": "https://github.com/Lignum" - } - ] - }, - "fr": { - "name": "French", - "nativeName": "Français", - "country": "fr", - "contributors": [ - { - "name": "Anavrins", - "url": "https://github.com/xAnavrins" - } - ] - }, - "ja": { - "name": "Japanese", - "nativeName": "日本語", - "country": "jp", - "contributors": [] - }, - "vi": { - "name": "Vietnamese", - "nativeName": "Tiếng Việt", - "country": "vn", - "contributors": [ - { - "name": "Boom", - "url": "https://github.com/signalhunter" - } - ] - } -} diff --git a/package.json b/package.json index d028bff..7ec95cf 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,9 @@ "private": true, "dependencies": { "@ant-design/icons": "^4.5.0", + "@testing-library/jest-dom": "^5.11.9", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.7.1", "antd": "^4.12.3", "base64-arraybuffer": "^0.2.0", "csv-stringify": "^5.6.1", @@ -33,11 +36,13 @@ "semver": "^7.3.4", "spu-md5": "0.0.4", "typesafe-actions": "^5.1.0", + "uuid": "^8.3.2", "web-vitals": "^1.1.0" }, "scripts": { "start": "craco start", - "build": "craco build" + "build": "craco build", + "test": "craco test" }, "eslintConfig": { "extends": "react-app" @@ -57,12 +62,14 @@ "devDependencies": { "@craco/craco": "^6.1.1", "@types/file-saver": "^2.0.1", + "@types/jest": "^26.0.20", "@types/node": "^12.19.16", "@types/react": "^17.0.1", "@types/react-dom": "^17.0.0", "@types/react-redux": "^7.1.16", "@types/react-router-dom": "^5.1.7", "@types/semver": "^7.3.4", + "@types/uuid": "^8.3.0", "@types/webpack-env": "^1.16.0", "@typescript-eslint/eslint-plugin": "^4.15.0", "@typescript-eslint/parser": "^4.15.0", @@ -90,5 +97,10 @@ } ] } + }, + "jest": { + "transformIgnorePatterns": [ + "/node_modules/(?!antd|@ant-design|rc-.+?|@babel/runtime).+(js|jsx)$" + ] } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8913fc6..27da77b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,8 @@ dependencies: '@ant-design/icons': 4.5.0_react-dom@17.0.1+react@17.0.1 + '@testing-library/jest-dom': 5.11.9 + '@testing-library/react': 11.2.5_react-dom@17.0.1+react@17.0.1 + '@testing-library/user-event': 12.7.1 antd: 4.12.3_react-dom@17.0.1+react@17.0.1 base64-arraybuffer: 0.2.0 csv-stringify: 5.6.1 @@ -17,16 +20,19 @@ semver: 7.3.4 spu-md5: 0.0.4 typesafe-actions: 5.1.0 + uuid: 8.3.2 web-vitals: 1.1.0 devDependencies: '@craco/craco': 6.1.1_react-scripts@4.0.2 '@types/file-saver': 2.0.1 + '@types/jest': 26.0.20 '@types/node': 12.20.0 '@types/react': 17.0.2 '@types/react-dom': 17.0.1 '@types/react-redux': 7.1.16 '@types/react-router-dom': 5.1.7 '@types/semver': 7.3.4 + '@types/uuid': 8.3.0 '@types/webpack-env': 1.16.0 '@typescript-eslint/eslint-plugin': 4.15.0_bc16c4564afe16e3219e549b81836acd '@typescript-eslint/parser': 4.15.0_eslint@7.20.0+typescript@4.1.5 @@ -95,7 +101,6 @@ /@babel/code-frame/7.12.13: dependencies: '@babel/highlight': 7.12.13 - dev: true resolution: integrity: sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g== /@babel/compat-data/7.12.13: @@ -279,7 +284,6 @@ resolution: integrity: sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg== /@babel/helper-validator-identifier/7.12.11: - dev: true resolution: integrity: sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== /@babel/helper-validator-option/7.12.16: @@ -308,7 +312,6 @@ '@babel/helper-validator-identifier': 7.12.11 chalk: 2.4.2 js-tokens: 4.0.0 - dev: true resolution: integrity: sha512-kocDQvIbgMKlWxXe9fof3TQ+gkIPOUSEYhJjqUjvKMez3krV7vbzYCDq39Oj11UAVK7JqPVGQPlgE85dPNlQww== /@babel/parser/7.12.16: @@ -1292,7 +1295,6 @@ dependencies: core-js-pure: 3.8.3 regenerator-runtime: 0.13.7 - dev: true resolution: integrity: sha512-8fSpqYRETHATtNitsCXq8QQbKJP31/KnDl2Wz2Vtui9nKzjss2ysuZtyVsWjBtvkeEFo346gkwjYPab1hvrXkQ== /@babel/runtime/7.12.1: @@ -1622,10 +1624,9 @@ dependencies: '@types/istanbul-lib-coverage': 2.0.3 '@types/istanbul-reports': 3.0.0 - '@types/node': 12.20.0 + '@types/node': 12.20.1 '@types/yargs': 15.0.13 chalk: 4.1.0 - dev: true engines: node: '>= 10.14.2' resolution: @@ -1888,10 +1889,71 @@ node: '>=10' resolution: integrity: sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g== + /@testing-library/dom/7.29.4: + dependencies: + '@babel/code-frame': 7.12.13 + '@babel/runtime': 7.12.13 + '@types/aria-query': 4.2.1 + aria-query: 4.2.2 + chalk: 4.1.0 + dom-accessibility-api: 0.5.4 + lz-string: 1.4.4 + pretty-format: 26.6.2 + dev: false + engines: + node: '>=10' + resolution: + integrity: sha512-CtrJRiSYEfbtNGtEsd78mk1n1v2TUbeABlNIcOCJdDfkN5/JTOwQEbbQpoSRxGqzcWPgStMvJ4mNolSuBRv1NA== + /@testing-library/jest-dom/5.11.9: + dependencies: + '@babel/runtime': 7.12.13 + '@types/testing-library__jest-dom': 5.9.5 + aria-query: 4.2.2 + chalk: 3.0.0 + css: 3.0.0 + css.escape: 1.5.1 + lodash: 4.17.20 + redent: 3.0.0 + dev: false + engines: + node: '>=8' + npm: '>=6' + yarn: '>=1' + resolution: + integrity: sha512-Mn2gnA9d1wStlAIT2NU8J15LNob0YFBVjs2aEQ3j8rsfRQo+lAs7/ui1i2TGaJjapLmuNPLTsrm+nPjmZDwpcQ== + /@testing-library/react/11.2.5_react-dom@17.0.1+react@17.0.1: + dependencies: + '@babel/runtime': 7.12.13 + '@testing-library/dom': 7.29.4 + react: 17.0.1 + react-dom: 17.0.1_react@17.0.1 + dev: false + engines: + node: '>=10' + peerDependencies: + react: '*' + react-dom: '*' + resolution: + integrity: sha512-yEx7oIa/UWLe2F2dqK0FtMF9sJWNXD+2PPtp39BvE0Kh9MJ9Kl0HrZAgEuhUJR+Lx8Di6Xz+rKwSdEPY2UV8ZQ== + /@testing-library/user-event/12.7.1: + dependencies: + '@babel/runtime': 7.12.13 + dev: false + engines: + node: '>=10' + npm: '>=6' + peerDependencies: + '@testing-library/dom': '>=7.21.4' + resolution: + integrity: sha512-COfCkYgcxc+P9+pEAIGlmBuIDjO91Chf9GOBHI8AhIiMyaoOrKVPQny1uf0HIAYNoHKL5slhkqOPP2ZyNaVQGw== /@types/anymatch/1.3.1: dev: true resolution: integrity: sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA== + /@types/aria-query/4.2.1: + dev: false + resolution: + integrity: sha512-S6oPal772qJZHoRZLFc/XoZW2gFvwXusYUmXPXkgxJLuEk2vOt7jc4Yo6z/vtI0EBkbPBVrJJ0B+prLIKiWqHg== /@types/babel__core/7.1.12: dependencies: '@babel/parser': 7.12.16 @@ -1969,21 +2031,24 @@ resolution: integrity: sha512-giAlZwstKbmvMk1OO7WXSj4OZ0keXAcl2TQq4LWHiiPH2ByaH7WeUzng+Qej8UPxxv+8lRTuouo0iaNDBuzIBA== /@types/istanbul-lib-coverage/2.0.3: - dev: true resolution: integrity: sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw== /@types/istanbul-lib-report/3.0.0: dependencies: '@types/istanbul-lib-coverage': 2.0.3 - dev: true resolution: integrity: sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== /@types/istanbul-reports/3.0.0: dependencies: '@types/istanbul-lib-report': 3.0.0 - dev: true resolution: integrity: sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA== + /@types/jest/26.0.20: + dependencies: + jest-diff: 26.6.2 + pretty-format: 26.6.2 + resolution: + integrity: sha512-9zi2Y+5USJRxd0FsahERhBwlcvFh6D2GLQnY2FH2BzK8J9s9omvNHIbvABwIluXa0fD8XVKMLTO0aOEuUfACAA== /@types/json-schema/7.0.7: dev: true resolution: @@ -2000,6 +2065,9 @@ dev: true resolution: integrity: sha512-0/41wHcurotvSOTHQUFkgL702c3pyWR1mToSrrX3pGPvGfpHTv3Ksx0M4UVuU5VJfjVb62Eyr1eKO1tWNUCg2Q== + /@types/node/12.20.1: + resolution: + integrity: sha512-tCkE96/ZTO+cWbln2xfyvd6ngHLanvVlJ3e5BeirJ3BYI5GbAyubIrmV4JjjugDly5D9fHjOL5MNsqsCnqwW6g== /@types/normalize-package-data/2.4.0: dev: true resolution: @@ -2079,19 +2147,29 @@ dev: true resolution: integrity: sha512-W+bw9ds02rAQaMvaLYxAbJ6cvguW/iJXNT6lTssS1ps6QdrMKttqEAMEG/b5CR8TZl3/L7/lH0ZV5nNR1LXikA== + /@types/testing-library__jest-dom/5.9.5: + dependencies: + '@types/jest': 26.0.20 + dev: false + resolution: + integrity: sha512-ggn3ws+yRbOHog9GxnXiEZ/35Mow6YtPZpd7Z5mKDeZS/o7zx3yAle0ov/wjhVB5QT4N2Dt+GNoGCdqkBGCajQ== /@types/uglify-js/3.12.0: dependencies: source-map: 0.6.1 dev: true resolution: integrity: sha512-sYAF+CF9XZ5cvEBkI7RtrG9g2GtMBkviTnBxYYyq+8BWvO4QtXfwwR6a2LFwCi4evMKZfpv6U43ViYvv17Wz3Q== + /@types/uuid/8.3.0: + dev: true + resolution: + integrity: sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ== /@types/webpack-env/1.16.0: dev: true resolution: integrity: sha512-Fx+NpfOO0CpeYX2g9bkvX8O5qh9wrU1sOF4g8sft4Mu7z+qfe387YlyY8w8daDyDsKY5vUxM0yxkAYnbkRbZEw== /@types/webpack-sources/2.1.0: dependencies: - '@types/node': 12.20.0 + '@types/node': 12.20.1 '@types/source-list-map': 0.1.2 source-map: 0.7.3 dev: true @@ -2100,7 +2178,7 @@ /@types/webpack/4.41.26: dependencies: '@types/anymatch': 1.3.1 - '@types/node': 12.20.0 + '@types/node': 12.20.1 '@types/tapable': 1.0.6 '@types/uglify-js': 3.12.0 '@types/webpack-sources': 2.1.0 @@ -2109,13 +2187,11 @@ resolution: integrity: sha512-7ZyTfxjCRwexh+EJFwRUM+CDB2XvgHl4vfuqf1ZKrgGvcS5BrNvPQqJh3tsZ0P6h6Aa1qClVHaJZszLPzpqHeA== /@types/yargs-parser/20.2.0: - dev: true resolution: integrity: sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA== /@types/yargs/15.0.13: dependencies: '@types/yargs-parser': 20.2.0 - dev: true resolution: integrity: sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ== /@typescript-eslint/eslint-plugin/4.15.0_bc16c4564afe16e3219e549b81836acd: @@ -2559,7 +2635,6 @@ resolution: integrity: sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== /ansi-regex/5.0.0: - dev: true engines: node: '>=8' resolution: @@ -2567,7 +2642,6 @@ /ansi-styles/3.2.1: dependencies: color-convert: 1.9.3 - dev: true engines: node: '>=4' resolution: @@ -2575,7 +2649,6 @@ /ansi-styles/4.3.0: dependencies: color-convert: 2.0.1 - dev: true engines: node: '>=8' resolution: @@ -2662,7 +2735,6 @@ dependencies: '@babel/runtime': 7.12.13 '@babel/runtime-corejs3': 7.12.13 - dev: true engines: node: '>=6.0' resolution: @@ -2843,7 +2915,6 @@ resolution: integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== /atob/2.1.2: - dev: true engines: node: '>= 4.5.0' hasBin: true @@ -3499,16 +3570,23 @@ ansi-styles: 3.2.1 escape-string-regexp: 1.0.5 supports-color: 5.5.0 - dev: true engines: node: '>=4' resolution: integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + /chalk/3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== /chalk/4.1.0: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - dev: true engines: node: '>=10' resolution: @@ -3676,23 +3754,19 @@ /color-convert/1.9.3: dependencies: color-name: 1.1.3 - dev: true resolution: integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== /color-convert/2.0.1: dependencies: color-name: 1.1.4 - dev: true engines: node: '>=7.0.0' resolution: integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== /color-name/1.1.3: - dev: true resolution: integrity: sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= /color-name/1.1.4: - dev: true resolution: integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== /color-string/1.5.4: @@ -3887,7 +3961,6 @@ resolution: integrity: sha512-1sCb0wBXnBIL16pfFG1Gkvei6UzvKyTNYpiC41yrdjEv0UoJoq9E/abTMzyYJ6JpTkAj15dLjbqifIzEBDVvog== /core-js-pure/3.8.3: - dev: true requiresBuild: true resolution: integrity: sha512-V5qQZVAr9K0xu7jXg1M7qTEwuxUgqr7dUOezGaNa7i+Xn9oXAU/d1fzqD9ObuwpVQOaorO5s70ckyi1woP9lVA== @@ -4125,6 +4198,10 @@ node: '>= 6' resolution: integrity: sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== + /css.escape/1.5.1: + dev: false + resolution: + integrity: sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= /css/2.2.4: dependencies: inherits: 2.0.4 @@ -4134,6 +4211,14 @@ dev: true resolution: integrity: sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw== + /css/3.0.0: + dependencies: + inherits: 2.0.4 + source-map: 0.6.1 + source-map-resolve: 0.6.0 + dev: false + resolution: + integrity: sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== /cssdb/4.4.0: dev: true resolution: @@ -4347,7 +4432,6 @@ resolution: integrity: sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw== /decode-uri-component/0.2.0: - dev: true engines: node: '>=0.10' resolution: @@ -4477,7 +4561,6 @@ resolution: integrity: sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== /diff-sequences/26.6.2: - dev: true engines: node: '>= 10.14.2' resolution: @@ -4540,6 +4623,10 @@ node: '>=6.0.0' resolution: integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + /dom-accessibility-api/0.5.4: + dev: false + resolution: + integrity: sha512-TvrjBckDy2c6v6RLxPv5QXOnU+SmF9nBII5621Ve5fu6Z/BDrENurBEvlC1f44lKEUVqOpK4w9E5Idc5/EgkLQ== /dom-align/1.12.0: dev: false resolution: @@ -4835,7 +4922,6 @@ resolution: integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= /escape-string-regexp/1.0.5: - dev: true engines: node: '>=0.8.0' resolution: @@ -5931,13 +6017,11 @@ resolution: integrity: sha512-WJTeyp0JzGtHcuMsi7rw2VwtkvLa+JyfEKJCFyfcS0+CDkjQ5lHPu7zEhFZP+PDSRrEgXa5Ah0l1MbgbE41XjA== /has-flag/3.0.0: - dev: true engines: node: '>=4' resolution: integrity: sha1-tdRU3CGZriJWmfNGfloH87lVuv0= /has-flag/4.0.0: - dev: true engines: node: '>=8' resolution: @@ -6363,7 +6447,6 @@ resolution: integrity: sha1-khi5srkoojixPcT7a21XbyMUU+o= /indent-string/4.0.0: - dev: true engines: node: '>=8' resolution: @@ -6392,7 +6475,6 @@ resolution: integrity: sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= /inherits/2.0.4: - dev: true resolution: integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== /ini/1.3.8: @@ -6976,7 +7058,6 @@ diff-sequences: 26.6.2 jest-get-type: 26.3.0 pretty-format: 26.6.2 - dev: true engines: node: '>= 10.14.2' resolution: @@ -7029,7 +7110,6 @@ resolution: integrity: sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== /jest-get-type/26.3.0: - dev: true engines: node: '>= 10.14.2' resolution: @@ -7792,6 +7872,11 @@ node: '>=10' resolution: integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + /lz-string/1.4.4: + dev: false + hasBin: true + resolution: + integrity: sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= /magic-string/0.25.7: dependencies: sourcemap-codec: 1.4.8 @@ -7974,6 +8059,12 @@ node: '>=6' resolution: integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + /min-indent/1.0.1: + dev: false + engines: + node: '>=4' + resolution: + integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== /mini-create-react-context/0.4.1_prop-types@15.7.2+react@17.0.1: dependencies: '@babel/runtime': 7.12.13 @@ -9705,7 +9796,6 @@ ansi-regex: 5.0.0 ansi-styles: 4.3.0 react-is: 17.0.1 - dev: true engines: node: '>= 10' resolution: @@ -10479,7 +10569,6 @@ resolution: integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== /react-is/17.0.1: - dev: true resolution: integrity: sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== /react-redux/7.2.2_380dc38591053d98779d1f25fc7202b9: @@ -10734,6 +10823,15 @@ node: '>=0.10.0' resolution: integrity: sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== + /redent/3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== /redux-devtools-extension/2.13.8_redux@4.0.5: dependencies: redux: 4.0.5 @@ -11537,6 +11635,13 @@ dev: true resolution: integrity: sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + /source-map-resolve/0.6.0: + dependencies: + atob: 2.1.2 + decode-uri-component: 0.2.0 + dev: false + resolution: + integrity: sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== /source-map-support/0.5.19: dependencies: buffer-from: 1.1.1 @@ -11555,7 +11660,6 @@ resolution: integrity: sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= /source-map/0.6.1: - dev: true engines: node: '>=0.10.0' resolution: @@ -11879,6 +11983,14 @@ node: '>=6' resolution: integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + /strip-indent/3.0.0: + dependencies: + min-indent: 1.0.1 + dev: false + engines: + node: '>=8' + resolution: + integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== /strip-json-comments/3.1.1: dev: true engines: @@ -11910,7 +12022,6 @@ /supports-color/5.5.0: dependencies: has-flag: 3.0.0 - dev: true engines: node: '>=4' resolution: @@ -11926,7 +12037,6 @@ /supports-color/7.2.0: dependencies: has-flag: 4.0.0 - dev: true engines: node: '>=8' resolution: @@ -12580,9 +12690,7 @@ resolution: integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== /uuid/8.3.2: - dev: true hasBin: true - optional: true resolution: integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== /v8-compile-cache/2.2.0: @@ -13219,13 +13327,18 @@ specifiers: '@ant-design/icons': ^4.5.0 '@craco/craco': ^6.1.1 + '@testing-library/jest-dom': ^5.11.9 + '@testing-library/react': ^11.2.5 + '@testing-library/user-event': ^12.7.1 '@types/file-saver': ^2.0.1 + '@types/jest': ^26.0.20 '@types/node': ^12.19.16 '@types/react': ^17.0.1 '@types/react-dom': ^17.0.0 '@types/react-redux': ^7.1.16 '@types/react-router-dom': ^5.1.7 '@types/semver': ^7.3.4 + '@types/uuid': ^8.3.0 '@types/webpack-env': ^1.16.0 '@typescript-eslint/eslint-plugin': ^4.15.0 '@typescript-eslint/parser': ^4.15.0 @@ -13257,4 +13370,5 @@ typesafe-actions: ^5.1.0 typescript: ^4.1.5 utility-types: ^3.10.0 + uuid: ^8.3.2 web-vitals: ^1.1.0 diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json index 6f18bce..c70202e 100644 --- a/public/locales/en/translation.json +++ b/public/locales/en/translation.json @@ -77,6 +77,7 @@ "errorPasswordIncorrect": "Incorrect password.", "errorPasswordInequal": "Passwords must match.", "errorStorageCorrupt": "Wallet storage is corrupted.", + "errorNoPassword": "Master password is required.", "errorUnknown": "Unknown error.", "helpWalletStorageTitle": "Help: Wallet storage", "helpWalletStorage": "When you add a wallet to KristWeb, the private key for the wallet is saved to your browser's local storage and encrypted with your master password.\nEvery wallet you save is encrypted using the same master password, and you will need to enter it every time you open KristWeb. Your actual Krist wallet is not modified in any way.\nWhen browsing KristWeb as a guest, you do not need to enter a master password, but it also means that you will not be able to add or use any wallets. You will still be able to explore the Krist network.", @@ -150,7 +151,13 @@ "walletFormatJwalelset": "jwalelset", "walletFormatApi": "Raw/API (advanced users)", - "walletSave": "Save this wallet in KristWeb" + "walletSave": "Save this wallet in KristWeb", + + "messageSuccessAdd": "Added wallet successfully!", + "messageSuccessCreate": "Created wallet successfully!", + + "errorUnexpectedTitle": "Unexpected error", + "errorUnexpectedDescription": "There was an error while adding the wallet. See console for details." }, "credits": { diff --git a/src/__data__/languages.json b/src/__data__/languages.json new file mode 100644 index 0000000..209c45d --- /dev/null +++ b/src/__data__/languages.json @@ -0,0 +1,46 @@ +{ + "en": { + "name": "English (GB)", + "country": "GB", + "contributors": [] + }, + "de": { + "name": "German", + "nativeName": "Deutsch", + "country": "de", + "contributors": [ + { + "name": "Lignum", + "url": "https://github.com/Lignum" + } + ] + }, + "fr": { + "name": "French", + "nativeName": "Français", + "country": "fr", + "contributors": [ + { + "name": "Anavrins", + "url": "https://github.com/xAnavrins" + } + ] + }, + "ja": { + "name": "Japanese", + "nativeName": "日本語", + "country": "jp", + "contributors": [] + }, + "vi": { + "name": "Vietnamese", + "nativeName": "Tiếng Việt", + "country": "vn", + "contributors": [ + { + "name": "Boom", + "url": "https://github.com/signalhunter" + } + ] + } +} diff --git a/src/__tests__/App.tsx b/src/__tests__/App.tsx new file mode 100644 index 0000000..ef001a1 --- /dev/null +++ b/src/__tests__/App.tsx @@ -0,0 +1,10 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import App from "../App"; + +test("renders the app", async () => { + render(); + + const appLayout = await screen.findByTestId("site-app-layout"); + expect(appLayout).toBeInTheDocument(); +}); diff --git a/src/components/auth/ForcedAuth.tsx b/src/components/auth/ForcedAuth.tsx index c9b3c10..e72ed88 100644 --- a/src/components/auth/ForcedAuth.tsx +++ b/src/components/auth/ForcedAuth.tsx @@ -34,7 +34,7 @@ if (attemptedAuth || isAuthed || !hasMasterPassword || !salt || !tester) return; setAttemptedAuth(true); - forceAuth(t, dispatch, salt!, tester!); + forceAuth(t, dispatch, salt, tester); }, [attemptedAuth]); return null; diff --git a/src/krist/wallets/Wallet.ts b/src/krist/wallets/Wallet.ts index 3f418f8..51a9d7f 100644 --- a/src/krist/wallets/Wallet.ts +++ b/src/krist/wallets/Wallet.ts @@ -1,4 +1,9 @@ -import { WalletFormatName } from "./formats/WalletFormat"; +import { v4 as uuid } from "uuid"; + +import { applyWalletFormat, WalletFormatName } from "./formats/WalletFormat"; +import { makeV2Address } from "../AddressAlgo"; + +import { aesGcmEncrypt } from "../../utils/crypto"; import { AppDispatch } from "../../App"; import * as actions from "../../store/actions/WalletsActions"; @@ -13,26 +18,31 @@ category?: string; // Login info - password: string; // Encrypted with master password, decrypted on-demand + encPassword: string; // Encrypted with master password, decrypted on-demand + encPrivatekey: string; // The password with the password + wallet format applied username?: string; format: WalletFormatName; // Fetched from API address: string; - balance: number; - names: number; + balance?: number; + names?: number; firstSeen?: Date; + lastSynced?: Date; } +/** Properties of Wallet that are required to create a new wallet. */ +export type WalletNewKeys = "label" | "category" | "username" | "format"; +export type WalletNew = Pick; + /** Properties of Wallet that are allowed to be updated. */ -export type WalletUpdatableKeys = "label" | "category" | "password" | "username" | "format" | "address"; +export type WalletUpdatableKeys = "label" | "category" | "encPassword" | "encPrivatekey" | "username" | "format" | "address"; export type WalletUpdatable = Pick; /** Properties of Wallet that are allowed to be synced. */ -export type WalletSyncableKeys = "balance" | "names" | "firstSeen"; +export type WalletSyncableKeys = "balance" | "names" | "firstSeen" | "lastSynced"; export type WalletSyncable = Pick; - /** Get the local storage key for a given wallet. */ export function getWalletKey(wallet: Wallet): string { return `wallet2-${wallet.id}`; @@ -81,3 +91,50 @@ dispatch(actions.loadWallets(walletMap)); } + +/** Adds a new wallet, encrypting its privatekey and password, saving it to + * local storage, and dispatching the changes to the Redux store. + * + * @param dispatch - The AppDispatch instance used to dispatch the new wallet to + * the Redux store. + * @param masterPassword - The master password used to encrypt the wallet + * password and privatekey. + * @param wallet - The information for the new wallet. + * @param password - The password of the new wallet. + * @param save - Whether or not to save this wallet to local storage. + */ +export async function addWallet( + dispatch: AppDispatch, + masterPassword: string, + wallet: WalletNew, + password: string, + save: boolean +): Promise { + // Calculate the privatekey for the given wallet format + const privatekey = await applyWalletFormat(wallet.format || "kristwallet", password, wallet.username); + const address = await makeV2Address(privatekey); + + const id = uuid(); + + // Encrypt the password and privatekey. These will be decrypted on-demand. + const encPassword = await aesGcmEncrypt(password, masterPassword); + const encPrivatekey = await aesGcmEncrypt(privatekey, masterPassword); + + const newWallet = { + ...wallet, + id, address, + encPassword, encPrivatekey + }; + + // Save the wallet to local storage if wanted + if (save) { + const key = getWalletKey(newWallet); + const serialised = JSON.stringify(newWallet); + localStorage.setItem(key, serialised); + } + + // Dispatch the changes to the redux store + dispatch(actions.addWallet(newWallet)); + + return newWallet; +} diff --git a/src/layout/AppLayout.tsx b/src/layout/AppLayout.tsx index 3277578..008e9b9 100644 --- a/src/layout/AppLayout.tsx +++ b/src/layout/AppLayout.tsx @@ -19,7 +19,7 @@ const [sidebarCollapsed, setSidebarCollapsed] = useState(true); const bps = useBreakpoint(); - return + return {/* Sidebar toggle for mobile */} {!bps.md && ( diff --git a/src/layout/sidebar/SidebarFooter.tsx b/src/layout/sidebar/SidebarFooter.tsx index 28d5f3b..b0ad641 100644 --- a/src/layout/sidebar/SidebarFooter.tsx +++ b/src/layout/sidebar/SidebarFooter.tsx @@ -1,24 +1,33 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation, Trans } from "react-i18next"; import packageJson from "../../../package.json"; import { Link } from "react-router-dom"; -const req = require.context("../../", false, /\.\/host.json$/); - export function SidebarFooter(): JSX.Element { const { t } = useTranslation(); + const [host, setHost] = useState<{ name: string; url: string } | false | undefined>(); + + useEffect(() => { + if (host !== undefined) return; + setHost(false); + + (async () => { + try { + // Add the host information if host.json exists + const hostFile = "host"; // Trick webpack into dynamic importing + const hostData = await import("../../__data__/" + hostFile + ".json"); + setHost(hostData); + } catch (ignored) { + // Ignored + } + })(); + }, [host]); const authorName = packageJson.author || "Lemmmy"; const authorURL = `https://github.com/${authorName}`; const gitURL = packageJson.repository.url.replace(/\.git$/, ""); - // Add the host information if host.json exists - let host; - if (req.keys().includes("./host.json")) { - host = req("./host.json"); - } - return (
diff --git a/src/pages/wallets/AddWalletModal.tsx b/src/pages/wallets/AddWalletModal.tsx index 882dfd5..0f2d643 100644 --- a/src/pages/wallets/AddWalletModal.tsx +++ b/src/pages/wallets/AddWalletModal.tsx @@ -1,7 +1,9 @@ import React, { useState, useRef, useEffect } from "react"; -import { Modal, Form, Input, Checkbox, Collapse, Select, Button, Tooltip, Typography, Row, Col } from "antd"; +import { Modal, Form, Input, Checkbox, Collapse, Button, Tooltip, Typography, Row, Col, message, notification } from "antd"; import { ReloadOutlined } from "@ant-design/icons"; +import { useDispatch, useSelector, shallowEqual } from "react-redux"; +import { RootState } from "../../store"; import { useTranslation, Trans } from "react-i18next"; import { generatePassword } from "../../utils"; @@ -13,6 +15,7 @@ import { WalletFormatName, applyWalletFormat, formatNeedsUsername } from "../../krist/wallets/formats/WalletFormat"; import { getSelectWalletFormat } from "./SelectWalletFormat"; import { makeV2Address } from "../../krist/AddressAlgo"; +import { addWallet } from "../../krist/wallets/Wallet"; const { Text } = Typography; @@ -37,18 +40,34 @@ export function AddWalletModal({ create, visible, setVisible }: Props): JSX.Element { const initialFormat = "kristwallet"; // TODO: change for edit modal + // Required to encrypt new wallets + const { masterPassword } = useSelector((s: RootState) => s.walletManager, shallowEqual); + const dispatch = useDispatch(); + const { t } = useTranslation(); + const [form] = Form.useForm(); const passwordInput = useRef(null); const [calculatedAddress, setCalculatedAddress] = useState(); const [formatState, setFormatState] = useState(initialFormat); async function onSubmit() { + if (!masterPassword) throw new Error(t("masterPassword.errorNoPassword")); const values = await form.validateFields(); - console.log(values); - form.resetFields(); // Make sure to generate another password on re-open - setVisible(false); + try { + await addWallet(dispatch, masterPassword, values, values.password, create || values.save); + message.success(create ? t("addWallet.messageSuccessCreate") : t("addWallet.messageSuccessAdd")); + + form.resetFields(); // Make sure to generate another password on re-open + setVisible(false); + } catch (err) { + console.error(err); + notification.error({ + message: t("addWallet.errorUnexpectedTitle"), + description: t("addWallet.errorUnexpectedDescription") + }); + } } function onValuesChange(changed: Partial, values: Partial) { diff --git a/src/pages/wallets/SelectWalletFormat.tsx b/src/pages/wallets/SelectWalletFormat.tsx index 1a9a97b..2ecf6b7 100644 --- a/src/pages/wallets/SelectWalletFormat.tsx +++ b/src/pages/wallets/SelectWalletFormat.tsx @@ -5,7 +5,6 @@ import { RootState } from "../../store"; import { useTranslation } from "react-i18next"; -import { SettingsState } from "../../utils/settings"; import { WalletFormatName, ADVANCED_FORMATS } from "../../krist/wallets/formats/WalletFormat"; @@ -14,7 +13,7 @@ } export function getSelectWalletFormat({ initialFormat }: Props): JSX.Element { - const advancedWalletFormats = useSelector((s: RootState) => (s.settings as SettingsState).walletFormats); + const advancedWalletFormats = useSelector((s: RootState) => s.settings.walletFormats); const { t } = useTranslation(); return