From 3d03fece74339f33995d3c2da3f51730b9e2146f Mon Sep 17 00:00:00 2001 From: = Date: Sat, 14 Dec 2024 19:22:50 +0530 Subject: [PATCH] feat: first login page base completed --- frontend-v2/eslint.config.js | 2 +- frontend-v2/package-lock.json | 1140 ++++++++++++++++- frontend-v2/package.json | 8 +- .../components/utilities/SecurityClient.ts | 7 +- .../utilities/cryptography/crypto.ts | 35 +- frontend-v2/src/components/v2/Menu/Menu.tsx | 4 +- frontend-v2/src/config/request.ts | 3 +- .../src/context/AuthContext/AuthContext.tsx | 33 + frontend-v2/src/context/AuthContext/index.tsx | 1 + .../OrgPermissionContext.tsx | 58 + .../context/OrgPermissionContext/index.tsx | 3 + .../src/context/OrgPermissionContext/types.ts | 52 + .../OrganizationContext.tsx | 44 + .../src/context/OrganizationContext/index.tsx | 1 + .../ProjectPermissionContext.tsx | 62 + .../ProjectPermissionContext/index.tsx | 8 + .../context/ProjectPermissionContext/types.ts | 176 +++ .../ServerConfigContext.tsx | 89 ++ .../src/context/ServerConfigContext/index.tsx | 1 + .../SubscriptionContext.tsx | 45 + .../src/context/SubscriptionContext/index.tsx | 1 + .../src/context/UserContext/UserContext.tsx | 53 + frontend-v2/src/context/UserContext/index.tsx | 1 + .../WorkspaceContext/WorkspaceContext.tsx | 55 + .../src/context/WorkspaceContext/index.tsx | 1 + frontend-v2/src/context/index.tsx | 22 + frontend-v2/src/helpers/key.ts | 84 ++ frontend-v2/src/helpers/members.ts | 12 + frontend-v2/src/helpers/parseEnvVar.ts | 31 + frontend-v2/src/helpers/policies.ts | 12 + frontend-v2/src/helpers/project.ts | 62 + frontend-v2/src/helpers/reverseTruncate.ts | 5 + frontend-v2/src/helpers/roles.ts | 30 + frontend-v2/src/helpers/string.ts | 15 + frontend-v2/src/hooks/api/reactQuery.tsx | 12 +- frontend-v2/src/routeTree.gen.ts | 169 ++- frontend-v2/src/routes/__root.tsx | 21 +- .../-components/InitialStep/InitialStep.tsx | 408 ++++++ .../login/-components/InitialStep/index.tsx | 1 + .../routes/login/-components/Login.utils.tsx | 51 + .../src/routes/login/-components/LoginSSO.tsx | 41 + .../src/routes/login/-components/Mfa.tsx | 222 ++++ .../-components/PasswordStep/PasswordStep.tsx | 379 ++++++ .../login/-components/PasswordStep/index.tsx | 1 + .../login/-components/SSOStep/SSOStep.tsx | 82 ++ .../login/-components/SSOStep/index.tsx | 1 + .../src/routes/login/-components/index.tsx | 5 + frontend-v2/src/routes/login/index.tsx | 81 ++ frontend-v2/src/routes/login/ldap/index.tsx | 157 +++ .../src/routes/login/provider/error.tsx | 15 + .../src/routes/login/provider/success.tsx | 19 + .../login/select-organization/index.tsx | 281 ++++ frontend-v2/src/routes/login/sso/index.tsx | 63 + frontend-v2/src/services/KeyService.ts | 104 ++ frontend-v2/src/services/ProjectService.ts | 19 + frontend-v2/src/services/index.ts | 4 + frontend-v2/tsconfig.app.json | 2 +- frontend-v2/vite.config.ts | 21 +- 58 files changed, 4268 insertions(+), 47 deletions(-) create mode 100644 frontend-v2/src/context/AuthContext/AuthContext.tsx create mode 100644 frontend-v2/src/context/AuthContext/index.tsx create mode 100644 frontend-v2/src/context/OrgPermissionContext/OrgPermissionContext.tsx create mode 100644 frontend-v2/src/context/OrgPermissionContext/index.tsx create mode 100644 frontend-v2/src/context/OrgPermissionContext/types.ts create mode 100644 frontend-v2/src/context/OrganizationContext/OrganizationContext.tsx create mode 100644 frontend-v2/src/context/OrganizationContext/index.tsx create mode 100644 frontend-v2/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx create mode 100644 frontend-v2/src/context/ProjectPermissionContext/index.tsx create mode 100644 frontend-v2/src/context/ProjectPermissionContext/types.ts create mode 100644 frontend-v2/src/context/ServerConfigContext/ServerConfigContext.tsx create mode 100644 frontend-v2/src/context/ServerConfigContext/index.tsx create mode 100644 frontend-v2/src/context/SubscriptionContext/SubscriptionContext.tsx create mode 100644 frontend-v2/src/context/SubscriptionContext/index.tsx create mode 100644 frontend-v2/src/context/UserContext/UserContext.tsx create mode 100644 frontend-v2/src/context/UserContext/index.tsx create mode 100644 frontend-v2/src/context/WorkspaceContext/WorkspaceContext.tsx create mode 100644 frontend-v2/src/context/WorkspaceContext/index.tsx create mode 100644 frontend-v2/src/context/index.tsx create mode 100644 frontend-v2/src/helpers/key.ts create mode 100644 frontend-v2/src/helpers/members.ts create mode 100644 frontend-v2/src/helpers/parseEnvVar.ts create mode 100644 frontend-v2/src/helpers/policies.ts create mode 100644 frontend-v2/src/helpers/project.ts create mode 100644 frontend-v2/src/helpers/reverseTruncate.ts create mode 100644 frontend-v2/src/helpers/roles.ts create mode 100644 frontend-v2/src/helpers/string.ts create mode 100644 frontend-v2/src/routes/login/-components/InitialStep/InitialStep.tsx create mode 100644 frontend-v2/src/routes/login/-components/InitialStep/index.tsx create mode 100644 frontend-v2/src/routes/login/-components/Login.utils.tsx create mode 100644 frontend-v2/src/routes/login/-components/LoginSSO.tsx create mode 100644 frontend-v2/src/routes/login/-components/Mfa.tsx create mode 100644 frontend-v2/src/routes/login/-components/PasswordStep/PasswordStep.tsx create mode 100644 frontend-v2/src/routes/login/-components/PasswordStep/index.tsx create mode 100644 frontend-v2/src/routes/login/-components/SSOStep/SSOStep.tsx create mode 100644 frontend-v2/src/routes/login/-components/SSOStep/index.tsx create mode 100644 frontend-v2/src/routes/login/-components/index.tsx create mode 100644 frontend-v2/src/routes/login/index.tsx create mode 100644 frontend-v2/src/routes/login/ldap/index.tsx create mode 100644 frontend-v2/src/routes/login/provider/error.tsx create mode 100644 frontend-v2/src/routes/login/provider/success.tsx create mode 100644 frontend-v2/src/routes/login/select-organization/index.tsx create mode 100644 frontend-v2/src/routes/login/sso/index.tsx create mode 100644 frontend-v2/src/services/KeyService.ts create mode 100644 frontend-v2/src/services/ProjectService.ts create mode 100644 frontend-v2/src/services/index.ts diff --git a/frontend-v2/eslint.config.js b/frontend-v2/eslint.config.js index 9168ba88d..33a019fa5 100644 --- a/frontend-v2/eslint.config.js +++ b/frontend-v2/eslint.config.js @@ -49,7 +49,7 @@ export default tseslint.config( }, rules: { ...reactHooks.configs.recommended.rules, - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + "react-refresh/only-export-components": "off", "@typescript-eslint/no-empty-function": "off", quotes: ["error", "double", { avoidEscape: true }], "comma-dangle": ["error", "only-multiline"], diff --git a/frontend-v2/package-lock.json b/frontend-v2/package-lock.json index 7fc2dda8d..d16a6344c 100644 --- a/frontend-v2/package-lock.json +++ b/frontend-v2/package-lock.json @@ -68,6 +68,7 @@ "react-code-input": "^3.10.1", "react-day-picker": "^9.4.3", "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", "react-hook-form": "^7.54.0", "react-i18next": "^15.2.0", "react-icons": "^5.4.0", @@ -97,6 +98,7 @@ "@types/qrcode": "^1.5.5", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "@types/react-helmet": "^6.1.11", "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.20", "eslint": "^8.57.1", @@ -115,7 +117,11 @@ "tailwindcss": "^3.4.16", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", - "vite": "^6.0.1" + "vite": "^6.0.1", + "vite-plugin-node-polyfills": "^0.22.0", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0", + "vite-tsconfig-paths": "^5.1.4" } }, "node_modules/@alloc/quick-lru": { @@ -2920,6 +2926,70 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", "license": "MIT" }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-virtual": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-virtual/-/plugin-virtual-3.0.2.tgz", + "integrity": "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.3.tgz", + "integrity": "sha512-Pnsb6f32CD2W3uCaLZIzDmeFyQ2b8UWMFI7xtwUezpcGBDVDW6y9XgAWIlARiGAo6eNF5FK5aQTr0LFyNyqq5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz", @@ -3947,6 +4017,16 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-transition-group": { "version": "4.4.12", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", @@ -4518,6 +4598,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/asn1js": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.5.tgz", @@ -4532,6 +4631,20 @@ "node": ">=12.0.0" } }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, "node_modules/ast-types-flow": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", @@ -4690,6 +4803,27 @@ "node": ">= 0.6.0" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/before-after-hook": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", @@ -4709,6 +4843,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4733,6 +4874,171 @@ "node": ">=8" } }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.17.0" + } + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "dev": true, + "license": "ISC", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.5", + "hash-base": "~3.0", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/browserify-sign/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-sign/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pako": "~1.0.5" + } + }, "node_modules/browserslist": { "version": "4.24.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", @@ -4778,6 +5084,45 @@ "node": ">= 0.4.0" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", @@ -5092,6 +5437,19 @@ "dev": true, "license": "MIT" }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", + "dev": true + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -5110,6 +5468,13 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", @@ -5135,6 +5500,24 @@ "node": ">= 6" } }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/create-hash": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", @@ -5148,6 +5531,28 @@ "sha.js": "^2.4.0" } }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-fetch": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", @@ -5172,6 +5577,47 @@ "node": ">= 8" } }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/crypto-browserify/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/css-line-break": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", @@ -5364,6 +5810,17 @@ "node": ">=0.4.0" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/detect-node-es": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", @@ -5377,6 +5834,25 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -5413,6 +5889,19 @@ "csstype": "^3.0.2" } }, + "node_modules/domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://bevry.me/fund" + } + }, "node_modules/dompurify": { "version": "2.5.8", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", @@ -5449,6 +5938,29 @@ "dev": true, "license": "ISC" }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -6314,6 +6826,13 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6324,6 +6843,27 @@ "node": ">=0.10.0" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6786,6 +7326,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true, + "license": "MIT" + }, "node_modules/goober": { "version": "2.1.16", "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", @@ -6915,6 +7462,17 @@ "node": ">=4" } }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -6927,6 +7485,18 @@ "node": ">= 0.4" } }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", @@ -6959,6 +7529,13 @@ "node": ">=8.0.0" } }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", + "dev": true, + "license": "MIT" + }, "node_modules/i18next": { "version": "24.1.0", "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.1.0.tgz", @@ -7008,6 +7585,27 @@ "cross-fetch": "4.0.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -7095,6 +7693,23 @@ "node": ">= 10" } }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", @@ -7329,6 +7944,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -7535,6 +8167,16 @@ "dev": true, "license": "ISC" }, + "node_modules/isomorphic-timers-promises": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz", + "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/iterator.prototype": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", @@ -7852,6 +8494,16 @@ "dev": true, "license": "ISC" }, + "node_modules/magic-string": { + "version": "0.30.15", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.15.tgz", + "integrity": "sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/math-intrinsics": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.0.0.tgz", @@ -7916,6 +8568,27 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -7937,6 +8610,20 @@ "node": ">= 0.6" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", + "dev": true, + "license": "MIT" + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -8053,6 +8740,52 @@ "dev": true, "license": "MIT" }, + "node_modules/node-stdlib-browser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.0.tgz", + "integrity": "sha512-g/koYzOr9Fb1Jc+tHUHlFd5gODjGn48tHexUK8q6iqOVriEgSnd3/1T7myBYc+0KBVze/7F7n65ec9rW6OD7xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "browser-resolve": "^2.0.0", + "browserify-zlib": "^0.2.0", + "buffer": "^5.7.1", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "create-require": "^1.1.1", + "crypto-browserify": "^3.11.0", + "domain-browser": "4.22.0", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "isomorphic-timers-promises": "^1.0.1", + "os-browserify": "^0.3.0", + "path-browserify": "^1.0.1", + "pkg-dir": "^5.0.0", + "process": "^0.11.10", + "punycode": "^1.4.1", + "querystring-es3": "^0.2.1", + "readable-stream": "^3.6.0", + "stream-browserify": "^3.0.0", + "stream-http": "^3.2.0", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.1", + "url": "^0.11.4", + "util": "^0.12.4", + "vm-browserify": "^1.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-stdlib-browser/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -8111,6 +8844,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -8235,6 +8985,13 @@ "node": ">= 0.8.0" } }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", + "dev": true, + "license": "MIT" + }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8283,6 +9040,13 @@ "dev": true, "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8295,6 +9059,38 @@ "node": ">=6" } }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "dev": true, + "license": "ISC", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse-asn1/node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -8313,6 +9109,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8374,6 +9177,23 @@ "node": ">=8" } }, + "node_modules/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -8419,6 +9239,19 @@ "node": ">= 6" } }, + "node_modules/pkg-dir": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz", + "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/pngjs": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", @@ -8734,6 +9567,23 @@ } } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -8751,6 +9601,28 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", + "dev": true, + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8796,6 +9668,31 @@ "node": ">=10.13.0" } }, + "node_modules/qs": { + "version": "6.13.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.1.tgz", + "integrity": "sha512-EJPeIn0CYrGu+hli1xilKAPXODtJ12T0sP63Ijx2/khC2JtuaN3JyNIpvmnkmaEtha9ocbG4A4cMcr+TvqvwQg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "dev": true, + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8836,6 +9733,17 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -8935,6 +9843,27 @@ "react": "^18.3.1" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", + "license": "MIT" + }, + "node_modules/react-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-hook-form": { "version": "7.54.0", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.54.0.tgz", @@ -9056,6 +9985,15 @@ "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", @@ -9500,6 +10438,13 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, "node_modules/sha.js": { "version": "2.4.11", "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", @@ -9661,6 +10606,30 @@ "node": ">=0.1.14" } }, + "node_modules/stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "node_modules/stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -10130,6 +11099,19 @@ "node": ">=0.8" } }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -10181,6 +11163,27 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/tsconfck": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.4.tgz", + "integrity": "sha512-kdqWFGVJqe+KGYvlSO9NIaWn9jT1Ny4oKVzAJsKii5eoE9snzTJzL4+MMVOMn+fikWGFmKEylcXL710V/kIPJQ==", + "dev": true, + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -10686,6 +11689,13 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", "license": "0BSD" }, + "node_modules/tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==", + "dev": true, + "license": "MIT" + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", @@ -10923,6 +11933,27 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true, + "license": "MIT" + }, "node_modules/use-callback-ref": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", @@ -10989,6 +12020,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -11005,6 +12050,20 @@ "base64-arraybuffer": "^1.0.2" } }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/vite": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz", @@ -11077,6 +12136,75 @@ } } }, + "node_modules/vite-plugin-node-polyfills": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.22.0.tgz", + "integrity": "sha512-F+G3LjiGbG8QpbH9bZ//GSBr9i1InSTkaulfUHFa9jkLqVGORFBoqc2A/Yu5Mmh1kNAbiAeKeK+6aaQUf3x0JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-inject": "^5.0.5", + "node-stdlib-browser": "^1.2.0" + }, + "funding": { + "url": "https://github.com/sponsors/davidmyersdev" + }, + "peerDependencies": { + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/vite-plugin-top-level-await": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/vite-plugin-top-level-await/-/vite-plugin-top-level-await-1.4.4.tgz", + "integrity": "sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-virtual": "^3.0.2", + "@swc/core": "^1.7.0", + "uuid": "^10.0.0" + }, + "peerDependencies": { + "vite": ">=2.8" + } + }, + "node_modules/vite-plugin-wasm": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-wasm/-/vite-plugin-wasm-3.3.0.tgz", + "integrity": "sha512-tVhz6w+W9MVsOCHzxo6SSMSswCeIw4HTrXEi6qL3IRzATl83jl09JVO1djBqPSwfjgnpVHNLYcaMbaDX5WB/pg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vite": "^2 || ^3 || ^4 || ^5" + } + }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/void-elements": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", @@ -11342,6 +12470,16 @@ "dev": true, "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/frontend-v2/package.json b/frontend-v2/package.json index e89c1b45e..c3f818080 100644 --- a/frontend-v2/package.json +++ b/frontend-v2/package.json @@ -72,6 +72,7 @@ "react-code-input": "^3.10.1", "react-day-picker": "^9.4.3", "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", "react-hook-form": "^7.54.0", "react-i18next": "^15.2.0", "react-icons": "^5.4.0", @@ -101,6 +102,7 @@ "@types/qrcode": "^1.5.5", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", + "@types/react-helmet": "^6.1.11", "@vitejs/plugin-react-swc": "^3.5.0", "autoprefixer": "^10.4.20", "eslint": "^8.57.1", @@ -119,6 +121,10 @@ "tailwindcss": "^3.4.16", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", - "vite": "^6.0.1" + "vite": "^6.0.1", + "vite-plugin-node-polyfills": "^0.22.0", + "vite-plugin-top-level-await": "^1.4.4", + "vite-plugin-wasm": "^3.3.0", + "vite-tsconfig-paths": "^5.1.4" } } diff --git a/frontend-v2/src/components/utilities/SecurityClient.ts b/frontend-v2/src/components/utilities/SecurityClient.ts index f4011c2f0..ea8839320 100644 --- a/frontend-v2/src/components/utilities/SecurityClient.ts +++ b/frontend-v2/src/components/utilities/SecurityClient.ts @@ -1,4 +1,9 @@ -import { getAuthToken, setAuthToken, setMfaTempToken, setSignupTempToken } from "@app/reactQuery"; +import { + getAuthToken, + setAuthToken, + setMfaTempToken, + setSignupTempToken +} from "@app/hooks/api/reactQuery"; export const PROVIDER_AUTH_TOKEN_KEY = "infisical__provider-auth-token"; diff --git a/frontend-v2/src/components/utilities/cryptography/crypto.ts b/frontend-v2/src/components/utilities/cryptography/crypto.ts index c0e5d4c21..1469b0e89 100644 --- a/frontend-v2/src/components/utilities/cryptography/crypto.ts +++ b/frontend-v2/src/components/utilities/cryptography/crypto.ts @@ -1,10 +1,9 @@ -import argon2 from "argon2-browser"; +import argon2 from "argon2-browser/dist/argon2-bundled.min.js"; +import nacl from "tweetnacl"; +import { encodeBase64, decodeBase64, encodeUTF8, decodeUTF8 } from "tweetnacl-util"; import aes from "./aes-256-gcm"; -const nacl = require("tweetnacl"); -nacl.util = require("tweetnacl-util"); - /** * Return new base64, NaCl, public-private key pair. * @returns {Object} obj @@ -15,8 +14,8 @@ const generateKeyPair = () => { const pair = nacl.box.keyPair(); return { - publicKey: nacl.util.encodeBase64(pair.publicKey), - privateKey: nacl.util.encodeBase64(pair.secretKey) + publicKey: encodeBase64(pair.publicKey), + privateKey: encodeBase64(pair.secretKey) }; }; @@ -34,8 +33,8 @@ type EncryptAsymmetricProps = { * @param {String} - base64-encoded Nacl public key */ const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => { - const derivedPublicKey = nacl.util.encodeBase64( - nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(privateKey)).publicKey + const derivedPublicKey = encodeBase64( + nacl.box.keyPair.fromSecretKey(decodeBase64(privateKey)).publicKey ); if (derivedPublicKey !== publicKey) { @@ -108,15 +107,15 @@ const encryptAssymmetric = ({ } => { const nonce = nacl.randomBytes(24); const ciphertext = nacl.box( - nacl.util.decodeUTF8(plaintext), + decodeUTF8(plaintext), nonce, - nacl.util.decodeBase64(publicKey), - nacl.util.decodeBase64(privateKey) + decodeBase64(publicKey), + decodeBase64(privateKey) ); return { - ciphertext: nacl.util.encodeBase64(ciphertext), - nonce: nacl.util.encodeBase64(nonce) + ciphertext: encodeBase64(ciphertext), + nonce: encodeBase64(nonce) }; }; @@ -143,13 +142,13 @@ const decryptAssymmetric = ({ privateKey }: DecryptAsymmetricProps): string => { const plaintext = nacl.box.open( - nacl.util.decodeBase64(ciphertext), - nacl.util.decodeBase64(nonce), - nacl.util.decodeBase64(publicKey), - nacl.util.decodeBase64(privateKey) + decodeBase64(ciphertext), + decodeBase64(nonce), + decodeBase64(publicKey), + decodeBase64(privateKey) ); - return nacl.util.encodeUTF8(plaintext); + return encodeUTF8(plaintext); }; type EncryptSymmetricProps = { diff --git a/frontend-v2/src/components/v2/Menu/Menu.tsx b/frontend-v2/src/components/v2/Menu/Menu.tsx index 0555ef826..cc7d460b9 100644 --- a/frontend-v2/src/components/v2/Menu/Menu.tsx +++ b/frontend-v2/src/components/v2/Menu/Menu.tsx @@ -71,7 +71,7 @@ export const MenuItem = ({ lottieRef={iconRef} style={{ width: 22, height: 22 }} // eslint-disable-next-line import/no-dynamic-require - animationData={require(`../../../../public/lotties/${icon}.json`)} + // animationData={require(`../../../../public/lotties/${icon}.json`)} loop={false} autoplay={false} className="my-auto ml-[0.1rem] mr-3" @@ -121,7 +121,7 @@ export const SubMenuItem = ({ lottieRef={iconRef} style={{ width: 16, height: 16 }} // eslint-disable-next-line import/no-dynamic-require - animationData={require(`../../../../public/lotties/${icon}.json`)} + // animationData={require(`../../../../public/lotties/${icon}.json`)} loop={false} autoplay={false} className="my-auto ml-[0.1rem] mr-3" diff --git a/frontend-v2/src/config/request.ts b/frontend-v2/src/config/request.ts index 9a0629619..2d7520f5e 100644 --- a/frontend-v2/src/config/request.ts +++ b/frontend-v2/src/config/request.ts @@ -3,8 +3,9 @@ import axios from "axios"; import SecurityClient from "@app/components/utilities/SecurityClient"; import { getAuthToken, getMfaTempToken, getSignupTempToken } from "@app/hooks/api/reactQuery"; +// TODO(rbr): update this later export const apiRequest = axios.create({ - baseURL: "/", + baseURL: "http://localhost:8080", headers: { "Content-Type": "application/json" } diff --git a/frontend-v2/src/context/AuthContext/AuthContext.tsx b/frontend-v2/src/context/AuthContext/AuthContext.tsx new file mode 100644 index 000000000..239656e19 --- /dev/null +++ b/frontend-v2/src/context/AuthContext/AuthContext.tsx @@ -0,0 +1,33 @@ +import { ReactNode } from "react"; + +import { useGetAuthToken } from "@app/hooks/api"; + +type Props = { + children: ReactNode; +}; + +// TODO(akhilmhdh): Using react-simple-animate from hard dom offloading +// smoother dom offloading needs to be done + +// Authentication controller +// Does route checking +// Provide a context for whole app to notify user is authorized or not +export const AuthProvider = ({ children }: Props): JSX.Element => { + const { isLoading } = useGetAuthToken(); + + // wait for app to load the auth state + if (isLoading) { + return ( +
+ infisical loading indicator +
+ ); + } + + return children as JSX.Element; +}; diff --git a/frontend-v2/src/context/AuthContext/index.tsx b/frontend-v2/src/context/AuthContext/index.tsx new file mode 100644 index 000000000..f4b411794 --- /dev/null +++ b/frontend-v2/src/context/AuthContext/index.tsx @@ -0,0 +1 @@ +export { AuthProvider } from "./AuthContext"; diff --git a/frontend-v2/src/context/OrgPermissionContext/OrgPermissionContext.tsx b/frontend-v2/src/context/OrgPermissionContext/OrgPermissionContext.tsx new file mode 100644 index 000000000..36422b722 --- /dev/null +++ b/frontend-v2/src/context/OrgPermissionContext/OrgPermissionContext.tsx @@ -0,0 +1,58 @@ +import { createContext, ReactNode, useContext } from "react"; + +import { useGetUserOrgPermissions } from "@app/hooks/api"; +import { OrgUser } from "@app/hooks/api/types"; + +import { useOrganization } from "../OrganizationContext"; +import { TOrgPermission } from "./types"; + +type Props = { + children: ReactNode; +}; + +const OrgPermissionContext = createContext(null); + +export const OrgPermissionProvider = ({ children }: Props): JSX.Element => { + const { currentOrg } = useOrganization(); + const orgId = currentOrg?.id || ""; + const { data: permission, isLoading } = useGetUserOrgPermissions({ orgId }); + + if (isLoading) { + return ( +
+ infisical loading indicator +
+ ); + } + + if (!permission) { + return ( +
+ Failed to load user permissions +
+ ); + } + + return ( + {children} + ); +}; + +export const useOrgPermission = () => { + const ctx = useContext(OrgPermissionContext); + if (!ctx) { + throw new Error("useOrgPermission to be used within "); + } + + return ctx; +}; diff --git a/frontend-v2/src/context/OrgPermissionContext/index.tsx b/frontend-v2/src/context/OrgPermissionContext/index.tsx new file mode 100644 index 000000000..730fe55b1 --- /dev/null +++ b/frontend-v2/src/context/OrgPermissionContext/index.tsx @@ -0,0 +1,3 @@ +export { OrgPermissionProvider, useOrgPermission } from "./OrgPermissionContext"; +export type { TOrgPermission } from "./types"; +export { OrgPermissionActions, OrgPermissionSubjects } from "./types"; diff --git a/frontend-v2/src/context/OrgPermissionContext/types.ts b/frontend-v2/src/context/OrgPermissionContext/types.ts new file mode 100644 index 000000000..41a2e7e3c --- /dev/null +++ b/frontend-v2/src/context/OrgPermissionContext/types.ts @@ -0,0 +1,52 @@ +import { MongoAbility } from "@casl/ability"; + +export enum OrgPermissionActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum OrgPermissionSubjects { + Workspace = "workspace", + Role = "role", + Member = "member", + Settings = "settings", + IncidentAccount = "incident-contact", + Scim = "scim", + Sso = "sso", + Ldap = "ldap", + Groups = "groups", + Billing = "billing", + SecretScanning = "secret-scanning", + Identity = "identity", + Kms = "kms", + AdminConsole = "organization-admin-console", + AuditLogs = "audit-logs", + ProjectTemplates = "project-templates" +} + +export enum OrgPermissionAdminConsoleAction { + AccessAllProjects = "access-all-projects" +} + +export type OrgPermissionSet = + | [OrgPermissionActions.Create, OrgPermissionSubjects.Workspace] + | [OrgPermissionActions.Read, OrgPermissionSubjects.Workspace] + | [OrgPermissionActions, OrgPermissionSubjects.Role] + | [OrgPermissionActions, OrgPermissionSubjects.Member] + | [OrgPermissionActions, OrgPermissionSubjects.Settings] + | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] + | [OrgPermissionActions, OrgPermissionSubjects.Scim] + | [OrgPermissionActions, OrgPermissionSubjects.Sso] + | [OrgPermissionActions, OrgPermissionSubjects.Ldap] + | [OrgPermissionActions, OrgPermissionSubjects.Groups] + | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] + | [OrgPermissionActions, OrgPermissionSubjects.Billing] + | [OrgPermissionActions, OrgPermissionSubjects.Identity] + | [OrgPermissionActions, OrgPermissionSubjects.Kms] + | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] + | [OrgPermissionActions, OrgPermissionSubjects.AuditLogs] + | [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]; + +export type TOrgPermission = MongoAbility; diff --git a/frontend-v2/src/context/OrganizationContext/OrganizationContext.tsx b/frontend-v2/src/context/OrganizationContext/OrganizationContext.tsx new file mode 100644 index 000000000..e73578d13 --- /dev/null +++ b/frontend-v2/src/context/OrganizationContext/OrganizationContext.tsx @@ -0,0 +1,44 @@ +import { createContext, ReactNode, useContext, useMemo } from "react"; + +import { useGetOrganizations } from "@app/hooks/api"; +import { Organization } from "@app/hooks/api/types"; + +type TOrgContext = { + orgs?: Organization[]; + currentOrg?: Organization; + isLoading: boolean; +}; + +const OrgContext = createContext(null); + +type Props = { + children: ReactNode; +}; + +export const OrgProvider = ({ children }: Props): JSX.Element => { + const { data: userOrgs, isLoading } = useGetOrganizations(); + + // const currentWsOrgID = currentWorkspace?.organization; + const currentWsOrgID = localStorage.getItem("orgData.id"); + + // memorize the workspace details for the context + const value = useMemo( + () => ({ + orgs: userOrgs, + currentOrg: (userOrgs || []).find(({ id }) => id === currentWsOrgID), + isLoading + }), + [currentWsOrgID, userOrgs, isLoading] + ); + + return {children}; +}; + +export const useOrganization = () => { + const ctx = useContext(OrgContext); + if (!ctx) { + throw new Error("useOrganization to be used within "); + } + + return ctx; +}; diff --git a/frontend-v2/src/context/OrganizationContext/index.tsx b/frontend-v2/src/context/OrganizationContext/index.tsx new file mode 100644 index 000000000..65aba8ca4 --- /dev/null +++ b/frontend-v2/src/context/OrganizationContext/index.tsx @@ -0,0 +1 @@ +export { OrgProvider, useOrganization } from "./OrganizationContext"; diff --git a/frontend-v2/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx b/frontend-v2/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx new file mode 100644 index 000000000..b0ee59960 --- /dev/null +++ b/frontend-v2/src/context/ProjectPermissionContext/ProjectPermissionContext.tsx @@ -0,0 +1,62 @@ +import { createContext, ReactNode, useContext } from "react"; + +import { useGetUserProjectPermissions } from "@app/hooks/api"; +import { TProjectMembership } from "@app/hooks/api/users/types"; + +import { useWorkspace } from "../WorkspaceContext"; +import { TProjectPermission } from "./types"; + +type Props = { + children: ReactNode; +}; + +const ProjectPermissionContext = createContext(null); + +export const ProjectPermissionProvider = ({ children }: Props): JSX.Element => { + const { currentWorkspace, isLoading: isWsLoading } = useWorkspace(); + const workspaceId = currentWorkspace?.id || ""; + const { data: permission, isLoading } = useGetUserProjectPermissions({ workspaceId }); + + if ((isLoading && currentWorkspace) || isWsLoading) { + return ( +
+ infisical loading indicator +
+ ); + } + + if (!permission && currentWorkspace) { + return ( +
+ Failed to load user permissions +
+ ); + } + + return ( + + {children} + + ); +}; + +export const useProjectPermission = () => { + const ctx = useContext(ProjectPermissionContext); + if (!ctx) { + throw new Error("useProjectPermission to be used within "); + } + + const hasProjectRole = (role: string) => ctx?.membership?.roles?.includes(role) || false; + + return { ...ctx, hasProjectRole }; +}; diff --git a/frontend-v2/src/context/ProjectPermissionContext/index.tsx b/frontend-v2/src/context/ProjectPermissionContext/index.tsx new file mode 100644 index 000000000..4b04f5cb8 --- /dev/null +++ b/frontend-v2/src/context/ProjectPermissionContext/index.tsx @@ -0,0 +1,8 @@ +export { ProjectPermissionProvider, useProjectPermission } from "./ProjectPermissionContext"; +export type { ProjectPermissionSet, TProjectPermission } from "./types"; +export { + ProjectPermissionActions, + ProjectPermissionCmekActions, + ProjectPermissionDynamicSecretActions, + ProjectPermissionSub +} from "./types"; diff --git a/frontend-v2/src/context/ProjectPermissionContext/types.ts b/frontend-v2/src/context/ProjectPermissionContext/types.ts new file mode 100644 index 000000000..673b2b41a --- /dev/null +++ b/frontend-v2/src/context/ProjectPermissionContext/types.ts @@ -0,0 +1,176 @@ +import { ForcedSubject, MongoAbility } from "@casl/ability"; + +export enum ProjectPermissionActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete" +} + +export enum ProjectPermissionDynamicSecretActions { + ReadRootCredential = "read-root-credential", + CreateRootCredential = "create-root-credential", + EditRootCredential = "edit-root-credential", + DeleteRootCredential = "delete-root-credential", + Lease = "lease" +} + +export enum ProjectPermissionCmekActions { + Read = "read", + Create = "create", + Edit = "edit", + Delete = "delete", + Encrypt = "encrypt", + Decrypt = "decrypt" +} + +export enum PermissionConditionOperators { + $IN = "$in", + $ALL = "$all", + $REGEX = "$regex", + $EQ = "$eq", + $NEQ = "$ne", + $GLOB = "$glob" +} + +export type IdentityManagementSubjectFields = { + identityId: string; +}; + +export const formatedConditionsOperatorNames: { [K in PermissionConditionOperators]: string } = { + [PermissionConditionOperators.$EQ]: "equal to", + [PermissionConditionOperators.$IN]: "contains", + [PermissionConditionOperators.$ALL]: "contains all", + [PermissionConditionOperators.$NEQ]: "not equal to", + [PermissionConditionOperators.$GLOB]: "matches glob pattern", + [PermissionConditionOperators.$REGEX]: "matches regex pattern" +}; + +export type TPermissionConditionOperators = { + [PermissionConditionOperators.$IN]: string[]; + [PermissionConditionOperators.$ALL]: string[]; + [PermissionConditionOperators.$EQ]: string; + [PermissionConditionOperators.$NEQ]: string; + [PermissionConditionOperators.$REGEX]: string; + [PermissionConditionOperators.$GLOB]: string; +}; + +export type TPermissionCondition = Record< + string, + | string + | { $in: string[]; $all: string[]; $regex: string; $eq: string; $ne: string; $glob: string } +>; + +export enum ProjectPermissionSub { + Role = "role", + Member = "member", + Groups = "groups", + Settings = "settings", + Integrations = "integrations", + Webhooks = "webhooks", + ServiceTokens = "service-tokens", + Environments = "environments", + Tags = "tags", + AuditLogs = "audit-logs", + IpAllowList = "ip-allowlist", + Project = "workspace", + Secrets = "secrets", + SecretFolders = "secret-folders", + SecretImports = "secret-imports", + DynamicSecrets = "dynamic-secrets", + SecretRollback = "secret-rollback", + SecretApproval = "secret-approval", + SecretRotation = "secret-rotation", + Identity = "identity", + CertificateAuthorities = "certificate-authorities", + Certificates = "certificates", + CertificateTemplates = "certificate-templates", + PkiAlerts = "pki-alerts", + PkiCollections = "pki-collections", + Kms = "kms", + Cmek = "cmek" +} + +export type SecretSubjectFields = { + environment: string; + secretPath: string; + secretName: string; + secretTags: string[]; +}; + +export type SecretFolderSubjectFields = { + environment: string; + secretPath: string; +}; + +export type DynamicSecretSubjectFields = { + environment: string; + secretPath: string; +}; + +export type SecretImportSubjectFields = { + environment: string; + secretPath: string; +}; + +export type ProjectPermissionSet = + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.Secrets + | (ForcedSubject & SecretSubjectFields) + ) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.SecretFolders + | (ForcedSubject & SecretFolderSubjectFields) + ) + ] + | [ + ProjectPermissionDynamicSecretActions, + ( + | ProjectPermissionSub.DynamicSecrets + | (ForcedSubject & DynamicSecretSubjectFields) + ) + ] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.SecretImports + | (ForcedSubject & SecretImportSubjectFields) + ) + ] + | [ProjectPermissionActions, ProjectPermissionSub.Role] + | [ProjectPermissionActions, ProjectPermissionSub.Tags] + | [ProjectPermissionActions, ProjectPermissionSub.Member] + | [ProjectPermissionActions, ProjectPermissionSub.Groups] + | [ProjectPermissionActions, ProjectPermissionSub.Integrations] + | [ProjectPermissionActions, ProjectPermissionSub.Webhooks] + | [ProjectPermissionActions, ProjectPermissionSub.AuditLogs] + | [ProjectPermissionActions, ProjectPermissionSub.Environments] + | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] + | [ProjectPermissionActions, ProjectPermissionSub.Settings] + | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] + | [ProjectPermissionActions, ProjectPermissionSub.SecretApproval] + | [ProjectPermissionActions, ProjectPermissionSub.SecretRotation] + | [ + ProjectPermissionActions, + ( + | ProjectPermissionSub.Identity + | (ForcedSubject & IdentityManagementSubjectFields) + ) + ] + | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] + | [ProjectPermissionActions, ProjectPermissionSub.Certificates] + | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] + | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] + | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] + | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Project] + | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] + | [ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback] + | [ProjectPermissionCmekActions, ProjectPermissionSub.Cmek] + | [ProjectPermissionActions.Edit, ProjectPermissionSub.Kms]; +export type TProjectPermission = MongoAbility; diff --git a/frontend-v2/src/context/ServerConfigContext/ServerConfigContext.tsx b/frontend-v2/src/context/ServerConfigContext/ServerConfigContext.tsx new file mode 100644 index 000000000..79b0dce8e --- /dev/null +++ b/frontend-v2/src/context/ServerConfigContext/ServerConfigContext.tsx @@ -0,0 +1,89 @@ +import { createContext, ReactNode, useContext, useEffect, useMemo } from "react"; + +import { ContentLoader } from "@app/components/v2/ContentLoader"; +import { useGetServerConfig } from "@app/hooks/api"; +import { TServerConfig } from "@app/hooks/api/admin/types"; +import { Helmet } from "react-helmet"; +import { useNavigate } from "@tanstack/react-router"; + +type TServerConfigContext = { + config: TServerConfig; +}; + +const ServerConfigContext = createContext(null); + +type Props = { + children: ReactNode; +}; + +export const ServerConfigProvider = ({ children }: Props): JSX.Element => { + const navigate = useNavigate(); + const { data, isLoading } = useGetServerConfig(); + + // memorize the workspace details for the context + const value = useMemo(() => { + return { + config: data! + }; + }, [data]); + + useEffect(() => { + if (!isLoading && data && !data.initialized && !data.isMigrationModeOn) { + navigate({ to: "/admin/signup" }); + } + }, [isLoading, data]); + + if (!isLoading && data?.isMigrationModeOn) { + return ( +
+ + Infisical Maintenance Mode + + + maintenance mode +

+ Scheduled Maintenance +

+
+ Infisical is undergoing planned maintenance.
No action is required on your end — + your applications will continue to fetch secrets. +
If you have questions, please{" "} + + join our Slack community + + . +
+
+ ); + } + + if (isLoading) { + return ( +
+ +
+ ); + } + + return {children}; +}; + +export const useServerConfig = () => { + const ctx = useContext(ServerConfigContext); + if (!ctx) { + throw new Error("useServerConfig has to be used within "); + } + + return ctx; +}; diff --git a/frontend-v2/src/context/ServerConfigContext/index.tsx b/frontend-v2/src/context/ServerConfigContext/index.tsx new file mode 100644 index 000000000..856eb5c77 --- /dev/null +++ b/frontend-v2/src/context/ServerConfigContext/index.tsx @@ -0,0 +1 @@ +export { ServerConfigProvider, useServerConfig } from "./ServerConfigContext"; diff --git a/frontend-v2/src/context/SubscriptionContext/SubscriptionContext.tsx b/frontend-v2/src/context/SubscriptionContext/SubscriptionContext.tsx new file mode 100644 index 000000000..9c50e425f --- /dev/null +++ b/frontend-v2/src/context/SubscriptionContext/SubscriptionContext.tsx @@ -0,0 +1,45 @@ +import { createContext, ReactNode, useContext, useMemo } from "react"; + +import { useGetOrgSubscription } from "@app/hooks/api"; +import { SubscriptionPlan } from "@app/hooks/api/types"; + +import { useOrganization } from "../OrganizationContext"; + +type TSubscriptionContext = { + subscription?: SubscriptionPlan; + isLoading: boolean; +}; + +const SubscriptionContext = createContext(null); + +type Props = { + children: ReactNode; +}; + +export const SubscriptionProvider = ({ children }: Props): JSX.Element => { + const { currentOrg } = useOrganization(); + + const { data, isLoading } = useGetOrgSubscription({ + orgID: currentOrg?.id || "" + }); + + // memorize the workspace details for the context + const value = useMemo( + () => ({ + subscription: data, + isLoading + }), + [data, isLoading] + ); + + return {children}; +}; + +export const useSubscription = () => { + const ctx = useContext(SubscriptionContext); + if (!ctx) { + throw new Error("useSubscription has to be used within "); + } + + return ctx; +}; diff --git a/frontend-v2/src/context/SubscriptionContext/index.tsx b/frontend-v2/src/context/SubscriptionContext/index.tsx new file mode 100644 index 000000000..50a97eeb2 --- /dev/null +++ b/frontend-v2/src/context/SubscriptionContext/index.tsx @@ -0,0 +1 @@ +export { SubscriptionProvider, useSubscription } from "./SubscriptionContext"; diff --git a/frontend-v2/src/context/UserContext/UserContext.tsx b/frontend-v2/src/context/UserContext/UserContext.tsx new file mode 100644 index 000000000..0e23ccb7c --- /dev/null +++ b/frontend-v2/src/context/UserContext/UserContext.tsx @@ -0,0 +1,53 @@ +import { createContext, ReactNode, useContext, useMemo } from "react"; + +import { useGetUser } from "@app/hooks/api"; +import { User, UserEnc } from "@app/hooks/api/types"; + +type TUserContext = { + user: User & UserEnc; + isLoading: boolean; +}; + +const UserContext = createContext(null); + +type Props = { + children: ReactNode; +}; + +export const UserProvider = ({ children }: Props): JSX.Element => { + const { data, isLoading } = useGetUser(); + + // memorize the workspace details for the context + const value = useMemo(() => { + return { + user: data!, + isLoading + }; + }, [data, isLoading]); + + if (isLoading) { + return ( +
+ infisical loading indicator +
+ ); + } + + return {children}; +}; + +export const useUser = () => { + const ctx = useContext(UserContext); + if (!ctx) { + throw new Error("useUser has to be used within "); + } + + return ctx; +}; diff --git a/frontend-v2/src/context/UserContext/index.tsx b/frontend-v2/src/context/UserContext/index.tsx new file mode 100644 index 000000000..b4f6056dd --- /dev/null +++ b/frontend-v2/src/context/UserContext/index.tsx @@ -0,0 +1 @@ +export { UserProvider, useUser } from "./UserContext"; diff --git a/frontend-v2/src/context/WorkspaceContext/WorkspaceContext.tsx b/frontend-v2/src/context/WorkspaceContext/WorkspaceContext.tsx new file mode 100644 index 000000000..505297e69 --- /dev/null +++ b/frontend-v2/src/context/WorkspaceContext/WorkspaceContext.tsx @@ -0,0 +1,55 @@ +import { createContext, ReactNode, useContext, useEffect, useMemo } from "react"; + +import { createNotification } from "@app/components/notifications"; +import { useGetUserWorkspaces } from "@app/hooks/api"; +import { Workspace } from "@app/hooks/api/workspace/types"; +import { useParams } from "@tanstack/react-router"; + +type TWorkspaceContext = { + workspaces: Workspace[]; + currentWorkspace?: Workspace; + isLoading: boolean; +}; + +const WorkspaceContext = createContext(null); + +type Props = { + children: ReactNode; +}; + +export const WorkspaceProvider = ({ children }: Props): JSX.Element => { + const { data: ws, isLoading } = useGetUserWorkspaces(); + const params = useParams({ strict: false }); + const workspaceId = params.id; + + // memorize the workspace details for the context + const value = useMemo(() => { + const wsId = workspaceId || localStorage.getItem("projectData.id"); + return { + workspaces: ws || [], + currentWorkspace: (ws || []).find(({ id }) => id === wsId), + isLoading + }; + }, [ws, workspaceId, isLoading]); + + const shouldTriggerNoProjectAccess = !value.isLoading && !value.currentWorkspace; + + if (shouldTriggerNoProjectAccess) { + return ( +
+ You do not have sufficient access to this project. +
+ ); + } + + return {children}; +}; + +export const useWorkspace = () => { + const ctx = useContext(WorkspaceContext); + if (!ctx) { + throw new Error("useWorkspace has to be used within "); + } + + return ctx; +}; diff --git a/frontend-v2/src/context/WorkspaceContext/index.tsx b/frontend-v2/src/context/WorkspaceContext/index.tsx new file mode 100644 index 000000000..0ae8cb4c5 --- /dev/null +++ b/frontend-v2/src/context/WorkspaceContext/index.tsx @@ -0,0 +1 @@ +export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext"; diff --git a/frontend-v2/src/context/index.tsx b/frontend-v2/src/context/index.tsx new file mode 100644 index 000000000..91dae5d2d --- /dev/null +++ b/frontend-v2/src/context/index.tsx @@ -0,0 +1,22 @@ +export { AuthProvider } from "./AuthContext"; +export { OrgProvider, useOrganization } from "./OrganizationContext"; +export type { TOrgPermission } from "./OrgPermissionContext"; +export { + OrgPermissionActions, + OrgPermissionProvider, + OrgPermissionSubjects, + useOrgPermission +} from "./OrgPermissionContext"; +export type { TProjectPermission } from "./ProjectPermissionContext"; +export { + ProjectPermissionActions, + ProjectPermissionCmekActions, + ProjectPermissionDynamicSecretActions, + ProjectPermissionProvider, + ProjectPermissionSub, + useProjectPermission +} from "./ProjectPermissionContext"; +export { ServerConfigProvider, useServerConfig } from "./ServerConfigContext"; +export { SubscriptionProvider, useSubscription } from "./SubscriptionContext"; +export { UserProvider, useUser } from "./UserContext"; +export { useWorkspace, WorkspaceProvider } from "./WorkspaceContext"; diff --git a/frontend-v2/src/helpers/key.ts b/frontend-v2/src/helpers/key.ts new file mode 100644 index 000000000..9301bb77b --- /dev/null +++ b/frontend-v2/src/helpers/key.ts @@ -0,0 +1,84 @@ +import Aes256Gcm from "@app/components/utilities/cryptography/aes-256-gcm"; +import { deriveArgonKey } from "@app/components/utilities/cryptography/crypto"; + +/** + * @param {Object} obj + * @param {Number} obj.encryptionVersion + * @param {String} obj.encryptedPrivateKey + * @param {String} obj.iv + * @param {String} obj.tag + * @param {String} obj.password + * @param {String} obj.salt + * @param {String} obj.protectedKey + * @param {String} obj.protectedKeyIV + * @param {String} obj.protectedKeyTag + */ +const decryptPrivateKeyHelper = async ({ + encryptionVersion, + encryptedPrivateKey, + iv, + tag, + password, + salt, + protectedKey, + protectedKeyIV, + protectedKeyTag +}: { + encryptionVersion: number; + encryptedPrivateKey: string; + iv: string; + tag: string; + password: string; + salt: string; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; +}) => { + let privateKey; + try { + if (encryptionVersion === 1) { + privateKey = Aes256Gcm.decrypt({ + ciphertext: encryptedPrivateKey, + iv, + tag, + secret: password + .slice(0, 32) + .padStart(32 + (password.slice(0, 32).length - new Blob([password]).size), "0") + }); + } else if (encryptionVersion === 2 && protectedKey && protectedKeyIV && protectedKeyTag) { + const derivedKey = await deriveArgonKey({ + password, + salt, + mem: 65536, + time: 3, + parallelism: 1, + hashLen: 32 + }); + + if (!derivedKey) throw new Error("Failed to generate derived key"); + + const key = Aes256Gcm.decrypt({ + ciphertext: protectedKey, + iv: protectedKeyIV, + tag: protectedKeyTag, + secret: Buffer.from(derivedKey.hash) + }); + + // decrypt back the private key + privateKey = Aes256Gcm.decrypt({ + ciphertext: encryptedPrivateKey, + iv, + tag, + secret: Buffer.from(key, "hex") + }); + } else { + throw new Error("Insufficient details to decrypt private key"); + } + } catch (err) { + throw new Error("Failed to decrypt private key"); + } + + return privateKey; +}; + +export { decryptPrivateKeyHelper }; diff --git a/frontend-v2/src/helpers/members.ts b/frontend-v2/src/helpers/members.ts new file mode 100644 index 000000000..871ef3ec6 --- /dev/null +++ b/frontend-v2/src/helpers/members.ts @@ -0,0 +1,12 @@ +import { TWorkspaceUser } from "@app/hooks/api/users/types"; + +export const getMemberLabel = (member: TWorkspaceUser) => { + const { + inviteEmail, + user: { firstName, lastName, username, email } + } = member; + + return firstName || lastName + ? `${firstName ?? ""} ${lastName ?? ""}`.trim() + : username || email || inviteEmail; +}; diff --git a/frontend-v2/src/helpers/parseEnvVar.ts b/frontend-v2/src/helpers/parseEnvVar.ts new file mode 100644 index 000000000..8bde05084 --- /dev/null +++ b/frontend-v2/src/helpers/parseEnvVar.ts @@ -0,0 +1,31 @@ +/** Extracts the key and value from a passed in env string based on the provided delimiters. */ +export const getKeyValue = (pastedContent: string, delimiters: string[]) => { + if (!pastedContent) { + return { key: "", value: "" }; + } + + let firstDelimiterIndex = -1; + let foundDelimiter = ""; + + delimiters.forEach((delimiter) => { + const index = pastedContent.indexOf(delimiter); + if (index !== -1 && (firstDelimiterIndex === -1 || index < firstDelimiterIndex)) { + firstDelimiterIndex = index; + foundDelimiter = delimiter; + } + }); + + const hasValueAfterDelimiter = pastedContent.length > firstDelimiterIndex + foundDelimiter.length; + + if (firstDelimiterIndex === -1 || !hasValueAfterDelimiter) { + return { key: pastedContent.trim(), value: "" }; + } + + const key = pastedContent.substring(0, firstDelimiterIndex); + const value = pastedContent.substring(firstDelimiterIndex + foundDelimiter.length); + + return { + key: key.trim(), + value: value.trim() + }; +}; diff --git a/frontend-v2/src/helpers/policies.ts b/frontend-v2/src/helpers/policies.ts new file mode 100644 index 000000000..c6d7c935a --- /dev/null +++ b/frontend-v2/src/helpers/policies.ts @@ -0,0 +1,12 @@ +import { PolicyType } from "@app/hooks/api/policies/enums"; + +export const policyDetails: Record = { + [PolicyType.AccessPolicy]: { + className: "bg-lime-900 text-lime-100", + name: "Access Policy" + }, + [PolicyType.ChangePolicy]: { + className: "bg-indigo-900 text-indigo-100", + name: "Change Policy" + } +}; \ No newline at end of file diff --git a/frontend-v2/src/helpers/project.ts b/frontend-v2/src/helpers/project.ts new file mode 100644 index 000000000..b6338ce35 --- /dev/null +++ b/frontend-v2/src/helpers/project.ts @@ -0,0 +1,62 @@ +import { apiRequest } from "@app/config/request"; +import { createWorkspace } from "@app/hooks/api/workspace/queries"; + +const secretsToBeAdded = [ + { + secretKey: "DATABASE_URL", + // eslint-disable-next-line no-template-curly-in-string + secretValue: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net", + secretComment: "Secret referencing example" + }, + { + secretKey: "DB_USERNAME", + secretValue: "OVERRIDE_THIS", + secretComment: "Override secrets with personal value" + }, + { + secretKey: "DB_PASSWORD", + secretValue: "OVERRIDE_THIS", + secretComment: "Another secret override" + }, + { + secretKey: "DB_PASSWORD", + secretValue: "example_password" + }, + { + secretKey: "TWILIO_AUTH_TOKEN", + secretValue: "example_twillio_token" + }, + { + secretKey: "WEBSITE_URL", + secretValue: "http://localhost:3000" + } +]; + +/** + * Create and initialize a new project in organization with id [organizationId] + * Note: current user should be a member of the organization + */ +const initProjectHelper = async ({ projectName }: { projectName: string }) => { + // create new project + const { + data: { project } + } = await createWorkspace({ + projectName + }); + + try { + const { data } = await apiRequest.post("/api/v3/secrets/batch/raw", { + workspaceId: project.id, + environment: "dev", + secretPath: "/", + secrets: secretsToBeAdded + }); + return data; + } catch (err) { + console.error("Failed to upload secrets", err); + } + + return project; +}; + +export { initProjectHelper }; diff --git a/frontend-v2/src/helpers/reverseTruncate.ts b/frontend-v2/src/helpers/reverseTruncate.ts new file mode 100644 index 000000000..eba5f21d3 --- /dev/null +++ b/frontend-v2/src/helpers/reverseTruncate.ts @@ -0,0 +1,5 @@ +export const reverseTruncate = (text: string, maxLength = 42) => { + if (text.length < maxLength) return text; + + return `...${text.substring(text.length - maxLength + 3)}`; +}; diff --git a/frontend-v2/src/helpers/roles.ts b/frontend-v2/src/helpers/roles.ts new file mode 100644 index 000000000..4e26e1b15 --- /dev/null +++ b/frontend-v2/src/helpers/roles.ts @@ -0,0 +1,30 @@ +import { ProjectMembershipRole, TOrgRole } from "@app/hooks/api/roles/types"; + +enum OrgMembershipRole { + Admin = "admin", + Member = "member", + NoAccess = "no-access" +} + +enum ProjectMemberRole { + Admin = "admin", + Member = "member", + Viewer = "viewer", + NoAccess = "no-access" +} + +export const isCustomOrgRole = (slug: string) => + !Object.values(OrgMembershipRole).includes(slug as OrgMembershipRole); + +export const formatProjectRoleName = (name: string) => { + if (name === ProjectMemberRole.Member) return "developer"; + return name; +}; + +export const isCustomProjectRole = (slug: string) => + !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); + +export const findOrgMembershipRole = (roles: TOrgRole[], roleIdOrSlug: string) => + isCustomOrgRole(roleIdOrSlug) + ? roles.find((r) => r.id === roleIdOrSlug) + : roles.find((r) => r.slug === roleIdOrSlug); diff --git a/frontend-v2/src/helpers/string.ts b/frontend-v2/src/helpers/string.ts new file mode 100644 index 000000000..760d9c828 --- /dev/null +++ b/frontend-v2/src/helpers/string.ts @@ -0,0 +1,15 @@ +export const removeTrailingSlash = (str: string) => { + if (str === "/") return str; + + return str.endsWith("/") ? str.slice(0, -1) : str; +}; + +export const isValidPath = (val: string): boolean => { + if (val.length === 0) return false; + if (val === "/") return true; + + // Check for valid characters and no consecutive slashes + const validPathRegex = /^[a-zA-Z0-9-_.:]+(?:\/[a-zA-Z0-9-_.:]+)*$/; + return validPathRegex.test(val); +}; + diff --git a/frontend-v2/src/hooks/api/reactQuery.tsx b/frontend-v2/src/hooks/api/reactQuery.tsx index 0cfe0180f..af0ce7640 100644 --- a/frontend-v2/src/hooks/api/reactQuery.tsx +++ b/frontend-v2/src/hooks/api/reactQuery.tsx @@ -2,16 +2,16 @@ import { MutationCache, QueryClient } from "@tanstack/react-query"; import axios from "axios"; import { createNotification } from "@app/components/notifications"; - // akhilmhdh: doing individual imports to avoid cyclic import error -import { Button } from "./components/v2/Button"; -import { Modal, ModalContent, ModalTrigger } from "./components/v2/Modal"; -import { Table, TableContainer, TBody, Td, Th, THead, Tr } from "./components/v2/Table"; +import { Button } from "@app/components/v2/Button"; +import { Modal, ModalContent, ModalTrigger } from "@app/components/v2/Modal"; +import { Table, TableContainer, TBody, Td, Th, THead, Tr } from "@app/components/v2/Table"; import { formatedConditionsOperatorNames, PermissionConditionOperators -} from "./context/ProjectPermissionContext/types"; -import { ApiErrorTypes, TApiErrors } from "./hooks/api/types"; +} from "@app/context/ProjectPermissionContext/types"; + +import { ApiErrorTypes, TApiErrors } from "./types"; // this is saved in react-query cache export const SIGNUP_TEMP_TOKEN_CACHE_KEY = ["infisical__signup-temp-token"]; diff --git a/frontend-v2/src/routeTree.gen.ts b/frontend-v2/src/routeTree.gen.ts index 5e682936b..2b0df6f44 100644 --- a/frontend-v2/src/routeTree.gen.ts +++ b/frontend-v2/src/routeTree.gen.ts @@ -12,6 +12,12 @@ import { Route as rootRoute } from './routes/__root' import { Route as IndexImport } from './routes/index' +import { Route as LoginIndexImport } from './routes/login/index' +import { Route as LoginSsoIndexImport } from './routes/login/sso/index' +import { Route as LoginSelectOrganizationIndexImport } from './routes/login/select-organization/index' +import { Route as LoginLdapIndexImport } from './routes/login/ldap/index' +import { Route as LoginProviderSuccessImport } from './routes/login/provider/success' +import { Route as LoginProviderErrorImport } from './routes/login/provider/error' // Create/Update Routes @@ -21,6 +27,43 @@ const IndexRoute = IndexImport.update({ getParentRoute: () => rootRoute, } as any) +const LoginIndexRoute = LoginIndexImport.update({ + id: '/login/', + path: '/login/', + getParentRoute: () => rootRoute, +} as any) + +const LoginSsoIndexRoute = LoginSsoIndexImport.update({ + id: '/login/sso/', + path: '/login/sso/', + getParentRoute: () => rootRoute, +} as any) + +const LoginSelectOrganizationIndexRoute = + LoginSelectOrganizationIndexImport.update({ + id: '/login/select-organization/', + path: '/login/select-organization/', + getParentRoute: () => rootRoute, + } as any) + +const LoginLdapIndexRoute = LoginLdapIndexImport.update({ + id: '/login/ldap/', + path: '/login/ldap/', + getParentRoute: () => rootRoute, +} as any) + +const LoginProviderSuccessRoute = LoginProviderSuccessImport.update({ + id: '/login/provider/success', + path: '/login/provider/success', + getParentRoute: () => rootRoute, +} as any) + +const LoginProviderErrorRoute = LoginProviderErrorImport.update({ + id: '/login/provider/error', + path: '/login/provider/error', + getParentRoute: () => rootRoute, +} as any) + // Populate the FileRoutesByPath interface declare module '@tanstack/react-router' { @@ -32,6 +75,48 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof IndexImport parentRoute: typeof rootRoute } + '/login/': { + id: '/login/' + path: '/login' + fullPath: '/login' + preLoaderRoute: typeof LoginIndexImport + parentRoute: typeof rootRoute + } + '/login/provider/error': { + id: '/login/provider/error' + path: '/login/provider/error' + fullPath: '/login/provider/error' + preLoaderRoute: typeof LoginProviderErrorImport + parentRoute: typeof rootRoute + } + '/login/provider/success': { + id: '/login/provider/success' + path: '/login/provider/success' + fullPath: '/login/provider/success' + preLoaderRoute: typeof LoginProviderSuccessImport + parentRoute: typeof rootRoute + } + '/login/ldap/': { + id: '/login/ldap/' + path: '/login/ldap' + fullPath: '/login/ldap' + preLoaderRoute: typeof LoginLdapIndexImport + parentRoute: typeof rootRoute + } + '/login/select-organization/': { + id: '/login/select-organization/' + path: '/login/select-organization' + fullPath: '/login/select-organization' + preLoaderRoute: typeof LoginSelectOrganizationIndexImport + parentRoute: typeof rootRoute + } + '/login/sso/': { + id: '/login/sso/' + path: '/login/sso' + fullPath: '/login/sso' + preLoaderRoute: typeof LoginSsoIndexImport + parentRoute: typeof rootRoute + } } } @@ -39,32 +124,84 @@ declare module '@tanstack/react-router' { export interface FileRoutesByFullPath { '/': typeof IndexRoute + '/login': typeof LoginIndexRoute + '/login/provider/error': typeof LoginProviderErrorRoute + '/login/provider/success': typeof LoginProviderSuccessRoute + '/login/ldap': typeof LoginLdapIndexRoute + '/login/select-organization': typeof LoginSelectOrganizationIndexRoute + '/login/sso': typeof LoginSsoIndexRoute } export interface FileRoutesByTo { '/': typeof IndexRoute + '/login': typeof LoginIndexRoute + '/login/provider/error': typeof LoginProviderErrorRoute + '/login/provider/success': typeof LoginProviderSuccessRoute + '/login/ldap': typeof LoginLdapIndexRoute + '/login/select-organization': typeof LoginSelectOrganizationIndexRoute + '/login/sso': typeof LoginSsoIndexRoute } export interface FileRoutesById { __root__: typeof rootRoute '/': typeof IndexRoute + '/login/': typeof LoginIndexRoute + '/login/provider/error': typeof LoginProviderErrorRoute + '/login/provider/success': typeof LoginProviderSuccessRoute + '/login/ldap/': typeof LoginLdapIndexRoute + '/login/select-organization/': typeof LoginSelectOrganizationIndexRoute + '/login/sso/': typeof LoginSsoIndexRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath - fullPaths: '/' + fullPaths: + | '/' + | '/login' + | '/login/provider/error' + | '/login/provider/success' + | '/login/ldap' + | '/login/select-organization' + | '/login/sso' fileRoutesByTo: FileRoutesByTo - to: '/' - id: '__root__' | '/' + to: + | '/' + | '/login' + | '/login/provider/error' + | '/login/provider/success' + | '/login/ldap' + | '/login/select-organization' + | '/login/sso' + id: + | '__root__' + | '/' + | '/login/' + | '/login/provider/error' + | '/login/provider/success' + | '/login/ldap/' + | '/login/select-organization/' + | '/login/sso/' fileRoutesById: FileRoutesById } export interface RootRouteChildren { IndexRoute: typeof IndexRoute + LoginIndexRoute: typeof LoginIndexRoute + LoginProviderErrorRoute: typeof LoginProviderErrorRoute + LoginProviderSuccessRoute: typeof LoginProviderSuccessRoute + LoginLdapIndexRoute: typeof LoginLdapIndexRoute + LoginSelectOrganizationIndexRoute: typeof LoginSelectOrganizationIndexRoute + LoginSsoIndexRoute: typeof LoginSsoIndexRoute } const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, + LoginIndexRoute: LoginIndexRoute, + LoginProviderErrorRoute: LoginProviderErrorRoute, + LoginProviderSuccessRoute: LoginProviderSuccessRoute, + LoginLdapIndexRoute: LoginLdapIndexRoute, + LoginSelectOrganizationIndexRoute: LoginSelectOrganizationIndexRoute, + LoginSsoIndexRoute: LoginSsoIndexRoute, } export const routeTree = rootRoute @@ -77,11 +214,35 @@ export const routeTree = rootRoute "__root__": { "filePath": "__root.tsx", "children": [ - "/" + "/", + "/login/", + "/login/provider/error", + "/login/provider/success", + "/login/ldap/", + "/login/select-organization/", + "/login/sso/" ] }, "/": { "filePath": "index.tsx" + }, + "/login/": { + "filePath": "login/index.tsx" + }, + "/login/provider/error": { + "filePath": "login/provider/error.tsx" + }, + "/login/provider/success": { + "filePath": "login/provider/success.tsx" + }, + "/login/ldap/": { + "filePath": "login/ldap/index.tsx" + }, + "/login/select-organization/": { + "filePath": "login/select-organization/index.tsx" + }, + "/login/sso/": { + "filePath": "login/sso/index.tsx" } } } diff --git a/frontend-v2/src/routes/__root.tsx b/frontend-v2/src/routes/__root.tsx index 13c65f492..8099dbf9a 100644 --- a/frontend-v2/src/routes/__root.tsx +++ b/frontend-v2/src/routes/__root.tsx @@ -1,16 +1,21 @@ -import { createRootRoute, Link, Outlet } from "@tanstack/react-router"; +import { QueryClientProvider } from "@tanstack/react-query"; +import { createRootRoute, Outlet } from "@tanstack/react-router"; import { TanStackRouterDevtools } from "@tanstack/router-devtools"; +import { queryClient } from "@app/hooks/api/reactQuery"; +import { ServerConfigProvider } from "@app/context"; +import { TooltipProvider } from "@app/components/v2"; + export const Route = createRootRoute({ component: () => ( <> -
- - Home - -
-
- + + + + + + + ) diff --git a/frontend-v2/src/routes/login/-components/InitialStep/InitialStep.tsx b/frontend-v2/src/routes/login/-components/InitialStep/InitialStep.tsx new file mode 100644 index 000000000..63ead0173 --- /dev/null +++ b/frontend-v2/src/routes/login/-components/InitialStep/InitialStep.tsx @@ -0,0 +1,408 @@ +import { FormEvent, useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useNavigate } from "@tanstack/react-router"; +import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons"; +import { faLock } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import HCaptcha from "@hcaptcha/react-hcaptcha"; + +import Error from "@app/components/basic/Error"; +import { RegionSelect } from "@app/components/navigation/RegionSelect"; +import { createNotification } from "@app/components/notifications"; +import attemptCliLogin from "@app/components/utilities/attemptCliLogin"; +import attemptLogin from "@app/components/utilities/attemptLogin"; +import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config"; +import { Button, IconButton, Input, Tooltip } from "@app/components/v2"; +import { useServerConfig } from "@app/context"; +import { useFetchServerStatus } from "@app/hooks/api"; +import { LoginMethod } from "@app/hooks/api/admin/types"; +import { AuthMethod } from "@app/hooks/api/users/types"; + +import { useNavigateToSelectOrganization } from "../Login.utils"; + +type Props = { + setStep: (step: number) => void; + email: string; + setEmail: (email: string) => void; + password: string; + setPassword: (email: string) => void; +}; + +export const InitialStep = ({ setStep, email, setEmail, password, setPassword }: Props) => { + const navigate = useNavigate(); + + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [loginError, setLoginError] = useState(false); + const { config } = useServerConfig(); + const queryParams = new URLSearchParams(window.location.search); + const [captchaToken, setCaptchaToken] = useState(""); + const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false); + const captchaRef = useRef(null); + const { data: serverDetails } = useFetchServerStatus(); + + const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); + + const redirectToSaml = (orgSlug: string) => { + const callbackPort = queryParams.get("callback_port"); + const redirectUrl = `/api/v1/sso/redirect/saml2/organizations/${orgSlug}${ + callbackPort ? `?callback_port=${callbackPort}` : "" + }`; + navigate({ to: redirectUrl }); + }; + + const redirectToOidc = (orgSlug: string) => { + const callbackPort = queryParams.get("callback_port"); + const redirectUrl = `/api/v1/sso/oidc/login?orgSlug=${orgSlug}${ + callbackPort ? `&callbackPort=${callbackPort}` : "" + }`; + navigate({ to: redirectUrl }); + }; + + useEffect(() => { + if (serverDetails?.samlDefaultOrgSlug) redirectToSaml(serverDetails.samlDefaultOrgSlug); + }, [serverDetails?.samlDefaultOrgSlug]); + + const handleSaml = () => { + if (config.defaultAuthOrgSlug) { + redirectToSaml(config.defaultAuthOrgSlug); + } else { + setStep(2); + } + }; + + const handleOidc = () => { + if (config.defaultAuthOrgSlug) { + redirectToOidc(config.defaultAuthOrgSlug); + } else { + setStep(3); + } + }; + + const shouldDisplayLoginMethod = (method: LoginMethod) => + !config.enabledLoginMethods || config.enabledLoginMethods.includes(method); + + const handleLogin = async (e: FormEvent) => { + e.preventDefault(); + try { + if (!email || !password) { + return; + } + + setIsLoading(true); + if (queryParams && queryParams.get("callback_port")) { + const callbackPort = queryParams.get("callback_port"); + + // attemptCliLogin + const isCliLoginSuccessful = await attemptCliLogin({ + email: email.toLowerCase(), + password, + captchaToken + }); + + if (isCliLoginSuccessful && isCliLoginSuccessful.success) { + navigateToSelectOrganization(callbackPort!); + } else { + setLoginError(true); + createNotification({ + text: "CLI login unsuccessful. Double-check your credentials and try again.", + type: "error" + }); + } + } else { + const isLoginSuccessful = await attemptLogin({ + email: email.toLowerCase(), + password, + captchaToken + }); + + if (isLoginSuccessful && isLoginSuccessful.success) { + // case: login was successful + navigateToSelectOrganization(); + createNotification({ + text: "Successfully logged in", + type: "success" + }); + } + } + } catch (err: any) { + console.error(err); + if (err.response.data.error === "User Locked") { + createNotification({ + title: err.response.data.error, + text: err.response.data.message, + type: "error" + }); + setIsLoading(false); + return; + } + + if (err.response.data.error === "Captcha Required") { + setShouldShowCaptcha(true); + setIsLoading(false); + return; + } + + setLoginError(true); + createNotification({ + text: "Login unsuccessful. Double-check your credentials and try again.", + type: "error" + }); + } + + if (captchaRef.current) { + captchaRef.current.resetCaptcha(); + } + + setCaptchaToken(""); + setIsLoading(false); + }; + + if (config.defaultAuthOrgAuthEnforced && config.defaultAuthOrgAuthMethod) { + return ( +
+

+ Login to Infisical +

+ + {config.defaultAuthOrgAuthMethod === AuthMethod.SAML && ( +
+ +
+ )} + {config.defaultAuthOrgAuthMethod === AuthMethod.OIDC && ( +
+ +
+ )} + + ); + } + + return ( +
+

+ Login to Infisical +

+ + {shouldDisplayLoginMethod(LoginMethod.SAML) && ( +
+ +
+ )} + {shouldDisplayLoginMethod(LoginMethod.OIDC) && ( +
+ +
+ )} + {shouldDisplayLoginMethod(LoginMethod.LDAP) && ( +
+ +
+ )} +
+ {shouldDisplayLoginMethod(LoginMethod.GOOGLE) && ( + + { + const callbackPort = queryParams.get("callback_port"); + + window.open( + `/api/v1/sso/redirect/google${ + callbackPort ? `?callback_port=${callbackPort}` : "" + }` + ); + window.close(); + }} + className="h-10 w-full bg-mineshaft-600" + > + + + + )} + {shouldDisplayLoginMethod(LoginMethod.GITHUB) && ( + + { + const callbackPort = queryParams.get("callback_port"); + + window.open( + `/api/v1/sso/redirect/github${ + callbackPort ? `?callback_port=${callbackPort}` : "" + }` + ); + + window.close(); + }} + className="h-10 w-full bg-mineshaft-600" + > + + + + )} + {shouldDisplayLoginMethod(LoginMethod.GITLAB) && ( + + { + const callbackPort = queryParams.get("callback_port"); + + window.open( + `/api/v1/sso/redirect/gitlab${ + callbackPort ? `?callback_port=${callbackPort}` : "" + }` + ); + + window.close(); + }} + className="h-10 w-full bg-mineshaft-600" + > + + + + )} +
+ {(!config.enabledLoginMethods || + (shouldDisplayLoginMethod(LoginMethod.EMAIL) && config.enabledLoginMethods.length > 1)) && ( +
+
+ or +
+
+ )} + {shouldDisplayLoginMethod(LoginMethod.EMAIL) && ( + <> +
+ setEmail(e.target.value)} + type="email" + placeholder="Enter your email..." + isRequired + autoComplete="username" + className="h-10" + /> +
+
+ setPassword(e.target.value)} + type="password" + placeholder="Enter your password..." + isRequired + autoComplete="current-password" + id="current-password" + className="select:-webkit-autofill:focus h-10" + /> +
+ {shouldShowCaptcha && ( +
+ setCaptchaToken(token)} + ref={captchaRef} + /> +
+ )} +
+ +
+ + )} + {!isLoading && loginError && } + {config.allowSignUp && + (shouldDisplayLoginMethod(LoginMethod.EMAIL) || + shouldDisplayLoginMethod(LoginMethod.GOOGLE) || + shouldDisplayLoginMethod(LoginMethod.GITHUB) || + shouldDisplayLoginMethod(LoginMethod.GITLAB)) ? ( +
+ + + Don't have an account yet? {t("login.create-account")} + + +
+ ) : ( +
+ )} + {shouldDisplayLoginMethod(LoginMethod.EMAIL) && ( +
+ + + Forgot password? Recover your account + + +
+ )} + + ); +}; diff --git a/frontend-v2/src/routes/login/-components/InitialStep/index.tsx b/frontend-v2/src/routes/login/-components/InitialStep/index.tsx new file mode 100644 index 000000000..33142e0b0 --- /dev/null +++ b/frontend-v2/src/routes/login/-components/InitialStep/index.tsx @@ -0,0 +1 @@ +export { InitialStep } from "./InitialStep"; diff --git a/frontend-v2/src/routes/login/-components/Login.utils.tsx b/frontend-v2/src/routes/login/-components/Login.utils.tsx new file mode 100644 index 000000000..ea9ed4d9f --- /dev/null +++ b/frontend-v2/src/routes/login/-components/Login.utils.tsx @@ -0,0 +1,51 @@ +import { NavigateFn, useNavigate } from "@tanstack/react-router"; + +import { useServerConfig } from "@app/context"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { queryClient } from "@app/hooks/api/reactQuery"; +import { userKeys } from "@app/hooks/api/users"; + +export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: string) => { + const userOrgs = await fetchOrganizations(); + + const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced); + + if (organizationId) { + localStorage.setItem("orgData.id", organizationId); + navigate({ to: `/org/${organizationId}/overview` }); + return; + } + + if (nonAuthEnforcedOrgs.length > 0) { + // user is part of at least 1 non-auth enforced org + const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id; + localStorage.setItem("orgData.id", userOrg); + navigate({ to: `/org/${userOrg}/overview` }); + } else { + // user is not part of any non-auth enforced orgs + localStorage.removeItem("orgData.id"); + navigate({ to: "/org/none" }); + } +}; + +export const useNavigateToSelectOrganization = () => { + const { config } = useServerConfig(); + const navigate = useNavigate(); + + const navigateToSelectOrganization = async (cliCallbackPort?: string) => { + let redirectTo = "/login/select-organization?"; + if (config.defaultAuthOrgId) { + redirectTo += `org_id=${config.defaultAuthOrgId}&`; + } else { + queryClient.invalidateQueries(userKeys.getUser); + } + + if (cliCallbackPort) { + redirectTo += `callback_port=${cliCallbackPort}`; + } + + navigate({ to: redirectTo }); + }; + + return { navigateToSelectOrganization }; +}; diff --git a/frontend-v2/src/routes/login/-components/LoginSSO.tsx b/frontend-v2/src/routes/login/-components/LoginSSO.tsx new file mode 100644 index 000000000..887ecbee1 --- /dev/null +++ b/frontend-v2/src/routes/login/-components/LoginSSO.tsx @@ -0,0 +1,41 @@ +import { useEffect, useState } from "react"; +import { jwtDecode } from "jwt-decode"; + +import { PasswordStep } from "./PasswordStep"; + +type Props = { + providerAuthToken: string; +}; + +export const LoginSSO = ({ providerAuthToken }: Props) => { + const [step, setStep] = useState(0); + const [password, setPassword] = useState(""); + + const { username, isUserCompleted } = jwtDecode(providerAuthToken) as any; + + useEffect(() => { + if (isUserCompleted) { + setStep(1); + } + }, []); + + const renderView = () => { + switch (step) { + case 0: + return
; + case 1: + return ( + + ); + default: + return
; + } + }; + + return
{renderView()}
; +}; diff --git a/frontend-v2/src/routes/login/-components/Mfa.tsx b/frontend-v2/src/routes/login/-components/Mfa.tsx new file mode 100644 index 000000000..b4d077c47 --- /dev/null +++ b/frontend-v2/src/routes/login/-components/Mfa.tsx @@ -0,0 +1,222 @@ +import React, { useEffect, useState } from "react"; +import ReactCodeInput from "react-code-input"; +import { Link, useNavigate } from "@tanstack/react-router"; +import { t } from "i18next"; + +import Error from "@app/components/basic/Error"; +import TotpRegistration from "@app/components/mfa/TotpRegistration"; +import SecurityClient from "@app/components/utilities/SecurityClient"; +import { Button, Input } from "@app/components/v2"; +import { useSendMfaToken } from "@app/hooks/api"; +import { checkUserTotpMfa, verifyMfaToken } from "@app/hooks/api/auth/queries"; +import { MfaMethod } from "@app/hooks/api/auth/types"; + +// The style for the verification code input +const codeInputProps = { + inputStyle: { + fontFamily: "monospace", + margin: "4px", + MozAppearance: "textfield", + width: "48px", + borderRadius: "5px", + fontSize: "24px", + height: "48px", + paddingLeft: "7", + backgroundColor: "#0d1117", + color: "white", + border: "1px solid #2d2f33", + textAlign: "center", + outlineColor: "#8ca542", + borderColor: "#2d2f33" + } +} as const; + +type Props = { + successCallback: () => void | Promise; + closeMfa?: () => void; + hideLogo?: boolean; + email: string; + method: MfaMethod; +}; + +export const Mfa = ({ successCallback, closeMfa, hideLogo, email, method }: Props) => { + const [mfaCode, setMfaCode] = useState(""); + const navigate = useNavigate(); + const [isLoading, setIsLoading] = useState(false); + const [isLoadingResend, setIsLoadingResend] = useState(false); + const [triesLeft, setTriesLeft] = useState(undefined); + const [shouldShowTotpRegistration, setShouldShowTotpRegistration] = useState(false); + + const sendMfaToken = useSendMfaToken(); + + useEffect(() => { + if (method === MfaMethod.TOTP) { + checkUserTotpMfa().then((isVerified) => { + if (!isVerified) { + SecurityClient.setMfaToken(""); + setShouldShowTotpRegistration(true); + } + }); + } + }, []); + + const verifyMfa = async (event: React.FormEvent) => { + event.preventDefault(); + + setIsLoading(true); + try { + const { token } = await verifyMfaToken({ + email, + mfaCode, + mfaMethod: method + }); + + SecurityClient.setMfaToken(""); + SecurityClient.setToken(token); + + await successCallback(); + if (closeMfa) { + closeMfa(); + } + } catch { + if (triesLeft) { + setTriesLeft((left) => { + if (triesLeft === 1) { + navigate({ to: "/" }); + + SecurityClient.setMfaToken(""); + SecurityClient.setToken(""); + } + return (left as number) - 1; + }); + } else { + setTriesLeft(2); + } + } finally { + setIsLoading(false); + } + }; + + const handleResendMfaCode = async () => { + try { + setIsLoadingResend(true); + await sendMfaToken.mutateAsync({ email }); + setIsLoadingResend(false); + } catch (err) { + console.error(err); + setIsLoadingResend(false); + } + }; + + if (shouldShowTotpRegistration) { + return ( + <> +
+ Your organization requires mobile authentication to be configured. +
+
+ { + setShouldShowTotpRegistration(false); + await successCallback(); + }} + /> +
+ + ); + } + + return ( +
+ {!hideLogo && ( + +
+ Infisical logo +
+ + )} + {method === MfaMethod.EMAIL && ( + <> +

{t("mfa.step2-message")}

+

{email}

+ + )} + {method === MfaMethod.TOTP && ( + <> +

+ Authenticator MFA Required +

+

+ Open the authenticator app on your mobile device to get your verification code or enter + a recovery code. +

+ + )} +
+
+ {method === MfaMethod.EMAIL && ( + + )} + {method === MfaMethod.TOTP && ( +
+ setMfaCode(e.target.value)} /> +
+ )} +
+ {typeof triesLeft === "number" && ( + + )} +
+
+ +
+
+ + {method === MfaMethod.TOTP && ( +
+ + + Lost your recovery codes? Reset your account + + +
+ )} + {method === MfaMethod.EMAIL && ( +
+
+ {t("signup.step2-resend-alert")} +
+ +
+
+

{t("signup.step2-spam-alert")}

+
+ )} +
+ ); +}; diff --git a/frontend-v2/src/routes/login/-components/PasswordStep/PasswordStep.tsx b/frontend-v2/src/routes/login/-components/PasswordStep/PasswordStep.tsx new file mode 100644 index 000000000..e92edadb3 --- /dev/null +++ b/frontend-v2/src/routes/login/-components/PasswordStep/PasswordStep.tsx @@ -0,0 +1,379 @@ +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Link, useNavigate } from "@tanstack/react-router"; +import HCaptcha from "@hcaptcha/react-hcaptcha"; +import axios from "axios"; +import { addSeconds, formatISO } from "date-fns"; +import { jwtDecode } from "jwt-decode"; + +import { createNotification } from "@app/components/notifications"; +import attemptCliLogin from "@app/components/utilities/attemptCliLogin"; +import attemptLogin from "@app/components/utilities/attemptLogin"; +import { CAPTCHA_SITE_KEY } from "@app/components/utilities/config"; +import SecurityClient from "@app/components/utilities/SecurityClient"; +import { Button, Input, Spinner } from "@app/components/v2"; +import { SessionStorageKeys } from "@app/const"; +import { useToggle } from "@app/hooks"; +import { useOauthTokenExchange, useSelectOrganization } from "@app/hooks/api"; +import { MfaMethod } from "@app/hooks/api/auth/types"; +import { fetchOrganizations } from "@app/hooks/api/organization/queries"; +import { fetchMyPrivateKey } from "@app/hooks/api/users/queries"; + +import { navigateUserToOrg, useNavigateToSelectOrganization } from "../Login.utils"; +import { Mfa } from "../Mfa"; + +type Props = { + providerAuthToken: string; + email: string; + password: string; + setPassword: (password: string) => void; +}; + +export const PasswordStep = ({ providerAuthToken, email, password, setPassword }: Props) => { + const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation(); + const navigate = useNavigate(); + const { mutateAsync: selectOrganization } = useSelectOrganization(); + const { mutateAsync: oauthTokenExchange } = useOauthTokenExchange(); + const [shouldShowMfa, toggleShowMfa] = useToggle(false); + const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); + const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); + + const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); + + const { callbackPort, organizationId, hasExchangedPrivateKey } = jwtDecode( + providerAuthToken + ) as any; + + const handleExchange = async () => { + try { + setIsLoading(true); + const oauthLogin = await oauthTokenExchange({ + email, + providerAuthToken + }); + + // attemptCliLogin + const cliUrl = `http://127.0.0.1:${callbackPort}/`; + + // unset provider auth token in case it was used + SecurityClient.setProviderAuthToken(""); + // set JWT token + SecurityClient.setToken(oauthLogin.token); + + const privateKey = await fetchMyPrivateKey(); + localStorage.setItem("PRIVATE_KEY", privateKey); + + // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org + if (organizationId) { + const finishWithOrgWorkflow = async () => { + const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId }); + + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + setMfaSuccessCallback(() => finishWithOrgWorkflow); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + return; + } + + if (callbackPort) { + console.log("organization id was present. new JWT token to be used in CLI:", token); + const instance = axios.create(); + const payload = { + privateKey, + email, + JTWToken: token + }; + await instance.post(cliUrl, payload).catch(() => { + // if error happens to communicate we set the token with an expiry in sessino storage + // the cli-redirect page has logic to show this to user and ask them to paste it in terminal + sessionStorage.setItem( + SessionStorageKeys.CLI_TERMINAL_TOKEN, + JSON.stringify({ + expiry: formatISO(addSeconds(new Date(), 30)), + data: window.btoa(JSON.stringify(payload)) + }) + ); + }); + navigate({ to: "/cli-redirect" }); + return; + } + + await navigateUserToOrg(navigate, organizationId); + }; + + await finishWithOrgWorkflow(); + } + // case: no organization ID is present -- navigate to the select org page IF the user has any orgs + // if the user has no orgs, navigate to the create org page + else { + const userOrgs = await fetchOrganizations(); + + // case: user has orgs, so we navigate the user to select an org + if (userOrgs.length > 0) { + navigateToSelectOrganization(callbackPort); + } + // case: no orgs found, so we navigate the user to create an org + else { + await navigateUserToOrg(navigate); + } + } + } catch (err: any) { + setIsLoading(false); + console.error(err); + + if (err.response.data.error === "User Locked") { + createNotification({ + title: err.response.data.error, + text: err.response.data.message, + type: "error" + }); + return; + } + + createNotification({ + text: "Login unsuccessful. Double-check your master password and try again.", + type: "error" + }); + } + }; + + useEffect(() => { + if (hasExchangedPrivateKey) { + handleExchange(); + } + }, []); + + const [captchaToken, setCaptchaToken] = useState(""); + const [shouldShowCaptcha, setShouldShowCaptcha] = useState(false); + const captchaRef = useRef(null); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + try { + setIsLoading(true); + + if (callbackPort) { + // attemptCliLogin + const isCliLoginSuccessful = await attemptCliLogin({ + email, + password, + providerAuthToken, + captchaToken + }); + + if (isCliLoginSuccessful && isCliLoginSuccessful.success) { + const cliUrl = `http://127.0.0.1:${callbackPort}/`; + + // case: organization ID is present from the provider auth token -- select the org and use the new jwt token in the CLI, then navigate to the org + if (organizationId) { + const finishWithOrgWorkflow = async () => { + const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ + organizationId + }); + + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + setMfaSuccessCallback(() => finishWithOrgWorkflow); + return; + } + + console.log("organization id was present. new JWT token to be used in CLI:", token); + + const instance = axios.create(); + const payload = { + ...isCliLoginSuccessful.loginResponse, + JTWToken: token + }; + await instance.post(cliUrl, payload).catch(() => { + // if error happens to communicate we set the token with an expiry in sessino storage + // the cli-redirect page has logic to show this to user and ask them to paste it in terminal + sessionStorage.setItem( + SessionStorageKeys.CLI_TERMINAL_TOKEN, + JSON.stringify({ + expiry: formatISO(addSeconds(new Date(), 30)), + data: window.btoa(JSON.stringify(payload)) + }) + ); + }); + navigate({ to: "/cli-redirect" }); + }; + + await finishWithOrgWorkflow(); + return; + } + + // case: no organization ID is present -- navigate to the select org page IF the user has any orgs + // if the user has no orgs, navigate to the create org page + const userOrgs = await fetchOrganizations(); + + // case: user has orgs, so we navigate the user to select an org + if (userOrgs.length > 0) { + navigateToSelectOrganization(callbackPort); + } + // case: no orgs found, so we navigate the user to create an org + else { + await navigateUserToOrg(navigate); + } + } + } else { + const loginAttempt = await attemptLogin({ + email, + password, + providerAuthToken, + captchaToken + }); + + if (loginAttempt && loginAttempt.success) { + // case: login was successful + setIsLoading(false); + createNotification({ + text: "Successfully logged in", + type: "success" + }); + + // case: organization ID is present from the provider auth token -- navigate directly to the org + if (organizationId) { + await navigateUserToOrg(navigate, organizationId); + } + // case: no organization ID is present -- navigate to the select org page IF the user has any orgs + // if the user has no orgs, navigate to the create org page + else { + const userOrgs = await fetchOrganizations(); + + if (userOrgs.length > 0) { + navigateToSelectOrganization(); + } else { + await navigateUserToOrg(navigate); + } + } + } + } + } catch (err: any) { + setIsLoading(false); + console.error(err); + + if (err.response.data.error === "User Locked") { + createNotification({ + title: err.response.data.error, + text: err.response.data.message, + type: "error" + }); + return; + } + + if (err.response.data.error === "Captcha Required") { + setShouldShowCaptcha(true); + return; + } + + createNotification({ + text: "Login unsuccessful. Double-check your master password and try again.", + type: "error" + }); + } + + if (captchaRef.current) { + captchaRef.current.resetCaptcha(); + } + setCaptchaToken(""); + }; + + if (shouldShowMfa) { + return ( +
+ toggleShowMfa.off()} + /> +
+ ); + } + + if (hasExchangedPrivateKey) { + return ( +
+ +

Loading, please wait

+
+ ); + } + + return ( +
+
+

+ What's your Infisical password? +

+
+
+
+ setPassword(e.target.value)} + type="password" + placeholder="Enter your password..." + isRequired + autoComplete="current-password" + id="current-password" + className="h-12" + /> +
+
+ {shouldShowCaptcha && ( +
+ setCaptchaToken(token)} + ref={captchaRef} + /> +
+ )} +
+ +
+
+ + Infisical Master Password serves as a decryption mechanism so that even Google is not able + to access your secrets. + + + + {t("login.forgot-password")} + + +
+
+ +
+
+ ); +}; diff --git a/frontend-v2/src/routes/login/-components/PasswordStep/index.tsx b/frontend-v2/src/routes/login/-components/PasswordStep/index.tsx new file mode 100644 index 000000000..e2d3993e4 --- /dev/null +++ b/frontend-v2/src/routes/login/-components/PasswordStep/index.tsx @@ -0,0 +1 @@ +export { PasswordStep } from "./PasswordStep"; diff --git a/frontend-v2/src/routes/login/-components/SSOStep/SSOStep.tsx b/frontend-v2/src/routes/login/-components/SSOStep/SSOStep.tsx new file mode 100644 index 000000000..9663c04a9 --- /dev/null +++ b/frontend-v2/src/routes/login/-components/SSOStep/SSOStep.tsx @@ -0,0 +1,82 @@ +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { Button, Input } from "@app/components/v2"; + +type Props = { + setStep: (step: number) => void; + type: "SAML" | "OIDC"; +}; + +export const SSOStep = ({ setStep, type }: Props) => { + const [ssoIdentifier, setSSOIdentifier] = useState(""); + const { t } = useTranslation(); + + const queryParams = new URLSearchParams(window.location.search); + + const handleSubmission = (e: React.FormEvent) => { + e.preventDefault(); + const callbackPort = queryParams.get("callback_port"); + if (type === "SAML") { + window.open( + `/api/v1/sso/redirect/saml2/organizations/${ssoIdentifier}${ + callbackPort ? `?callback_port=${callbackPort}` : "" + }` + ); + } else { + window.open( + `/api/v1/sso/oidc/login?orgSlug=${ssoIdentifier}${ + callbackPort ? `&callbackPort=${callbackPort}` : "" + }` + ); + } + + window.close(); + }; + + return ( +
+

+ What's your organization slug? +

+
+
+
+ setSSOIdentifier(e.target.value)} + type="text" + placeholder="acme-123" + isRequired + autoComplete="email" + id="email" + className="h-12" + /> +
+
+
+ +
+
+
+ +
+
+ ); +}; diff --git a/frontend-v2/src/routes/login/-components/SSOStep/index.tsx b/frontend-v2/src/routes/login/-components/SSOStep/index.tsx new file mode 100644 index 000000000..e7d80b2c0 --- /dev/null +++ b/frontend-v2/src/routes/login/-components/SSOStep/index.tsx @@ -0,0 +1 @@ +export { SSOStep } from "./SSOStep"; diff --git a/frontend-v2/src/routes/login/-components/index.tsx b/frontend-v2/src/routes/login/-components/index.tsx new file mode 100644 index 000000000..84ad4f73d --- /dev/null +++ b/frontend-v2/src/routes/login/-components/index.tsx @@ -0,0 +1,5 @@ +export { InitialStep } from "./InitialStep"; +export { SSOStep } from "./SSOStep"; + +// SSO-specific step +export { PasswordStep } from "./PasswordStep"; diff --git a/frontend-v2/src/routes/login/index.tsx b/frontend-v2/src/routes/login/index.tsx new file mode 100644 index 000000000..3b2f9b3b2 --- /dev/null +++ b/frontend-v2/src/routes/login/index.tsx @@ -0,0 +1,81 @@ +import { useState } from "react"; +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; +import { createFileRoute, Link } from "@tanstack/react-router"; + +import { InitialStep, SSOStep } from "./-components"; + +const LoginPage = () => { + const { t } = useTranslation(); + const [step, setStep] = useState(0); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + // TODO(rbr): move this to beforeload + // const { navigateToSelectOrganization } = useNavigateToSelectOrganization(); + // + // const queryParams = new URLSearchParams(window.location.search); + // + // useEffect(() => { + // // TODO(akhilmhdh): workspace will be controlled by a workspace context + // const handleRedirects = async () => { + // try { + // const callbackPort = queryParams?.get("callback_port"); + // // case: a callback port is set, meaning it's a cli login request: redirect to select org with callback port + // if (callbackPort) { + // navigateToSelectOrganization(callbackPort); + // } else { + // // case: no callback port, meaning it's a regular login request: redirect to select org + // navigateToSelectOrganization(); + // } + // } catch (error) { + // console.log("Error - Not logged in yet"); + // } + // }; + // if (isLoggedIn()) { + // handleRedirects(); + // } + // }, []); + + const renderView = () => { + switch (step) { + case 0: + return ( + + ); + case 2: + return ; + case 3: + return ; + default: + return
; + } + }; + + return ( +
+ + {t("common.head-title", { title: t("login.title") })} + + + + + + +
+ Infisical logo +
+ +
{renderView()}
; +
+ ); +}; + +export const Route = createFileRoute("/login/")({ + component: LoginPage +}); diff --git a/frontend-v2/src/routes/login/ldap/index.tsx b/frontend-v2/src/routes/login/ldap/index.tsx new file mode 100644 index 000000000..a3c87c884 --- /dev/null +++ b/frontend-v2/src/routes/login/ldap/index.tsx @@ -0,0 +1,157 @@ +import { useTranslation } from "react-i18next"; +import { Helmet } from "react-helmet"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { useServerConfig } from "@app/context"; +import { useState } from "react"; +import { loginLDAPRedirect } from "@app/hooks/api/auth/queries"; +import { createNotification } from "@app/components/notifications"; +import { Input, Button } from "@app/components/v2"; + +const LoginLDAPPage = () => { + const { t } = useTranslation(); + const navigate = useNavigate(); + const { config } = useServerConfig(); + const queryParams = new URLSearchParams(window.location.search); + const passedOrgSlug = queryParams.get("organizationSlug"); + const passedUsername = queryParams.get("username"); + + const [organizationSlug, setOrganizationSlug] = useState( + config.defaultAuthOrgSlug || passedOrgSlug || "" + ); + const [username, setUsername] = useState(passedUsername || ""); + const [password, setPassword] = useState(""); + + const handleSubmission = async (e: React.FormEvent) => { + e.preventDefault(); + try { + const { nextUrl } = await loginLDAPRedirect({ + organizationSlug, + username, + password + }); + + if (!nextUrl) { + createNotification({ + text: "Login unsuccessful. Double-check your credentials and try again.", + type: "error" + }); + + return; + } + + createNotification({ + text: "Successfully logged in", + type: "success" + }); + + window.open(nextUrl); + window.close(); + } catch { + createNotification({ + text: "Login unsuccessful. Double-check your credentials and try again.", + type: "error" + }); + } + + // TODO: add callback port support + + // const callbackPort = queryParams.get("callback_port"); + // window.open(`/api/v1/ldap/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`); + // window.close(); + }; + + return ( +
+ + {t("common.head-title", { title: t("login.title") })} + + + + + + +
+ Infisical logo +
+ +
+

+ What's your LDAP Login? +

+
+ {!config.defaultAuthOrgSlug && !passedOrgSlug && ( +
+
+ setOrganizationSlug(e.target.value)} + type="text" + placeholder="Enter your organization slug..." + isRequired + autoComplete="email" + id="email" + className="h-12" + /> +
+
+ )} +
+
+ setUsername(e.target.value)} + type="text" + placeholder="Enter your LDAP username..." + isRequired + autoComplete="email" + id="email" + className="h-12" + isDisabled={passedUsername !== null} + /> +
+
+
+
+ setPassword(e.target.value)} + type="password" + placeholder="Enter your LDAP password..." + isRequired + autoComplete="current-password" + id="current-password" + className="select:-webkit-autofill:focus h-10" + /> +
+
+
+ +
+
+
+ +
+
+
+ ); +}; + +export const Route = createFileRoute("/login/ldap/")({ + component: LoginLDAPPage +}); diff --git a/frontend-v2/src/routes/login/provider/error.tsx b/frontend-v2/src/routes/login/provider/error.tsx new file mode 100644 index 000000000..efb476284 --- /dev/null +++ b/frontend-v2/src/routes/login/provider/error.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { useEffect } from "react"; + +const LoginProviderError = () => { + useEffect(() => { + window.localStorage.setItem("PROVIDER_AUTH_ERROR", "err"); + window.close(); + }, []); + + return
; +}; + +export const Route = createFileRoute("/login/provider/error")({ + component: LoginProviderError +}); diff --git a/frontend-v2/src/routes/login/provider/success.tsx b/frontend-v2/src/routes/login/provider/success.tsx new file mode 100644 index 000000000..50a6f6727 --- /dev/null +++ b/frontend-v2/src/routes/login/provider/success.tsx @@ -0,0 +1,19 @@ +import { useEffect } from "react"; + +import SecurityClient from "@app/components/utilities/SecurityClient"; +import { createFileRoute, useSearch } from "@tanstack/react-router"; + +const LoginProviderSuccess = () => { + const search = useSearch({ from: "/login/provider/success" }); + + useEffect(() => { + SecurityClient.setProviderAuthToken(search.token); + window.close(); + }, []); + + return
; +}; + +export const Route = createFileRoute("/login/provider/success")({ + component: LoginProviderSuccess +}); diff --git a/frontend-v2/src/routes/login/select-organization/index.tsx b/frontend-v2/src/routes/login/select-organization/index.tsx new file mode 100644 index 000000000..713e2e3b3 --- /dev/null +++ b/frontend-v2/src/routes/login/select-organization/index.tsx @@ -0,0 +1,281 @@ +import { useCallback, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Helmet } from "react-helmet"; +import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; +import { faArrowRight } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import axios from "axios"; +import { addSeconds, formatISO } from "date-fns"; +import { jwtDecode } from "jwt-decode"; + +import { createNotification } from "@app/components/notifications"; +import { IsCliLoginSuccessful } from "@app/components/utilities/attemptCliLogin"; +import SecurityClient from "@app/components/utilities/SecurityClient"; +import { Button, Spinner } from "@app/components/v2"; +import { SessionStorageKeys } from "@app/const"; +import { useToggle } from "@app/hooks"; +import { + useGetOrganizations, + useGetUser, + useLogoutUser, + useSelectOrganization +} from "@app/hooks/api"; +import { MfaMethod, UserAgentType } from "@app/hooks/api/auth/types"; +import { Organization } from "@app/hooks/api/types"; +import { AuthMethod } from "@app/hooks/api/users/types"; +import { getAuthToken, isLoggedIn } from "@app/hooks/api/reactQuery"; +import { navigateUserToOrg } from "../-components/Login.utils"; +import { Mfa } from "../-components/Mfa"; + +const LoadingScreen = () => { + return ( +
+ +

Loading, please wait

+
+ ); +}; + +const SelectOrganizationPage = () => { + const navigate = useNavigate(); + const { t } = useTranslation(); + + const organizations = useGetOrganizations(); + const selectOrg = useSelectOrganization(); + const { data: user, isLoading: userLoading } = useGetUser(); + const [shouldShowMfa, toggleShowMfa] = useToggle(false); + const [requiredMfaMethod, setRequiredMfaMethod] = useState(MfaMethod.EMAIL); + const [isInitialOrgCheckLoading, setIsInitialOrgCheckLoading] = useState(true); + + const [mfaSuccessCallback, setMfaSuccessCallback] = useState<() => void>(() => {}); + + const queryParams = new URLSearchParams(window.location.search); + const orgId = queryParams.get("org_id"); + const callbackPort = queryParams.get("callback_port"); + const defaultSelectedOrg = organizations.data?.find((org) => org.id === orgId); + + const logout = useLogoutUser(true); + const handleLogout = useCallback(async () => { + try { + console.log("Logging out..."); + await logout.mutateAsync(); + navigate({ to: "/login" }); + } catch (error) { + console.error(error); + } + }, [logout, navigate]); + + const handleSelectOrganization = useCallback( + async (organization: Organization) => { + if (organization.authEnforced) { + // org has an org-level auth method enabled (e.g. SAML) + // -> logout + redirect to SAML SSO + await logout.mutateAsync(); + let url = ""; + if (organization.orgAuthMethod === AuthMethod.OIDC) { + url = `/api/v1/sso/oidc/login?orgSlug=${organization.slug}${ + callbackPort ? `&callbackPort=${callbackPort}` : "" + }`; + } else { + url = `/api/v1/sso/redirect/saml2/organizations/${organization.slug}`; + + if (callbackPort) { + url += `?callback_port=${callbackPort}`; + } + } + + window.open(url); + window.close(); + return; + } + + const { token, isMfaEnabled, mfaMethod } = await selectOrg + .mutateAsync({ + organizationId: organization.id, + userAgent: callbackPort ? UserAgentType.CLI : undefined + }) + .finally(() => setIsInitialOrgCheckLoading(false)); + + if (isMfaEnabled) { + SecurityClient.setMfaToken(token); + if (mfaMethod) { + setRequiredMfaMethod(mfaMethod); + } + toggleShowMfa.on(); + setMfaSuccessCallback(() => () => handleSelectOrganization(organization)); + return; + } + + if (callbackPort) { + const privateKey = localStorage.getItem("PRIVATE_KEY"); + + let error: string | null = null; + + if (!privateKey) error = "Private key not found"; + if (!user?.email) error = "User email not found"; + if (!token) error = "No token found"; + + if (error) { + createNotification({ + text: error, + type: "error" + }); + return; + } + + const payload = { + JTWToken: token, + email: user?.email, + privateKey + } as IsCliLoginSuccessful["loginResponse"]; + + // send request to server endpoint + const instance = axios.create(); + await instance.post(`http://127.0.0.1:${callbackPort}/`, payload).catch(() => { + // if error happens to communicate we set the token with an expiry in sessino storage + // the cli-redirect page has logic to show this to user and ask them to paste it in terminal + sessionStorage.setItem( + SessionStorageKeys.CLI_TERMINAL_TOKEN, + JSON.stringify({ + expiry: formatISO(addSeconds(new Date(), 30)), + data: window.btoa(JSON.stringify(payload)) + }) + ); + }); + navigate({ to: "/cli-redirect" }); + // cli page + } else { + navigateUserToOrg(navigate, organization.id); + } + }, + [selectOrg] + ); + + const handleCliRedirect = useCallback(() => { + const authToken = getAuthToken(); + + if (authToken && !callbackPort) { + const decodedJwt = jwtDecode(authToken) as any; + + if (decodedJwt?.organizationId) { + navigateUserToOrg(navigate, decodedJwt.organizationId); + } + } + + if (!isLoggedIn()) { + navigate({ to: "/login" }); + } + }, []); + + useEffect(() => { + if (callbackPort) { + handleCliRedirect(); + } + }, [navigate]); + + useEffect(() => { + if (organizations.isLoading || !organizations.data) return; + + // Case: User has no organizations. + // This can happen if the user was previously a member, but the organization was deleted or the user was removed. + if (organizations.data.length === 0) { + navigate({ to: "/org/none" }); + } else if (organizations.data.length === 1) { + if (callbackPort) { + handleCliRedirect(); + setIsInitialOrgCheckLoading(false); + } else { + handleSelectOrganization(organizations.data[0]); + } + } else { + setIsInitialOrgCheckLoading(false); + } + }, [organizations.isLoading, organizations.data]); + + useEffect(() => { + if (defaultSelectedOrg) { + handleSelectOrganization(defaultSelectedOrg); + } + }, [defaultSelectedOrg]); + + if ( + userLoading || + !user || + ((isInitialOrgCheckLoading || defaultSelectedOrg) && !shouldShowMfa) + ) { + return ; + } + + return ( +
+ + {t("common.head-title", { title: t("login.title") })} + + + + + + {shouldShowMfa ? ( + + ) : ( +
+ +
+ Infisical logo +
+ +
+
+

+ Choose your organization +

+ +
+

+ You‘re currently logged in as {user.username} +

+

+ Not you?{" "} + +

+
+
+
+ {organizations.isLoading ? ( + + ) : ( + organizations.data?.map((org) => ( + // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions +
handleSelectOrganization(org)} + key={org.id} + className="group flex cursor-pointer items-center justify-between rounded-md bg-mineshaft-700 px-4 py-3 capitalize text-gray-200 shadow-md transition-colors hover:bg-mineshaft-600" + > +

{org.name}

+ + +
+ )) + )} +
+
+
+ )} + +
+
+ ); +}; + +export const Route = createFileRoute("/login/select-organization/")({ + component: SelectOrganizationPage +}); diff --git a/frontend-v2/src/routes/login/sso/index.tsx b/frontend-v2/src/routes/login/sso/index.tsx new file mode 100644 index 000000000..19c915fc6 --- /dev/null +++ b/frontend-v2/src/routes/login/sso/index.tsx @@ -0,0 +1,63 @@ +import { useTranslation } from "react-i18next"; +import { Helmet } from "react-helmet"; +import { createFileRoute, Link, useSearch } from "@tanstack/react-router"; + +import { useEffect, useState } from "react"; +import { jwtDecode } from "jwt-decode"; +import { PasswordStep } from "../-components"; + +const LoginSSOPage = () => { + const { t } = useTranslation(); + const search = useSearch({ from: "/login/sso" }); + const token = search.token as string; + const [step, setStep] = useState(0); + const [password, setPassword] = useState(""); + + const { username, isUserCompleted } = jwtDecode(token) as any; + + useEffect(() => { + if (isUserCompleted) { + setStep(1); + } + }, []); + + const renderView = () => { + switch (step) { + case 0: + return
; + case 1: + return ( + + ); + default: + return
; + } + }; + + return ( +
+ + {t("common.head-title", { title: t("login.title") })} + + + + + + +
+ Infisical logo +
+ +
{renderView()}
; +
+ ); +}; + +export const Route = createFileRoute("/login/sso/")({ + component: LoginSSOPage +}); diff --git a/frontend-v2/src/services/KeyService.ts b/frontend-v2/src/services/KeyService.ts new file mode 100644 index 000000000..1cbc06cd3 --- /dev/null +++ b/frontend-v2/src/services/KeyService.ts @@ -0,0 +1,104 @@ +import { + decryptAssymmetric, + encryptAssymmetric +} from "@app/components/utilities/cryptography/crypto"; +import { decryptPrivateKeyHelper } from "@app/helpers/key"; + +/** + * Class to handle key actions + * TODO: in future, all private key-related encryption operations + * must pass through this class + */ +class KeyService { + private static privateKey: string = ""; + + static setPrivateKey(privateKey: string) { + KeyService.privateKey = privateKey; + } + + /** Return the user's decrypted private key + * @param {Object} obj + * @param {Number} obj.encryptionVersion + * @param {String} obj.encryptedPrivateKey + * @param {String} obj.iv + * @param {String} obj.tag + * @param {String} obj.password + * @param {String} obj.salt + * @param {String} obj.protectedKey + * @param {String} obj.protectedKeyIV + * @param {String} obj.protectedKeyTag + * @returns {String} privateKey - decrypted private key + */ + static async decryptPrivateKey({ + encryptionVersion, + encryptedPrivateKey, + iv, + tag, + password, + salt, + protectedKey, + protectedKeyIV, + protectedKeyTag + }: { + encryptionVersion: number; + encryptedPrivateKey: string; + iv: string; + tag: string; + password: string; + salt: string; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + }) { + return decryptPrivateKeyHelper({ + encryptionVersion, + encryptedPrivateKey, + iv, + tag, + password, + salt, + protectedKey, + protectedKeyIV, + protectedKeyTag + }); + } + + /** + * Return [plaintext] encrypted by the user's private key + * @param {Object} obj + * @param {String} obj.plaintext - plaintext to encrypt + */ + static encryptWithPrivateKey({ plaintext, publicKey }: { plaintext: string; publicKey: string }) { + return encryptAssymmetric({ + plaintext, + publicKey, + privateKey: KeyService.privateKey + }); + } + + /** + * Return [ciphertext] decrypted by the user's private key + * @param {Object} obj + * @param {String} obj.ciphertext - ciphertext to decrypt + * @param {String} obj.ciphertext - iv of ciphertext + * @param {String} obj.ciphertext - tag of ciphertext + */ + static decryptWithPrivateKey({ + ciphertext, + nonce, + publicKey + }: { + ciphertext: string; + nonce: string; + publicKey: string; + }) { + return decryptAssymmetric({ + ciphertext, + nonce, + publicKey, + privateKey: KeyService.privateKey + }); + } +} + +export default KeyService; diff --git a/frontend-v2/src/services/ProjectService.ts b/frontend-v2/src/services/ProjectService.ts new file mode 100644 index 000000000..afdc7c9f9 --- /dev/null +++ b/frontend-v2/src/services/ProjectService.ts @@ -0,0 +1,19 @@ +import { initProjectHelper } from "@app/helpers/project"; + +class ProjectService { + /** + * Create and initialize a new project in organization with id [organizationId] + * Note: current user should be a member of the organization + * @param {Object} obj + * @param {String} obj.organizationId - id of organization + * @param {String} obj.projectName - name of new project + * @returns {Project} project - new project + */ + static async initProject({ projectName }: { projectName: string }) { + return initProjectHelper({ + projectName + }); + } +} + +export default ProjectService; diff --git a/frontend-v2/src/services/index.ts b/frontend-v2/src/services/index.ts new file mode 100644 index 000000000..53addcd23 --- /dev/null +++ b/frontend-v2/src/services/index.ts @@ -0,0 +1,4 @@ +import KeyService from "./KeyService"; +import ProjectService from "./ProjectService"; + +export { KeyService, ProjectService }; diff --git a/frontend-v2/tsconfig.app.json b/frontend-v2/tsconfig.app.json index 5a4250d30..49ac1742b 100644 --- a/frontend-v2/tsconfig.app.json +++ b/frontend-v2/tsconfig.app.json @@ -3,7 +3,7 @@ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": ["ES2021", "DOM", "DOM.Iterable"], "module": "ESNext", "skipLibCheck": true, "baseUrl": ".", diff --git a/frontend-v2/vite.config.ts b/frontend-v2/vite.config.ts index 77ccdc9e6..4bba223c7 100644 --- a/frontend-v2/vite.config.ts +++ b/frontend-v2/vite.config.ts @@ -1,8 +1,23 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react-swc"; import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; +import react from "@vitejs/plugin-react-swc"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import wasm from "vite-plugin-wasm"; +import topLevelAwait from "vite-plugin-top-level-await"; +import { nodePolyfills } from "vite-plugin-node-polyfills"; // https://vite.dev/config/ export default defineConfig({ - plugins: [TanStackRouterVite(), react()] + plugins: [ + tsconfigPaths(), + nodePolyfills({ + globals: { + Buffer: true + } + }), + wasm(), + topLevelAwait(), + TanStackRouterVite(), + react() + ] });