diff --git a/.babelrc b/.babelrc index 3e40ea4f..2127576b 100644 --- a/.babelrc +++ b/.babelrc @@ -1,8 +1,10 @@ { "presets": [ - "@babel/preset-env", "@babel/preset-react" + "@babel/preset-env", + "@babel/preset-react" ], "plugins": [ + ["@babel/plugin-transform-runtime"], [ "@babel/plugin-proposal-class-properties" ] diff --git a/package-lock.json b/package-lock.json index 6a4d897d..4f74096f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,11 +9,13 @@ "version": "0.1.0", "license": "Apache-2.0", "dependencies": { + "@babel/runtime": "^7.16.7", "@date-io/date-fns": "^1.3.13", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/pickers": "^3.3.10", + "@openpgp/hkp-client": "^0.0.2", "axios": "^0.24.0", "babel-preset-react-app": "3", "clientjs": "^0.2.1", @@ -33,11 +35,13 @@ "js-sha512": "^0.8.0", "localforage": "^1.10.0", "mui-datatables": "^3.8.2", + "openpgp": "^5.0.1", "otpauth": "^7.0.7", "papaparse": "^5.3.1", "qrcode": "^1.5.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-fastclick": "^3.0.2", "react-fontawesome": "^1.6.1", "react-i18next": "^11.13.0", "react-redux": "^7.2.6", @@ -51,6 +55,7 @@ "uuid-js": "^0.7.5" }, "devDependencies": { + "@babel/plugin-transform-runtime": "^7.16.7", "@babel/preset-env": "^7.16.0", "@babel/preset-react": "^7.16.0", "babel-cli": "^6.26.0", @@ -457,10 +462,11 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.16.0", - "license": "MIT", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", "dependencies": { - "@babel/types": "^7.16.0" + "@babel/types": "^7.16.7" }, "engines": { "node": ">=6.9.0" @@ -496,9 +502,9 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.16.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.5.tgz", - "integrity": "sha512-59KHWHXxVA9K4HNF4sbHCf+eJeFe0Te/ZFGqBT4OjXhrwvA04sGfaEGsVTdsjoszq0YTP49RC9UKe5g8uN2RwQ==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==", "engines": { "node": ">=6.9.0" } @@ -562,8 +568,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.15.7", - "license": "MIT", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==", "engines": { "node": ">=6.9.0" } @@ -1674,12 +1681,12 @@ } }, "node_modules/@babel/plugin-transform-runtime": { - "version": "7.16.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.5.tgz", - "integrity": "sha512-gxpfS8XQWDbQ8oP5NcmpXxtEgCJkbO+W9VhZlOhr0xPyVaRjAQPOv7ZDj9fg0d5s9+NiVvMCE6gbkEkcsxwGRw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.7.tgz", + "integrity": "sha512-2FoHiSAWkdq4L06uaDN3rS43i6x28desUVxq+zAFuE6kbWYQeiLPJI5IC7Sg9xKYVcrBKSQkVUfH6aeQYbl9QA==", "dependencies": { - "@babel/helper-module-imports": "^7.16.0", - "@babel/helper-plugin-utils": "^7.16.5", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", "babel-plugin-polyfill-corejs2": "^0.3.0", "babel-plugin-polyfill-corejs3": "^0.4.0", "babel-plugin-polyfill-regenerator": "^0.3.0", @@ -1959,9 +1966,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.16.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.5.tgz", - "integrity": "sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", + "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", "dependencies": { "regenerator-runtime": "^0.13.4" }, @@ -2047,10 +2054,11 @@ "license": "MIT" }, "node_modules/@babel/types": { - "version": "7.16.0", - "license": "MIT", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.7.tgz", + "integrity": "sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg==", "dependencies": { - "@babel/helper-validator-identifier": "^7.15.7", + "@babel/helper-validator-identifier": "^7.16.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -3249,6 +3257,14 @@ "node": ">= 8" } }, + "node_modules/@openpgp/hkp-client": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@openpgp/hkp-client/-/hkp-client-0.0.2.tgz", + "integrity": "sha512-hA71RhqfLfNltZsy/USTQehE2QAVB3eK4xx8p76XtFJy5Zg6gK2XbZvOC/x/yG8i2Ipbyul1DMTMxH9v8rfPKw==", + "dependencies": { + "node-fetch": "^2.6.1" + } + }, "node_modules/@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.3.tgz", @@ -4882,6 +4898,17 @@ "safer-buffer": "~2.1.0" } }, + "node_modules/asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/assert-plus": { "version": "1.0.0", "dev": true, @@ -6245,6 +6272,11 @@ "version": "3.7.2", "license": "MIT" }, + "node_modules/bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "node_modules/body-parser": { "version": "1.19.0", "license": "MIT", @@ -17419,6 +17451,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/openpgp": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-5.0.1.tgz", + "integrity": "sha512-J9HGIcXumwczJwX3JvgshWYtkhsOJHm5ZPd1ipJ1BqrZL06NgqV/EfJyF3ThOlNV2rY0MGWdS8L8/kKyeo3sXg==", + "dependencies": { + "asn1.js": "^5.0.0" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -19799,6 +19842,17 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz", "integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==" }, + "node_modules/react-fastclick": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-fastclick/-/react-fastclick-3.0.2.tgz", + "integrity": "sha1-KZTGAIjNqQsLLL+sS258a8c9ajo=", + "engines": { + "node": "*" + }, + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-fontawesome": { "version": "1.6.1", "license": "MIT", @@ -25007,9 +25061,11 @@ } }, "@babel/helper-module-imports": { - "version": "7.16.0", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz", + "integrity": "sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg==", "requires": { - "@babel/types": "^7.16.0" + "@babel/types": "^7.16.7" } }, "@babel/helper-module-transforms": { @@ -25036,9 +25092,9 @@ } }, "@babel/helper-plugin-utils": { - "version": "7.16.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.5.tgz", - "integrity": "sha512-59KHWHXxVA9K4HNF4sbHCf+eJeFe0Te/ZFGqBT4OjXhrwvA04sGfaEGsVTdsjoszq0YTP49RC9UKe5g8uN2RwQ==" + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz", + "integrity": "sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA==" }, "@babel/helper-remap-async-to-generator": { "version": "7.16.5", @@ -25081,7 +25137,9 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.15.7" + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz", + "integrity": "sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw==" }, "@babel/helper-validator-option": { "version": "7.14.5" @@ -25768,12 +25826,12 @@ } }, "@babel/plugin-transform-runtime": { - "version": "7.16.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.5.tgz", - "integrity": "sha512-gxpfS8XQWDbQ8oP5NcmpXxtEgCJkbO+W9VhZlOhr0xPyVaRjAQPOv7ZDj9fg0d5s9+NiVvMCE6gbkEkcsxwGRw==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.7.tgz", + "integrity": "sha512-2FoHiSAWkdq4L06uaDN3rS43i6x28desUVxq+zAFuE6kbWYQeiLPJI5IC7Sg9xKYVcrBKSQkVUfH6aeQYbl9QA==", "requires": { - "@babel/helper-module-imports": "^7.16.0", - "@babel/helper-plugin-utils": "^7.16.5", + "@babel/helper-module-imports": "^7.16.7", + "@babel/helper-plugin-utils": "^7.16.7", "babel-plugin-polyfill-corejs2": "^0.3.0", "babel-plugin-polyfill-corejs3": "^0.4.0", "babel-plugin-polyfill-regenerator": "^0.3.0", @@ -25973,9 +26031,9 @@ } }, "@babel/runtime": { - "version": "7.16.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.5.tgz", - "integrity": "sha512-TXWihFIS3Pyv5hzR7j6ihmeLkZfrXGxAr5UfSl8CHf+6q/wpiYDkUau0czckpYG8QmnCIuPpdLtuA9VmuGGyMA==", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz", + "integrity": "sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ==", "requires": { "regenerator-runtime": "^0.13.4" }, @@ -26037,9 +26095,11 @@ } }, "@babel/types": { - "version": "7.16.0", + "version": "7.16.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.16.7.tgz", + "integrity": "sha512-E8HuV7FO9qLpx6OtoGfUQ2cjIYnbFwvZWYBS+87EwtdMvmUPJSwykpovFB+8insbpF0uJcpr8KMUi64XZntZcg==", "requires": { - "@babel/helper-validator-identifier": "^7.15.7", + "@babel/helper-validator-identifier": "^7.16.7", "to-fast-properties": "^2.0.0" }, "dependencies": { @@ -26852,6 +26912,14 @@ "fastq": "^1.6.0" } }, + "@openpgp/hkp-client": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@openpgp/hkp-client/-/hkp-client-0.0.2.tgz", + "integrity": "sha512-hA71RhqfLfNltZsy/USTQehE2QAVB3eK4xx8p76XtFJy5Zg6gK2XbZvOC/x/yG8i2Ipbyul1DMTMxH9v8rfPKw==", + "requires": { + "node-fetch": "^2.6.1" + } + }, "@pmmmwh/react-refresh-webpack-plugin": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.3.tgz", @@ -27919,6 +27987,17 @@ "safer-buffer": "~2.1.0" } }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + } + }, "assert-plus": { "version": "1.0.0", "dev": true @@ -29005,6 +29084,11 @@ "bluebird": { "version": "3.7.2" }, + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + }, "body-parser": { "version": "1.19.0", "requires": { @@ -36764,6 +36848,14 @@ "is-wsl": "^2.2.0" } }, + "openpgp": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/openpgp/-/openpgp-5.0.1.tgz", + "integrity": "sha512-J9HGIcXumwczJwX3JvgshWYtkhsOJHm5ZPd1ipJ1BqrZL06NgqV/EfJyF3ThOlNV2rY0MGWdS8L8/kKyeo3sXg==", + "requires": { + "asn1.js": "^5.0.0" + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -38281,6 +38373,12 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz", "integrity": "sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA==" }, + "react-fastclick": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/react-fastclick/-/react-fastclick-3.0.2.tgz", + "integrity": "sha1-KZTGAIjNqQsLLL+sS258a8c9ajo=", + "requires": {} + }, "react-fontawesome": { "version": "1.6.1", "requires": { diff --git a/package.json b/package.json index 746c1942..b56373d6 100644 --- a/package.json +++ b/package.json @@ -22,11 +22,13 @@ "keywords": [], "license": "Apache-2.0", "dependencies": { + "@babel/runtime": "^7.16.7", "@date-io/date-fns": "^1.3.13", "@material-ui/core": "^4.12.3", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", "@material-ui/pickers": "^3.3.10", + "@openpgp/hkp-client": "^0.0.2", "axios": "^0.24.0", "babel-preset-react-app": "3", "clientjs": "^0.2.1", @@ -46,11 +48,13 @@ "js-sha512": "^0.8.0", "localforage": "^1.10.0", "mui-datatables": "^3.8.2", + "openpgp": "^5.0.1", "otpauth": "^7.0.7", "papaparse": "^5.3.1", "qrcode": "^1.5.0", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-fastclick": "^3.0.2", "react-fontawesome": "^1.6.1", "react-i18next": "^11.13.0", "react-redux": "^7.2.6", @@ -64,6 +68,7 @@ "uuid-js": "^0.7.5" }, "devDependencies": { + "@babel/plugin-transform-runtime": "^7.16.7", "@babel/preset-env": "^7.16.0", "@babel/preset-react": "^7.16.0", "babel-cli": "^6.26.0", diff --git a/src/common/data/css/datastore.css b/src/common/data/css/datastore.css index 688d2cb7..0734392b 100644 --- a/src/common/data/css/datastore.css +++ b/src/common/data/css/datastore.css @@ -84,8 +84,7 @@ body { border-color: #151f2b; } .path_box_parent { - margin-bottom: 5px; - font-family: 'Fira Code', monospace; } + margin-bottom: 5px; } .path_box_parent .path_box_breadcrumb { cursor: pointer; } .path_box_parent .disabled { diff --git a/src/common/data/css/style.css b/src/common/data/css/style.css index 09fa05f6..dfa58ba5 100644 --- a/src/common/data/css/style.css +++ b/src/common/data/css/style.css @@ -1,7 +1,7 @@ /*fbfbfc*/ /* Generic styles */ html { - height: 100%; } + height: 100%;} body { font-family: 'Open Sans', sans-serif; position: relative; @@ -14,6 +14,10 @@ body { overflow: hidden; } +html, body { + -webkit-tap-highlight-color: transparent; +} + h1, h2 { font-size: 20px; margin: 0; } @@ -48,9 +52,6 @@ a { .btn-link { color: #151f2b; } -.monospace { - font-family: 'Fira Code', monospace; } - p.horizontalline { width: 100%; text-align: center; @@ -313,10 +314,6 @@ p.horizontalline span { border-top-color: #2ba880; border-left-color: #2ba880; } - -.text-center { - text-align: center; } - .progress-box { width: 340px; padding: 20px; diff --git a/src/common/data/translations/locale-de.json b/src/common/data/translations/locale-de.json index dfdbc592..efe5bf74 100644 --- a/src/common/data/translations/locale-de.json +++ b/src/common/data/translations/locale-de.json @@ -1,4 +1,14 @@ { + "EDIT_WEBSITE_PASSWORD": "Webseiten Passwort bearbeiten", + "EDIT_APPLICATION_PASSWORD": "Applikations Passwort bearbeiten", + "EDIT_TOTP_AUTHENTICATOR": "TOTP Authenticator bearbeiten", + "EDIT_NOTE": "Notiz bearbeiten", + "EDIT_ENVIRONMENT_VARIABLES": "Umgebungsvariablen bearbeiten", + "EDIT_GPG_KEY": "GPG Key bearbeiten", + "EDIT_BOOKMARK": "Bookmark bearbeiten", + "EDIT_FILE": "Datei bearbeiten", + "USER_NOT_FOUND": "Benutzer nicht gefunden", + "ACCEPT_SHARE": "Share annehmen", "DATE_TIME_YYYY_MM_DD_HH_MM": "dd.MM.yyyy HH:mm", "TABLE_BODY_NO_MATCH": "Entschulding, keine übereinstimmenden Einträge gefunden", "TABLE_BODY_TOOL_TIP": "Sortieren", diff --git a/src/common/data/translations/locale-en.json b/src/common/data/translations/locale-en.json index 79ba2039..cbd20162 100644 --- a/src/common/data/translations/locale-en.json +++ b/src/common/data/translations/locale-en.json @@ -1,4 +1,14 @@ { + "EDIT_WEBSITE_PASSWORD": "Edit Website Password", + "EDIT_APPLICATION_PASSWORD": "Edit Application Password", + "EDIT_TOTP_AUTHENTICATOR": "Edit TOTP Authenticator", + "EDIT_NOTE": "Edit Note", + "EDIT_ENVIRONMENT_VARIABLES": "Edit Environment Variables", + "EDIT_GPG_KEY": "Edit GPG Key", + "EDIT_BOOKMARK": "Edit Bookmark", + "EDIT_FILE": "Edit File", + "USER_NOT_FOUND": "User not found", + "ACCEPT_SHARE": "Accept Share", "DATE_TIME_YYYY_MM_DD_HH_MM": "yyyy/MM/dd HH:mm", "TABLE_BODY_NO_MATCH": "Sorry, no matching records found", "TABLE_BODY_TOOL_TIP": "Sort", diff --git a/src/js/actions/action-creators.js b/src/js/actions/action-creators.js index 1ceffb71..8baa28e4 100644 --- a/src/js/actions/action-creators.js +++ b/src/js/actions/action-creators.js @@ -23,6 +23,7 @@ import { SET_REMOTE_CONFIG_JSON, SET_FINGERPRINT, SET_EMAIL, + SET_HIDE_DOWNLOAD_BANNER, } from "./action-types"; import datastoreSettingService from "../services/datastore-setting"; @@ -163,6 +164,14 @@ function setDisableBrowserPm(disableBrowserPm) { }); }; } +function setHideDownloadBanner(hideDownloadBanner) { + return (dispatch) => { + dispatch({ + type: SET_HIDE_DOWNLOAD_BANNER, + hideDownloadBanner, + }); + }; +} function settingsDatastoreLoaded(data) { return (dispatch) => { dispatch({ @@ -285,6 +294,7 @@ const actionCreators = { enableOfflineMode, setNotificationOnCopy, setDisableBrowserPm, + setHideDownloadBanner, setPasswordConfig, setGpgConfig, settingsDatastoreLoaded, diff --git a/src/js/actions/action-types.js b/src/js/actions/action-types.js index 2dada291..9914cc4f 100644 --- a/src/js/actions/action-types.js +++ b/src/js/actions/action-types.js @@ -16,6 +16,7 @@ export const ENABLE_OFFLINE_MODE = "ENABLE_OFFLINE_MODE"; export const DISABLE_OFFLINE_MODE = "DISABLE_OFFLINE_MODE"; export const SET_NOTIFICATION_ON_COPY = "SET_NOTIFICATION_ON_COPY"; export const SET_DISABLE_BROWSER_PM = "SET_DISABLE_BROWSER_PM"; +export const SET_HIDE_DOWNLOAD_BANNER = "SET_HIDE_DOWNLOAD_BANNER"; export const SETTINGS_DATASTORE_LOADED = "SETTINGS_DATASTORE_LOADED"; export const SET_PASSWORD_CONFIG = "SET_PASSWORD_CONFIG"; export const SET_GPG_CONFIG = "SET_GPG_CONFIG"; diff --git a/src/js/app.js b/src/js/app.js index 8a3b4e36..70df0e1f 100644 --- a/src/js/app.js +++ b/src/js/app.js @@ -12,28 +12,34 @@ import { PersistGate } from "redux-persist/integration/react"; import { Provider } from "react-redux"; import { MuiPickersUtilsProvider } from "@material-ui/pickers"; import DateFnsUtils from "@date-io/date-fns"; - +import initReactFastclick from "react-fastclick"; import store from "./services/store"; import datastoreSettingService from "./services/datastore-setting"; import i18n from "./i18n"; import theme from "./theme"; import IndexView from "./views/index"; +import DownloadBanner from "./containers/download-banner"; /** * Loads the datastore * @param dispatch * @param getState */ -function fetchTodos(dispatch, getState) { - datastoreSettingService.getSettingsDatastore(); +function loadSettingsDatastore(dispatch, getState) { + const state = getState(); + if (state.user.isLoggedIn) { + datastoreSettingService.getSettingsDatastore(); + } } let persistor = persistStore(store, null, () => { - store.dispatch(fetchTodos); + store.dispatch(loadSettingsDatastore); }); const customHistory = createBrowserHistory(); +initReactFastclick(); + const App = () => { return ( @@ -44,6 +50,7 @@ const App = () => { + diff --git a/src/js/components/datastore-tree-folder.js b/src/js/components/datastore-tree-folder.js index 4d8b8429..537aa8a4 100644 --- a/src/js/components/datastore-tree-folder.js +++ b/src/js/components/datastore-tree-folder.js @@ -13,6 +13,7 @@ import Typography from "@material-ui/core/Typography"; import EditIcon from "@material-ui/icons/Edit"; import CreateNewFolderIcon from "@material-ui/icons/CreateNewFolder"; import AddIcon from "@material-ui/icons/Add"; +import PersonAddIcon from "@material-ui/icons/PersonAdd"; import OpenWithIcon from "@material-ui/icons/OpenWith"; import DeleteIcon from "@material-ui/icons/Delete"; import Divider from "@material-ui/core/Divider"; @@ -28,14 +29,18 @@ const useStyles = makeStyles((theme) => ({ icon: { fontSize: "18px", }, + listItemIcon: { + minWidth: theme.spacing(4), + }, })); const DatastoreTreeFolder = (props) => { const { t } = useTranslation(); - const { content, search, offline, isExpandedDefault } = props; + const { content, offline, isExpandedDefault, nodePath } = props; const classes = useStyles(); const [isExpanded, setIsExpanded] = React.useState(isExpandedDefault); const [anchorEl, setAnchorEl] = React.useState(null); + const isSelectable = props.isSelectable ? props.isSelectable(content) : true; const openMenu = (event) => { event.preventDefault(); @@ -56,7 +61,7 @@ const DatastoreTreeFolder = (props) => { } registrations["read_share_rights"](content.share_id).then(function (share_details) { - var modalInstance = $uibModal.open({ + const modalInstance = $uibModal.open({ templateUrl: "view/modal/display-share-rights.html", controller: "ModalDisplayShareRightsCtrl", backdrop: "static", @@ -78,7 +83,7 @@ const DatastoreTreeFolder = (props) => { const onEdit = (event) => { handleClose(event); - // TODO editNode + props.onEditFolder(content, content.path); }; const onNewFolder = (event) => { @@ -93,7 +98,12 @@ const DatastoreTreeFolder = (props) => { const onNewEntry = (event) => { handleClose(event); - // TODO newEntryNode + props.onNewEntry(content, content.path); + }; + + const onNewUser = (event) => { + handleClose(event); + props.onNewUser(content, content.path); }; const onMove = (event) => { @@ -104,27 +114,32 @@ const DatastoreTreeFolder = (props) => { const selectNode = (event) => { event.stopPropagation(); setIsExpanded(!isExpanded); + if (props.onSelectNode && isSelectable) { + props.onSelectNode(content, content.path, nodePath); + } }; React.useEffect(() => { setIsExpanded(isExpandedDefault); }, [isExpandedDefault]); - const hideShare = offline || (content.hasOwnProperty("share_rights") && content.share_rights.grant === false); + const hideShare = offline || (content.hasOwnProperty("share_rights") && content.share_rights.grant === false) || !props.onNewShare; const hideRightsOverview = offline || (content.hasOwnProperty("share_rights") && content.share_rights.grant === false) || !content.hasOwnProperty("share_id") || typeof content.share_id === "undefined"; - const hideEdit = offline || content.share_rights.write === false; - const hideNewFolder = offline || content.share_rights.write === false; - const hideNewEntry = offline || content.share_rights.write === false; - const hideMove = offline || content.share_rights.delete === false; - const hideDelete = offline || content.share_rights.delete === false; + const hideEdit = offline || (content.hasOwnProperty("share_rights") && content.share_rights.write === false) || !props.onEditFolder; + const hideNewFolder = offline || (content.hasOwnProperty("share_rights") && content.share_rights.write === false) || !props.onNewFolder; + const hideNewEntry = offline || (content.hasOwnProperty("share_rights") && content.share_rights.write === false) || !props.onNewEntry; + const hideNewUser = offline || (content.hasOwnProperty("share_rights") && content.share_rights.write === false) || !props.onNewUser; + const hideMove = offline || (content.hasOwnProperty("share_rights") && content.share_rights.delete === false); + const hideDelete = offline || (content.hasOwnProperty("share_rights") && content.share_rights.delete === false); + const disableMenu = hideShare && hideRightsOverview && hideEdit && hideNewFolder && hideNewEntry && hideNewUser && hideMove && hideDelete; return (
-
+
{isExpanded && } {!isExpanded && } @@ -133,14 +148,14 @@ const DatastoreTreeFolder = (props) => { {content.name} - {!hideShare && onNewShare && ( - + @@ -150,7 +165,7 @@ const DatastoreTreeFolder = (props) => { )} {!hideRightsOverview && ( - + @@ -161,7 +176,7 @@ const DatastoreTreeFolder = (props) => { {(!hideShare || !hideRightsOverview) && } {!hideEdit && ( - + @@ -171,7 +186,7 @@ const DatastoreTreeFolder = (props) => { )} {!hideNewFolder && ( - + @@ -181,7 +196,7 @@ const DatastoreTreeFolder = (props) => { )} {!hideNewEntry && ( - + @@ -189,9 +204,19 @@ const DatastoreTreeFolder = (props) => { )} + {!hideNewUser && ( + + + + + + {t("NEW_USER")} + + + )} {!hideMove && ( - + @@ -202,7 +227,7 @@ const DatastoreTreeFolder = (props) => { {!hideDelete && } {!hideDelete && ( - + @@ -219,11 +244,22 @@ const DatastoreTreeFolder = (props) => { content.folders .filter((folder) => !folder["hidden"] && !folder["deleted"]) .map(function (content, i) { + const nodePathClone = Array.from(nodePath); + nodePathClone.push(content); return ( { content.items .filter((item) => !item["hidden"] && !item["deleted"]) .map(function (content, i) { - return ; + const nodePathClone = Array.from(nodePath); + nodePathClone.push(content); + return ( + + ); })}
)} @@ -243,12 +293,20 @@ const DatastoreTreeFolder = (props) => { }; DatastoreTreeFolder.propTypes = { - search: PropTypes.string.isRequired, + isSelectable: PropTypes.func, isExpandedDefault: PropTypes.bool.isRequired, content: PropTypes.object, + nodePath: PropTypes.array.isRequired, offline: PropTypes.bool.isRequired, onNewFolder: PropTypes.func.isRequired, onNewShare: PropTypes.func, + onNewUser: PropTypes.func, + onNewEntry: PropTypes.func, + onEditEntry: PropTypes.func, + onLinkItem: PropTypes.func, + onEditFolder: PropTypes.func, + onSelectNode: PropTypes.func, + onSelectItem: PropTypes.func, }; export default DatastoreTreeFolder; diff --git a/src/js/components/datastore-tree-item.js b/src/js/components/datastore-tree-item.js index b52b6add..807b8f6f 100644 --- a/src/js/components/datastore-tree-item.js +++ b/src/js/components/datastore-tree-item.js @@ -14,6 +14,7 @@ import ShareIcon from "@material-ui/icons/Share"; import Typography from "@material-ui/core/Typography"; import LinkIcon from "@material-ui/icons/Link"; import EditIcon from "@material-ui/icons/Edit"; +import VisibilityIcon from "@material-ui/icons/Visibility"; import OpenWithIcon from "@material-ui/icons/OpenWith"; import DeleteIcon from "@material-ui/icons/Delete"; import FileCopyIcon from "@material-ui/icons/FileCopy"; @@ -23,7 +24,6 @@ import { makeStyles } from "@material-ui/core/styles"; import ContentCopy from "./icons/ContentCopy"; import secretService from "../services/secret"; -import fileTransferService from "../services/file-transfer"; import store from "../services/store"; import widgetService from "../services/widget"; @@ -35,13 +35,17 @@ const useStyles = makeStyles((theme) => ({ icon: { fontSize: "18px", }, + listItemIcon: { + minWidth: theme.spacing(4), + }, })); const DatastoreTreeItem = (props) => { const { t } = useTranslation(); - const { content, search, offline } = props; + const { content, offline } = props; const classes = useStyles(); const [anchorEl, setAnchorEl] = React.useState(null); + const isSelectable = props.isSelectable ? props.isSelectable(content) : true; const openMenu = (event) => { event.preventDefault(); @@ -66,11 +70,11 @@ const DatastoreTreeItem = (props) => { * * @param content */ - var on_modal_close_success = function (content) { + const on_modal_close_success = function (content) { console.log(content); }; - var modalInstance = $uibModal.open({ + const modalInstance = $uibModal.open({ templateUrl: "view/modal/create-link-share.html", controller: "ModalCreateLinkShareCtrl", backdrop: "static", @@ -94,7 +98,7 @@ const DatastoreTreeItem = (props) => { } registrations["read_share_rights"](content.share_id).then(function (share_details) { - var modalInstance = $uibModal.open({ + const modalInstance = $uibModal.open({ templateUrl: "view/modal/display-share-rights.html", controller: "ModalDisplayShareRightsCtrl", backdrop: "static", @@ -131,7 +135,7 @@ const DatastoreTreeItem = (props) => { const onEdit = (event) => { handleClose(event); - // TODO editNode + props.onEditEntry(content, content.path, props.nodePath); }; const onNewShare = (event) => { @@ -143,20 +147,26 @@ const DatastoreTreeItem = (props) => { handleClose(event); // TODO moveNode }; - const clickItem = function () { - if (content.type === "file") { - return fileTransferService.onItemClick(content, content.path); - } else { - return secretService.onItemClick(content); + const selectItem = function (event) { + event.stopPropagation(); + if (props.onSelectItem && isSelectable) { + props.onSelectItem(content, content.path, props.nodePath); + } + }; + const linkItem = function (event) { + event.stopPropagation(); + if (props.onLinkItem) { + props.onLinkItem(content, content.path, props.nodePath); } }; - const hideShare = offline || (content.hasOwnProperty("share_rights") && content.share_rights.grant === false); + const hideShare = offline || (content.hasOwnProperty("share_rights") && content.share_rights.grant === false) || content.type === "user"; const hideLinkShare = offline || !content.hasOwnProperty("type") || (content.hasOwnProperty("share_rights") && content.share_rights.grant === false) || - store.getState().server.complianceDisableLinkShares; + store.getState().server.complianceDisableLinkShares || + content.type === "user"; const hideRightsOverview = offline || (content.hasOwnProperty("share_rights") && content.share_rights.grant === false) || @@ -174,7 +184,8 @@ const DatastoreTreeItem = (props) => { (content.hasOwnProperty("share_rights") && content.share_rights.read !== true) || !content.hasOwnProperty("type") || !["website_password", "application_password"].includes(content["type"]); - const hideEdit = offline || content.share_rights.write === false; + const hideEdit = offline || content.share_rights.write === false || !props.onEditEntry; + const hideShow = !hideEdit || content.share_rights.read === false || !props.onEditEntry; const hideClone = offline || content.share_rights.write === false || content.share_rights.read === false || content.type === "file" || content.type === "user"; const hideMove = offline || content.share_rights.delete === false; @@ -182,7 +193,7 @@ const DatastoreTreeItem = (props) => { return (
-
+
{content.share_id && } @@ -190,13 +201,13 @@ const DatastoreTreeItem = (props) => { {content.name} - {["bookmark", "website_password"].indexOf(content.type) !== -1 && ( - )} - {["file"].indexOf(content.type) !== -1 && ( - )} @@ -207,7 +218,7 @@ const DatastoreTreeItem = (props) => { {!hideShare && ( - + @@ -217,7 +228,7 @@ const DatastoreTreeItem = (props) => { )} {!hideLinkShare && ( - + @@ -227,7 +238,7 @@ const DatastoreTreeItem = (props) => { )} {!hideRightsOverview && ( - + @@ -237,7 +248,7 @@ const DatastoreTreeItem = (props) => { )} {!hideCopyTotpToken && ( - + @@ -247,7 +258,7 @@ const DatastoreTreeItem = (props) => { )} {!hideCopyUsername && ( - + @@ -257,7 +268,7 @@ const DatastoreTreeItem = (props) => { )} {!hideCopyPassword && ( - + @@ -265,20 +276,32 @@ const DatastoreTreeItem = (props) => { )} - + {(!hideShare || !hideLinkShare || !hideRightsOverview || !hideCopyTotpToken || !hideCopyUsername || !hideCopyPassword) && ( + + )} {!hideEdit && ( - + - {content.type === "user" ? t("SHOW") : t("SHOW_OR_EDIT")} + {t("SHOW_OR_EDIT")} + + + )} + {!hideShow && ( + + + + + + {t("SHOW")} )} {!hideClone && ( - + @@ -288,7 +311,7 @@ const DatastoreTreeItem = (props) => { )} {!hideMove && ( - + @@ -299,7 +322,7 @@ const DatastoreTreeItem = (props) => { {!hideDelete && } {!hideDelete && ( - + @@ -314,9 +337,13 @@ const DatastoreTreeItem = (props) => { }; DatastoreTreeItem.propTypes = { - search: PropTypes.string.isRequired, + isSelectable: PropTypes.func, + nodePath: PropTypes.array.isRequired, content: PropTypes.object, offline: PropTypes.bool.isRequired, onNewShare: PropTypes.func, + onEditEntry: PropTypes.func, + onLinkItem: PropTypes.func, + onSelectItem: PropTypes.func, }; export default DatastoreTreeItem; diff --git a/src/js/components/datastore-tree.js b/src/js/components/datastore-tree.js index 0930a368..794143ee 100644 --- a/src/js/components/datastore-tree.js +++ b/src/js/components/datastore-tree.js @@ -3,11 +3,14 @@ import PropTypes from "prop-types"; import offlineCache from "../services/offline-cache"; import DatastoreTreeItem from "./datastore-tree-item"; import DatastoreTreeFolder from "./datastore-tree-folder"; +import datastorePassword from "../services/datastore-password"; const DatastoreTree = (props) => { - const { datastore, search, onNewFolder, onNewShare } = props; + const { datastore, search } = props; const offline = offlineCache.isActive(); + datastorePassword.modifyTreeForSearch(search, datastore); + return (
{datastore.folders && @@ -16,10 +19,18 @@ const DatastoreTree = (props) => { .map(function (content, i) { return ( { datastore.items .filter((item) => !item["hidden"] && !item["deleted"]) .map(function (content, i) { - return ; + return ( + + ); })}
); }; DatastoreTree.propTypes = { - search: PropTypes.string.isRequired, + search: PropTypes.string, datastore: PropTypes.object.isRequired, onNewFolder: PropTypes.func.isRequired, + onNewUser: PropTypes.func, onNewShare: PropTypes.func, + onNewEntry: PropTypes.func, + onEditEntry: PropTypes.func, + onLinkItem: PropTypes.func, + onEditFolder: PropTypes.func, + onSelectItem: PropTypes.func, + onSelectNode: PropTypes.func, + isSelectable: PropTypes.func, }; export default DatastoreTree; diff --git a/src/js/components/dialogs/accept-share.js b/src/js/components/dialogs/accept-share.js new file mode 100644 index 00000000..556e6390 --- /dev/null +++ b/src/js/components/dialogs/accept-share.js @@ -0,0 +1,374 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import { makeStyles } from "@material-ui/core/styles"; +import Dialog from "@material-ui/core/Dialog"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; + +import { Grid } from "@material-ui/core"; +import TextField from "@material-ui/core/TextField"; +import MuiAlert from "@material-ui/lab/Alert"; + +import datastoreUserService from "../../services/datastore-user"; +import cryptoLibrary from "../../services/crypto-library"; +import DatastoreTree from "../datastore-tree"; +import widget from "../../services/widget"; +import datastorePassword from "../../services/datastore-password"; +import DialogNewFolder from "./new-folder"; +import TextFieldPath from "../text-field-path"; +import shareService from "../../services/share"; + +const useStyles = makeStyles((theme) => ({ + textField: { + width: "100%", + }, + checked: { + color: "#9c27b0", + }, + checkedIcon: { + width: "20px", + height: "20px", + border: "1px solid #666", + borderRadius: "3px", + }, + uncheckedIcon: { + width: "0px", + height: "0px", + padding: "9px", + border: "1px solid #666", + borderRadius: "3px", + }, + tree: { + marginTop: "8px", + marginBottom: "8px", + }, +})); + +const DialogAcceptShare = (props) => { + const { open, onClose, item, hideUser } = props; + const { t } = useTranslation(); + const classes = useStyles(); + const [path, setPath] = useState([]); + const [newFolderOpen, setNewFolderOpen] = useState(false); + const [newFolderData, setNewFolderData] = useState({}); + + const [datastore, setDatastore] = useState(null); + const [userIsTrusted, setUserIsTrusted] = useState(false); + const [user, setUser] = useState({ + data: { + user_name: "", + user_username: "", + user_public_key: "", + }, + }); + + let isSubscribed = true; + React.useEffect(() => { + datastorePassword.getPasswordDatastore().then(onNewDatastoreLoaded); + datastoreUserService.searchUserDatastore(item.share_right_create_user_id, item.share_right_create_user_username).then(function (user) { + if (!isSubscribed) { + return; + } + if (user !== null) { + setUserIsTrusted(true); + setUser(user); + return; + } + + const onSuccess = function (data) { + const users = data.data; + if (Object.prototype.toString.call(users) === "[object Array]") { + users.map((user) => { + if (user.username === item.share_right_create_user_username) { + setUser({ + data: { + user_id: user.id, + user_username: user.username, + user_public_key: user.public_key, + }, + name: user.username, + }); + } + }); + } else { + setUser({ + data: { + user_id: users.id, + user_username: users.username, + user_public_key: users.public_key, + }, + name: users.username, + }); + } + }; + const onError = function (data) { + //pass + }; + return datastoreUserService.searchUser(item.share_right_create_user_username).then(onSuccess, onError); + }); + // cancel subscription to useEffect + return () => (isSubscribed = false); + }, []); + + const onNewDatastoreLoaded = (data) => { + if (!isSubscribed) { + return; + } + setDatastore(data); + }; + + const trust = () => { + const onSuccess = function (user_data_store) { + if (typeof user_data_store.items === "undefined") { + user_data_store.items = []; + } + + const userObject = { + id: cryptoLibrary.generateUuid(), + name: "", + type: "user", + data: user.data, + }; + + if (user.data.user_name) { + userObject.name += user.data.user_name; + } else { + userObject.name += user.data.user_username; + } + userObject.name += " (" + user.data.user_public_key + ")"; + + user_data_store.items.push(userObject); + + datastoreUserService.saveDatastoreContent(user_data_store); + setUserIsTrusted(true); + }; + const onError = function (data) { + //pass + }; + + datastoreUserService.getUserDatastore().then(onSuccess, onError); + }; + + const onNewFolderCreate = (name) => { + // called once someone clicked the CREATE button in the dialog closes with the new name + setNewFolderOpen(false); + widget.openNewFolder(newFolderData["parent"], newFolderData["path"], datastore, datastorePassword, name); + }; + const onNewFolder = (parent, path) => { + // called whenever someone clicks on a new folder Icon + setNewFolderOpen(true); + setNewFolderData({ + parent: parent, + path: path, + }); + }; + + const onSelectNode = (parent, path, nodePath) => { + setPath(Array.from(nodePath)); + }; + + const isSelectable = (node) => { + // filter out all targets that are a share if the item is not allowed to be shared + if (!item.share_right_grant && node.share_id) { + return false; + } + // filter out all targets that are inside of a share if the item is not allowed to be shared + if (!item.share_right_grant && node.parent_share_id) { + return false; + } + // + if (!node.hasOwnProperty("share_rights")) { + return true; + } + // we need both read and write permission on the target folder in order to update it with the new content + if (!!(node.share_rights.read && node.share_rights.write)) { + return true; + } + + return false; + }; + + const onConfirm = () => { + const onSuccess = function (datastore) { + const breadcrumbs = { id_breadcrumbs: path.map((node) => node.id) }; + + console.log(breadcrumbs); + + const analyzedBreadcrumbs = datastorePassword.analyzeBreadcrumbs(breadcrumbs, datastore); + + if (item.share_right_grant === false && typeof analyzedBreadcrumbs["parent_share_id"] !== "undefined") { + // No grant right, yet the parent is a a share?!? + alert("Wups, this should not happen. Error: 781f3da7-d38b-470e-a3c8-dd5787642230"); + } + + const onSuccess = function (share) { + if (typeof share.name === "undefined") { + share.name = item.share_right_title; + } + + const shares = [share]; + + const onSuccess = function () { + onClose(); + }; + const onError = function (data) { + console.log(data); + }; + + return datastorePassword + .createShareLinksInDatastore( + shares, + analyzedBreadcrumbs["target"], + analyzedBreadcrumbs["parent_path"], + analyzedBreadcrumbs["path"], + analyzedBreadcrumbs["parent_share_id"], + analyzedBreadcrumbs["parent_datastore_id"], + datastore, + analyzedBreadcrumbs["parent_share"] + ) + .then(onSuccess, onError); + }; + + const onError = function (data) { + //pass + console.log(data); + }; + console.log(item); + return shareService + .acceptShareRight(item.share_right_id, item.share_right_key, item.share_right_key_nonce, user.data.user_public_key) + .then(onSuccess, onError); + }; + const onError = function (data) { + //pass + console.log(data); + }; + + return datastorePassword.getPasswordDatastore().then(onSuccess, onError); + }; + + return ( + { + onClose(); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {t("ACCEPT_SHARE")} + + + + + + + {datastore && } + {newFolderOpen && setNewFolderOpen(false)} onCreate={onNewFolderCreate} />} + + {!hideUser && ( + + {t("SHARED_BY")}: + + )} + {!hideUser && Boolean(user.data.user_name) && ( + + + + )} + {!hideUser && !Boolean(user.data.user_name) && ( + + + + )} + {!hideUser && ( + + + + )} + {!userIsTrusted && !hideUser && ( + + + {t("YOU_NEVER_CONFIRMED_THIS_USERS_IDENTITY")}{" "} + { + event.preventDefault(); + trust(); + }} + > + {t("ADD_TO_TRUSTED_USERS")} + + + + )} + + + + + + + + ); +}; + +DialogAcceptShare.defaultProps = { + hideUser: false, +}; + +DialogAcceptShare.propTypes = { + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + item: PropTypes.object.isRequired, + hideUser: PropTypes.bool.isRequired, +}; + +export default DialogAcceptShare; diff --git a/src/js/components/dialogs/decrypt-gpg-message.js b/src/js/components/dialogs/decrypt-gpg-message.js new file mode 100644 index 00000000..2cf19fcb --- /dev/null +++ b/src/js/components/dialogs/decrypt-gpg-message.js @@ -0,0 +1,166 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import Dialog from "@material-ui/core/Dialog"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import TextField from "@material-ui/core/TextField"; +import { makeStyles } from "@material-ui/core/styles"; +import { Grid } from "@material-ui/core"; + +import HKP from "@openpgp/hkp-client"; +import * as openpgp from "openpgp"; + +import { BarLoader } from "react-spinners"; + +import GridContainerErrors from "../grid-container-errors"; +import store from "../../services/store"; +import datastorePasswordService from "../../services/datastore-password"; + +const useStyles = makeStyles((theme) => ({ + textField: { + width: "100%", + }, +})); + +const DialogDecryptGpgMessage = (props) => { + const classes = useStyles(); + const { open, onClose } = props; + const { t } = useTranslation(); + const [encryptedMessage, setEncryptedMessage] = useState(""); + const [decryptedMessage, setDecryptedMessage] = useState(""); + const [decrypting, setDecrypting] = useState(false); + const [decryptingComplete, setDecryptingComplete] = useState(false); + const [errors, setErrors] = useState([]); + + const decrypt = () => { + setDecrypting(true); + + const pgpSender = []; + + function decrypt(publicKey) { + return datastorePasswordService.getAllOwnPgpKeys().then(async function (privateKeys) { + const privateKeysArray = []; + + for (let i = 0; i < privateKeys.length; i++) { + const privateKey = await openpgp.readPrivateKey({ armoredKey: privateKeys[i] }); + privateKeysArray.push(privateKey); + } + + //console.log(pgpSender); + const message = await openpgp.readMessage({ + armoredMessage: encryptedMessage, // parse armored message + }); + let options; + if (publicKey) { + options = { + message: message, // parse armored message + verificationKeys: await openpgp.readKey({ armoredKey: publicKey }), + decryptionKeys: privateKeysArray, + }; + } else { + options = { + message: message, // parse armored message + decryptionKeys: privateKeysArray, + }; + } + + openpgp.decrypt(options).then( + function (plaintext) { + setDecryptedMessage(plaintext.data); + setDecryptingComplete(true); + setDecrypting(false); + }, + function (error) { + console.log(error); + setErrors([error.message]); + setDecrypting(false); + } + ); + }); + } + const gpgHkpSearch = store.getState().settingsDatastore.gpgHkpSearch; + if (gpgHkpSearch && pgpSender && pgpSender.length) { + const hkp = new HKP(store.getState().settingsDatastore.gpgHkpKeyServer); + const options = { + query: pgpSender, + }; + hkp.lookup(options).then( + function (public_key) { + decrypt(public_key); + }, + function (error) { + console.log(error); + console.log(error.message); + decrypt(); + } + ); + } else { + decrypt(); + } + }; + + return ( + + {t("DECRYPT_MESSAGE")} + + + + {!decryptingComplete && ( + + { + setEncryptedMessage(event.target.value); + }} + disabled={decrypting} + multiline + minRows={3} + /> + + )} + {decryptingComplete && ( + + + + )} + + + + + + + + ); +}; + +DialogDecryptGpgMessage.propTypes = { + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, +}; + +export default DialogDecryptGpgMessage; diff --git a/src/js/components/dialogs/edit-entry.js b/src/js/components/dialogs/edit-entry.js new file mode 100644 index 00000000..085ac4db --- /dev/null +++ b/src/js/components/dialogs/edit-entry.js @@ -0,0 +1,1309 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import { makeStyles } from "@material-ui/core/styles"; +import Dialog from "@material-ui/core/Dialog"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import { Checkbox, Grid } from "@material-ui/core"; +import { Check } from "@material-ui/icons"; +import TextField from "@material-ui/core/TextField"; +import InputAdornment from "@material-ui/core/InputAdornment"; +import IconButton from "@material-ui/core/IconButton"; +import Visibility from "@material-ui/icons/Visibility"; +import VisibilityOff from "@material-ui/icons/VisibilityOff"; +import MenuOpenIcon from "@material-ui/icons/MenuOpen"; +import Menu from "@material-ui/core/Menu"; +import MenuItem from "@material-ui/core/MenuItem"; +import ListItemIcon from "@material-ui/core/ListItemIcon"; +import Typography from "@material-ui/core/Typography"; +import VisibilityOffIcon from "@material-ui/icons/VisibilityOff"; +import PhonelinkSetupIcon from "@material-ui/icons/PhonelinkSetup"; +import DeleteIcon from "@material-ui/icons/Delete"; +import PlaylistAddIcon from "@material-ui/icons/PlaylistAdd"; + +import itemBlueprintService from "../../services/item-blueprint"; +import secretService from "../../services/secret"; +import helperService from "../../services/helper"; +import offlineCache from "../../services/offline-cache"; +import ContentCopy from "../icons/ContentCopy"; +import datastorePasswordService from "../../services/datastore-password"; +import browserClientService from "../../services/browser-client"; +import TotpCircle from "../totp-circle"; +import DialogDecryptGpgMessage from "./decrypt-gpg-message"; + +const useStyles = makeStyles((theme) => ({ + textField: { + width: "100%", + }, + textField5: { + width: "100%", + marginRight: theme.spacing(2), + }, + checked: { + color: "#9c27b0", + }, + checkedIcon: { + width: "20px", + height: "20px", + border: "1px solid #666", + borderRadius: "3px", + }, + uncheckedIcon: { + width: "0px", + height: "0px", + padding: "9px", + border: "1px solid #666", + borderRadius: "3px", + }, + passwordField: { + fontFamily: "'Fira Code', monospace", + }, + right: { + textAlign: "right", + }, + icon: { + fontSize: "18px", + }, + listItemIcon: { + minWidth: theme.spacing(4), + }, + totpCircleGridItem: { + textAlign: "center", + }, + totpCircle: { + width: "200px", + height: "200px", + padding: "10px", + }, + iconButton: { + padding: 10, + }, + iconButton2: { + padding: 14, + }, +})); + +const DialogEditEntry = (props) => { + const { open, onClose, item } = props; + const { t } = useTranslation(); + const classes = useStyles(); + const offline = offlineCache.isActive(); + + const [decryptMessageDialogOpen, setDecryptMessageDialogOpen] = useState(false); + + const [websitePasswordTitle, setWebsitePasswordTitle] = useState(""); + const [websitePasswordUrl, setWebsitePasswordUrl] = useState(""); + const [websitePasswordUsername, setWebsitePasswordUsername] = useState(""); + const [websitePasswordPassword, setWebsitePasswordPassword] = useState(""); + const [websitePasswordNotes, setWebsitePasswordNotes] = useState(""); + const [websitePasswordAutoSubmit, setWebsitePasswordAutoSubmit] = useState(false); + const [websitePasswordUrlFilter, setWebsitePasswordUrlFilter] = useState(""); + + const [applicationPasswordTitle, setApplicationPasswordTitle] = useState(""); + const [applicationPasswordUsername, setApplicationPasswordUsername] = useState(""); + const [applicationPasswordPassword, setApplicationPasswordPassword] = useState(""); + const [applicationPasswordNotes, setApplicationPasswordNotes] = useState(""); + + const [bookmarkTitle, setBookmarkTitle] = useState(""); + const [bookmarkUrl, setBookmarkUrl] = useState(""); + const [bookmarkNotes, setBookmarkNotes] = useState(""); + const [bookmarkUrlFilter, setBookmarkUrlFilter] = useState(""); + + const [noteTitle, setNoteTitle] = useState(""); + const [noteNotes, setNoteNotes] = useState(""); + + const [totpTitle, setTotpTitle] = useState(""); + const [totpPeriod, setTotpPeriod] = useState(30); + const [totpAlgorithm, setTotpAlgorithm] = useState("SHA1"); + const [totpDigits, setTotpDigits] = useState(6); + const [totpCode, setTotpCode] = useState(""); + const [totpNotes, setTotpNotes] = useState(""); + + const [environmentVariablesTitle, setEnvironmentVariablesTitle] = useState(""); + const [environmentVariablesVariables, setEnvironmentVariablesVariables] = useState([]); + const [environmentVariablesNotes, setEnvironmentVariablesNotes] = useState(""); + + const [fileTitle, setFileTitle] = useState(""); + + const [mailGpgOwnKeyTitle, setMailGpgOwnKeyTitle] = useState(""); + const [mailGpgOwnKeyEmail, setMailGpgOwnKeyEmail] = useState(""); + const [mailGpgOwnKeyName, setMailGpgOwnKeyName] = useState(""); + const [mailGpgOwnKeyPublic, setMailGpgOwnKeyPublic] = useState(""); + + const [anchorEl, setAnchorEl] = React.useState(null); + + const [callbackUrl, setCallbackUrl] = useState(""); + const [callbackPass, setCallbackPass] = useState(""); + const [callbackUser, setCallbackUser] = useState(""); + + const [createDate, setCreateDate] = useState(new Date()); + const [writeDate, setWriteDate] = useState(new Date()); + + const [showPassword, setShowPassword] = useState(false); + const [showAdvanced, setShowAdvanced] = useState(false); + + const itemBlueprint = itemBlueprintService.getEntryTypes().find((entryType) => entryType.value === item.type); + const hasHistory = ["file"].indexOf(item.type) === -1; // only files have no history + const hasCallback = ["file"].indexOf(item.type) === -1; // only files have no callbacks + + const isValidWebsitePassword = Boolean(websitePasswordTitle); + const isValidApplicationPassword = Boolean(applicationPasswordTitle); + const isValidBookmark = Boolean(bookmarkTitle); + const isValidNote = Boolean(noteTitle); + const isValidTotp = Boolean(totpTitle) && Boolean(totpCode); + const isValidEnvironmentVariables = Boolean(environmentVariablesTitle); + const isValidFile = Boolean(fileTitle); + const canSave = + (item.type === "website_password" && isValidWebsitePassword) || + (item.type === "application_password" && isValidApplicationPassword) || + (item.type === "bookmark" && isValidBookmark) || + (item.type === "note" && isValidNote) || + (item.type === "totp" && isValidTotp) || + (item.type === "environment_variables" && isValidEnvironmentVariables) || + (item.type === "file" && isValidFile); + + React.useEffect(() => { + const onError = function (result) { + console.log(result); + // pass + }; + + const onSuccess = function (data) { + // general infos + if (data.hasOwnProperty("create_date")) { + setCreateDate(new Date(data["create_date"])); + } + if (data.hasOwnProperty("write_date")) { + setWriteDate(new Date(data["write_date"])); + } + + // callback infos + if (data.hasOwnProperty("callback_pass")) { + setCallbackPass(data["callback_pass"]); + } + if (data.hasOwnProperty("callback_url")) { + setCallbackUrl(data["callback_url"]); + } + if (data.hasOwnProperty("callback_user")) { + setCallbackUser(data["callback_user"]); + } + + // website passwords + if (data.hasOwnProperty("website_password_title")) { + setWebsitePasswordTitle(data["website_password_title"]); + } + if (data.hasOwnProperty("website_password_url")) { + setWebsitePasswordUrl(data["website_password_url"]); + } + if (data.hasOwnProperty("website_password_username")) { + setWebsitePasswordUsername(data["website_password_username"]); + } + if (data.hasOwnProperty("website_password_password")) { + setWebsitePasswordPassword(data["website_password_password"]); + } + if (data.hasOwnProperty("website_password_notes")) { + setWebsitePasswordNotes(data["website_password_notes"]); + } + if (data.hasOwnProperty("website_password_auto_submit")) { + setWebsitePasswordAutoSubmit(data["website_password_auto_submit"]); + } + if (data.hasOwnProperty("website_password_url_filter")) { + setWebsitePasswordUrlFilter(data["website_password_url_filter"]); + } + + // application passwords + if (data.hasOwnProperty("application_password_title")) { + setApplicationPasswordTitle(data["application_password_title"]); + } + if (data.hasOwnProperty("application_password_username")) { + setApplicationPasswordUsername(data["application_password_username"]); + } + if (data.hasOwnProperty("application_password_password")) { + setApplicationPasswordPassword(data["application_password_password"]); + } + if (data.hasOwnProperty("application_password_notes")) { + setApplicationPasswordNotes(data["application_password_notes"]); + } + + // bookmarks + if (data.hasOwnProperty("bookmark_title")) { + setBookmarkTitle(data["bookmark_title"]); + } + if (data.hasOwnProperty("bookmark_url")) { + setBookmarkUrl(data["bookmark_url"]); + } + if (data.hasOwnProperty("bookmark_notes")) { + setBookmarkNotes(data["bookmark_notes"]); + } + if (data.hasOwnProperty("bookmark_url_filter")) { + setBookmarkUrlFilter(data["bookmark_url_filter"]); + } + + // notes + if (data.hasOwnProperty("note_title")) { + setNoteTitle(data["note_title"]); + } + if (data.hasOwnProperty("note_notes")) { + setNoteNotes(data["note_notes"]); + } + + // totp + if (data.hasOwnProperty("totp_title")) { + setTotpTitle(data["totp_title"]); + } + if (data.hasOwnProperty("totp_period")) { + setTotpPeriod(data["totp_period"]); + } + if (data.hasOwnProperty("totp_algorithm")) { + setTotpAlgorithm(data["totp_algorithm"]); + } + if (data.hasOwnProperty("totp_digits")) { + setTotpDigits(data["totp_digits"]); + } + if (data.hasOwnProperty("totp_code")) { + setTotpCode(data["totp_code"]); + } + if (data.hasOwnProperty("totp_notes")) { + setTotpNotes(data["totp_notes"]); + } + + // environment variables + if (data.hasOwnProperty("environment_variables_title")) { + setEnvironmentVariablesTitle(data["environment_variables_title"]); + } + if (data.hasOwnProperty("environment_variables_variables")) { + setEnvironmentVariablesVariables(data["environment_variables_variables"]); + } + if (data.hasOwnProperty("environment_variables_notes")) { + setEnvironmentVariablesNotes(data["environment_variables_notes"]); + } + + // file + if (data.hasOwnProperty("file_title")) { + setFileTitle(data["file_title"]); + } + + // mail_gpg_own_key + if (data.hasOwnProperty("mail_gpg_own_key_title")) { + setMailGpgOwnKeyTitle(data["mail_gpg_own_key_title"]); + } + if (data.hasOwnProperty("mail_gpg_own_key_email")) { + setMailGpgOwnKeyEmail(data["mail_gpg_own_key_email"]); + } + if (data.hasOwnProperty("mail_gpg_own_key_name")) { + setMailGpgOwnKeyName(data["mail_gpg_own_key_name"]); + } + if (data.hasOwnProperty("mail_gpg_own_key_public")) { + setMailGpgOwnKeyPublic(data["mail_gpg_own_key_public"]); + } + + console.log(data); + + // function onSave(new_content) { + // // update visual representation + // const secret_object = {}; + // for (let i = new_content.fields.length - 1; i >= 0; i--) { + // if (!new_content.fields[i].hasOwnProperty("value")) { + // continue; + // } + // if (new_content.title_field === new_content.fields[i].name) { + // node.name = new_content.fields[i].value; + // } + // if (new_content.hasOwnProperty("urlfilter_field") && new_content.urlfilter_field === new_content.fields[i].name) { + // node.urlfilter = new_content.fields[i].value; + // } + // if (new_content.hasOwnProperty("autosubmit_field") && new_content.autosubmit_field === new_content.fields[i].name) { + // node.autosubmit = new_content.fields[i].value; + // } + // secret_object[new_content.fields[i].name] = new_content.fields[i].value; + // } + // + // const onError = function (result) { + // // pass + // }; + // + // const onSuccess = function (e) { + // let onSuccess, onError; + // + // const closest_share_info = shareService.getClosestParentShare(path.slice(), datastore, datastore, 0); + // + // const closest_share = closest_share_info["closest_share"]; + // + // if (closest_share.hasOwnProperty("share_id")) { + // // refresh share content before updating the share + // onSuccess = function (content) { + // const search = datastorePasswordService.findInDatastore(closest_share_info["relative_path"], content.data); + // node = search[0][search[1]]; + // + // for (let i = new_content.fields.length - 1; i >= 0; i--) { + // if (!new_content.fields[i].hasOwnProperty("value")) { + // continue; + // } + // if (new_content.title_field === new_content.fields[i].name) { + // node.name = new_content.fields[i].value; + // } + // if (new_content.hasOwnProperty("urlfilter_field") && new_content.urlfilter_field === new_content.fields[i].name) { + // node.urlfilter = new_content.fields[i].value; + // } + // if (new_content.hasOwnProperty("autosubmit_field") && new_content.autosubmit_field === new_content.fields[i].name) { + // node.autosubmit = new_content.fields[i].value; + // } + // } + // + // shareService.writeShare(closest_share["share_id"], content.data, closest_share["share_secret_key"]); + // manager.handleDatastoreContentChanged(datastore); + // }; + // + // onError = function (e) { + // // pass + // }; + // shareService.readShare(closest_share["share_id"], closest_share["share_secret_key"]).then(onSuccess, onError); + // } else { + // // refresh datastore content before updating it + // onError = function (result) { + // // pass + // }; + // + // onSuccess = function (datastore) { + // const search = datastorePasswordService.findInDatastore(closest_share_info["relative_path"], datastore); + // const node = search[0][search[1]]; + // + // for (let i = new_content.fields.length - 1; i >= 0; i--) { + // if (!new_content.fields[i].hasOwnProperty("value")) { + // continue; + // } + // if (new_content.title_field === new_content.fields[i].name) { + // node.name = new_content.fields[i].value; + // } + // if (new_content.hasOwnProperty("urlfilter_field") && new_content.urlfilter_field === new_content.fields[i].name) { + // node.urlfilter = new_content.fields[i].value; + // } + // if (new_content.hasOwnProperty("autosubmit_field") && new_content.autosubmit_field === new_content.fields[i].name) { + // node.autosubmit = new_content.fields[i].value; + // } + // } + // + // datastorePasswordService.saveDatastoreContent(datastore, [path]); + // manager.handleDatastoreContentChanged(datastore); + // }; + // + // return manager.getDatastoreWithId(closest_share["datastore_id"]).then(onSuccess, onError); + // } + // + // //datastorePasswordService.saveDatastoreContent(datastore, [path]); + // }; + // + // const bp = itemBlueprint.get_blueprint(node.type); + // + // if (bp.hasOwnProperty("preUpdate")) { + // bp.preUpdate(node, secret_object).then(onSuccess, onError); + // } else { + // secretService + // .writeSecret( + // node.secret_id, + // node.secret_key, + // secret_object, + // new_content["callback_data"]["callback_url"], + // new_content["callback_data"]["callback_user"], + // new_content["callback_data"]["callback_pass"] + // ) + // .then(onSuccess, onError); + // } + // } + + // if (window.innerWidth > 1199) { + // $rootScope.$broadcast("show-entry-big", { + // node: node, + // path: path, + // data: data, + // onClose: function () {}, + // onSave: onSave, + // }); + // } else { + // const modalInstance = $uibModal.open({ + // templateUrl: "view/modal/edit-entry.html", + // controller: "ModalEditEntryCtrl", + // backdrop: "static", + // size: size, + // resolve: { + // node: function () { + // return node; + // }, + // path: function () { + // return path; + // }, + // data: function () { + // return data; + // }, + // }, + // }); + // + // modalInstance.result.then(onSave, function () { + // // cancel triggered + // }); + // } + }; + + if (typeof item.secret_id === "undefined") { + if (item.hasOwnProperty("type")) { + if (item.type === "file") { + const secret = helperService.duplicateObject(item); + secret["file_title"] = item.name; + onSuccess(secret); + return; + } + } + onSuccess(item); + } else { + secretService.readSecret(item.secret_id, item.secret_key).then(onSuccess, onError); + } + }, []); + + const onSave = (event) => { + console.log(event); + // TODO + }; + + const showHistory = (event) => { + console.log(event); + // TODO + }; + + const onShowHidePassword = (event) => { + handleClose(); + setShowPassword(!showPassword); + }; + + const onCopyPassword = (event) => { + handleClose(); + if (item.type === "website_password") { + browserClientService.copyToClipboard(websitePasswordPassword); + } + if (item.type === "application_password") { + browserClientService.copyToClipboard(applicationPasswordPassword); + } + }; + const onGeneratePassword = (event) => { + handleClose(); + const password = datastorePasswordService.generate(); + if (item.type === "website_password") { + setWebsitePasswordPassword(password); + } + if (item.type === "application_password") { + setApplicationPasswordPassword(password); + } + }; + const openMenu = (event) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + { + onClose(); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {t(itemBlueprint.edit_title)} + + + {item.type === "website_password" && ( + + { + setWebsitePasswordTitle(event.target.value); + }} + /> + + )} + {item.type === "website_password" && ( + + { + // get only toplevel domain + const parsedUrl = helperService.parseUrl(event.target.value); + if (!event.target.value) { + setWebsitePasswordUrlFilter(""); + } else if (typeof parsedUrl.authority === "undefined") { + setWebsitePasswordUrlFilter(""); + } else { + setWebsitePasswordUrlFilter(parsedUrl.authority); + } + setWebsitePasswordUrl(event.target.value); + }} + /> + + )} + {item.type === "website_password" && ( + + { + setWebsitePasswordUsername(event.target.value); + }} + /> + + )} + {item.type === "website_password" && ( + + { + setWebsitePasswordPassword(event.target.value); + }} + InputProps={{ + type: showPassword ? "text" : "password", + classes: { + input: classes.passwordField, + }, + endAdornment: ( + + + + + + + + + + + {t("SHOW_OR_HIDE_PASSWORD")} + + + + + + + + {t("COPY_PASSWORD")} + + + + + + + + {t("GENERATE_PASSWORD")} + + + + + ), + }} + /> + + )} + {item.type === "website_password" && ( + + { + setWebsitePasswordNotes(event.target.value); + }} + multiline + minRows={3} + /> + + )} + + {item.type === "application_password" && ( + + { + setApplicationPasswordTitle(event.target.value); + }} + /> + + )} + {item.type === "application_password" && ( + + { + setApplicationPasswordUsername(event.target.value); + }} + /> + + )} + {item.type === "application_password" && ( + + { + setApplicationPasswordPassword(event.target.value); + }} + InputProps={{ + type: showPassword ? "text" : "password", + classes: { + input: classes.passwordField, + }, + endAdornment: ( + + + + + + + + + + + {t("SHOW_OR_HIDE_PASSWORD")} + + + + + + + + {t("COPY_PASSWORD")} + + + + + + + + {t("GENERATE_PASSWORD")} + + + + + ), + }} + /> + + )} + {item.type === "application_password" && ( + + { + setApplicationPasswordNotes(event.target.value); + }} + multiline + minRows={3} + /> + + )} + + {item.type === "bookmark" && ( + + { + setBookmarkTitle(event.target.value); + }} + /> + + )} + {item.type === "bookmark" && ( + + { + // get only toplevel domain + const parsedUrl = helperService.parseUrl(event.target.value); + if (!event.target.value) { + setBookmarkUrlFilter(""); + } else if (typeof parsedUrl.authority === "undefined") { + setBookmarkUrlFilter(""); + } else { + setBookmarkUrlFilter(parsedUrl.authority); + } + setBookmarkUrl(event.target.value); + }} + /> + + )} + {item.type === "bookmark" && ( + + { + setBookmarkNotes(event.target.value); + }} + multiline + minRows={3} + /> + + )} + + {item.type === "note" && ( + + { + setNoteTitle(event.target.value); + }} + /> + + )} + {item.type === "note" && ( + + { + setNoteNotes(event.target.value); + }} + multiline + minRows={3} + /> + + )} + + {item.type === "totp" && ( + + { + setTotpTitle(event.target.value); + }} + /> + + )} + + {item.type === "totp" && ( + + + + )} + {item.type === "totp" && ( + + { + setTotpNotes(event.target.value); + }} + multiline + minRows={3} + /> + + )} + + {item.type === "environment_variables" && ( + + { + setEnvironmentVariablesTitle(event.target.value); + }} + /> + + )} + + {item.type === "environment_variables" && ( + + {environmentVariablesVariables.map((variable, index) => { + return ( + + + { + const newEnvs = helperService.duplicateObject(environmentVariablesVariables); + newEnvs[index]["key"] = event.target.value; + setEnvironmentVariablesVariables(newEnvs); + }} + /> + + + { + const newEnvs = helperService.duplicateObject(environmentVariablesVariables); + newEnvs[index]["value"] = event.target.value; + setEnvironmentVariablesVariables(newEnvs); + }} + /> + + + { + const newEnvs = helperService.duplicateObject(environmentVariablesVariables); + newEnvs.splice(index, 1); + setEnvironmentVariablesVariables(newEnvs); + }} + > + + + + + ); + })} + + )} + {item.type === "environment_variables" && ( + + + + )} + {item.type === "environment_variables" && ( + + { + setEnvironmentVariablesNotes(event.target.value); + }} + multiline + minRows={3} + /> + + )} + + {item.type === "file" && ( + + { + setFileTitle(event.target.value); + }} + /> + + )} + + {item.type === "mail_gpg_own_key" && ( + + { + setMailGpgOwnKeyTitle(event.target.value); + }} + /> + + )} + {item.type === "mail_gpg_own_key" && ( + + { + setMailGpgOwnKeyEmail(event.target.value); + }} + disabled + /> + + )} + {item.type === "mail_gpg_own_key" && ( + + { + setMailGpgOwnKeyName(event.target.value); + }} + disabled + /> + + )} + {item.type === "mail_gpg_own_key" && ( + + { + setMailGpgOwnKeyPublic(event.target.value); + }} + disabled + multiline + minRows={3} + maxRows={15} + /> + + )} + {item.type === "mail_gpg_own_key" && ( + + + + + )} + + + + {hasHistory && !offline && ( + + )} + + + {item.type === "website_password" && showAdvanced && ( + + { + setWebsitePasswordAutoSubmit(event.target.checked); + }} + checkedIcon={} + icon={} + classes={{ + checked: classes.checked, + }} + />{" "} + {t("AUTOMATIC_SUBMIT")} + + )} + {item.type === "website_password" && showAdvanced && ( + + { + setWebsitePasswordUrlFilter(event.target.value); + }} + /> + + )} + {item.type === "bookmark" && showAdvanced && ( + + { + setBookmarkUrlFilter(event.target.value); + }} + /> + + )} + {hasCallback && showAdvanced && ( + + { + setCallbackUrl(event.target.value); + }} + /> + + )} + {hasCallback && showAdvanced && ( + + { + setCallbackUser(event.target.value); + }} + /> + + )} + {hasCallback && showAdvanced && ( + + { + setCallbackPass(event.target.value); + }} + InputProps={{ + type: showPassword ? "text" : "password", + classes: { + input: classes.passwordField, + }, + endAdornment: ( + + setShowPassword(!showPassword)} edge="end"> + {showPassword ? : } + + + ), + }} + /> + + )} + + {showAdvanced && ( + + {t("ENTRY_LINK")}: {item.id} + + )} + + + + + {item.share_rights.write && !offline && ( + + )} + + {decryptMessageDialogOpen && setDecryptMessageDialogOpen(false)} />} + + ); +}; + +DialogEditEntry.propTypes = { + onClose: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + item: PropTypes.object.isRequired, +}; + +export default DialogEditEntry; diff --git a/src/js/components/dialogs/edit-folder.js b/src/js/components/dialogs/edit-folder.js new file mode 100644 index 00000000..e3086a41 --- /dev/null +++ b/src/js/components/dialogs/edit-folder.js @@ -0,0 +1,104 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import { makeStyles } from "@material-ui/core/styles"; +import Dialog from "@material-ui/core/Dialog"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import { Grid } from "@material-ui/core"; +import TextField from "@material-ui/core/TextField"; + +const useStyles = makeStyles((theme) => ({ + textField: { + width: "100%", + }, + checked: { + color: "#9c27b0", + }, + checkedIcon: { + width: "20px", + height: "20px", + border: "1px solid #666", + borderRadius: "3px", + }, + uncheckedIcon: { + width: "0px", + height: "0px", + padding: "9px", + border: "1px solid #666", + borderRadius: "3px", + }, +})); + +const DialogEditFolder = (props) => { + const { open, onClose, node } = props; + const { t } = useTranslation(); + const classes = useStyles(); + const [folderName, setFolderName] = useState(node.name); + + const onSave = (event) => { + node.name = folderName; + props.onSave(node); + }; + + return ( + { + onClose(); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {t("EDIT_FOLDER")} + + + + { + setFolderName(event.target.value); + }} + /> + + + {t("FOLDER_LINK")}: {node.id} + + + + + + + + + ); +}; + +DialogEditFolder.propTypes = { + onClose: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + node: PropTypes.object.isRequired, +}; + +export default DialogEditFolder; diff --git a/src/js/components/dialogs/edit-user.js b/src/js/components/dialogs/edit-user.js new file mode 100644 index 00000000..41b364cd --- /dev/null +++ b/src/js/components/dialogs/edit-user.js @@ -0,0 +1,164 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import { makeStyles } from "@material-ui/core/styles"; +import Dialog from "@material-ui/core/Dialog"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import CheckBoxOutlineBlankIcon from "@material-ui/icons/CheckBoxOutlineBlank"; +import { Grid } from "@material-ui/core"; +import TextField from "@material-ui/core/TextField"; +import InputAdornment from "@material-ui/core/InputAdornment"; +import IconButton from "@material-ui/core/IconButton"; + +import GridContainerErrors from "../grid-container-errors"; +import Table from "../table"; +import helperService from "../../services/helper"; +import store from "../../services/store"; +import browserClient from "../../services/browser-client"; +import datastoreUserService from "../../services/datastore-user"; +import cryptoLibrary from "../../services/crypto-library"; + +const useStyles = makeStyles((theme) => ({ + textField: { + width: "100%", + }, + checked: { + color: "#9c27b0", + }, + checkedIcon: { + width: "20px", + height: "20px", + border: "1px solid #666", + borderRadius: "3px", + }, + uncheckedIcon: { + width: "0px", + height: "0px", + padding: "9px", + border: "1px solid #666", + borderRadius: "3px", + }, +})); + +const DialogEditUser = (props) => { + const { open, onClose, item } = props; + const { t } = useTranslation(); + const classes = useStyles(); + const [errors, setErrors] = useState([]); + const [visualUsername, setVisualUsername] = useState(item.data.user_name || ""); + const [username, setUsername] = useState(item.data.user_username); + const [userId, setFoundUserId] = useState(item.data.user_id); + const [publicKey, setFoundPublicKey] = useState(item.data.user_public_key); + + const onSave = (event) => { + if (item.data.user_name) { + delete item.data.user_name; + } + if (visualUsername) { + item.data.user_name = visualUsername; + } + + item.name = ""; + if (item.data.user_name) { + item.name += item.data.user_name; + } else { + item.name += item.data.user_username; + } + item.name += " (" + item.data.user_public_key + ")"; + + props.onSave(item); + }; + + return ( + { + onClose(); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {t("EDIT_USER")} + + + + { + setVisualUsername(event.target.value); + }} + /> + + + + + + + + + + + + + + + + ); +}; + +DialogEditUser.propTypes = { + onClose: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + item: PropTypes.object.isRequired, +}; + +export default DialogEditUser; diff --git a/src/js/components/dialogs/new-entry.js b/src/js/components/dialogs/new-entry.js new file mode 100644 index 00000000..2ec4f778 --- /dev/null +++ b/src/js/components/dialogs/new-entry.js @@ -0,0 +1,114 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import { makeStyles } from "@material-ui/core/styles"; +import Dialog from "@material-ui/core/Dialog"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import { Grid } from "@material-ui/core"; +import TextField from "@material-ui/core/TextField"; +import SelectFieldEntryType from "../select-field/entry-type"; + +const useStyles = makeStyles((theme) => ({ + textField: { + width: "100%", + }, + checked: { + color: "#9c27b0", + }, + checkedIcon: { + width: "20px", + height: "20px", + border: "1px solid #666", + borderRadius: "3px", + }, + uncheckedIcon: { + width: "0px", + height: "0px", + padding: "9px", + border: "1px solid #666", + borderRadius: "3px", + }, +})); + +const DialogNewEntry = (props) => { + const { open, onClose, onCreate } = props; + const { t } = useTranslation(); + const classes = useStyles(); + const [folderName, setDescription] = useState(""); + const [entryType, setEntryType] = useState("website_password"); + + return ( + { + onClose(); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {t("NEW_ENTRY")} + + + + + + + { + setDescription(event.target.value); + }} + /> + + + + + + + + + ); +}; + +DialogNewEntry.propTypes = { + onClose: PropTypes.func.isRequired, + onCreate: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, +}; + +export default DialogNewEntry; diff --git a/src/js/components/dialogs/new-user.js b/src/js/components/dialogs/new-user.js new file mode 100644 index 00000000..b90113ca --- /dev/null +++ b/src/js/components/dialogs/new-user.js @@ -0,0 +1,373 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import { makeStyles } from "@material-ui/core/styles"; +import Dialog from "@material-ui/core/Dialog"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import CheckBoxOutlineBlankIcon from "@material-ui/icons/CheckBoxOutlineBlank"; +import { Grid } from "@material-ui/core"; +import TextField from "@material-ui/core/TextField"; +import InputAdornment from "@material-ui/core/InputAdornment"; +import IconButton from "@material-ui/core/IconButton"; + +import GridContainerErrors from "../grid-container-errors"; +import Table from "../table"; +import helperService from "../../services/helper"; +import store from "../../services/store"; +import browserClient from "../../services/browser-client"; +import datastoreUserService from "../../services/datastore-user"; +import cryptoLibrary from "../../services/crypto-library"; + +const useStyles = makeStyles((theme) => ({ + textField: { + width: "100%", + }, + checked: { + color: "#9c27b0", + }, + checkedIcon: { + width: "20px", + height: "20px", + border: "1px solid #666", + borderRadius: "3px", + }, + uncheckedIcon: { + width: "0px", + height: "0px", + padding: "9px", + border: "1px solid #666", + borderRadius: "3px", + }, +})); + +const DialogNewUser = (props) => { + const { open, onClose } = props; + const { t } = useTranslation(); + const classes = useStyles(); + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); + const [domain, setDomain] = useState(""); + const allowUserSearchByUsernamePartial = store.getState().server.allowUserSearchByUsernamePartial; + const allowUserSearchByEmail = store.getState().server.allowUserSearchByEmail; + const [errors, setErrors] = useState([]); + const [visualUsername, setVisualUsername] = useState(""); + const [foundUsername, setFoundUsername] = useState(""); + const [foundUserId, setFoundUserId] = useState(""); + const [foundPublicKey, setFoundPublicKey] = useState(""); + const [users, setUsers] = useState([]); + + let isSubscribed = true; + React.useEffect(() => { + const onError = function (data) { + console.log(data); + }; + + browserClient.getConfig().then(onNewConfigLoaded, onError); + return () => (isSubscribed = false); + }, []); + + const onNewConfigLoaded = (configJson) => { + if (!isSubscribed) { + return; + } + const domain = configJson["backend_servers"][0]["domain"]; + setDomain(domain); + }; + + const showUser = (userId, username, publicKey) => { + setUsers([]); + setFoundUserId(userId); + setFoundUsername(username); + setFoundPublicKey(publicKey); + }; + + const onSearch = (event) => { + setErrors([]); + setVisualUsername(""); + setFoundUserId(""); + setFoundUsername(""); + setFoundPublicKey(""); + + let searchUsername = username; + let searchEmail = email; + + if (!allowUserSearchByUsernamePartial) { + searchUsername = helperService.formFullUsername(searchUsername, domain); + } + + const onSuccess = function (data) { + data = data.data; + + if (Object.prototype.toString.call(data) === "[object Array]") { + setUsers( + data.map((user) => { + return [user.id, user.username, user.public_key]; + }) + ); + } else { + showUser(data.id, data.username, data.public_key); + } + }; + + const onError = function (data) { + if (data.status === 400) { + setErrors(["USER_NOT_FOUND"]); + } else { + console.log(data); + } + }; + datastoreUserService.searchUser(searchUsername, searchEmail).then(onSuccess, onError); + }; + + const onCreate = (event) => { + const userObject = { + id: cryptoLibrary.generateUuid(), + type: "user", + name: "", + data: { + user_id: foundUserId, + user_public_key: foundPublicKey, + user_username: foundUsername, + }, + }; + if (visualUsername) { + userObject["data"]["user_name"] = visualUsername; + } + + if (userObject.data.user_name) { + userObject.name += userObject.data.user_name; + } else { + userObject.name += userObject.data.user_username; + } + userObject.name += " (" + userObject.data.user_public_key + ")"; + + props.onCreate(userObject); + }; + + if (users.length > 0) { + const columns = [ + { name: t("ID"), options: { display: false } }, + { + name: "", + options: { + filter: false, + sort: false, + empty: false, + customBodyRender: (value, tableMeta, updateValue) => { + return ( + { + showUser(tableMeta.rowData[0], tableMeta.rowData[1], tableMeta.rowData[2]); + }} + > + + + ); + }, + }, + }, + { + name: t("USERNAME"), + options: { + filter: true, + sort: true, + empty: false, + customBodyRender: (value, tableMeta, updateValue) => { + let username = tableMeta.rowData[1].substring(0, 20); + if (tableMeta.rowData[1].length > 20) { + username = username + "..."; + } + return username; + }, + }, + }, + { + name: t("PUBLIC_KEY"), + options: { + filter: true, + sort: true, + empty: false, + customBodyRender: (value, tableMeta, updateValue) => { + let publicKey = tableMeta.rowData[2].substring(0, 50); + if (tableMeta.rowData[2].length > 50) { + publicKey = publicKey + "..."; + } + return publicKey; + }, + }, + }, + ]; + + const options = { + filterType: "checkbox", + }; + + return ( + { + setUsers([]); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {t("PICK_USER")} + + + + + + + + ); + } else { + return ( + { + onClose(); + }} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + {t("NEW_USER")} + + + + {"@" + domain} + ) : null, + }} + name="username" + autoComplete="username" + value={username} + onChange={(event) => { + setUsername(event.target.value); + }} + /> + + {allowUserSearchByEmail && ( + + { + setEmail(event.target.value); + }} + /> + + )} + + + + {Boolean(foundUserId) && ( + + { + setVisualUsername(event.target.value); + }} + /> + + )} + {Boolean(foundUserId) && ( + + + + )} + {Boolean(foundUserId) && ( + + + + )} + + + + + + + + + ); + } +}; + +DialogNewUser.propTypes = { + onClose: PropTypes.func.isRequired, + onCreate: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, +}; + +export default DialogNewUser; diff --git a/src/js/containers/verify-dialog.js b/src/js/components/dialogs/verify.js similarity index 96% rename from src/js/containers/verify-dialog.js rename to src/js/components/dialogs/verify.js index ed95a725..ee4a17ad 100644 --- a/src/js/containers/verify-dialog.js +++ b/src/js/components/dialogs/verify.js @@ -9,7 +9,7 @@ import MuiAlert from "@material-ui/lab/Alert"; import Button from "@material-ui/core/Button"; import { Grid } from "@material-ui/core"; -const VerifyDialog = (props) => { +const DialogVerify = (props) => { const { open, onClose, onConfirm, entries, affectedEntriesText, title, description } = props; const { t } = useTranslation(); @@ -55,7 +55,7 @@ const VerifyDialog = (props) => { ); }; -VerifyDialog.propTypes = { +DialogVerify.propTypes = { title: PropTypes.string.isRequired, description: PropTypes.string.isRequired, affectedEntriesText: PropTypes.string, @@ -65,4 +65,4 @@ VerifyDialog.propTypes = { open: PropTypes.bool.isRequired, }; -export default VerifyDialog; +export default DialogVerify; diff --git a/src/js/components/icons/Rule.js b/src/js/components/icons/Rule.js new file mode 100644 index 00000000..8e15c70d --- /dev/null +++ b/src/js/components/icons/Rule.js @@ -0,0 +1,9 @@ +import * as React from "react"; +import { createSvgIcon } from "@material-ui/core/utils"; + +export default createSvgIcon( + + + , + "ContentCopy" +); diff --git a/src/js/components/select-field/entry-type.js b/src/js/components/select-field/entry-type.js new file mode 100644 index 00000000..d4a014be --- /dev/null +++ b/src/js/components/select-field/entry-type.js @@ -0,0 +1,98 @@ +import React from "react"; +import TextField from "@material-ui/core/TextField"; +import Autocomplete from "@material-ui/lab/Autocomplete"; +import { useTranslation } from "react-i18next"; +import PropTypes from "prop-types"; +import { makeStyles } from "@material-ui/core/styles"; + +import itemBlueprintService from "../../services/item-blueprint"; + +const useStyles = makeStyles((theme) => ({ + option: { + fontSize: 15, + "& > span": { + marginRight: 10, + fontSize: 18, + }, + }, +})); + +const SelectFieldEntryType = (props) => { + const classes = useStyles(); + const { t } = useTranslation(); + + const { fullWidth, variant, margin, helperText, error, required, onChange, value, className } = props; + + const entryTypes = itemBlueprintService.getEntryTypes(); + + let defaultValue = null; + if (value) { + defaultValue = entryTypes.find(function (country) { + return country.value === value; + }); + } + + return ( + { + return option ? t(option.title) : ""; + }} + renderOption={(option) => <>{option ? t(option.title) : ""}} + onChange={(event, newValue) => { + if (newValue) { + onChange(newValue.value); + } else { + onChange(""); + } + }} + getOptionSelected={(option, value) => { + if (option) { + return option.value === value.value; + } else { + return ""; + } + }} + value={defaultValue} + renderInput={(params) => ( + + )} + /> + ); +}; + +SelectFieldEntryType.defaultProps = { + error: false, +}; + +SelectFieldEntryType.propTypes = { + value: PropTypes.string, + fullWidth: PropTypes.bool, + error: PropTypes.bool, + required: PropTypes.bool, + helperText: PropTypes.string, + variant: PropTypes.string, + margin: PropTypes.string, + onChange: PropTypes.func, + className: PropTypes.string, +}; + +export default SelectFieldEntryType; diff --git a/src/js/containers/file-repository-type-select-field.js b/src/js/components/select-field/file-repository-type.js similarity index 91% rename from src/js/containers/file-repository-type-select-field.js rename to src/js/components/select-field/file-repository-type.js index 7fa12a8c..852776ec 100644 --- a/src/js/containers/file-repository-type-select-field.js +++ b/src/js/components/select-field/file-repository-type.js @@ -3,7 +3,7 @@ import TextField from "@material-ui/core/TextField"; import Autocomplete from "@material-ui/lab/Autocomplete"; import { useTranslation } from "react-i18next"; import PropTypes from "prop-types"; -import fileRepository from "../services/file-repository"; +import fileRepository from "../../services/file-repository"; import { makeStyles } from "@material-ui/core/styles"; const useStyles = makeStyles((theme) => ({ @@ -16,7 +16,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -const FileRepositoryTypeSelectField = (props) => { +const SelectFieldFileRepositoryType = (props) => { const classes = useStyles(); const { t } = useTranslation(); @@ -78,11 +78,11 @@ const FileRepositoryTypeSelectField = (props) => { ); }; -FileRepositoryTypeSelectField.defaultProps = { +SelectFieldFileRepositoryType.defaultProps = { error: false, }; -FileRepositoryTypeSelectField.propTypes = { +SelectFieldFileRepositoryType.propTypes = { value: PropTypes.string, fullWidth: PropTypes.bool, error: PropTypes.bool, @@ -94,4 +94,4 @@ FileRepositoryTypeSelectField.propTypes = { className: PropTypes.string, }; -export default FileRepositoryTypeSelectField; +export default SelectFieldFileRepositoryType; diff --git a/src/js/containers/language-select-field.js b/src/js/components/select-field/language.js similarity index 88% rename from src/js/containers/language-select-field.js rename to src/js/components/select-field/language.js index 373a84ae..17e7796d 100644 --- a/src/js/containers/language-select-field.js +++ b/src/js/components/select-field/language.js @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import PropTypes from "prop-types"; import { makeStyles } from "@material-ui/core/styles"; -import { languages } from "../i18n"; +import { languages } from "../../i18n"; const useStyles = makeStyles((theme) => ({ option: { @@ -17,12 +17,10 @@ const useStyles = makeStyles((theme) => ({ }, })); -const LanguageSelectField = (props) => { +const SelectFieldLanguage = (props) => { const classes = useStyles(); const { t } = useTranslation(); - //const lngs = fileRepository.getPossibleTypes(); - const lngs = []; Object.entries(languages).forEach(([key, value]) => { @@ -33,9 +31,9 @@ const LanguageSelectField = (props) => { const { fullWidth, variant, margin, helperText, error, required, onChange, value, className } = props; - let defaulValue = null; + let defaultValue = null; if (value && lngs && lngs.length) { - defaulValue = lngs.find(function (country) { + defaultValue = lngs.find(function (country) { return country.value === value; }); } @@ -65,7 +63,7 @@ const LanguageSelectField = (props) => { return ""; } }} - value={defaulValue} + value={defaultValue} renderInput={(params) => ( { ); }; -LanguageSelectField.defaultProps = { +SelectFieldLanguage.defaultProps = { error: false, }; -LanguageSelectField.propTypes = { +SelectFieldLanguage.propTypes = { value: PropTypes.string, fullWidth: PropTypes.bool, error: PropTypes.bool, @@ -103,4 +101,4 @@ LanguageSelectField.propTypes = { className: PropTypes.string, }; -export default LanguageSelectField; +export default SelectFieldLanguage; diff --git a/src/js/components/table.js b/src/js/components/table.js index 4a7d311b..897d9ca6 100644 --- a/src/js/components/table.js +++ b/src/js/components/table.js @@ -22,7 +22,7 @@ class Table extends Component { }; componentDidMount() { if (this.props.dataFunction) { - var rowsPerPage = 10; + let rowsPerPage = 10; if (this.props.options.hasOwnProperty("rowsPerPage")) { rowsPerPage = this.props.options["rowsPerPage"]; } @@ -99,9 +99,9 @@ class Table extends Component { defaultOptions["serverSide"] = true; defaultOptions["count"] = this.state.count; defaultOptions["onTableChange"] = (action, tableState) => { - var ordering; + let ordering; if (tableState.sortOrder.hasOwnProperty("name")) { - for (var i = 0; i < tableState.columns.length; i++) { + for (let i = 0; i < tableState.columns.length; i++) { if (tableState.columns[i].name === tableState.sortOrder["name"]) { if (tableState.sortOrder["direction"] === "asc") { ordering = tableState.columns[i].id; @@ -113,7 +113,7 @@ class Table extends Component { } } if (["changePage", "sort", "search", "changeRowsPerPage", "changePage", "filterChange"].includes(action)) { - var params = { + const params = { page: tableState.page, page_size: tableState.rowsPerPage, }; diff --git a/src/js/components/text-field-path.js b/src/js/components/text-field-path.js new file mode 100644 index 00000000..7d31a3bc --- /dev/null +++ b/src/js/components/text-field-path.js @@ -0,0 +1,65 @@ +import * as React from "react"; +import PropTypes from "prop-types"; +import InputLabel from "@material-ui/core/InputLabel"; +import FormControl from "@material-ui/core/FormControl"; +import InputAdornment from "@material-ui/core/InputAdornment"; +import { useTranslation } from "react-i18next"; +import OutlinedInput from "@material-ui/core/OutlinedInput"; +import IconButton from "@material-ui/core/IconButton"; +import ClearIcon from "@material-ui/icons/Clear"; + +const TextFieldPath = (props) => { + const { value, setPath, ...other } = props; + const { t } = useTranslation(); + + return ( + + + {t("PATH")} + + + \ + {value.map((path, index) => { + return ( + { + setPath(value.slice(0, index + 1)); + }} + > + {path.name + "\\"} + + ); + })} + + ), + }} + endAdornment={ + value.length > 0 && ( + + setPath([])} edge="end"> + + + + ) + } + /> + + ); +}; + +TextFieldPath.propTypes = { + value: PropTypes.array.isRequired, + setPath: PropTypes.func.isRequired, +}; + +export default TextFieldPath; diff --git a/src/js/components/totp-circle.js b/src/js/components/totp-circle.js new file mode 100644 index 00000000..99574d31 --- /dev/null +++ b/src/js/components/totp-circle.js @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import PropTypes from "prop-types"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import Button from "@material-ui/core/Button"; +import Box from "@material-ui/core/Box"; + +import ContentCopy from "./icons/ContentCopy"; +import cryptoLibraryService from "../services/crypto-library"; +import browserClientService from "../services/browser-client"; + +const TotpCircle = (props) => { + const { period, digits, algorithm, code, ...rest } = props; + const [progress, setProgress] = React.useState(10); + const [token, setToken] = useState(""); + + React.useEffect(() => { + const timer = setInterval(() => { + setToken(cryptoLibraryService.getTotpToken(code, period, algorithm, digits)); + const percentage = 100 - (((period || 30) - (Math.round(new Date().getTime() / 1000.0) % (period || 30))) / (period || 30)) * 100; + setProgress(percentage); + }, 500); + return () => { + clearInterval(timer); + }; + }, []); + return ( + + + + + + + ); +}; + +TotpCircle.defaultProps = { + period: 30, + digits: 6, + algorithm: "SHA1", + code: "", +}; + +TotpCircle.propTypes = { + period: PropTypes.number, + digits: PropTypes.number, + algorithm: PropTypes.string, + code: PropTypes.string, +}; + +export default TotpCircle; diff --git a/src/js/containers/download-banner.js b/src/js/containers/download-banner.js new file mode 100644 index 00000000..440aea37 --- /dev/null +++ b/src/js/containers/download-banner.js @@ -0,0 +1,146 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import { makeStyles } from "@material-ui/core/styles"; +import Typography from "@material-ui/core/Typography"; +import GetAppIcon from "@material-ui/icons/GetApp"; + +import browserClient from "../services/browser-client"; +import deviceService from "../services/device"; +import action from "../actions/bound-action-creators"; + +const useStyles = makeStyles((theme) => ({ + overlay: { + width: "100%", + position: "fixed", + zIndex: 1400, + top: 0, + left: 0, + backgroundColor: "#2dbb93", + overflowY: "hidden", + transition: "0.5s", + "& a": { + padding: "8px", + textDecoration: "none", + display: "block", + transition: "0.3s", + }, + "& a:hover": { + color: "#fff", + }, + "& a:focus": { + color: "#fff", + }, + }, + overlayContent: { + position: "relative", + width: "90%", + textAlign: "center", + paddingLeft: "5%", + }, + closeBtn: { + position: "absolute", + top: 0, + right: "5px", + }, + wrapIcon: { + verticalAlign: "middle", + display: "inline-flex", + }, + downloadIcon: { + fontSize: "1.2rem", + }, +})); + +const DownloadBanner = (props) => { + const classes = useStyles(); + const { t } = useTranslation(); + const [disableDownloadBanner, setDisableDownloadBanner] = useState(false); + const isDownloadBannerHidden = useSelector((state) => state.client.hideDownloadBanner); + const showAndroidDownload = deviceService.isMobileAndroid(); + const showIosDownload = deviceService.isMobileIos(); + const showChromeDownload = !deviceService.isMobile() && deviceService.isChrome() && browserClient.getClientType() === "webclient"; + const showFirefoxDownload = !deviceService.isMobile() && deviceService.isFirefox() && browserClient.getClientType() === "webclient"; + + React.useEffect(() => { + browserClient.getConfig().then(onNewConfigLoaded); + // cancel subscription to useEffect + return () => (isSubscribed = false); + }, []); + + let isSubscribed = true; + const onNewConfigLoaded = (configJson) => { + if (!isSubscribed) { + return; + } + setDisableDownloadBanner(Boolean(configJson["disable_download_bar"])); + }; + + const hideDownloadBanner = (event) => { + action.setHideDownloadBanner(true); + }; + + return ( +
+ {!disableDownloadBanner && !isDownloadBannerHidden && showAndroidDownload && ( +