diff --git a/backend/package-lock.json b/backend/package-lock.json index 808cf3204..d25407aca 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -53,7 +53,7 @@ "@opentelemetry/semantic-conventions": "^1.27.0", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.12.1", - "@react-email/components": "0.0.36", + "@react-email/components": "^1.0.1", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "1.1.0", "@slack/oauth": "^3.0.2", @@ -145,7 +145,7 @@ "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.24.7", - "@react-email/preview-server": "^4.3.0", + "@react-email/preview-server": "^5.0.6", "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", @@ -183,7 +183,7 @@ "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", - "react-email": "^4.3.0", + "react-email": "^5.0.6", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", @@ -203,19 +203,6 @@ "node": ">=0.10.0" } }, - "node_modules/@alloc/quick-lru": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -674,6 +661,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.637.0.tgz", "integrity": "sha512-xUi7x4qDubtA8QREtlblPuAcn91GS/09YVEY/RwU7xCY0aqGuFwgszAANlha4OUIqva8oVj2WO4gJuG+iaSnhw==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2120,6 +2108,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.682.0.tgz", "integrity": "sha512-ZPZ7Y/r/w3nx/xpPzGSqSQsB090Xk5aZZOH+WBhTDn/pBEuim09BYXCLzvvxb7R7NnuoQdrTJiwimdJAhHl7ZQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2173,6 +2162,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.682.0.tgz", "integrity": "sha512-xKuo4HksZ+F8m9DOfx/ZuWNhaPuqZFPwwy0xqcBT6sWH7OAuBjv/fnpOTzyQhpVTWddlf+ECtMAMrxjxuOExGQ==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2672,6 +2662,7 @@ "version": "3.632.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.632.0.tgz", "integrity": "sha512-Oh1fIWaoZluihOCb/zDEpRTi+6an82fgJz7fyRBugyLhEtDjmvpCQ3oKjzaOhoN+4EvXAm1ZS/ZgpvXBlIRTgw==", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -2748,6 +2739,7 @@ "version": "3.632.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.632.0.tgz", "integrity": "sha512-Ss5cBH09icpTvT+jtGGuQlRdwtO7RyE9BF4ZV/CEPATdd9whtJt4Qxdya8BUnkWR7h5HHTrQHqai3YVYjku41A==", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -5185,6 +5177,7 @@ "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.26.2", @@ -7216,6 +7209,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" }, @@ -7237,6 +7231,7 @@ "url": "https://opencollective.com/csstools" } ], + "peer": true, "engines": { "node": ">=18" } @@ -7446,9 +7441,9 @@ "license": "BSD-3-Clause" }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz", + "integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==", "dev": true, "license": "MIT", "optional": true, @@ -7813,23 +7808,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/sunos-x64": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", @@ -8336,48 +8314,6 @@ "p-limit": "^3.1.0" } }, - "node_modules/@floating-ui/core": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", - "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", - "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/core": "^1.7.3", - "@floating-ui/utils": "^0.2.10" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", - "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/dom": "^1.7.4" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", - "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@gitbeaker/core": { "version": "42.5.0", "resolved": "https://registry.npmjs.org/@gitbeaker/core/-/core-42.5.0.tgz", @@ -8685,14 +8621,15 @@ "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", "dev": true, "license": "MIT", + "optional": true, "engines": { "node": ">=18" } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz", - "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", "cpu": [ "arm64" ], @@ -8709,13 +8646,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.3" + "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz", - "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", "cpu": [ "x64" ], @@ -8732,13 +8669,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.3" + "@img/sharp-libvips-darwin-x64": "1.2.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz", - "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", "cpu": [ "arm64" ], @@ -8753,9 +8690,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz", - "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", "cpu": [ "x64" ], @@ -8770,9 +8707,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz", - "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", "cpu": [ "arm" ], @@ -8787,9 +8724,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz", - "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", "cpu": [ "arm64" ], @@ -8804,9 +8741,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz", - "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", "cpu": [ "ppc64" ], @@ -8820,10 +8757,27 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz", - "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", "cpu": [ "s390x" ], @@ -8838,9 +8792,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz", - "integrity": "sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", "cpu": [ "x64" ], @@ -8855,9 +8809,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz", - "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", "cpu": [ "arm64" ], @@ -8872,9 +8826,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz", - "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -8889,9 +8843,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz", - "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", "cpu": [ "arm" ], @@ -8908,13 +8862,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.3" + "@img/sharp-libvips-linux-arm": "1.2.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz", - "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", "cpu": [ "arm64" ], @@ -8931,13 +8885,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.3" + "@img/sharp-libvips-linux-arm64": "1.2.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz", - "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", "cpu": [ "ppc64" ], @@ -8954,13 +8908,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.3" + "@img/sharp-libvips-linux-ppc64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.2.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz", - "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", "cpu": [ "s390x" ], @@ -8977,13 +8954,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.3" + "@img/sharp-libvips-linux-s390x": "1.2.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz", - "integrity": "sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", "cpu": [ "x64" ], @@ -9000,13 +8977,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.3" + "@img/sharp-libvips-linux-x64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz", - "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", "cpu": [ "arm64" ], @@ -9023,13 +9000,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3" + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz", - "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -9046,13 +9023,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.3" + "@img/sharp-libvips-linuxmusl-x64": "1.2.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz", - "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", "cpu": [ "wasm32" ], @@ -9060,7 +9037,7 @@ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.5.0" + "@emnapi/runtime": "^1.7.0" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -9070,9 +9047,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz", - "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", "cpu": [ "arm64" ], @@ -9090,9 +9067,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz", - "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", "cpu": [ "ia32" ], @@ -9110,9 +9087,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz", - "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", "cpu": [ "x64" ], @@ -9235,6 +9212,9 @@ "win32" ] }, + "node_modules/@infisical/quic/node_modules/@infisical/quic-linux-arm": { + "optional": true + }, "node_modules/@ioredis/commands": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", @@ -9338,28 +9318,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/source-map/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -9468,26 +9426,6 @@ "resolved": "https://registry.npmjs.org/@ldapjs/protocol/-/protocol-1.2.1.tgz", "integrity": "sha512-O89xFDLW2gBoZWNXuXpBSM32/KealKCTb3JGtJdtUQc7RjAk8XzrRgyz02cPAwGKwKPxy0ivuC7UP9bmN87egQ==" }, - "node_modules/@lottiefiles/dotlottie-react": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-react/-/dotlottie-react-0.13.3.tgz", - "integrity": "sha512-V4FfdYlqzjBUX7f0KV6vfQOOI0Cp+3XeG/ZqSDFSEVg5P7fpROpDv5/I9aTM8sOCESK1SWT96Fem+QVUnBV1wQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@lottiefiles/dotlottie-web": "0.42.0" - }, - "peerDependencies": { - "react": "^17 || ^18 || ^19" - } - }, - "node_modules/@lottiefiles/dotlottie-web": { - "version": "0.42.0", - "resolved": "https://registry.npmjs.org/@lottiefiles/dotlottie-web/-/dotlottie-web-0.42.0.tgz", - "integrity": "sha512-Zr2LCaOAoPCsdAQgeLyCSiQ1+xrAJtRCyuEYDj0qR5heUwpc+Pxbb88JyTVumcXFfKOBMOMmrlsTScLz2mrvQQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@lukeed/ms": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", @@ -9727,16 +9665,16 @@ } }, "node_modules/@next/env": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz", - "integrity": "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz", + "integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==", "dev": true, "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.2.tgz", - "integrity": "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz", + "integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==", "cpu": [ "arm64" ], @@ -9751,9 +9689,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.2.tgz", - "integrity": "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz", + "integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==", "cpu": [ "x64" ], @@ -9768,9 +9706,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.2.tgz", - "integrity": "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz", + "integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==", "cpu": [ "arm64" ], @@ -9785,9 +9723,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.2.tgz", - "integrity": "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz", + "integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==", "cpu": [ "arm64" ], @@ -9802,9 +9740,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.2.tgz", - "integrity": "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz", + "integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==", "cpu": [ "x64" ], @@ -9819,9 +9757,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz", - "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz", + "integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==", "cpu": [ "x64" ], @@ -9836,9 +9774,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.2.tgz", - "integrity": "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz", + "integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==", "cpu": [ "arm64" ], @@ -9853,9 +9791,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.2.tgz", - "integrity": "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz", + "integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==", "cpu": [ "x64" ], @@ -10321,6 +10259,7 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz", "integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==", "license": "MIT", + "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -10764,6 +10703,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -11399,801 +11339,22 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, - "node_modules/@radix-ui/colors": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", - "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", - "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", - "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", - "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", - "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", - "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", - "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", - "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", - "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-escape-keydown": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dropdown-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", - "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-menu": "2.1.16", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", - "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", - "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", - "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-menu": { - "version": "2.1.16", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", - "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", - "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-focus-guards": "1.1.3", - "@radix-ui/react-focus-scope": "1.1.7", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "aria-hidden": "^1.2.4", - "react-remove-scroll": "^2.6.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", - "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-layout-effect": "1.1.1", - "@radix-ui/react-use-rect": "1.1.1", - "@radix-ui/react-use-size": "1.1.1", - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", - "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", - "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", - "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-slot": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", - "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-collection": "1.1.7", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-callback-ref": "1.1.1", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", - "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.2" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tabs": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", - "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", - "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", - "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-direction": "1.1.1", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-roving-focus": "1.1.11", - "@radix-ui/react-toggle": "1.1.10", - "@radix-ui/react-use-controllable-state": "1.2.2" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", - "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/primitive": "1.1.3", - "@radix-ui/react-compose-refs": "1.1.2", - "@radix-ui/react-context": "1.1.2", - "@radix-ui/react-dismissable-layer": "1.1.11", - "@radix-ui/react-id": "1.1.1", - "@radix-ui/react-popper": "1.2.8", - "@radix-ui/react-portal": "1.1.9", - "@radix-ui/react-presence": "1.1.5", - "@radix-ui/react-primitive": "2.1.3", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-use-controllable-state": "1.2.2", - "@radix-ui/react-visually-hidden": "1.2.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", - "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", - "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-effect-event": "0.0.2", - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-effect-event": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", - "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", - "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", - "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", - "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/rect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", - "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", - "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@radix-ui/react-primitive": "2.1.3" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", - "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", - "dev": true, - "license": "MIT" - }, "node_modules/@react-email/body": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.11.tgz", - "integrity": "sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.2.0.tgz", + "integrity": "sha512-9GCWmVmKUAoRfloboCd+RKm6X17xn7eGL7HnpAZUnjBXBilWCxsKnLMTC/ixSHDKS/A/057M1Tx6ZUXd89sVBw==", "license": "MIT", + "peer": true, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/button": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.19.tgz", - "integrity": "sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.2.0.tgz", + "integrity": "sha512-8i+v6cMxr2emz4ihCrRiYJPp2/sdYsNNsBzXStlcA+/B9Umpm5Jj3WJKYpgTPM+aeyiqlG/MMI1AucnBm4f1oQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12202,15 +11363,16 @@ } }, "node_modules/@react-email/code-block": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.12.tgz", - "integrity": "sha512-Faw3Ij9+/Qwq6moWaeHnV8Hn7ekc/EqyAzPi6yUar21dhcqYugCC4Da1x4d9nA9zC0H9KU3lYVJczh8D3cA+Eg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.2.0.tgz", + "integrity": "sha512-eIrPW9PIFgDopQU0e/OPpwCW2QWQDtNZDSsiN4sJO8KdMnWWnXJicnRfzrit5rHwFo+Y98i+w/Y5ScnBAFr1dQ==", "license": "MIT", + "peer": true, "dependencies": { - "prismjs": "1.30.0" + "prismjs": "^1.30.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" @@ -12221,6 +11383,7 @@ "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.5.tgz", "integrity": "sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12241,14 +11404,14 @@ } }, "node_modules/@react-email/components": { - "version": "0.0.36", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.36.tgz", - "integrity": "sha512-VMh+OQplAnG8JMLlJjdnjt+ThJZ+JVkp0q2YMS2NEz+T88N22bLD2p7DZO0QgtNaKgumOhJI/0a2Q7VzCrwu5g==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.1.tgz", + "integrity": "sha512-HnL0Y/up61sOBQT2cQg9N/kCoW0bP727gDs2MkFWQYELg6+iIHidMDvENXFC0f1ZE6hTB+4t7sszptvTcJWsDA==", "license": "MIT", "dependencies": { - "@react-email/body": "0.0.11", - "@react-email/button": "0.0.19", - "@react-email/code-block": "0.0.12", + "@react-email/body": "0.2.0", + "@react-email/button": "0.2.0", + "@react-email/code-block": "0.2.0", "@react-email/code-inline": "0.0.5", "@react-email/column": "0.0.13", "@react-email/container": "0.0.15", @@ -12259,26 +11422,44 @@ "@react-email/html": "0.0.11", "@react-email/img": "0.0.11", "@react-email/link": "0.0.12", - "@react-email/markdown": "0.0.14", - "@react-email/preview": "0.0.12", - "@react-email/render": "1.0.6", + "@react-email/markdown": "0.0.17", + "@react-email/preview": "0.0.13", + "@react-email/render": "2.0.0", "@react-email/row": "0.0.12", "@react-email/section": "0.0.16", - "@react-email/tailwind": "1.0.4", - "@react-email/text": "0.1.1" + "@react-email/tailwind": "2.0.1", + "@react-email/text": "0.1.5" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, + "node_modules/@react-email/components/node_modules/@react-email/render": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-2.0.0.tgz", + "integrity": "sha512-rdjNj6iVzv8kRKDPFas+47nnoe6B40+nwukuXwY4FCwM7XBg6tmYr+chQryCuavUj2J65MMf6fztk1bxOUiSVA==", + "license": "MIT", + "dependencies": { + "html-to-text": "^9.0.5", + "prettier": "^3.5.3" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + } + }, "node_modules/@react-email/container": { "version": "0.0.15", "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.15.tgz", "integrity": "sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12312,6 +11493,7 @@ "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.15.tgz", "integrity": "sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12324,6 +11506,7 @@ "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.11.tgz", "integrity": "sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12348,6 +11531,7 @@ "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.11.tgz", "integrity": "sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12360,6 +11544,7 @@ "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.12.tgz", "integrity": "sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12368,25 +11553,26 @@ } }, "node_modules/@react-email/markdown": { - "version": "0.0.14", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.14.tgz", - "integrity": "sha512-5IsobCyPkb4XwnQO8uFfGcNOxnsg3311GRXhJ3uKv51P7Jxme4ycC/MITnwIZ10w2zx7HIyTiqVzTj4XbuIHbg==", + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.17.tgz", + "integrity": "sha512-6op3AfsBC9BJKkhG+eoMFRFWlr0/f3FYbtQrK+VhGzJocEAY0WINIFN+W8xzXr//3IL0K/aKtnH3FtpIuescQQ==", "license": "MIT", "dependencies": { - "md-to-react-email": "5.0.5" + "marked": "^15.0.12" }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/preview": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.12.tgz", - "integrity": "sha512-g/H5fa9PQPDK6WUEG7iTlC19sAktI23qyoiJtMLqQiXFCfWeQMhqjLGKeLSKkfzszqmfJCjZtpSiKtBoOdxp3Q==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.13.tgz", + "integrity": "sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -12395,864 +11581,13 @@ } }, "node_modules/@react-email/preview-server": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-4.3.0.tgz", - "integrity": "sha512-cUaSrxezCzdg2hF6PzIxVrtagLdw3z3ovHeB3y2RDkmDZpp7EeIoNyJm22Ch2S0uAqTZNAgqu67aroLn3mFC1A==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-5.0.6.tgz", + "integrity": "sha512-hyaQyNeDTJKHrzdnPFdvw7nbohS+jBRzgdQLVVyBcRhiSV3iltqLlsaDVI/x+GJBWxtigbneL9SQ4v/EtOcHKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "7.26.10", - "@babel/parser": "7.27.0", - "@babel/traverse": "7.27.0", - "@lottiefiles/dotlottie-react": "0.13.3", - "@radix-ui/colors": "3.0.0", - "@radix-ui/react-collapsible": "1.1.12", - "@radix-ui/react-dropdown-menu": "2.1.16", - "@radix-ui/react-popover": "1.1.15", - "@radix-ui/react-slot": "1.2.3", - "@radix-ui/react-tabs": "1.1.13", - "@radix-ui/react-toggle-group": "1.1.11", - "@radix-ui/react-tooltip": "1.2.8", - "@types/node": "22.14.1", - "@types/normalize-path": "3.0.2", - "@types/react": "19.0.10", - "@types/react-dom": "19.0.4", - "@types/webpack": "5.28.5", - "autoprefixer": "10.4.21", - "clsx": "2.1.1", - "esbuild": "0.25.10", - "framer-motion": "12.23.22", - "json5": "2.2.3", - "log-symbols": "4.1.0", - "module-punycode": "npm:punycode@2.3.1", - "next": "15.5.2", - "node-html-parser": "7.0.1", - "ora": "5.4.1", - "pretty-bytes": "6.1.1", - "prism-react-renderer": "2.4.1", - "react": "19.0.0", - "react-dom": "19.0.0", - "sharp": "0.34.4", - "socket.io-client": "4.8.1", - "sonner": "2.0.3", - "source-map-js": "1.2.1", - "spamc": "0.0.5", - "stacktrace-parser": "0.1.11", - "tailwind-merge": "3.2.0", - "tailwindcss": "3.4.0", - "use-debounce": "10.0.4", - "zod": "3.24.3" - } - }, - "node_modules/@react-email/preview-server/node_modules/@babel/parser": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz", - "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/@babel/traverse": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz", - "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.27.0", - "@babel/parser": "^7.27.0", - "@babel/template": "^7.27.0", - "@babel/types": "^7.27.0", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~6.21.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/@types/react": { - "version": "19.0.10", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", - "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@react-email/preview-server/node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/@react-email/preview-server/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/@react-email/preview-server/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/@react-email/preview-server/node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@react-email/preview-server/node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" - } - }, - "node_modules/@react-email/preview-server/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@react-email/preview-server/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-email/preview-server/node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-email/preview-server/node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@react-email/preview-server/node_modules/react": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", - "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/react-dom": { - "version": "19.0.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", - "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "scheduler": "^0.25.0" - }, - "peerDependencies": { - "react": "^19.0.0" - } - }, - "node_modules/@react-email/preview-server/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@react-email/preview-server/node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/scheduler": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", - "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@react-email/preview-server/node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@react-email/preview-server/node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@react-email/render": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz", - "integrity": "sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==", - "license": "MIT", - "dependencies": { - "html-to-text": "9.0.5", - "prettier": "3.5.3", - "react-promise-suspense": "0.3.4" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "react": "^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" + "next": "16.0.7" } }, "node_modules/@react-email/row": { @@ -13280,22 +11615,69 @@ } }, "node_modules/@react-email/tailwind": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-1.0.4.tgz", - "integrity": "sha512-tJdcusncdqgvTUYZIuhNC6LYTfL9vNTSQpwWdTCQhQ1lsrNCEE4OKCSdzSV3S9F32pi0i0xQ+YPJHKIzGjdTSA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.1.tgz", + "integrity": "sha512-/xq0IDYVY7863xPY7cdI45Xoz7M6CnIQBJcQvbqN7MNVpopfH9f+mhjayV1JGfKaxlGWuxfLKhgi9T2shsnEFg==", "license": "MIT", + "dependencies": { + "tailwindcss": "^4.1.12" + }, "engines": { - "node": ">=18.0.0" + "node": ">=22.0.0" }, "peerDependencies": { + "@react-email/body": "0.2.0", + "@react-email/button": "0.2.0", + "@react-email/code-block": "0.2.0", + "@react-email/code-inline": "0.0.5", + "@react-email/container": "0.0.15", + "@react-email/heading": "0.0.15", + "@react-email/hr": "0.0.11", + "@react-email/img": "0.0.11", + "@react-email/link": "0.0.12", + "@react-email/preview": "0.0.13", + "@react-email/text": "0.1.5", "react": "^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@react-email/body": { + "optional": true + }, + "@react-email/button": { + "optional": true + }, + "@react-email/code-block": { + "optional": true + }, + "@react-email/code-inline": { + "optional": true + }, + "@react-email/container": { + "optional": true + }, + "@react-email/heading": { + "optional": true + }, + "@react-email/hr": { + "optional": true + }, + "@react-email/img": { + "optional": true + }, + "@react-email/link": { + "optional": true + }, + "@react-email/preview": { + "optional": true + } } }, "node_modules/@react-email/text": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.1.tgz", - "integrity": "sha512-Zo9tSEzkO3fODLVH1yVhzVCiwETfeEL5wU93jXKWo2DHoMuiZ9Iabaso3T0D0UjhrCB1PBMeq2YiejqeToTyIQ==", + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.5.tgz", + "integrity": "sha512-o5PNHFSE085VMXayxH+SJ1LSOtGsTv+RpNKnTiJDrJUwoBu77G3PlKOsZZQHCNyD28WsQpl9v2WcJLbQudqwPg==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -15138,28 +13520,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -15306,6 +13666,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -15344,13 +13705,6 @@ "@types/node": "*" } }, - "node_modules/@types/normalize-path": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/normalize-path/-/normalize-path-3.0.2.tgz", - "integrity": "sha512-DO++toKYPaFn0Z8hQ7Tx+3iT9t77IJo/nDiqTXilgEP+kPNIYdpS9kh3fXuc53ugqwp9pxC1PVjCpV1tQDyqMA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/oauth": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/@types/oauth/-/oauth-0.9.4.tgz", @@ -15494,13 +13848,6 @@ "pkcs11js": "*" } }, - "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/prompt-sync": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@types/prompt-sync/-/prompt-sync-4.2.3.tgz", @@ -15528,16 +13875,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-dom": { - "version": "19.0.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", - "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^19.0.0" - } - }, "node_modules/@types/readable-stream": { "version": "4.0.14", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz", @@ -15710,18 +14047,6 @@ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, "node_modules/@types/whatwg-url": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz", @@ -15820,6 +14145,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.20.0.tgz", "integrity": "sha512-bYerPDF/H5v6V76MdMYhjwmwgMA+jlPVqjSDq2cRqMi8bP5sR3Z+RLOiOMad3nsnmDVmn2gAFCyNgh/dIrfP/w==", "dev": true, + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "6.20.0", "@typescript-eslint/types": "6.20.0", @@ -16188,167 +14514,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, "node_modules/@xmldom/is-dom-node": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@xmldom/is-dom-node/-/is-dom-node-1.0.1.tgz", @@ -16367,20 +14532,6 @@ "node": ">=10.0.0" } }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/@yao-pkg/pkg": { "version": "5.12.0", "resolved": "https://registry.npmjs.org/@yao-pkg/pkg/-/pkg-5.12.0.tgz", @@ -16659,6 +14810,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -16674,19 +14826,6 @@ "acorn": "^8" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -16764,14 +14903,15 @@ } }, "node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "require-from-string": "^2.0.2" }, "funding": { "type": "github", @@ -16794,18 +14934,21 @@ } } }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } + "node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" }, "node_modules/ansi-regex": { "version": "6.0.1", @@ -17002,19 +15145,6 @@ "node": ">=0.8.0" } }, - "node_modules/aria-hidden": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", - "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/array-back": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.2.tgz", @@ -17169,6 +15299,7 @@ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", "license": "MIT", + "peer": true, "dependencies": { "bn.js": "^4.0.0", "inherits": "^2.0.1", @@ -17265,42 +15396,15 @@ "node": ">=8.0.0" } }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", + "node_modules/atomically": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz", + "integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" } }, "node_modules/available-typed-arrays": { @@ -17420,6 +15524,7 @@ "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.4", @@ -17723,13 +15828,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "license": "MIT" }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true, - "license": "ISC" - }, "node_modules/botbuilder": { "version": "4.23.2", "resolved": "https://registry.npmjs.org/botbuilder/-/botbuilder-4.23.2.tgz", @@ -18028,6 +16126,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -18083,13 +16182,6 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true, - "license": "MIT" - }, "node_modules/buildcheck": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.6.tgz", @@ -18343,16 +16435,6 @@ "node": ">=6" } }, - "node_modules/camelcase-css": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001748", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz", @@ -18513,16 +16595,6 @@ "node": ">=10" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, "node_modules/cipher-base": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.5.tgz", @@ -18660,26 +16732,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -18825,6 +16877,61 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, + "node_modules/conf": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.0.2.tgz", + "integrity": "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "atomically": "^2.0.3", + "debounce-fn": "^6.0.0", + "dot-prop": "^10.0.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.7.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/conf/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/confbox": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", @@ -19018,49 +17125,6 @@ "resolved": "https://registry.npmjs.org/crypto-randomuuid/-/crypto-randomuuid-1.0.0.tgz", "integrity": "sha512-/RC5F4l1SCqD/jazwUF6+t34Cd8zTSAGZ7rvvZu1whZUhD2a5MOGKjSGowoGcpj/cbVZk1ZODIooJEQQq3nNAA==" }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/cssstyle": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", @@ -19237,6 +17301,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/debounce-fn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", + "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -19327,19 +17407,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -19477,26 +17544,12 @@ "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", - "dev": true, - "license": "MIT" - }, "node_modules/dev-null": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/dev-null/-/dev-null-0.1.1.tgz", "integrity": "sha512-nMNZG0zfMgmdv8S5O0TM5cpwNbGKRGPCxVsr0SmA3NZZy9CYBbuNLL0PD3Acx9e5LIUgwONXtM9kM6RlawPxEQ==", "license": "MIT" }, - "node_modules/didyoumean": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -19518,13 +17571,6 @@ "node": ">=8" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "dev": true, - "license": "MIT" - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -19600,10 +17646,43 @@ "url": "https://github.com/fb55/domutils?sponsor=1" } }, + "node_modules/dot-prop": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", + "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz", + "integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "16.4.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.1.tgz", "integrity": "sha512-CjA3y+Dr3FyFDOAMnxZEGtnW9KBR2M0JvvUtXNW+dYJL5ROWxP9DUHCwgFqpMk0OXCc0ljhaNTr2w/kutYIcHQ==", + "peer": true, "engines": { "node": ">=12" }, @@ -19750,60 +17829,6 @@ "node": ">=10.2.0" } }, - "node_modules/engine.io-client": { - "version": "6.6.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", - "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.1.1" - } - }, - "node_modules/engine.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/engine.io-client/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/engine.io-parser": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", @@ -20043,6 +18068,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -20125,6 +18151,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", "dev": true, + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -20223,6 +18250,7 @@ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz", "integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==", "dev": true, + "peer": true, "bin": { "eslint-config-prettier": "bin/cli.js" }, @@ -20311,6 +18339,7 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", "dev": true, + "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlastindex": "^1.2.3", @@ -20768,7 +18797,6 @@ "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", "license": "MIT", - "peer": true, "dependencies": { "cookie": "0.7.2", "cookie-signature": "1.0.7", @@ -20786,14 +18814,12 @@ "node_modules/express-session/node_modules/cookie-signature": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "peer": true + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==" }, "node_modules/express-session/node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "peer": true, "dependencies": { "ms": "2.0.0" } @@ -20801,8 +18827,7 @@ "node_modules/express-session/node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "peer": true + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, "node_modules/express/node_modules/cookie": { "version": "0.7.1", @@ -21424,48 +19449,6 @@ "resolved": "https://registry.npmjs.org/forwarded-parse/-/forwarded-parse-2.1.2.tgz", "integrity": "sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==" }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "12.23.22", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.22.tgz", - "integrity": "sha512-ZgGvdxXCw55ZYvhoZChTlG6pUuehecgvEAJz0BHoC5pQKW1EC5xf1Mul1ej5+ai+pVY0pylyFfdl45qnM1/GsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "motion-dom": "^12.23.21", - "motion-utils": "^12.23.6", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -21820,16 +19803,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -21929,13 +19902,6 @@ "node": ">= 6" } }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "dev": true, - "license": "BSD-2-Clause" - }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -22330,16 +20296,6 @@ "node": ">=0.10.0" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/helmet": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-7.1.0.tgz", @@ -23291,47 +21247,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jest-worker/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/jiti": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", @@ -23541,13 +21456,6 @@ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT" - }, "node_modules/json-schema": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", @@ -23604,6 +21512,13 @@ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -24105,16 +22020,6 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -24369,15 +22274,15 @@ } }, "node_modules/marked": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/marked/-/marked-7.0.4.tgz", - "integrity": "sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==", + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", "license": "MIT", "bin": { "marked": "bin/marked.js" }, "engines": { - "node": ">= 16" + "node": ">= 18" } }, "node_modules/math-intrinsics": { @@ -24388,18 +22293,6 @@ "node": ">= 0.4" } }, - "node_modules/md-to-react-email": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/md-to-react-email/-/md-to-react-email-5.0.5.tgz", - "integrity": "sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A==", - "license": "MIT", - "dependencies": { - "marked": "7.0.4" - }, - "peerDependencies": { - "react": "^18.0 || ^19.0" - } - }, "node_modules/md5.js": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", @@ -24433,12 +22326,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -24763,17 +22650,6 @@ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" }, - "node_modules/module-punycode": { - "name": "punycode", - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -24880,23 +22756,6 @@ "node": ">=16" } }, - "node_modules/motion-dom": { - "version": "12.23.21", - "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz", - "integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "motion-utils": "^12.23.6" - } - }, - "node_modules/motion-utils": { - "version": "12.23.6", - "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", - "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", - "dev": true, - "license": "MIT" - }, "node_modules/mri": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/mri/-/mri-1.1.4.tgz", @@ -25103,13 +22962,13 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/next": { - "version": "15.5.2", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.2.tgz", - "integrity": "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q==", + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz", + "integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==", "dev": true, "license": "MIT", "dependencies": { - "@next/env": "15.5.2", + "@next/env": "16.0.7", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", @@ -25119,18 +22978,18 @@ "next": "dist/bin/next" }, "engines": { - "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.5.2", - "@next/swc-darwin-x64": "15.5.2", - "@next/swc-linux-arm64-gnu": "15.5.2", - "@next/swc-linux-arm64-musl": "15.5.2", - "@next/swc-linux-x64-gnu": "15.5.2", - "@next/swc-linux-x64-musl": "15.5.2", - "@next/swc-win32-arm64-msvc": "15.5.2", - "@next/swc-win32-x64-msvc": "15.5.2", - "sharp": "^0.34.3" + "@next/swc-darwin-arm64": "16.0.7", + "@next/swc-darwin-x64": "16.0.7", + "@next/swc-linux-arm64-gnu": "16.0.7", + "@next/swc-linux-arm64-musl": "16.0.7", + "@next/swc-linux-x64-gnu": "16.0.7", + "@next/swc-linux-x64-musl": "16.0.7", + "@next/swc-win32-arm64-msvc": "16.0.7", + "@next/swc-win32-x64-msvc": "16.0.7", + "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", @@ -25410,17 +23269,6 @@ "node": "^16.13.0 || >=18.0.0" } }, - "node_modules/node-html-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-7.0.1.tgz", - "integrity": "sha512-KGtmPY2kS0thCWGK0VuPyOS+pBKhhe8gXztzA2ilAOhbUbxa9homF1bOyKvhGzMLXUoRds9IOmr/v5lr/lqNmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "css-select": "^5.1.0", - "he": "1.2.0" - } - }, "node_modules/node-releases": { "version": "2.0.23", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz", @@ -25510,16 +23358,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npmlog": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", @@ -25531,19 +23369,6 @@ "set-blocking": "^2.0.0" } }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, "node_modules/nwsapi": { "version": "2.2.18", "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.18.tgz", @@ -27649,7 +25474,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -28349,6 +26173,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "peer": true, "engines": { "node": ">=10" }, @@ -28682,6 +26507,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -28691,132 +26517,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-import": { - "version": "15.1.0", - "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", - "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.0.0", - "read-cache": "^1.0.0", - "resolve": "^1.1.7" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.0.0" - } - }, - "node_modules/postcss-js": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", - "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "camelcase-css": "^2.0.1" - }, - "engines": { - "node": "^12 || ^14 || >= 16" - }, - "peerDependencies": { - "postcss": "^8.4.21" - } - }, - "node_modules/postcss-load-config": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", - "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "lilconfig": "^3.0.0", - "yaml": "^2.3.4" - }, - "engines": { - "node": ">= 14" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "node_modules/postcss-nested": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", - "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.1.1" - }, - "engines": { - "node": ">=12.0" - }, - "peerDependencies": { - "postcss": "^8.2.14" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true, - "license": "MIT" - }, "node_modules/postgres-array": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", @@ -28923,6 +26623,7 @@ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -28945,33 +26646,6 @@ "node": ">=6.0.0" } }, - "node_modules/pretty-bytes": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", - "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/prism-react-renderer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", - "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, "node_modules/prismjs": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", @@ -29413,7 +27087,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", - "peer": true, "engines": { "node": ">= 0.8" } @@ -29509,6 +27182,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -29518,6 +27192,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -29526,9 +27201,9 @@ } }, "node_modules/react-email": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-4.3.0.tgz", - "integrity": "sha512-XFHCSfhdlO7k5q2TYGwC0HsVh5Yn13YaOdahuJEUEOfOJKHEpSP4PKg7R/RiKFoK9cDvzunhY+58pXxz0vE2zA==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-5.0.6.tgz", + "integrity": "sha512-DEGzWpEiC3CquPEaaEJuipNT3WZ9mK58rbkpOe4Slbgyf60PLa1wONnt5a3afbBBRbNdW2aYhIvVI41yS6UIRA==", "dev": true, "license": "MIT", "dependencies": { @@ -29536,6 +27211,7 @@ "@babel/traverse": "^7.27.0", "chokidar": "^4.0.3", "commander": "^13.0.0", + "conf": "^15.0.2", "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", @@ -29553,7 +27229,7 @@ "email": "dist/index.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/react-email/node_modules/chokidar": { @@ -29889,113 +27565,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-promise-suspense": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", - "integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^2.0.1" - } - }, - "node_modules/react-promise-suspense/node_modules/fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==", - "license": "MIT" - }, - "node_modules/react-remove-scroll": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", - "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", - "dev": true, - "license": "MIT", - "dependencies": { - "react-remove-scroll-bar": "^2.3.7", - "react-style-singleton": "^2.2.3", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.3", - "use-sidecar": "^1.1.3" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", - "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "react-style-singleton": "^2.2.2", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", - "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-nonce": "^1.0.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } - }, - "node_modules/read-cache/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/readable-stream": { "version": "4.5.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", @@ -30583,26 +28152,6 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", "license": "MIT" }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/scim-patch": { "version": "0.8.3", "resolved": "https://registry.npmjs.org/scim-patch/-/scim-patch-0.8.3.tgz", @@ -30649,9 +28198,9 @@ "integrity": "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw==" }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -30739,16 +28288,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -30831,16 +28370,17 @@ } }, "node_modules/sharp": { - "version": "0.34.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.4.tgz", - "integrity": "sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==", + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", + "optional": true, "dependencies": { "@img/colour": "^1.0.0", - "detect-libc": "^2.1.0", - "semver": "^7.7.2" + "detect-libc": "^2.1.2", + "semver": "^7.7.3" }, "engines": { "node": "^18.17.0 || ^20.3.0 || >=21.0.0" @@ -30849,28 +28389,30 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.4", - "@img/sharp-darwin-x64": "0.34.4", - "@img/sharp-libvips-darwin-arm64": "1.2.3", - "@img/sharp-libvips-darwin-x64": "1.2.3", - "@img/sharp-libvips-linux-arm": "1.2.3", - "@img/sharp-libvips-linux-arm64": "1.2.3", - "@img/sharp-libvips-linux-ppc64": "1.2.3", - "@img/sharp-libvips-linux-s390x": "1.2.3", - "@img/sharp-libvips-linux-x64": "1.2.3", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.3", - "@img/sharp-libvips-linuxmusl-x64": "1.2.3", - "@img/sharp-linux-arm": "0.34.4", - "@img/sharp-linux-arm64": "0.34.4", - "@img/sharp-linux-ppc64": "0.34.4", - "@img/sharp-linux-s390x": "0.34.4", - "@img/sharp-linux-x64": "0.34.4", - "@img/sharp-linuxmusl-arm64": "0.34.4", - "@img/sharp-linuxmusl-x64": "0.34.4", - "@img/sharp-wasm32": "0.34.4", - "@img/sharp-win32-arm64": "0.34.4", - "@img/sharp-win32-ia32": "0.34.4", - "@img/sharp-win32-x64": "0.34.4" + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" } }, "node_modules/shebang-command": { @@ -31323,40 +28865,6 @@ } } }, - "node_modules/socket.io-client": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", - "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.6.1", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/socket.io-client/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -31412,6 +28920,7 @@ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.4.tgz", "integrity": "sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ==", "license": "MIT", + "peer": true, "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" @@ -31469,17 +28978,6 @@ "atomic-sleep": "^1.0.0" } }, - "node_modules/sonner": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz", - "integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" - } - }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -31498,23 +28996,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/spamc": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/spamc/-/spamc-0.0.5.tgz", - "integrity": "sha512-jYXItuZuiWZyG9fIdvgTUbp2MNRuyhuSwvvhhpPJd4JK/9oSZxkD7zAj53GJtowSlXwCJzLg6sCKAoE9wXsKgg==", - "dev": true - }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -31635,29 +29116,6 @@ "dev": true, "license": "MIT" }, - "node_modules/stacktrace-parser": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", - "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "dev": true, - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=8" - } - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -31952,6 +29410,23 @@ "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "dev": true, + "license": "MIT" + }, "node_modules/stubs": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", @@ -32143,105 +29618,25 @@ "node": ">=12.17" } }, - "node_modules/tailwind-merge": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.2.0.tgz", - "integrity": "sha512-FQT/OVqCD+7edmmJpsgCsY820RTD5AkBryuG5IUqR5YQZSdj5xlH5nLgH7YPths7WsLPSpSBNneJdM8aS8aeFA==", + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", "dev": true, "license": "MIT", + "engines": { + "node": ">=20" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tailwindcss/node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "dev": true, + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", + "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", "license": "MIT" }, - "node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/tailwindcss/node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/tailwindcss/node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/tailwindcss/node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -32440,78 +29835,6 @@ "node": ">= 6" } }, - "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT" - }, "node_modules/text-hex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", @@ -33096,6 +30419,7 @@ "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" @@ -33273,6 +30597,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -33313,7 +30638,6 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", - "peer": true, "dependencies": { "random-bytes": "~1.0.0" }, @@ -33326,6 +30650,19 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz", "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -33525,64 +30862,6 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" }, - "node_modules/use-callback-ref": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", - "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-debounce": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/use-debounce/-/use-debounce-10.0.4.tgz", - "integrity": "sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 16.0.0" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/use-sidecar": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", - "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/util": { "version": "0.12.5", "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", @@ -33946,6 +31225,7 @@ "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -34026,118 +31306,11 @@ "node": ">=18" } }, - "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "dev": true, - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, - "node_modules/webpack": { - "version": "5.102.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz", - "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.26.3", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", @@ -34172,6 +31345,13 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "dev": true, + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -34579,15 +31759,6 @@ "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", - "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xpath": { "version": "0.0.34", "resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz", @@ -34736,6 +31907,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.3.tgz", "integrity": "sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/backend/package.json b/backend/package.json index 4f9cfdc97..433e7a9b0 100644 --- a/backend/package.json +++ b/backend/package.json @@ -91,7 +91,7 @@ "@babel/plugin-syntax-import-attributes": "^7.24.7", "@babel/preset-env": "^7.18.10", "@babel/preset-react": "^7.24.7", - "@react-email/preview-server": "^4.3.0", + "@react-email/preview-server": "^5.0.6", "@smithy/types": "^4.3.1", "@types/bcrypt": "^5.0.2", "@types/jmespath": "^0.15.2", @@ -129,7 +129,7 @@ "nodemon": "^3.0.2", "pino-pretty": "^10.2.3", "prompt-sync": "^4.2.0", - "react-email": "^4.3.0", + "react-email": "^5.0.6", "rimraf": "^5.0.5", "ts-node": "^10.9.2", "tsc-alias": "^1.8.8", @@ -184,7 +184,7 @@ "@opentelemetry/semantic-conventions": "^1.27.0", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/x509": "^1.12.1", - "@react-email/components": "0.0.36", + "@react-email/components": "^1.0.1", "@serdnam/pino-cloudwatch-transport": "^1.0.4", "@sindresorhus/slugify": "1.1.0", "@slack/oauth": "^3.0.2", @@ -267,4 +267,4 @@ "zod": "^3.22.4", "zod-to-json-schema": "^3.24.5" } -} \ No newline at end of file +} diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 02394de4d..be6f45943 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -55,6 +55,7 @@ import { TAuthMode } from "@app/server/plugins/auth/inject-identity"; import { TAdditionalPrivilegeServiceFactory } from "@app/services/additional-privilege/additional-privilege-service"; import { TApiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; +import { TApprovalPolicyServiceFactory } from "@app/services/approval-policy/approval-policy-service"; import { TAuthLoginFactory } from "@app/services/auth/auth-login-service"; import { TAuthPasswordFactory } from "@app/services/auth/auth-password-service"; import { TAuthSignupFactory } from "@app/services/auth/auth-signup-service"; @@ -361,6 +362,7 @@ declare module "fastify" { convertor: TConvertorServiceFactory; subOrganization: TSubOrgServiceFactory; pkiAlertV2: TPkiAlertV2ServiceFactory; + approvalPolicy: TApprovalPolicyServiceFactory; }; // this is exclusive use for middlewares in which we need to inject data // everywhere else access using service layer diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 4bdd3849d..1301000d5 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -26,6 +26,30 @@ import { TAppConnections, TAppConnectionsInsert, TAppConnectionsUpdate, + TApprovalPolicies, + TApprovalPoliciesInsert, + TApprovalPoliciesUpdate, + TApprovalPolicyStepApprovers, + TApprovalPolicyStepApproversInsert, + TApprovalPolicyStepApproversUpdate, + TApprovalPolicySteps, + TApprovalPolicyStepsInsert, + TApprovalPolicyStepsUpdate, + TApprovalRequestApprovals, + TApprovalRequestApprovalsInsert, + TApprovalRequestApprovalsUpdate, + TApprovalRequestGrants, + TApprovalRequestGrantsInsert, + TApprovalRequestGrantsUpdate, + TApprovalRequests, + TApprovalRequestsInsert, + TApprovalRequestStepEligibleApprovers, + TApprovalRequestStepEligibleApproversInsert, + TApprovalRequestStepEligibleApproversUpdate, + TApprovalRequestSteps, + TApprovalRequestStepsInsert, + TApprovalRequestStepsUpdate, + TApprovalRequestsUpdate, TAuditLogs, TAuditLogsInsert, TAuditLogStreams, @@ -573,16 +597,16 @@ import { TWorkflowIntegrationsInsert, TWorkflowIntegrationsUpdate } from "@app/db/schemas"; -import { - TCertificateRequests, - TCertificateRequestsInsert, - TCertificateRequestsUpdate -} from "@app/db/schemas/certificate-requests"; import { TAccessApprovalPoliciesEnvironments, TAccessApprovalPoliciesEnvironmentsInsert, TAccessApprovalPoliciesEnvironmentsUpdate } from "@app/db/schemas/access-approval-policies-environments"; +import { + TCertificateRequests, + TCertificateRequestsInsert, + TCertificateRequestsUpdate +} from "@app/db/schemas/certificate-requests"; import { TIdentityAuthTemplates, TIdentityAuthTemplatesInsert, @@ -1475,5 +1499,45 @@ declare module "knex/types/tables" { TVaultExternalMigrationConfigsInsert, TVaultExternalMigrationConfigsUpdate >; + [TableName.ApprovalPolicies]: KnexOriginal.CompositeTableType< + TApprovalPolicies, + TApprovalPoliciesInsert, + TApprovalPoliciesUpdate + >; + [TableName.ApprovalPolicyStepApprovers]: KnexOriginal.CompositeTableType< + TApprovalPolicyStepApprovers, + TApprovalPolicyStepApproversInsert, + TApprovalPolicyStepApproversUpdate + >; + [TableName.ApprovalPolicySteps]: KnexOriginal.CompositeTableType< + TApprovalPolicySteps, + TApprovalPolicyStepsInsert, + TApprovalPolicyStepsUpdate + >; + [TableName.ApprovalRequestApprovals]: KnexOriginal.CompositeTableType< + TApprovalRequestApprovals, + TApprovalRequestApprovalsInsert, + TApprovalRequestApprovalsUpdate + >; + [TableName.ApprovalRequestGrants]: KnexOriginal.CompositeTableType< + TApprovalRequestGrants, + TApprovalRequestGrantsInsert, + TApprovalRequestGrantsUpdate + >; + [TableName.ApprovalRequestStepEligibleApprovers]: KnexOriginal.CompositeTableType< + TApprovalRequestStepEligibleApprovers, + TApprovalRequestStepEligibleApproversInsert, + TApprovalRequestStepEligibleApproversUpdate + >; + [TableName.ApprovalRequestSteps]: KnexOriginal.CompositeTableType< + TApprovalRequestSteps, + TApprovalRequestStepsInsert, + TApprovalRequestStepsUpdate + >; + [TableName.ApprovalRequests]: KnexOriginal.CompositeTableType< + TApprovalRequests, + TApprovalRequestsInsert, + TApprovalRequestsUpdate + >; } } diff --git a/backend/src/db/migrations/20251127192155_adds-sub-organization-id-to-identity-access-tokens.ts b/backend/src/db/migrations/20251127192155_adds-sub-organization-id-to-identity-access-tokens.ts new file mode 100644 index 000000000..31de94dd3 --- /dev/null +++ b/backend/src/db/migrations/20251127192155_adds-sub-organization-id-to-identity-access-tokens.ts @@ -0,0 +1,22 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasSubOrganizationIdColumn = await knex.schema.hasColumn(TableName.IdentityAccessToken, "subOrganizationId"); + if (!hasSubOrganizationIdColumn) { + await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => { + t.uuid("subOrganizationId").nullable(); + t.foreign("subOrganizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasSubOrganizationIdColumn = await knex.schema.hasColumn(TableName.IdentityAccessToken, "subOrganizationId"); + if (hasSubOrganizationIdColumn) { + await knex.schema.alterTable(TableName.IdentityAccessToken, (t) => { + t.dropColumn("subOrganizationId"); + }); + } +} diff --git a/backend/src/db/migrations/20251203002657_global-approvals.ts b/backend/src/db/migrations/20251203002657_global-approvals.ts new file mode 100644 index 000000000..51610f879 --- /dev/null +++ b/backend/src/db/migrations/20251203002657_global-approvals.ts @@ -0,0 +1,194 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; +import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; + +export async function up(knex: Knex): Promise { + if (!(await knex.schema.hasTable(TableName.ApprovalPolicies))) { + await knex.schema.createTable(TableName.ApprovalPolicies, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable().index(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + + t.uuid("organizationId").notNullable().index(); + t.foreign("organizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.string("type").notNullable().index(); + t.string("name").notNullable(); + + t.boolean("isActive").defaultTo(true); + + t.string("maxRequestTtl").nullable(); // 1hour, 30seconds, etc + + t.jsonb("conditions").notNullable(); + t.jsonb("constraints").notNullable(); + + t.timestamps(true, true, true); + }); + await createOnUpdateTrigger(knex, TableName.ApprovalPolicies); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalPolicySteps))) { + await knex.schema.createTable(TableName.ApprovalPolicySteps, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("policyId").notNullable().index(); + t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("CASCADE"); + + t.string("name").nullable(); + t.integer("stepNumber").notNullable(); + + t.integer("requiredApprovals").notNullable(); + t.boolean("notifyApprovers").defaultTo(false); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalPolicyStepApprovers))) { + await knex.schema.createTable(TableName.ApprovalPolicyStepApprovers, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("policyStepId").notNullable().index(); + t.foreign("policyStepId").references("id").inTable(TableName.ApprovalPolicySteps).onDelete("CASCADE"); + + t.uuid("userId").nullable().index(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + + t.uuid("groupId").nullable().index(); + t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE"); + + t.check('("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)'); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequests))) { + await knex.schema.createTable(TableName.ApprovalRequests, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable().index(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + + t.uuid("organizationId").notNullable().index(); + t.foreign("organizationId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); + + t.uuid("policyId").nullable().index(); + t.foreign("policyId").references("id").inTable(TableName.ApprovalPolicies).onDelete("SET NULL"); + + t.uuid("requesterId").nullable().index(); + t.foreign("requesterId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + + // To be used in the event of requester deletion + t.string("requesterName").notNullable(); + t.string("requesterEmail").notNullable(); + + t.string("type").notNullable().index(); + + t.string("status").notNullable().index(); + t.text("justification").nullable(); + t.integer("currentStep").notNullable(); + + t.jsonb("requestData").notNullable(); + + t.timestamp("expiresAt").nullable(); + t.timestamps(true, true, true); + }); + await createOnUpdateTrigger(knex, TableName.ApprovalRequests); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequestSteps))) { + await knex.schema.createTable(TableName.ApprovalRequestSteps, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("requestId").notNullable().index(); + t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("CASCADE"); + + t.integer("stepNumber").notNullable(); + + t.string("name").nullable(); + t.string("status").notNullable().index(); + + t.integer("requiredApprovals").notNullable(); + t.boolean("notifyApprovers").defaultTo(false); + + t.timestamp("startedAt").nullable(); + t.timestamp("completedAt").nullable(); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequestStepEligibleApprovers))) { + await knex.schema.createTable(TableName.ApprovalRequestStepEligibleApprovers, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("stepId").notNullable().index(); + t.foreign("stepId").references("id").inTable(TableName.ApprovalRequestSteps).onDelete("CASCADE"); + + t.uuid("userId").nullable().index(); + t.foreign("userId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + + t.uuid("groupId").nullable().index(); + t.foreign("groupId").references("id").inTable(TableName.Groups).onDelete("CASCADE"); + + t.check('("userId" IS NOT NULL AND "groupId" IS NULL) OR ("userId" IS NULL AND "groupId" IS NOT NULL)'); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequestApprovals))) { + await knex.schema.createTable(TableName.ApprovalRequestApprovals, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.uuid("stepId").notNullable().index(); + t.foreign("stepId").references("id").inTable(TableName.ApprovalRequestSteps).onDelete("CASCADE"); + + t.uuid("approverUserId").notNullable().index(); + t.foreign("approverUserId").references("id").inTable(TableName.Users).onDelete("CASCADE"); + + t.string("decision").notNullable(); + t.text("comment").nullable(); + + t.timestamp("createdAt").defaultTo(knex.fn.now()); + }); + } + + if (!(await knex.schema.hasTable(TableName.ApprovalRequestGrants))) { + await knex.schema.createTable(TableName.ApprovalRequestGrants, (t) => { + t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); + + t.string("projectId").notNullable().index(); + t.foreign("projectId").references("id").inTable(TableName.Project).onDelete("CASCADE"); + + t.uuid("requestId").nullable().index(); + t.foreign("requestId").references("id").inTable(TableName.ApprovalRequests).onDelete("SET NULL"); + + t.uuid("granteeUserId").nullable().index(); + t.foreign("granteeUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + + t.uuid("revokedByUserId").nullable().index(); + t.foreign("revokedByUserId").references("id").inTable(TableName.Users).onDelete("SET NULL"); + + t.text("revocationReason").nullable(); + + t.string("status").notNullable().index(); + t.string("type").notNullable().index(); + + t.jsonb("attributes").notNullable(); + + t.timestamp("createdAt").defaultTo(knex.fn.now()); + t.timestamp("expiresAt").nullable(); + t.timestamp("revokedAt").nullable(); + }); + } +} + +export async function down(knex: Knex): Promise { + await knex.schema.dropTableIfExists(TableName.ApprovalRequestGrants); + await knex.schema.dropTableIfExists(TableName.ApprovalRequestApprovals); + await knex.schema.dropTableIfExists(TableName.ApprovalRequestStepEligibleApprovers); + await knex.schema.dropTableIfExists(TableName.ApprovalRequestSteps); + await knex.schema.dropTableIfExists(TableName.ApprovalRequests); + await knex.schema.dropTableIfExists(TableName.ApprovalPolicyStepApprovers); + await knex.schema.dropTableIfExists(TableName.ApprovalPolicySteps); + await knex.schema.dropTableIfExists(TableName.ApprovalPolicies); + + await dropOnUpdateTrigger(knex, TableName.ApprovalRequests); + await dropOnUpdateTrigger(knex, TableName.ApprovalPolicies); +} diff --git a/backend/src/db/migrations/20251203224427_pam-aws-console.ts b/backend/src/db/migrations/20251203224427_pam-aws-console.ts new file mode 100644 index 000000000..adadb9e99 --- /dev/null +++ b/backend/src/db/migrations/20251203224427_pam-aws-console.ts @@ -0,0 +1,21 @@ +import { Knex } from "knex"; + +import { TableName } from "../schemas"; + +export async function up(knex: Knex): Promise { + const hasGatewayId = await knex.schema.hasColumn(TableName.PamResource, "gatewayId"); + if (hasGatewayId) { + await knex.schema.alterTable(TableName.PamResource, (t) => { + t.uuid("gatewayId").nullable().alter(); + }); + } +} + +export async function down(knex: Knex): Promise { + const hasGatewayId = await knex.schema.hasColumn(TableName.PamResource, "gatewayId"); + if (hasGatewayId) { + await knex.schema.alterTable(TableName.PamResource, (t) => { + t.uuid("gatewayId").notNullable().alter(); + }); + } +} diff --git a/backend/src/db/schemas/approval-policies.ts b/backend/src/db/schemas/approval-policies.ts new file mode 100644 index 000000000..d7f8fe8da --- /dev/null +++ b/backend/src/db/schemas/approval-policies.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalPoliciesSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + organizationId: z.string().uuid(), + type: z.string(), + name: z.string(), + isActive: z.boolean().default(true).nullable().optional(), + maxRequestTtl: z.string().nullable().optional(), + conditions: z.unknown(), + constraints: z.unknown(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TApprovalPolicies = z.infer; +export type TApprovalPoliciesInsert = Omit, TImmutableDBKeys>; +export type TApprovalPoliciesUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/approval-policy-step-approvers.ts b/backend/src/db/schemas/approval-policy-step-approvers.ts new file mode 100644 index 000000000..909d99d15 --- /dev/null +++ b/backend/src/db/schemas/approval-policy-step-approvers.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalPolicyStepApproversSchema = z.object({ + id: z.string().uuid(), + policyStepId: z.string().uuid(), + userId: z.string().uuid().nullable().optional(), + groupId: z.string().uuid().nullable().optional() +}); + +export type TApprovalPolicyStepApprovers = z.infer; +export type TApprovalPolicyStepApproversInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TApprovalPolicyStepApproversUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/approval-policy-steps.ts b/backend/src/db/schemas/approval-policy-steps.ts new file mode 100644 index 000000000..d4831fa2d --- /dev/null +++ b/backend/src/db/schemas/approval-policy-steps.ts @@ -0,0 +1,21 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalPolicyStepsSchema = z.object({ + id: z.string().uuid(), + policyId: z.string().uuid(), + name: z.string().nullable().optional(), + stepNumber: z.number(), + requiredApprovals: z.number(), + notifyApprovers: z.boolean().default(false).nullable().optional() +}); + +export type TApprovalPolicySteps = z.infer; +export type TApprovalPolicyStepsInsert = Omit, TImmutableDBKeys>; +export type TApprovalPolicyStepsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/approval-request-approvals.ts b/backend/src/db/schemas/approval-request-approvals.ts new file mode 100644 index 000000000..25b305d85 --- /dev/null +++ b/backend/src/db/schemas/approval-request-approvals.ts @@ -0,0 +1,23 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestApprovalsSchema = z.object({ + id: z.string().uuid(), + stepId: z.string().uuid(), + approverUserId: z.string().uuid(), + decision: z.string(), + comment: z.string().nullable().optional(), + createdAt: z.date().nullable().optional() +}); + +export type TApprovalRequestApprovals = z.infer; +export type TApprovalRequestApprovalsInsert = Omit, TImmutableDBKeys>; +export type TApprovalRequestApprovalsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/approval-request-grants.ts b/backend/src/db/schemas/approval-request-grants.ts new file mode 100644 index 000000000..5056a5b1d --- /dev/null +++ b/backend/src/db/schemas/approval-request-grants.ts @@ -0,0 +1,27 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestGrantsSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + requestId: z.string().uuid().nullable().optional(), + granteeUserId: z.string().uuid().nullable().optional(), + revokedByUserId: z.string().uuid().nullable().optional(), + revocationReason: z.string().nullable().optional(), + status: z.string(), + type: z.string(), + attributes: z.unknown(), + createdAt: z.date().nullable().optional(), + expiresAt: z.date().nullable().optional(), + revokedAt: z.date().nullable().optional() +}); + +export type TApprovalRequestGrants = z.infer; +export type TApprovalRequestGrantsInsert = Omit, TImmutableDBKeys>; +export type TApprovalRequestGrantsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/approval-request-step-eligible-approvers.ts b/backend/src/db/schemas/approval-request-step-eligible-approvers.ts new file mode 100644 index 000000000..987861e6b --- /dev/null +++ b/backend/src/db/schemas/approval-request-step-eligible-approvers.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestStepEligibleApproversSchema = z.object({ + id: z.string().uuid(), + stepId: z.string().uuid(), + userId: z.string().uuid().nullable().optional(), + groupId: z.string().uuid().nullable().optional() +}); + +export type TApprovalRequestStepEligibleApprovers = z.infer; +export type TApprovalRequestStepEligibleApproversInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TApprovalRequestStepEligibleApproversUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/db/schemas/approval-request-steps.ts b/backend/src/db/schemas/approval-request-steps.ts new file mode 100644 index 000000000..7b5233601 --- /dev/null +++ b/backend/src/db/schemas/approval-request-steps.ts @@ -0,0 +1,24 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestStepsSchema = z.object({ + id: z.string().uuid(), + requestId: z.string().uuid(), + stepNumber: z.number(), + name: z.string().nullable().optional(), + status: z.string(), + requiredApprovals: z.number(), + notifyApprovers: z.boolean().default(false).nullable().optional(), + startedAt: z.date().nullable().optional(), + completedAt: z.date().nullable().optional() +}); + +export type TApprovalRequestSteps = z.infer; +export type TApprovalRequestStepsInsert = Omit, TImmutableDBKeys>; +export type TApprovalRequestStepsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/approval-requests.ts b/backend/src/db/schemas/approval-requests.ts new file mode 100644 index 000000000..c5d53fdf8 --- /dev/null +++ b/backend/src/db/schemas/approval-requests.ts @@ -0,0 +1,30 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const ApprovalRequestsSchema = z.object({ + id: z.string().uuid(), + projectId: z.string(), + organizationId: z.string().uuid(), + policyId: z.string().uuid().nullable().optional(), + requesterId: z.string().uuid().nullable().optional(), + requesterName: z.string(), + requesterEmail: z.string(), + type: z.string(), + status: z.string(), + justification: z.string().nullable().optional(), + currentStep: z.number(), + requestData: z.unknown(), + expiresAt: z.date().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TApprovalRequests = z.infer; +export type TApprovalRequestsInsert = Omit, TImmutableDBKeys>; +export type TApprovalRequestsUpdate = Partial, TImmutableDBKeys>>; diff --git a/backend/src/db/schemas/identity-access-tokens.ts b/backend/src/db/schemas/identity-access-tokens.ts index 8f2b8b73b..fcda42d4b 100644 --- a/backend/src/db/schemas/identity-access-tokens.ts +++ b/backend/src/db/schemas/identity-access-tokens.ts @@ -22,7 +22,8 @@ export const IdentityAccessTokensSchema = z.object({ updatedAt: z.date(), name: z.string().nullable().optional(), authMethod: z.string(), - accessTokenPeriod: z.coerce.number().default(0) + accessTokenPeriod: z.coerce.number().default(0), + subOrganizationId: z.string().uuid().nullable().optional() }); export type TIdentityAccessTokens = z.infer; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 7db6e847d..528582c59 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -6,6 +6,14 @@ export * from "./access-approval-requests-reviewers"; export * from "./additional-privileges"; export * from "./api-keys"; export * from "./app-connections"; +export * from "./approval-policies"; +export * from "./approval-policy-step-approvers"; +export * from "./approval-policy-steps"; +export * from "./approval-request-approvals"; +export * from "./approval-request-grants"; +export * from "./approval-request-step-eligible-approvers"; +export * from "./approval-request-steps"; +export * from "./approval-requests"; export * from "./audit-log-streams"; export * from "./audit-logs"; export * from "./auth-token-sessions"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 040d6e278..fe38a9c9b 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -223,7 +223,17 @@ export enum TableName { PkiAcmeOrder = "pki_acme_orders", PkiAcmeOrderAuth = "pki_acme_order_auths", PkiAcmeAuth = "pki_acme_auths", - PkiAcmeChallenge = "pki_acme_challenges" + PkiAcmeChallenge = "pki_acme_challenges", + + // Approval Policies + ApprovalPolicies = "approval_policies", + ApprovalPolicySteps = "approval_policy_steps", + ApprovalPolicyStepApprovers = "approval_policy_step_approvers", + ApprovalRequests = "approval_requests", + ApprovalRequestSteps = "approval_request_steps", + ApprovalRequestStepEligibleApprovers = "approval_request_step_eligible_approvers", + ApprovalRequestApprovals = "approval_request_approvals", + ApprovalRequestGrants = "approval_request_grants" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; diff --git a/backend/src/db/schemas/pam-resources.ts b/backend/src/db/schemas/pam-resources.ts index 325f6eddc..f59aae5d8 100644 --- a/backend/src/db/schemas/pam-resources.ts +++ b/backend/src/db/schemas/pam-resources.ts @@ -13,7 +13,7 @@ export const PamResourcesSchema = z.object({ id: z.string().uuid(), projectId: z.string(), name: z.string(), - gatewayId: z.string().uuid(), + gatewayId: z.string().uuid().nullable().optional(), resourceType: z.string(), encryptedConnectionDetails: zodBuffer, createdAt: z.date(), diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index bc3a4f602..0e34ebcd4 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -12,7 +12,6 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router" import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerExternalKmsRouter } from "./external-kms-router"; - import { EXTERNAL_KMS_REGISTER_ROUTER_MAP } from "./external-kms-routers"; import { registerGatewayRouter } from "./gateway-router"; import { registerGithubOrgSyncRouter } from "./github-org-sync-router"; diff --git a/backend/src/ee/routes/v1/license-router.ts b/backend/src/ee/routes/v1/license-router.ts index 2ccdce93a..c3c48bdc9 100644 --- a/backend/src/ee/routes/v1/license-router.ts +++ b/backend/src/ee/routes/v1/license-router.ts @@ -58,7 +58,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { const plan = await server.services.license.getOrgPlan({ actorId: req.permission.id, actor: req.permission.type, - actorOrgId: req.permission.rootOrgId, + actorOrgId: req.permission.orgId, + rootOrgId: req.permission.rootOrgId, actorAuthMethod: req.permission.authMethod, orgId: req.params.organizationId, refreshCache: req.query.refreshCache @@ -87,7 +88,8 @@ export const registerLicenseRouter = async (server: FastifyZodProvider) => { actor: req.permission.type, actorOrgId: req.permission.orgId, actorAuthMethod: req.permission.authMethod, - orgId: req.params.organizationId + orgId: req.params.organizationId, + rootOrgId: req.permission.rootOrgId }); return data; } diff --git a/backend/src/ee/routes/v1/pam-account-routers/index.ts b/backend/src/ee/routes/v1/pam-account-routers/index.ts index 61a29970e..9c7cf161d 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/index.ts @@ -1,3 +1,8 @@ +import { + CreateAwsIamAccountSchema, + SanitizedAwsIamAccountWithResourceSchema, + UpdateAwsIamAccountSchema +} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas"; import { CreateKubernetesAccountSchema, SanitizedKubernetesAccountWithResourceSchema, @@ -58,5 +63,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.AwsIam, + accountResponseSchema: SanitizedAwsIamAccountWithResourceSchema, + createAccountSchema: CreateAwsIamAccountSchema, + updateAccountSchema: UpdateAwsIamAccountSchema + }); } }; diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts index 44e2a5ea1..4043c3cc8 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-endpoints.ts @@ -22,7 +22,7 @@ export const registerPamResourceEndpoints = ({ folderId?: C["folderId"]; name: C["name"]; description?: C["description"]; - rotationEnabled: C["rotationEnabled"]; + rotationEnabled?: C["rotationEnabled"]; rotationIntervalSeconds?: C["rotationIntervalSeconds"]; }>; updateAccountSchema: z.ZodType<{ @@ -65,7 +65,7 @@ export const registerPamResourceEndpoints = ({ folderId: req.body.folderId, name: req.body.name, description: req.body.description, - rotationEnabled: req.body.rotationEnabled, + rotationEnabled: req.body.rotationEnabled ?? false, rotationIntervalSeconds: req.body.rotationIntervalSeconds } } diff --git a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts index f6d6c1cf7..d36702cb7 100644 --- a/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts +++ b/backend/src/ee/routes/v1/pam-account-routers/pam-account-router.ts @@ -3,9 +3,11 @@ import { z } from "zod"; import { PamFoldersSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { PamAccountOrderBy, PamAccountView } from "@app/ee/services/pam-account/pam-account-enums"; +import { SanitizedAwsIamAccountWithResourceSchema } from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas"; import { SanitizedKubernetesAccountWithResourceSchema } from "@app/ee/services/pam-resource/kubernetes/kubernetes-resource-schemas"; import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas"; import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums"; +import { GatewayAccessResponseSchema } from "@app/ee/services/pam-resource/pam-resource-schemas"; import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas"; import { SanitizedSSHAccountWithResourceSchema } from "@app/ee/services/pam-resource/ssh/ssh-resource-schemas"; import { BadRequestError } from "@app/lib/errors"; @@ -20,7 +22,8 @@ const SanitizedAccountSchema = z.union([ SanitizedSSHAccountWithResourceSchema, // ORDER MATTERS SanitizedPostgresAccountWithResourceSchema, SanitizedMySQLAccountWithResourceSchema, - SanitizedKubernetesAccountWithResourceSchema + SanitizedKubernetesAccountWithResourceSchema, + SanitizedAwsIamAccountWithResourceSchema ]); const ListPamAccountsResponseSchema = z.object({ @@ -129,18 +132,19 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { }) }), response: { - 200: z.object({ - sessionId: z.string(), - resourceType: z.nativeEnum(PamResource), - relayClientCertificate: z.string(), - relayClientPrivateKey: z.string(), - relayServerCertificateChain: z.string(), - gatewayClientCertificate: z.string(), - gatewayClientPrivateKey: z.string(), - gatewayServerCertificateChain: z.string(), - relayHost: z.string(), - metadata: z.record(z.string(), z.string().optional()).optional() - }) + 200: z.discriminatedUnion("resourceType", [ + // Gateway-based resources (Postgres, MySQL, SSH) + GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.Postgres) }), + GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.MySQL) }), + GatewayAccessResponseSchema.extend({ resourceType: z.literal(PamResource.SSH) }), + // AWS IAM (no gateway, returns console URL) + z.object({ + sessionId: z.string(), + resourceType: z.literal(PamResource.AwsIam), + consoleUrl: z.string().url(), + metadata: z.record(z.string(), z.string().optional()).optional() + }) + ]) } }, onRequest: verifyAuth([AuthMode.JWT]), @@ -166,7 +170,7 @@ export const registerPamAccountRouter = async (server: FastifyZodProvider) => { await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, orgId: req.permission.orgId, - projectId: response.projectId, + projectId: req.body.projectId, event: { type: EventType.PAM_ACCOUNT_ACCESS, metadata: { diff --git a/backend/src/ee/routes/v1/pam-resource-routers/index.ts b/backend/src/ee/routes/v1/pam-resource-routers/index.ts index e26cc5f70..e3c9cf60c 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/index.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/index.ts @@ -1,3 +1,8 @@ +import { + CreateAwsIamResourceSchema, + SanitizedAwsIamResourceSchema, + UpdateAwsIamResourceSchema +} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas"; import { CreateKubernetesResourceSchema, SanitizedKubernetesResourceSchema, @@ -58,5 +63,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record { + registerPamResourceEndpoints({ + server, + resourceType: PamResource.AwsIam, + resourceResponseSchema: SanitizedAwsIamResourceSchema, + createResourceSchema: CreateAwsIamResourceSchema, + updateResourceSchema: UpdateAwsIamResourceSchema + }); } }; diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts index ffbeae5c0..e3803316e 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-endpoints.ts @@ -19,7 +19,7 @@ export const registerPamResourceEndpoints = ({ createResourceSchema: z.ZodType<{ projectId: T["projectId"]; connectionDetails: T["connectionDetails"]; - gatewayId: T["gatewayId"]; + gatewayId?: T["gatewayId"]; name: T["name"]; rotationAccountCredentials?: T["rotationAccountCredentials"]; }>; @@ -103,7 +103,7 @@ export const registerPamResourceEndpoints = ({ type: EventType.PAM_RESOURCE_CREATE, metadata: { resourceType, - gatewayId: req.body.gatewayId, + ...(req.body.gatewayId && { gatewayId: req.body.gatewayId }), name: req.body.name } } @@ -150,8 +150,8 @@ export const registerPamResourceEndpoints = ({ metadata: { resourceId: req.params.resourceId, resourceType, - gatewayId: req.body.gatewayId, - name: req.body.name + ...(req.body.gatewayId && { gatewayId: req.body.gatewayId }), + ...(req.body.name && { name: req.body.name }) } } }); diff --git a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts index 2c97312b4..8e4326f3f 100644 --- a/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts +++ b/backend/src/ee/routes/v1/pam-resource-routers/pam-resource-router.ts @@ -1,6 +1,10 @@ import { z } from "zod"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { + AwsIamResourceListItemSchema, + SanitizedAwsIamResourceSchema +} from "@app/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas"; import { KubernetesResourceListItemSchema, SanitizedKubernetesResourceSchema @@ -27,14 +31,16 @@ const SanitizedResourceSchema = z.union([ SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema, SanitizedSSHResourceSchema, - SanitizedKubernetesResourceSchema + SanitizedKubernetesResourceSchema, + SanitizedAwsIamResourceSchema ]); const ResourceOptionsSchema = z.discriminatedUnion("resource", [ PostgresResourceListItemSchema, MySQLResourceListItemSchema, SSHResourceListItemSchema, - KubernetesResourceListItemSchema + KubernetesResourceListItemSchema, + AwsIamResourceListItemSchema ]); export const registerPamResourceRouter = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/routes/v1/project-role-router.ts b/backend/src/ee/routes/v1/project-role-router.ts index acf34cb3b..f1ee79481 100644 --- a/backend/src/ee/routes/v1/project-role-router.ts +++ b/backend/src/ee/routes/v1/project-role-router.ts @@ -315,6 +315,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => { memberships: z .object({ id: z.string(), + actorGroupId: z.string().nullish(), + actorUserId: z.string().nullish(), roles: z .object({ role: z.string() diff --git a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts index 47b3f5258..23ba27b8a 100644 --- a/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts +++ b/backend/src/ee/routes/v2/identity-project-additional-privilege-router.ts @@ -84,7 +84,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: req.body.identityId, - projectMembershipId: req.body.projectId, projectId: req.body.projectId, slug: privilege.name } @@ -168,7 +167,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: privilegeDoc.actorIdentityId as string, - projectMembershipId: privilegeDoc.projectId as string, projectId: privilegeDoc.projectId as string, slug: privilege.name } @@ -222,7 +220,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: privilegeDoc.actorIdentityId as string, - projectMembershipId: privilegeDoc.projectId as string, projectId: privilegeDoc.projectId as string, slug: privilege.name } @@ -276,7 +273,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: privilegeDoc.actorIdentityId as string, - projectMembershipId: privilegeDoc.projectId as string, projectId: privilegeDoc.projectId as string, slug: privilege.name } @@ -339,7 +335,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privilege: { ...privilege, identityId: req.query.identityId, - projectMembershipId: privilege.projectId as string, projectId, slug: privilege.name } @@ -391,7 +386,6 @@ export const registerIdentityProjectAdditionalPrivilegeRouter = async (server: F privileges: privileges.map((privilege) => ({ ...privilege, identityId: req.query.identityId, - projectMembershipId: privilege.projectId as string, projectId: req.query.projectId, slug: privilege.name })) diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts index 8d1702850..d58f3c2f7 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/index.ts @@ -4,6 +4,7 @@ import { registerAuth0ClientSecretRotationRouter } from "./auth0-client-secret-r import { registerAwsIamUserSecretRotationRouter } from "./aws-iam-user-secret-rotation-router"; import { registerAzureClientSecretRotationRouter } from "./azure-client-secret-rotation-router"; import { registerLdapPasswordRotationRouter } from "./ldap-password-rotation-router"; +import { registerMongoDBCredentialsRotationRouter } from "./mongodb-credentials-rotation-router"; import { registerMsSqlCredentialsRotationRouter } from "./mssql-credentials-rotation-router"; import { registerMySqlCredentialsRotationRouter } from "./mysql-credentials-rotation-router"; import { registerOktaClientSecretRotationRouter } from "./okta-client-secret-rotation-router"; @@ -26,5 +27,6 @@ export const SECRET_ROTATION_REGISTER_ROUTER_MAP: Record< [SecretRotation.AwsIamUserSecret]: registerAwsIamUserSecretRotationRouter, [SecretRotation.LdapPassword]: registerLdapPasswordRotationRouter, [SecretRotation.OktaClientSecret]: registerOktaClientSecretRotationRouter, - [SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter + [SecretRotation.RedisCredentials]: registerRedisCredentialsRotationRouter, + [SecretRotation.MongoDBCredentials]: registerMongoDBCredentialsRotationRouter }; diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/mongodb-credentials-rotation-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/mongodb-credentials-rotation-router.ts new file mode 100644 index 000000000..0b41f24f9 --- /dev/null +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/mongodb-credentials-rotation-router.ts @@ -0,0 +1,19 @@ +import { + CreateMongoDBCredentialsRotationSchema, + MongoDBCredentialsRotationGeneratedCredentialsSchema, + MongoDBCredentialsRotationSchema, + UpdateMongoDBCredentialsRotationSchema +} from "@app/ee/services/secret-rotation-v2/mongodb-credentials"; +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; + +import { registerSecretRotationEndpoints } from "./secret-rotation-v2-endpoints"; + +export const registerMongoDBCredentialsRotationRouter = async (server: FastifyZodProvider) => + registerSecretRotationEndpoints({ + type: SecretRotation.MongoDBCredentials, + server, + responseSchema: MongoDBCredentialsRotationSchema, + createSchema: CreateMongoDBCredentialsRotationSchema, + updateSchema: UpdateMongoDBCredentialsRotationSchema, + generatedCredentialsSchema: MongoDBCredentialsRotationGeneratedCredentialsSchema + }); diff --git a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts index 6ea6497e4..53346657b 100644 --- a/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts +++ b/backend/src/ee/routes/v2/secret-rotation-v2-routers/secret-rotation-v2-router.ts @@ -5,6 +5,7 @@ import { Auth0ClientSecretRotationListItemSchema } from "@app/ee/services/secret import { AwsIamUserSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/aws-iam-user-secret"; import { AzureClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/azure-client-secret"; import { LdapPasswordRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/ldap-password"; +import { MongoDBCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mongodb-credentials"; import { MsSqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mssql-credentials"; import { MySqlCredentialsRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/mysql-credentials"; import { OktaClientSecretRotationListItemSchema } from "@app/ee/services/secret-rotation-v2/okta-client-secret"; @@ -27,7 +28,8 @@ const SecretRotationV2OptionsSchema = z.discriminatedUnion("type", [ AwsIamUserSecretRotationListItemSchema, LdapPasswordRotationListItemSchema, OktaClientSecretRotationListItemSchema, - RedisCredentialsRotationListItemSchema + RedisCredentialsRotationListItemSchema, + MongoDBCredentialsRotationListItemSchema ]); export const registerSecretRotationV2Router = async (server: FastifyZodProvider) => { diff --git a/backend/src/ee/services/audit-log/audit-log-types.ts b/backend/src/ee/services/audit-log/audit-log-types.ts index 504339d18..b89137ec6 100644 --- a/backend/src/ee/services/audit-log/audit-log-types.ts +++ b/backend/src/ee/services/audit-log/audit-log-types.ts @@ -368,6 +368,7 @@ export enum EventType { ORG_ADMIN_BYPASS_SSO = "org-admin-bypassed-sso", USER_LOGIN = "user-login", SELECT_ORGANIZATION = "select-organization", + SELECT_SUB_ORGANIZATION = "select-sub-organization", CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template", UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template", DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template", @@ -559,7 +560,21 @@ export enum EventType { PAM_RESOURCE_GET = "pam-resource-get", PAM_RESOURCE_CREATE = "pam-resource-create", PAM_RESOURCE_UPDATE = "pam-resource-update", - PAM_RESOURCE_DELETE = "pam-resource-delete" + PAM_RESOURCE_DELETE = "pam-resource-delete", + APPROVAL_POLICY_CREATE = "approval-policy-create", + APPROVAL_POLICY_UPDATE = "approval-policy-update", + APPROVAL_POLICY_DELETE = "approval-policy-delete", + APPROVAL_POLICY_LIST = "approval-policy-list", + APPROVAL_POLICY_GET = "approval-policy-get", + APPROVAL_REQUEST_GET = "approval-request-get", + APPROVAL_REQUEST_LIST = "approval-request-list", + APPROVAL_REQUEST_CREATE = "approval-request-create", + APPROVAL_REQUEST_APPROVE = "approval-request-approve", + APPROVAL_REQUEST_REJECT = "approval-request-reject", + APPROVAL_REQUEST_CANCEL = "approval-request-cancel", + APPROVAL_REQUEST_GRANT_LIST = "approval-request-grant-list", + APPROVAL_REQUEST_GRANT_GET = "approval-request-grant-get", + APPROVAL_REQUEST_GRANT_REVOKE = "approval-request-grant-revoke" } export const filterableSecretEvents: EventType[] = [ @@ -2690,6 +2705,15 @@ interface SelectOrganizationEvent { }; } +interface SelectSubOrganizationEvent { + type: EventType.SELECT_SUB_ORGANIZATION; + metadata: { + organizationId: string; + organizationName: string; + rootOrganizationId: string; + }; +} + interface CreateCertificateTemplateEstConfig { type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG; metadata: { @@ -4159,7 +4183,7 @@ interface PamResourceCreateEvent { type: EventType.PAM_RESOURCE_CREATE; metadata: { resourceType: string; - gatewayId: string; + gatewayId?: string; name: string; }; } @@ -4224,6 +4248,126 @@ interface GetCertificateFromRequestEvent { }; } +interface ApprovalPolicyCreateEvent { + type: EventType.APPROVAL_POLICY_CREATE; + metadata: { + policyType: string; + name: string; + }; +} + +interface ApprovalPolicyUpdateEvent { + type: EventType.APPROVAL_POLICY_UPDATE; + metadata: { + policyType: string; + policyId: string; + name: string; + }; +} + +interface ApprovalPolicyDeleteEvent { + type: EventType.APPROVAL_POLICY_DELETE; + metadata: { + policyType: string; + policyId: string; + }; +} + +interface ApprovalPolicyListEvent { + type: EventType.APPROVAL_POLICY_LIST; + metadata: { + policyType: string; + count: number; + }; +} + +interface ApprovalPolicyGetEvent { + type: EventType.APPROVAL_POLICY_GET; + metadata: { + policyType: string; + policyId: string; + name: string; + }; +} + +interface ApprovalRequestGetEvent { + type: EventType.APPROVAL_REQUEST_GET; + metadata: { + policyType: string; + requestId: string; + status: string; + }; +} + +interface ApprovalRequestListEvent { + type: EventType.APPROVAL_REQUEST_LIST; + metadata: { + policyType: string; + count: number; + }; +} + +interface ApprovalRequestCreateEvent { + type: EventType.APPROVAL_REQUEST_CREATE; + metadata: { + policyType: string; + justification?: string; + requestDuration: string; + }; +} + +interface ApprovalRequestApproveEvent { + type: EventType.APPROVAL_REQUEST_APPROVE; + metadata: { + policyType: string; + requestId: string; + comment?: string; + }; +} + +interface ApprovalRequestRejectEvent { + type: EventType.APPROVAL_REQUEST_REJECT; + metadata: { + policyType: string; + requestId: string; + comment?: string; + }; +} + +interface ApprovalRequestCancelEvent { + type: EventType.APPROVAL_REQUEST_CANCEL; + metadata: { + policyType: string; + requestId: string; + }; +} + +interface ApprovalRequestGrantListEvent { + type: EventType.APPROVAL_REQUEST_GRANT_LIST; + metadata: { + policyType: string; + count: number; + }; +} + +interface ApprovalRequestGrantGetEvent { + type: EventType.APPROVAL_REQUEST_GRANT_GET; + metadata: { + policyType: string; + grantId: string; + status: string; + }; +} + +interface ApprovalRequestGrantRevokeEvent { + type: EventType.APPROVAL_REQUEST_GRANT_REVOKE; + metadata: { + policyType: string; + grantId: string; + revocationReason?: string; + }; +} + export type Event = | CreateSubOrganizationEvent | UpdateSubOrganizationEvent @@ -4609,4 +4753,19 @@ export type Event = | AutomatedRenewCertificate | AutomatedRenewCertificateFailed | UserLoginEvent - | SelectOrganizationEvent; + | SelectOrganizationEvent + | SelectSubOrganizationEvent + | ApprovalPolicyCreateEvent + | ApprovalPolicyUpdateEvent + | ApprovalPolicyDeleteEvent + | ApprovalPolicyListEvent + | ApprovalPolicyGetEvent + | ApprovalRequestGetEvent + | ApprovalRequestListEvent + | ApprovalRequestCreateEvent + | ApprovalRequestApproveEvent + | ApprovalRequestRejectEvent + | ApprovalRequestCancelEvent + | ApprovalRequestGrantListEvent + | ApprovalRequestGrantGetEvent + | ApprovalRequestGrantRevokeEvent; diff --git a/backend/src/ee/services/license/license-service.ts b/backend/src/ee/services/license/license-service.ts index e34f9273f..c8e311621 100644 --- a/backend/src/ee/services/license/license-service.ts +++ b/backend/src/ee/services/license/license-service.ts @@ -350,6 +350,7 @@ export const licenseServiceFactory = ({ actor, actorId, actorOrgId, + rootOrgId, actorAuthMethod, projectId, refreshCache @@ -360,12 +361,12 @@ export const licenseServiceFactory = ({ orgId, actorOrgId, actorAuthMethod, - scope: OrganizationActionScope.ParentOrganization + scope: OrganizationActionScope.Any }); if (refreshCache) { - await refreshPlan(orgId); + await refreshPlan(rootOrgId); } - const plan = await getPlan(orgId, projectId); + const plan = await getPlan(rootOrgId, projectId); return plan; }; diff --git a/backend/src/ee/services/license/license-types.ts b/backend/src/ee/services/license/license-types.ts index 8897eaabc..75bf4b067 100644 --- a/backend/src/ee/services/license/license-types.ts +++ b/backend/src/ee/services/license/license-types.ts @@ -102,6 +102,7 @@ export type TOrgPlansTableDTO = { export type TOrgPlanDTO = { projectId?: string; refreshCache?: boolean; + rootOrgId: string; } & TOrgPermission; export type TStartOrgTrialDTO = { diff --git a/backend/src/ee/services/pam-account/pam-account-fns.ts b/backend/src/ee/services/pam-account/pam-account-fns.ts index aae703eeb..71ef0fd7b 100644 --- a/backend/src/ee/services/pam-account/pam-account-fns.ts +++ b/backend/src/ee/services/pam-account/pam-account-fns.ts @@ -72,17 +72,24 @@ export const decryptAccount = async < account: T, projectId: string, kmsService: Pick -): Promise => { +): Promise< + Omit & { + credentials: TPamAccountCredentials; + lastRotationMessage: string | null; + } +> => { + const { encryptedCredentials, encryptedLastRotationMessage, ...rest } = account; + return { - ...account, + ...rest, credentials: await decryptAccountCredentials({ - encryptedCredentials: account.encryptedCredentials, + encryptedCredentials, projectId, kmsService }), - lastRotationMessage: account.encryptedLastRotationMessage + lastRotationMessage: encryptedLastRotationMessage ? await decryptAccountMessage({ - encryptedMessage: account.encryptedLastRotationMessage, + encryptedMessage: encryptedLastRotationMessage, projectId, kmsService }) diff --git a/backend/src/ee/services/pam-account/pam-account-service.ts b/backend/src/ee/services/pam-account/pam-account-service.ts index 565507d08..f901aee07 100644 --- a/backend/src/ee/services/pam-account/pam-account-service.ts +++ b/backend/src/ee/services/pam-account/pam-account-service.ts @@ -1,6 +1,13 @@ +import path from "node:path"; + import { ForbiddenError, subject } from "@casl/ability"; import { ActionProjectType, OrganizationActionScope, TPamAccounts, TPamFolders, TPamResources } from "@app/db/schemas"; +import { + extractAwsAccountIdFromArn, + generateConsoleFederationUrl, + TAwsIamAccountCredentials +} from "@app/ee/services/pam-resource/aws-iam"; import { PAM_RESOURCE_FACTORY_MAP } from "@app/ee/services/pam-resource/pam-resource-factory"; import { decryptResource, decryptResourceConnectionDetails } from "@app/ee/services/pam-resource/pam-resource-fns"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; @@ -10,12 +17,23 @@ import { ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { DatabaseErrorCode } from "@app/lib/error-codes"; -import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { + BadRequestError, + DatabaseError, + ForbiddenRequestError, + NotFoundError, + PolicyViolationError +} from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { OrgServiceActor } from "@app/lib/types"; +import { TApprovalPolicyDALFactory } from "@app/services/approval-policy/approval-policy-dal"; +import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums"; +import { APPROVAL_POLICY_FACTORY_MAP } from "@app/services/approval-policy/approval-policy-factory"; +import { TApprovalRequestGrantsDALFactory } from "@app/services/approval-policy/approval-request-dal"; import { ActorType } from "@app/services/auth/auth-type"; import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { KmsDataKey } from "@app/services/kms/kms-types"; +import { TPamSessionExpirationServiceFactory } from "@app/services/pam-session-expiration/pam-session-expiration-queue"; import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TUserDALFactory } from "@app/services/user/user-dal"; @@ -52,6 +70,9 @@ type TPamAccountServiceFactoryDep = { >; userDAL: TUserDALFactory; auditLogService: Pick; + approvalPolicyDAL: TApprovalPolicyDALFactory; + approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory; + pamSessionExpirationService: Pick; }; export type TPamAccountServiceFactory = ReturnType; @@ -68,7 +89,10 @@ export const pamAccountServiceFactory = ({ licenseService, kmsService, gatewayV2Service, - auditLogService + auditLogService, + approvalPolicyDAL, + approvalRequestGrantsDAL, + pamSessionExpirationService }: TPamAccountServiceFactoryDep) => { const create = async ( { @@ -136,7 +160,8 @@ export const pamAccountServiceFactory = ({ resource.resourceType as PamResource, connectionDetails, resource.gatewayId, - gatewayV2Service + gatewayV2Service, + resource.projectId ); const validatedCredentials = await factory.validateAccountCredentials(credentials); @@ -251,7 +276,8 @@ export const pamAccountServiceFactory = ({ resource.resourceType as PamResource, connectionDetails, resource.gatewayId, - gatewayV2Service + gatewayV2Service, + account.projectId ); const decryptedCredentials = await decryptAccountCredentials({ @@ -280,17 +306,27 @@ export const pamAccountServiceFactory = ({ return decryptAccount(account, account.projectId, kmsService); } - const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc); + try { + const updatedAccount = await pamAccountDAL.updateById(accountId, updateDoc); - return { - ...(await decryptAccount(updatedAccount, account.projectId, kmsService)), - resource: { - id: resource.id, - name: resource.name, - resourceType: resource.resourceType, - rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + return { + ...(await decryptAccount(updatedAccount, account.projectId, kmsService)), + resource: { + id: resource.id, + name: resource.name, + resourceType: resource.resourceType, + rotationCredentialsConfigured: !!resource.encryptedRotationAccountCredentials + } + }; + } catch (err) { + if (err instanceof DatabaseError && (err.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation) { + throw new BadRequestError({ + message: `Account with name '${name}' already exists for this path` + }); } - }; + + throw err; + } }; const deleteById = async (id: string, actor: OrgServiceActor) => { @@ -429,7 +465,7 @@ export const pamAccountServiceFactory = ({ const totalCount = totalFolderCount + totalAccountCount; const decryptedAndPermittedAccounts: Array< - TPamAccounts & { + Omit & { resource: Pick & { rotationCredentialsConfigured: boolean }; credentials: TPamAccountCredentials; lastRotationMessage: string | null; @@ -532,24 +568,108 @@ export const pamAccountServiceFactory = ({ const resource = await pamResourceDAL.findById(account.resourceId); if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); - const { permission } = await permissionService.getProjectPermission({ - actor: actor.type, - actorAuthMethod: actor.authMethod, - actorId: actor.id, - actorOrgId: actor.orgId, - projectId, - actionProjectType: ActionProjectType.PAM - }); + const fac = APPROVAL_POLICY_FACTORY_MAP[ApprovalPolicyType.PamAccess](ApprovalPolicyType.PamAccess); - ForbiddenError.from(permission).throwUnlessCan( - ProjectPermissionPamAccountActions.Access, - subject(ProjectPermissionSub.PamAccounts, { - resourceName: resource.name, - accountName: account.name, - accountPath: folderPath - }) + const inputs = { + resourceId: resource.id, + accountPath: path.join(folderPath, account.name) + }; + + const canAccess = await fac.canAccess(approvalRequestGrantsDAL, resource.projectId, actor.id, inputs); + + // Grant does not exist, check policy and fallback to permission check + if (!canAccess) { + const policy = await fac.matchPolicy(approvalPolicyDAL, resource.projectId, inputs); + + if (policy) { + throw new PolicyViolationError({ + message: "A policy is in place for this resource", + details: { + policyId: policy.id, + policyName: policy.name, + policyType: policy.type + } + }); + } + + // If there isn't a policy in place, continue with checking permission + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: account.projectId, + actionProjectType: ActionProjectType.PAM + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionPamAccountActions.Access, + subject(ProjectPermissionSub.PamAccounts, { + resourceName: resource.name, + accountName: account.name, + accountPath: folderPath + }) + ); + } + + const { connectionDetails, gatewayId, resourceType } = await decryptResource( + resource, + account.projectId, + kmsService ); + const user = await userDAL.findById(actor.id); + if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` }); + + if (resourceType === PamResource.AwsIam) { + const awsCredentials = (await decryptAccountCredentials({ + encryptedCredentials: account.encryptedCredentials, + kmsService, + projectId: account.projectId + })) as TAwsIamAccountCredentials; + + const { consoleUrl, expiresAt } = await generateConsoleFederationUrl({ + connectionDetails, + targetRoleArn: awsCredentials.targetRoleArn, + roleSessionName: actorEmail, + projectId: account.projectId, // Use project ID as External ID for security + sessionDuration: awsCredentials.defaultSessionDuration + }); + + const session = await pamSessionDAL.create({ + accountName: account.name, + actorEmail, + actorIp, + actorName, + actorUserAgent, + projectId: account.projectId, + resourceName: resource.name, + resourceType: resource.resourceType, + status: PamSessionStatus.Active, // AWS IAM sessions are immediately active + accountId: account.id, + userId: actor.id, + expiresAt, + startedAt: new Date() + }); + + // Schedule session expiration job to run at expiresAt + await pamSessionExpirationService.scheduleSessionExpiration(session.id, expiresAt); + + return { + sessionId: session.id, + resourceType, + account, + consoleUrl, + metadata: { + awsAccountId: extractAwsAccountIdFromArn(connectionDetails.roleArn), + targetRoleArn: awsCredentials.targetRoleArn, + federatedUsername: actorEmail, + expiresAt: expiresAt.toISOString() + } + }; + } + + // For gateway-based resources (Postgres, MySQL, SSH), create session first const session = await pamSessionDAL.create({ accountName: account.name, actorEmail, @@ -565,10 +685,9 @@ export const pamAccountServiceFactory = ({ expiresAt: new Date(Date.now() + duration) }); - const { connectionDetails, gatewayId, resourceType } = await decryptResource(resource, projectId, kmsService); - - const user = await userDAL.findById(actor.id); - if (!user) throw new NotFoundError({ message: `User with ID '${actor.id}' not found` }); + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required for this resource type" }); + } const { host, port } = resourceType !== PamResource.Kubernetes @@ -616,11 +735,11 @@ export const pamAccountServiceFactory = ({ projectId })) as TSqlResourceConnectionDetails; - const credentials = await decryptAccountCredentials({ + const credentials = (await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, kmsService, projectId - }); + })) as TSqlAccountCredentials; metadata = { username: (credentials as TSqlAccountCredentials).username, @@ -632,11 +751,11 @@ export const pamAccountServiceFactory = ({ break; case PamResource.SSH: { - const credentials = await decryptAccountCredentials({ + const credentials = (await decryptAccountCredentials({ encryptedCredentials: account.encryptedCredentials, kmsService, projectId - }); + })) as TSSHAccountCredentials; metadata = { username: (credentials as TSSHAccountCredentials).username @@ -716,7 +835,7 @@ export const pamAccountServiceFactory = ({ const resource = await pamResourceDAL.findById(account.resourceId); if (!resource) throw new NotFoundError({ message: `Resource with ID '${account.resourceId}' not found` }); - if (resource.gatewayIdentityId !== actor.id) { + if (resource.gatewayId && resource.gatewayIdentityId !== actor.id) { throw new ForbiddenRequestError({ message: "Identity does not have access to fetch the PAM session credentials" }); @@ -780,7 +899,8 @@ export const pamAccountServiceFactory = ({ resourceType as PamResource, connectionDetails, gatewayId, - gatewayV2Service + gatewayV2Service, + account.projectId ); const newCredentials = await factory.rotateAccountCredentials( diff --git a/backend/src/ee/services/pam-account/pam-account-types.ts b/backend/src/ee/services/pam-account/pam-account-types.ts index ac799d869..a20d2f737 100644 --- a/backend/src/ee/services/pam-account/pam-account-types.ts +++ b/backend/src/ee/services/pam-account/pam-account-types.ts @@ -6,8 +6,10 @@ import { PamAccountOrderBy, PamAccountView } from "./pam-account-enums"; // DTOs export type TCreateAccountDTO = Pick< TPamAccount, - "name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationEnabled" | "rotationIntervalSeconds" ->; + "name" | "description" | "credentials" | "folderId" | "resourceId" | "rotationIntervalSeconds" +> & { + rotationEnabled?: boolean; +}; export type TUpdateAccountDTO = Partial> & { accountId: string; diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts new file mode 100644 index 000000000..97415a088 --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-federation.ts @@ -0,0 +1,245 @@ +import { AssumeRoleCommand, Credentials, STSClient, STSClientConfig } from "@aws-sdk/client-sts"; + +import { CustomAWSHasher } from "@app/lib/aws/hashing"; +import { getConfig } from "@app/lib/config/env"; +import { request } from "@app/lib/config/request"; +import { crypto } from "@app/lib/crypto/cryptography"; +import { BadRequestError, InternalServerError } from "@app/lib/errors"; + +import { TAwsIamResourceConnectionDetails } from "./aws-iam-resource-types"; + +const AWS_STS_MIN_DURATION_SECONDS = 900; + +// We hardcode us-east-1 because: +// 1. IAM is global - roles can be assumed from any STS regional endpoint +// 2. The temporary credentials returned work globally across all AWS regions +// 3. The target account's resources can be in any region - it doesn't affect STS calls +const AWS_STS_DEFAULT_REGION = "us-east-1"; + +const createStsClient = (credentials?: Credentials): STSClient => { + const appCfg = getConfig(); + + const config: STSClientConfig = { + region: AWS_STS_DEFAULT_REGION, + useFipsEndpoint: crypto.isFipsModeEnabled(), + sha256: CustomAWSHasher + }; + + if (credentials) { + // Use provided credentials (for role chaining) + config.credentials = { + accessKeyId: credentials.AccessKeyId!, + secretAccessKey: credentials.SecretAccessKey!, + sessionToken: credentials.SessionToken + }; + } else if (appCfg.PAM_AWS_ACCESS_KEY_ID && appCfg.PAM_AWS_SECRET_ACCESS_KEY) { + // Use configured static credentials + config.credentials = { + accessKeyId: appCfg.PAM_AWS_ACCESS_KEY_ID, + secretAccessKey: appCfg.PAM_AWS_SECRET_ACCESS_KEY + }; + } + // Otherwise uses instance profile if hosting on AWS + + return new STSClient(config); +}; + +/** + * Assumes the PAM role and returns the credentials. + * Returns null if assumption fails (for validation) or throws if throwOnError is true. + */ +const assumePamRole = async ({ + connectionDetails, + projectId, + sessionDuration = AWS_STS_MIN_DURATION_SECONDS, + sessionNameSuffix = "validation", + throwOnError = false +}: { + connectionDetails: TAwsIamResourceConnectionDetails; + projectId: string; + sessionDuration?: number; + sessionNameSuffix?: string; + throwOnError?: boolean; +}): Promise => { + const stsClient = createStsClient(); + + try { + const result = await stsClient.send( + new AssumeRoleCommand({ + RoleArn: connectionDetails.roleArn, + RoleSessionName: `infisical-pam-${sessionNameSuffix}-${Date.now()}`, + DurationSeconds: sessionDuration, + ExternalId: projectId + }) + ); + + if (!result.Credentials) { + if (throwOnError) { + throw new InternalServerError({ + message: "Failed to assume PAM role - AWS STS did not return credentials" + }); + } + return null; + } + + return result.Credentials; + } catch (error) { + if (throwOnError) { + throw new InternalServerError({ + message: `Failed to assume PAM role - AWS STS did not return credentials: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + return null; + } +}; + +/** + * Assumes a target role using PAM role credentials (role chaining). + * Returns null if assumption fails (for validation) or throws if throwOnError is true. + */ +const assumeTargetRole = async ({ + pamCredentials, + targetRoleArn, + projectId, + roleSessionName, + sessionDuration = AWS_STS_MIN_DURATION_SECONDS, + throwOnError = false +}: { + pamCredentials: Credentials; + targetRoleArn: string; + projectId: string; + roleSessionName: string; + sessionDuration?: number; + throwOnError?: boolean; +}): Promise => { + const chainedStsClient = createStsClient(pamCredentials); + + try { + const result = await chainedStsClient.send( + new AssumeRoleCommand({ + RoleArn: targetRoleArn, + RoleSessionName: roleSessionName, + DurationSeconds: sessionDuration, + ExternalId: projectId + }) + ); + + if (!result.Credentials) { + if (throwOnError) { + throw new BadRequestError({ + message: "Failed to assume target role - verify the target role trust policy allows the PAM role to assume it" + }); + } + return null; + } + + return result.Credentials; + } catch (error) { + if (throwOnError) { + throw new InternalServerError({ + message: `Failed to assume target role - AWS STS did not return credentials: ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + return null; + } +}; + +export const validatePamRoleConnection = async ( + connectionDetails: TAwsIamResourceConnectionDetails, + projectId: string +): Promise => { + try { + const credentials = await assumePamRole({ connectionDetails, projectId }); + return credentials !== null; + } catch { + return false; + } +}; + +export const validateTargetRoleAssumption = async ({ + connectionDetails, + targetRoleArn, + projectId +}: { + connectionDetails: TAwsIamResourceConnectionDetails; + targetRoleArn: string; + projectId: string; +}): Promise => { + try { + const pamCredentials = await assumePamRole({ connectionDetails, projectId }); + if (!pamCredentials) return false; + + const targetCredentials = await assumeTargetRole({ + pamCredentials, + targetRoleArn, + projectId, + roleSessionName: `infisical-pam-target-validation-${Date.now()}` + }); + return targetCredentials !== null; + } catch { + return false; + } +}; + +/** + * Assumes the target role and generates a federated console sign-in URL. + */ +export const generateConsoleFederationUrl = async ({ + connectionDetails, + targetRoleArn, + roleSessionName, + projectId, + sessionDuration +}: { + connectionDetails: TAwsIamResourceConnectionDetails; + targetRoleArn: string; + roleSessionName: string; + projectId: string; + sessionDuration: number; +}): Promise<{ consoleUrl: string; expiresAt: Date }> => { + const pamCredentials = await assumePamRole({ + connectionDetails, + projectId, + sessionDuration, + sessionNameSuffix: "session", + throwOnError: true + }); + + const targetCredentials = await assumeTargetRole({ + pamCredentials: pamCredentials!, + targetRoleArn, + projectId, + roleSessionName, + sessionDuration, + throwOnError: true + }); + + const { AccessKeyId, SecretAccessKey, SessionToken, Expiration } = targetCredentials!; + + // Generate federation URL + const sessionJson = JSON.stringify({ + sessionId: AccessKeyId, + sessionKey: SecretAccessKey, + sessionToken: SessionToken + }); + + const federationEndpoint = "https://signin.aws.amazon.com/federation"; + + const signinTokenUrl = `${federationEndpoint}?Action=getSigninToken&Session=${encodeURIComponent(sessionJson)}`; + + const tokenResponse = await request.get<{ SigninToken?: string }>(signinTokenUrl); + + if (!tokenResponse.data.SigninToken) { + throw new InternalServerError({ + message: `AWS federation endpoint did not return a SigninToken: ${JSON.stringify(tokenResponse.data).substring(0, 200)}` + }); + } + + const consoleDestination = `https://console.aws.amazon.com/`; + const consoleUrl = `${federationEndpoint}?Action=login&SigninToken=${encodeURIComponent(tokenResponse.data.SigninToken)}&Destination=${encodeURIComponent(consoleDestination)}`; + + return { + consoleUrl, + expiresAt: Expiration ?? new Date(Date.now() + sessionDuration * 1000) + }; +}; diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts new file mode 100644 index 000000000..844908671 --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-factory.ts @@ -0,0 +1,110 @@ +import { BadRequestError } from "@app/lib/errors"; +import { logger } from "@app/lib/logger"; + +import { PamResource } from "../pam-resource-enums"; +import { + TPamResourceFactory, + TPamResourceFactoryRotateAccountCredentials, + TPamResourceFactoryValidateAccountCredentials +} from "../pam-resource-types"; +import { validatePamRoleConnection, validateTargetRoleAssumption } from "./aws-iam-federation"; +import { TAwsIamAccountCredentials, TAwsIamResourceConnectionDetails } from "./aws-iam-resource-types"; + +export const awsIamResourceFactory: TPamResourceFactory = ( + resourceType: PamResource, + connectionDetails: TAwsIamResourceConnectionDetails, + // AWS IAM doesn't use gateway + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _gatewayId, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _gatewayV2Service, + projectId +) => { + const validateConnection = async () => { + try { + const isValid = await validatePamRoleConnection(connectionDetails, projectId ?? ""); + + if (!isValid) { + throw new BadRequestError({ + message: + "Unable to assume the PAM role. Verify the role ARN and ensure the trust policy allows Infisical to assume the role." + }); + } + + logger.info( + { roleArn: connectionDetails.roleArn }, + "[AWS IAM Resource Factory] PAM role connection validated successfully" + ); + + return connectionDetails; + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + logger.error(error, "[AWS IAM Resource Factory] Failed to validate PAM role connection"); + + throw new BadRequestError({ + message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + + const validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials = async ( + credentials + ) => { + try { + const isValid = await validateTargetRoleAssumption({ + connectionDetails, + targetRoleArn: credentials.targetRoleArn, + projectId: projectId ?? "" + }); + + if (!isValid) { + throw new BadRequestError({ + message: `Unable to assume the target role. Verify the target role ARN and ensure the PAM role (ARN: ${connectionDetails.roleArn}) has permission to assume it.` + }); + } + + logger.info( + { targetRoleArn: credentials.targetRoleArn }, + "[AWS IAM Resource Factory] Target role credentials validated successfully" + ); + + return credentials; + } catch (error) { + if (error instanceof BadRequestError) { + throw error; + } + + logger.error(error, "[AWS IAM Resource Factory] Failed to validate target role credentials"); + + throw new BadRequestError({ + message: `Unable to validate account credentials for ${resourceType}: ${(error as Error).message || String(error)}` + }); + } + }; + + const rotateAccountCredentials: TPamResourceFactoryRotateAccountCredentials = async ( + _rotationAccountCredentials, + currentCredentials + ) => { + return currentCredentials; + }; + + const handleOverwritePreventionForCensoredValues = async ( + updatedAccountCredentials: TAwsIamAccountCredentials, + // AWS IAM has no censored credential values - role ARNs are not secrets + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _currentCredentials: TAwsIamAccountCredentials + ) => { + return updatedAccountCredentials; + }; + + return { + validateConnection, + validateAccountCredentials, + rotateAccountCredentials, + handleOverwritePreventionForCensoredValues + }; +}; diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-fns.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-fns.ts new file mode 100644 index 000000000..d04018d49 --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-fns.ts @@ -0,0 +1,24 @@ +import RE2 from "re2"; + +import { BadRequestError } from "@app/lib/errors"; + +import { AwsIamResourceListItemSchema } from "./aws-iam-resource-schemas"; + +export const getAwsIamResourceListItem = () => { + return { + name: AwsIamResourceListItemSchema.shape.name.value, + resource: AwsIamResourceListItemSchema.shape.resource.value + }; +}; + +/** + * Extract the AWS Account ID from an IAM Role ARN + * ARN format: arn:aws:iam::123456789012:role/RoleName + */ +export const extractAwsAccountIdFromArn = (roleArn: string): string => { + const match = roleArn.match(new RE2("^arn:aws:iam::(\\d{12}):role/")); + if (!match) { + throw new BadRequestError({ message: "Invalid IAM Role ARN format" }); + } + return match[1]; +}; diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas.ts new file mode 100644 index 000000000..2762977eb --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-schemas.ts @@ -0,0 +1,81 @@ +import { z } from "zod"; + +import { PamResource } from "../pam-resource-enums"; +import { + BaseCreatePamAccountSchema, + BaseCreatePamResourceSchema, + BasePamAccountSchema, + BasePamAccountSchemaWithResource, + BasePamResourceSchema, + BaseUpdatePamAccountSchema, + BaseUpdatePamResourceSchema +} from "../pam-resource-schemas"; + +// AWS STS session duration limits (in seconds) +// Role chaining (Infisical → PAM role → target role) limits max session to 1 hour +// @see https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html +const AWS_STS_MIN_SESSION_DURATION = 900; // 15 minutes +const AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING = 3600; // 1 hour + +export const AwsIamResourceConnectionDetailsSchema = z.object({ + roleArn: z.string().trim().min(1) +}); + +export const AwsIamAccountCredentialsSchema = z.object({ + targetRoleArn: z.string().trim().min(1).max(2048), + defaultSessionDuration: z.coerce + .number() + .min(AWS_STS_MIN_SESSION_DURATION) + .max(AWS_STS_MAX_SESSION_DURATION_ROLE_CHAINING) +}); + +const BaseAwsIamResourceSchema = BasePamResourceSchema.extend({ + resourceType: z.literal(PamResource.AwsIam), + gatewayId: z.string().uuid().nullable().optional() +}); + +export const AwsIamResourceSchema = BaseAwsIamResourceSchema.extend({ + connectionDetails: AwsIamResourceConnectionDetailsSchema, + rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional() +}); + +export const SanitizedAwsIamResourceSchema = BaseAwsIamResourceSchema.extend({ + connectionDetails: AwsIamResourceConnectionDetailsSchema, + rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional() +}); + +export const AwsIamResourceListItemSchema = z.object({ + name: z.literal("AWS IAM"), + resource: z.literal(PamResource.AwsIam) +}); + +export const CreateAwsIamResourceSchema = BaseCreatePamResourceSchema.extend({ + connectionDetails: AwsIamResourceConnectionDetailsSchema, + rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional() +}); + +export const UpdateAwsIamResourceSchema = BaseUpdatePamResourceSchema.extend({ + connectionDetails: AwsIamResourceConnectionDetailsSchema.optional(), + rotationAccountCredentials: AwsIamAccountCredentialsSchema.nullable().optional() +}); + +export const AwsIamAccountSchema = BasePamAccountSchema.extend({ + credentials: AwsIamAccountCredentialsSchema +}); + +export const CreateAwsIamAccountSchema = BaseCreatePamAccountSchema.extend({ + credentials: AwsIamAccountCredentialsSchema, + // AWS IAM accounts don't support credential rotation - they use role assumption + rotationEnabled: z.boolean().default(false) +}); + +export const UpdateAwsIamAccountSchema = BaseUpdatePamAccountSchema.extend({ + credentials: AwsIamAccountCredentialsSchema.optional() +}); + +export const SanitizedAwsIamAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({ + credentials: AwsIamAccountCredentialsSchema.pick({ + targetRoleArn: true, + defaultSessionDuration: true + }) +}); diff --git a/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-types.ts b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-types.ts new file mode 100644 index 000000000..732355371 --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/aws-iam-resource-types.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; + +import { + AwsIamAccountCredentialsSchema, + AwsIamAccountSchema, + AwsIamResourceConnectionDetailsSchema, + AwsIamResourceSchema +} from "./aws-iam-resource-schemas"; + +// Resources +export type TAwsIamResource = z.infer; +export type TAwsIamResourceConnectionDetails = z.infer; + +// Accounts +export type TAwsIamAccount = z.infer; +export type TAwsIamAccountCredentials = z.infer; diff --git a/backend/src/ee/services/pam-resource/aws-iam/index.ts b/backend/src/ee/services/pam-resource/aws-iam/index.ts new file mode 100644 index 000000000..8e41fa48a --- /dev/null +++ b/backend/src/ee/services/pam-resource/aws-iam/index.ts @@ -0,0 +1,5 @@ +export * from "./aws-iam-federation"; +export * from "./aws-iam-resource-factory"; +export * from "./aws-iam-resource-fns"; +export * from "./aws-iam-resource-schemas"; +export * from "./aws-iam-resource-types"; diff --git a/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts b/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts index 8d3589a8a..cb12a4c8c 100644 --- a/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/mysql/mysql-resource-schemas.ts @@ -2,13 +2,13 @@ import { z } from "zod"; import { PamResource } from "../pam-resource-enums"; import { + BaseCreateGatewayPamResourceSchema, BaseCreatePamAccountSchema, - BaseCreatePamResourceSchema, BasePamAccountSchema, BasePamAccountSchemaWithResource, BasePamResourceSchema, - BaseUpdatePamAccountSchema, - BaseUpdatePamResourceSchema + BaseUpdateGatewayPamResourceSchema, + BaseUpdatePamAccountSchema } from "../pam-resource-schemas"; import { BaseSqlAccountCredentialsSchema, @@ -43,12 +43,12 @@ export const MySQLResourceListItemSchema = z.object({ resource: z.literal(PamResource.MySQL) }); -export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({ +export const CreateMySQLResourceSchema = BaseCreateGatewayPamResourceSchema.extend({ connectionDetails: MySQLResourceConnectionDetailsSchema, rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional() }); -export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({ +export const UpdateMySQLResourceSchema = BaseUpdateGatewayPamResourceSchema.extend({ connectionDetails: MySQLResourceConnectionDetailsSchema.optional(), rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional() }); diff --git a/backend/src/ee/services/pam-resource/pam-resource-dal.ts b/backend/src/ee/services/pam-resource/pam-resource-dal.ts index 9e5cbc985..e5b76a882 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-dal.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-dal.ts @@ -14,7 +14,7 @@ export const pamResourceDALFactory = (db: TDbClient) => { const findById = async (id: string, tx?: Knex) => { const doc = await (tx || db.replicaNode())(TableName.PamResource) - .join(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`) + .leftJoin(TableName.GatewayV2, `${TableName.PamResource}.gatewayId`, `${TableName.GatewayV2}.id`) .select(selectAllTableCols(TableName.PamResource)) .select(db.ref("name").withSchema(TableName.GatewayV2).as("gatewayName")) .select(db.ref("identityId").withSchema(TableName.GatewayV2).as("gatewayIdentityId")) diff --git a/backend/src/ee/services/pam-resource/pam-resource-enums.ts b/backend/src/ee/services/pam-resource/pam-resource-enums.ts index f9b5ab4bf..c8c57b03b 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-enums.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-enums.ts @@ -2,7 +2,8 @@ export enum PamResource { Postgres = "postgres", MySQL = "mysql", SSH = "ssh", - Kubernetes = "kubernetes" + Kubernetes = "kubernetes", + AwsIam = "aws-iam" } export enum PamResourceOrderBy { diff --git a/backend/src/ee/services/pam-resource/pam-resource-factory.ts b/backend/src/ee/services/pam-resource/pam-resource-factory.ts index 9fee549e1..bf8d13d66 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-factory.ts @@ -1,6 +1,7 @@ +import { awsIamResourceFactory } from "./aws-iam/aws-iam-resource-factory"; +import { kubernetesResourceFactory } from "./kubernetes/kubernetes-resource-factory"; import { PamResource } from "./pam-resource-enums"; import { TPamAccountCredentials, TPamResourceConnectionDetails, TPamResourceFactory } from "./pam-resource-types"; -import { kubernetesResourceFactory } from "./kubernetes/kubernetes-resource-factory"; import { sqlResourceFactory } from "./shared/sql/sql-resource-factory"; import { sshResourceFactory } from "./ssh/ssh-resource-factory"; @@ -10,5 +11,6 @@ export const PAM_RESOURCE_FACTORY_MAP: Record { - return [getPostgresResourceListItem(), getMySQLResourceListItem(), getKubernetesResourceListItem()].sort((a, b) => - a.name.localeCompare(b.name) - ); + return [ + getPostgresResourceListItem(), + getMySQLResourceListItem(), + getAwsIamResourceListItem(), + getKubernetesResourceListItem() + ].sort((a, b) => a.name.localeCompare(b.name)); }; // Resource diff --git a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts index 17ed1ccd1..a3db6b446 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-schemas.ts @@ -3,6 +3,18 @@ import { z } from "zod"; import { PamAccountsSchema, PamResourcesSchema } from "@app/db/schemas"; import { slugSchema } from "@app/server/lib/schemas"; +export const GatewayAccessResponseSchema = z.object({ + sessionId: z.string(), + relayClientCertificate: z.string(), + relayClientPrivateKey: z.string(), + relayServerCertificateChain: z.string(), + gatewayClientCertificate: z.string(), + gatewayClientPrivateKey: z.string(), + gatewayServerCertificateChain: z.string(), + relayHost: z.string(), + metadata: z.record(z.string(), z.string().optional()).optional() +}); + // Resources export const BasePamResourceSchema = PamResourcesSchema.omit({ encryptedConnectionDetails: true, @@ -10,17 +22,27 @@ export const BasePamResourceSchema = PamResourcesSchema.omit({ resourceType: true }); -export const BaseCreatePamResourceSchema = z.object({ +const CoreCreatePamResourceSchema = z.object({ projectId: z.string().uuid(), - gatewayId: z.string().uuid(), name: slugSchema({ field: "name" }) }); -export const BaseUpdatePamResourceSchema = z.object({ - gatewayId: z.string().uuid().optional(), +export const BaseCreateGatewayPamResourceSchema = CoreCreatePamResourceSchema.extend({ + gatewayId: z.string().uuid() +}); + +export const BaseCreatePamResourceSchema = CoreCreatePamResourceSchema; + +const CoreUpdatePamResourceSchema = z.object({ name: slugSchema({ field: "name" }).optional() }); +export const BaseUpdateGatewayPamResourceSchema = CoreUpdatePamResourceSchema.extend({ + gatewayId: z.string().uuid().optional() +}); + +export const BaseUpdatePamResourceSchema = CoreUpdatePamResourceSchema; + // Accounts export const BasePamAccountSchema = PamAccountsSchema.omit({ encryptedCredentials: true diff --git a/backend/src/ee/services/pam-resource/pam-resource-service.ts b/backend/src/ee/services/pam-resource/pam-resource-service.ts index 0ebca02b5..abbbf651b 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-service.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-service.ts @@ -92,7 +92,8 @@ export const pamResourceServiceFactory = ({ resourceType, connectionDetails, gatewayId, - gatewayV2Service + gatewayV2Service, + projectId ); const validatedConnectionDetails = await factory.validateConnection(); @@ -162,7 +163,8 @@ export const pamResourceServiceFactory = ({ resource.resourceType as PamResource, connectionDetails, resource.gatewayId, - gatewayV2Service + gatewayV2Service, + resource.projectId ); const validatedConnectionDetails = await factory.validateConnection(); const encryptedConnectionDetails = await encryptResourceConnectionDetails({ @@ -189,7 +191,8 @@ export const pamResourceServiceFactory = ({ resource.resourceType as PamResource, decryptedConnectionDetails, resource.gatewayId, - gatewayV2Service + gatewayV2Service, + resource.projectId ); let finalCredentials = { ...rotationAccountCredentials }; diff --git a/backend/src/ee/services/pam-resource/pam-resource-types.ts b/backend/src/ee/services/pam-resource/pam-resource-types.ts index 99c9030d8..5291e044a 100644 --- a/backend/src/ee/services/pam-resource/pam-resource-types.ts +++ b/backend/src/ee/services/pam-resource/pam-resource-types.ts @@ -1,6 +1,12 @@ import { OrderByDirection, TProjectPermission } from "@app/lib/types"; import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service"; +import { + TAwsIamAccount, + TAwsIamAccountCredentials, + TAwsIamResource, + TAwsIamResourceConnectionDetails +} from "./aws-iam/aws-iam-resource-types"; import { TKubernetesAccount, TKubernetesAccountCredentials, @@ -28,27 +34,30 @@ import { } from "./ssh/ssh-resource-types"; // Resource types -export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TKubernetesResource; +export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TAwsIamResource | TKubernetesResource; export type TPamResourceConnectionDetails = | TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails | TSSHResourceConnectionDetails - | TKubernetesResourceConnectionDetails; + | TKubernetesResourceConnectionDetails + | TAwsIamResourceConnectionDetails; // Account types -export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TKubernetesAccount; +export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TAwsIamAccount | TKubernetesAccount; + export type TPamAccountCredentials = | TPostgresAccountCredentials // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents | TMySQLAccountCredentials | TSSHAccountCredentials - | TKubernetesAccountCredentials; + | TKubernetesAccountCredentials + | TAwsIamAccountCredentials; // Resource DTOs -export type TCreateResourceDTO = Pick< - TPamResource, - "name" | "connectionDetails" | "resourceType" | "gatewayId" | "projectId" | "rotationAccountCredentials" ->; +export type TCreateResourceDTO = Pick & { + gatewayId?: string | null; + rotationAccountCredentials?: TPamAccountCredentials | null; +}; export type TUpdateResourceDTO = Partial> & { resourceId: string; @@ -76,8 +85,9 @@ export type TPamResourceFactoryRotateAccountCredentials = ( resourceType: PamResource, connectionDetails: T, - gatewayId: string, - gatewayV2Service: Pick + gatewayId: string | null | undefined, + gatewayV2Service: Pick, + projectId: string | null | undefined ) => { validateConnection: TPamResourceFactoryValidateConnection; validateAccountCredentials: TPamResourceFactoryValidateAccountCredentials; diff --git a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts index bbe83a3a4..fd58484f7 100644 --- a/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/postgres/postgres-resource-schemas.ts @@ -2,13 +2,13 @@ import { z } from "zod"; import { PamResource } from "../pam-resource-enums"; import { + BaseCreateGatewayPamResourceSchema, BaseCreatePamAccountSchema, - BaseCreatePamResourceSchema, BasePamAccountSchema, BasePamAccountSchemaWithResource, BasePamResourceSchema, - BaseUpdatePamAccountSchema, - BaseUpdatePamResourceSchema + BaseUpdateGatewayPamResourceSchema, + BaseUpdatePamAccountSchema } from "../pam-resource-schemas"; import { BaseSqlAccountCredentialsSchema, @@ -40,12 +40,12 @@ export const PostgresResourceListItemSchema = z.object({ resource: z.literal(PamResource.Postgres) }); -export const CreatePostgresResourceSchema = BaseCreatePamResourceSchema.extend({ +export const CreatePostgresResourceSchema = BaseCreateGatewayPamResourceSchema.extend({ connectionDetails: PostgresResourceConnectionDetailsSchema, rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); -export const UpdatePostgresResourceSchema = BaseUpdatePamResourceSchema.extend({ +export const UpdatePostgresResourceSchema = BaseUpdateGatewayPamResourceSchema.extend({ connectionDetails: PostgresResourceConnectionDetailsSchema.optional(), rotationAccountCredentials: PostgresAccountCredentialsSchema.nullable().optional() }); diff --git a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts index b3128c422..26fa7ff39 100644 --- a/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/shared/sql/sql-resource-factory.ts @@ -233,6 +233,10 @@ export const sqlResourceFactory: TPamResourceFactory { const validateConnection = async () => { + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + try { await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => { await client.validate(true); @@ -255,6 +259,10 @@ export const sqlResourceFactory: TPamResourceFactory { try { + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + await executeWithGateway( { connectionDetails, @@ -296,6 +304,10 @@ export const sqlResourceFactory: TPamResourceFactory { const newPassword = alphaNumericNanoId(32); + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + try { return await executeWithGateway( { diff --git a/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts b/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts index b90aa00c6..dfbb071e2 100644 --- a/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts +++ b/backend/src/ee/services/pam-resource/ssh/ssh-resource-factory.ts @@ -60,6 +60,10 @@ export const sshResourceFactory: TPamResourceFactory { const validateConnection = async () => { try { + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => { return new Promise((resolve, reject) => { const client = new Client(); @@ -131,6 +135,10 @@ export const sshResourceFactory: TPamResourceFactory { try { + if (!gatewayId) { + throw new BadRequestError({ message: "Gateway ID is required" }); + } + await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (proxyPort) => { return new Promise((resolve, reject) => { const client = new Client(); diff --git a/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts b/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts index 97d462369..01b8ef2c0 100644 --- a/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts +++ b/backend/src/ee/services/pam-resource/ssh/ssh-resource-schemas.ts @@ -2,13 +2,13 @@ import { z } from "zod"; import { PamResource } from "../pam-resource-enums"; import { + BaseCreateGatewayPamResourceSchema, BaseCreatePamAccountSchema, - BaseCreatePamResourceSchema, BasePamAccountSchema, BasePamAccountSchemaWithResource, BasePamResourceSchema, - BaseUpdatePamAccountSchema, - BaseUpdatePamResourceSchema + BaseUpdateGatewayPamResourceSchema, + BaseUpdatePamAccountSchema } from "../pam-resource-schemas"; import { SSHAuthMethod } from "./ssh-resource-enums"; @@ -73,12 +73,12 @@ export const SanitizedSSHResourceSchema = BaseSSHResourceSchema.extend({ .optional() }); -export const CreateSSHResourceSchema = BaseCreatePamResourceSchema.extend({ +export const CreateSSHResourceSchema = BaseCreateGatewayPamResourceSchema.extend({ connectionDetails: SSHResourceConnectionDetailsSchema, rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional() }); -export const UpdateSSHResourceSchema = BaseUpdatePamResourceSchema.extend({ +export const UpdateSSHResourceSchema = BaseUpdateGatewayPamResourceSchema.extend({ connectionDetails: SSHResourceConnectionDetailsSchema.optional(), rotationAccountCredentials: SSHAccountCredentialsSchema.nullable().optional() }); diff --git a/backend/src/ee/services/pam-session/pam-session-dal.ts b/backend/src/ee/services/pam-session/pam-session-dal.ts index f8b3a3393..094614859 100644 --- a/backend/src/ee/services/pam-session/pam-session-dal.ts +++ b/backend/src/ee/services/pam-session/pam-session-dal.ts @@ -4,6 +4,8 @@ import { TDbClient } from "@app/db"; import { TableName } from "@app/db/schemas"; import { ormify, selectAllTableCols } from "@app/lib/knex"; +import { PamSessionStatus } from "./pam-session-enums"; + export type TPamSessionDALFactory = ReturnType; export const pamSessionDALFactory = (db: TDbClient) => { const orm = ormify(db, TableName.PamSession); @@ -22,5 +24,19 @@ export const pamSessionDALFactory = (db: TDbClient) => { return session; }; - return { ...orm, findById }; + const expireSessionById = async (sessionId: string, tx?: Knex) => { + const now = new Date(); + + const updatedCount = await (tx || db)(TableName.PamSession) + .where("id", sessionId) + .whereIn("status", [PamSessionStatus.Active, PamSessionStatus.Starting]) + .update({ + status: PamSessionStatus.Ended, + endedAt: now + }); + + return updatedCount; + }; + + return { ...orm, findById, expireSessionById }; }; diff --git a/backend/src/ee/services/pam-session/pam-session-enums.ts b/backend/src/ee/services/pam-session/pam-session-enums.ts index 87731f577..33afe95e4 100644 --- a/backend/src/ee/services/pam-session/pam-session-enums.ts +++ b/backend/src/ee/services/pam-session/pam-session-enums.ts @@ -1,6 +1,6 @@ export enum PamSessionStatus { Starting = "starting", // Starting, user connecting to resource Active = "active", // Active, user is connected to resource - Ended = "ended", // Ended by user + Ended = "ended", // Ended by user or automatically expired after expiresAt timestamp Terminated = "terminated" // Terminated by an admin } diff --git a/backend/src/ee/services/pam-session/pam-session-service.ts b/backend/src/ee/services/pam-session/pam-session-service.ts index 18c185cac..bdb82e650 100644 --- a/backend/src/ee/services/pam-session/pam-session-service.ts +++ b/backend/src/ee/services/pam-session/pam-session-service.ts @@ -34,9 +34,40 @@ export const pamSessionServiceFactory = ({ licenseService, kmsService }: TPamSessionServiceFactoryDep) => { + // Helper to check and update expired sessions when viewing session details (redundancy for scheduled job) + // Only applies to non-gateway sessions (e.g., AWS IAM) - gateway sessions are managed by the gateway + // This is intentionally only called in getById (session details view), not in list + const checkAndExpireSessionIfNeeded = async < + T extends { id: string; status: string; expiresAt: Date | null; gatewayIdentityId?: string | null } + >( + session: T + ): Promise => { + // Skip gateway-based sessions - they have their own lifecycle managed by the gateway + if (session.gatewayIdentityId) { + return session; + } + + const isActive = session.status === PamSessionStatus.Active || session.status === PamSessionStatus.Starting; + const isExpired = session.expiresAt && new Date(session.expiresAt) <= new Date(); + + if (isActive && isExpired) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const updatedSession = await pamSessionDAL.updateById(session.id, { + status: PamSessionStatus.Ended, + endedAt: new Date() + }); + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + return { ...session, ...updatedSession }; + } + + return session; + }; + const getById = async (sessionId: string, actor: OrgServiceActor) => { - const session = await pamSessionDAL.findById(sessionId); - if (!session) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` }); + const sessionFromDb = await pamSessionDAL.findById(sessionId); + if (!sessionFromDb) throw new NotFoundError({ message: `Session with ID '${sessionId}' not found` }); + + const session = await checkAndExpireSessionIfNeeded(sessionFromDb); const { permission } = await permissionService.getProjectPermission({ actor: actor.type, @@ -116,7 +147,7 @@ export const pamSessionServiceFactory = ({ OrgPermissionSubjects.Gateway ); - if (session.gatewayIdentityId !== actor.id) { + if (session.gatewayIdentityId && session.gatewayIdentityId !== actor.id) { throw new ForbiddenRequestError({ message: "Identity does not have access to update logs for this session" }); } @@ -158,7 +189,7 @@ export const pamSessionServiceFactory = ({ OrgPermissionSubjects.Gateway ); - if (session.gatewayIdentityId !== actor.id) { + if (session.gatewayIdentityId && session.gatewayIdentityId !== actor.id) { throw new ForbiddenRequestError({ message: "Identity does not have access to end this session" }); } } else if (actor.type === ActorType.USER) { diff --git a/backend/src/ee/services/permission/default-roles.ts b/backend/src/ee/services/permission/default-roles.ts index 7b2e0ae00..561cb3bb5 100644 --- a/backend/src/ee/services/permission/default-roles.ts +++ b/backend/src/ee/services/permission/default-roles.ts @@ -3,6 +3,8 @@ import { AbilityBuilder, createMongoAbility, MongoAbility } from "@casl/ability" import { ProjectPermissionActions, ProjectPermissionAppConnectionActions, + ProjectPermissionApprovalRequestActions, + ProjectPermissionApprovalRequestGrantActions, ProjectPermissionAuditLogsActions, ProjectPermissionCertificateActions, ProjectPermissionCertificateAuthorityActions, @@ -339,6 +341,16 @@ const buildAdminPermissionRules = () => { can([ProjectPermissionPamSessionActions.Read], ProjectPermissionSub.PamSessions); + can( + [ProjectPermissionApprovalRequestActions.Read, ProjectPermissionApprovalRequestActions.Create], + ProjectPermissionSub.ApprovalRequests + ); + + can( + [ProjectPermissionApprovalRequestGrantActions.Read, ProjectPermissionApprovalRequestGrantActions.Revoke], + ProjectPermissionSub.ApprovalRequestGrants + ); + return rules; }; @@ -586,6 +598,8 @@ const buildMemberPermissionRules = () => { ProjectPermissionSub.PamAccounts ); + can([ProjectPermissionApprovalRequestActions.Create], ProjectPermissionSub.ApprovalRequests); + return rules; }; diff --git a/backend/src/ee/services/permission/project-permission.ts b/backend/src/ee/services/permission/project-permission.ts index 20f9c1f09..85efd19aa 100644 --- a/backend/src/ee/services/permission/project-permission.ts +++ b/backend/src/ee/services/permission/project-permission.ts @@ -224,6 +224,16 @@ export enum ProjectPermissionPamSessionActions { // Terminate = "terminate" } +export enum ProjectPermissionApprovalRequestActions { + Read = "read", + Create = "create" +} + +export enum ProjectPermissionApprovalRequestGrantActions { + Read = "read", + Revoke = "revoke" +} + export const isCustomProjectRole = (slug: string) => !Object.values(ProjectMembershipRole).includes(slug as ProjectMembershipRole); @@ -274,7 +284,9 @@ export enum ProjectPermissionSub { PamResources = "pam-resources", PamAccounts = "pam-accounts", PamSessions = "pam-sessions", - CertificateProfiles = "certificate-profiles" + CertificateProfiles = "certificate-profiles", + ApprovalRequests = "approval-requests", + ApprovalRequestGrants = "approval-request-grants" } export type SecretSubjectFields = { @@ -500,7 +512,9 @@ export type ProjectPermissionSet = | ProjectPermissionSub.CertificateProfiles | (ForcedSubject & CertificateProfileSubjectFields) ) - ]; + ] + | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests] + | [ProjectPermissionApprovalRequestGrantActions, ProjectPermissionSub.ApprovalRequestGrants]; const SECRET_PATH_MISSING_SLASH_ERR_MSG = "Invalid Secret Path; it must start with a '/'"; const SECRET_PATH_PERMISSION_OPERATOR_SCHEMA = z.union([ @@ -1105,6 +1119,18 @@ const GeneralPermissionSchema = [ action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionPamSessionActions).describe( "Describe what action an entity can take." ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.ApprovalRequests).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalRequestActions).describe( + "Describe what action an entity can take." + ) + }), + z.object({ + subject: z.literal(ProjectPermissionSub.ApprovalRequestGrants).describe("The entity this permission pertains to."), + action: CASL_ACTION_SCHEMA_NATIVE_ENUM(ProjectPermissionApprovalRequestGrantActions).describe( + "Describe what action an entity can take." + ) }) ]; diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/index.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/index.ts new file mode 100644 index 000000000..876ab836d --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/index.ts @@ -0,0 +1,4 @@ +export * from "./mongodb-credentials-rotation-constants"; +export * from "./mongodb-credentials-rotation-fns"; +export * from "./mongodb-credentials-rotation-schemas"; +export * from "./mongodb-credentials-rotation-types"; diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-constants.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-constants.ts new file mode 100644 index 000000000..82b43f45a --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-constants.ts @@ -0,0 +1,27 @@ +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { TSecretRotationV2ListItem } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const MONGODB_CREDENTIALS_ROTATION_LIST_OPTION: TSecretRotationV2ListItem = { + name: "MongoDB Credentials", + type: SecretRotation.MongoDBCredentials, + connection: AppConnection.MongoDB, + template: { + createUserStatement: `use [DATABASE_NAME] +db.createUser({ + user: "infisical_user_1", + pwd: "temporary_password", + roles: [{ role: "readWrite", db: "[DATABASE_NAME]" }] +}) + +db.createUser({ + user: "infisical_user_2", + pwd: "temporary_password", + roles: [{ role: "readWrite", db: "[DATABASE_NAME]" }] +})`, + secretsMapping: { + username: "MONGODB_DB_USERNAME", + password: "MONGODB_DB_PASSWORD" + } + } +}; diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-fns.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-fns.ts new file mode 100644 index 000000000..aad286ecc --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-fns.ts @@ -0,0 +1,191 @@ +/* eslint-disable no-await-in-loop */ +import { MongoClient } from "mongodb"; + +import { + TRotationFactory, + TRotationFactoryGetSecretsPayload, + TRotationFactoryIssueCredentials, + TRotationFactoryRevokeCredentials, + TRotationFactoryRotateCredentials +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-types"; +import { createMongoClient } from "@app/services/app-connection/mongodb/mongodb-connection-fns"; + +import { DEFAULT_PASSWORD_REQUIREMENTS, generatePassword } from "../shared/utils"; +import { + TMongoDBCredentialsRotationGeneratedCredentials, + TMongoDBCredentialsRotationWithConnection +} from "./mongodb-credentials-rotation-types"; + +const redactPasswords = (e: unknown, credentials: TMongoDBCredentialsRotationGeneratedCredentials) => { + const error = e as Error; + + if (!error?.message) return "Unknown error"; + + let redactedMessage = error.message; + + credentials.forEach(({ password }) => { + redactedMessage = redactedMessage.replaceAll(password, "*******************"); + }); + + return redactedMessage; +}; + +export const mongodbCredentialsRotationFactory: TRotationFactory< + TMongoDBCredentialsRotationWithConnection, + TMongoDBCredentialsRotationGeneratedCredentials +> = (secretRotation) => { + const { + connection, + parameters: { username1, username2 }, + activeIndex, + secretsMapping + } = secretRotation; + + const passwordRequirement = DEFAULT_PASSWORD_REQUIREMENTS; + + const $getClient = async () => { + let client: MongoClient | null = null; + try { + client = await createMongoClient(connection.credentials, { validateConnection: true }); + return client; + } catch (err) { + if (client) await client.close(); + throw err; + } + }; + + const $validateCredentials = async (credentials: TMongoDBCredentialsRotationGeneratedCredentials[number]) => { + let client: MongoClient | null = null; + try { + client = await createMongoClient(connection.credentials, { + authCredentials: { + username: credentials.username, + password: credentials.password + }, + validateConnection: true + }); + } catch (error) { + throw new Error(redactPasswords(error, [credentials])); + } finally { + if (client) await client.close(); + } + }; + + const issueCredentials: TRotationFactoryIssueCredentials = async ( + callback + ) => { + // For MongoDB, since we get existing users, we change both their passwords + // on issue to invalidate their existing passwords + const credentialsSet = [ + { username: username1, password: generatePassword(passwordRequirement) }, + { username: username2, password: generatePassword(passwordRequirement) } + ]; + + let client: MongoClient | null = null; + try { + client = await $getClient(); + const db = client.db(connection.credentials.database); + + for (const credentials of credentialsSet) { + await db.command({ + updateUser: credentials.username, + pwd: credentials.password + }); + } + } catch (error) { + throw new Error(redactPasswords(error, credentialsSet)); + } finally { + if (client) await client.close(); + } + + for (const credentials of credentialsSet) { + await $validateCredentials(credentials); + } + + return callback(credentialsSet[0]); + }; + + const revokeCredentials: TRotationFactoryRevokeCredentials = async ( + credentialsToRevoke, + callback + ) => { + const revokedCredentials = credentialsToRevoke.map(({ username }) => ({ + username, + password: generatePassword(passwordRequirement) + })); + + let client: MongoClient | null = null; + try { + client = await $getClient(); + const db = client.db(connection.credentials.database); + + for (const credentials of revokedCredentials) { + await db.command({ + updateUser: credentials.username, + pwd: credentials.password + }); + } + } catch (error) { + throw new Error(redactPasswords(error, revokedCredentials)); + } finally { + if (client) await client.close(); + } + + return callback(); + }; + + const rotateCredentials: TRotationFactoryRotateCredentials = async ( + _, + callback + ) => { + const credentials = { + username: activeIndex === 0 ? username2 : username1, + password: generatePassword(passwordRequirement) + }; + + let client: MongoClient | null = null; + try { + client = await $getClient(); + const db = client.db(connection.credentials.database); + + await db.command({ + updateUser: credentials.username, + pwd: credentials.password + }); + } catch (error) { + throw new Error(redactPasswords(error, [credentials])); + } finally { + if (client) await client.close(); + } + + await $validateCredentials(credentials); + + return callback(credentials); + }; + + const getSecretsPayload: TRotationFactoryGetSecretsPayload = ( + generatedCredentials + ) => { + const { username, password } = secretsMapping; + + const secrets = [ + { + key: username, + value: generatedCredentials.username + }, + { + key: password, + value: generatedCredentials.password + } + ]; + + return secrets; + }; + + return { + issueCredentials, + revokeCredentials, + rotateCredentials, + getSecretsPayload + }; +}; diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts new file mode 100644 index 000000000..9a5335f5f --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-schemas.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; + +import { SecretRotation } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-enums"; +import { + BaseCreateSecretRotationSchema, + BaseSecretRotationSchema, + BaseUpdateSecretRotationSchema +} from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-schemas"; +import { + SqlCredentialsRotationGeneratedCredentialsSchema, + SqlCredentialsRotationParametersSchema, + SqlCredentialsRotationTemplateSchema +} from "@app/ee/services/secret-rotation-v2/shared/sql-credentials/sql-credentials-rotation-schemas"; +import { SecretRotations } from "@app/lib/api-docs"; +import { SecretNameSchema } from "@app/server/lib/schemas"; +import { AppConnection } from "@app/services/app-connection/app-connection-enums"; + +export const MongoDBCredentialsRotationGeneratedCredentialsSchema = SqlCredentialsRotationGeneratedCredentialsSchema; +export const MongoDBCredentialsRotationParametersSchema = SqlCredentialsRotationParametersSchema; +export const MongoDBCredentialsRotationTemplateSchema = SqlCredentialsRotationTemplateSchema; + +const MongoDBCredentialsRotationSecretsMappingSchema = z.object({ + username: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.MONGODB_CREDENTIALS.username), + password: SecretNameSchema.describe(SecretRotations.SECRETS_MAPPING.MONGODB_CREDENTIALS.password) +}); + +export const MongoDBCredentialsRotationSchema = BaseSecretRotationSchema(SecretRotation.MongoDBCredentials).extend({ + type: z.literal(SecretRotation.MongoDBCredentials), + parameters: MongoDBCredentialsRotationParametersSchema, + secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema +}); + +export const CreateMongoDBCredentialsRotationSchema = BaseCreateSecretRotationSchema( + SecretRotation.MongoDBCredentials +).extend({ + parameters: MongoDBCredentialsRotationParametersSchema, + secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema +}); + +export const UpdateMongoDBCredentialsRotationSchema = BaseUpdateSecretRotationSchema( + SecretRotation.MongoDBCredentials +).extend({ + parameters: MongoDBCredentialsRotationParametersSchema.optional(), + secretsMapping: MongoDBCredentialsRotationSecretsMappingSchema.optional() +}); + +export const MongoDBCredentialsRotationListItemSchema = z.object({ + name: z.literal("MongoDB Credentials"), + connection: z.literal(AppConnection.MongoDB), + type: z.literal(SecretRotation.MongoDBCredentials), + template: MongoDBCredentialsRotationTemplateSchema +}); diff --git a/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-types.ts b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-types.ts new file mode 100644 index 000000000..3a53a8cc5 --- /dev/null +++ b/backend/src/ee/services/secret-rotation-v2/mongodb-credentials/mongodb-credentials-rotation-types.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +import { TMongoDBConnection } from "@app/services/app-connection/mongodb"; + +import { + CreateMongoDBCredentialsRotationSchema, + MongoDBCredentialsRotationGeneratedCredentialsSchema, + MongoDBCredentialsRotationListItemSchema, + MongoDBCredentialsRotationSchema +} from "./mongodb-credentials-rotation-schemas"; + +export type TMongoDBCredentialsRotation = z.infer; + +export type TMongoDBCredentialsRotationInput = z.infer; + +export type TMongoDBCredentialsRotationListItem = z.infer; + +export type TMongoDBCredentialsRotationWithConnection = TMongoDBCredentialsRotation & { + connection: TMongoDBConnection; +}; + +export type TMongoDBCredentialsRotationGeneratedCredentials = z.infer< + typeof MongoDBCredentialsRotationGeneratedCredentialsSchema +>; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts index cf236b56f..1718e09fd 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-dal.ts @@ -214,7 +214,10 @@ export const secretRotationV2DALFactory = ( tx?: Knex ) => { try { - const extendedQuery = baseSecretRotationV2Query({ filter, db, tx, options }) + const { limit, offset = 0, sort, ...queryOptions } = options || {}; + const baseOptions = { ...queryOptions }; + + const subquery = baseSecretRotationV2Query({ filter, db, tx, options: baseOptions }) .join( TableName.SecretRotationV2SecretMapping, `${TableName.SecretRotationV2SecretMapping}.rotationId`, @@ -233,6 +236,7 @@ export const secretRotationV2DALFactory = ( ) .leftJoin(TableName.ResourceMetadata, `${TableName.SecretV2}.id`, `${TableName.ResourceMetadata}.secretId`) .select( + selectAllTableCols(TableName.SecretRotationV2), db.ref("id").withSchema(TableName.SecretV2).as("secretId"), db.ref("key").withSchema(TableName.SecretV2).as("secretKey"), db.ref("version").withSchema(TableName.SecretV2).as("secretVersion"), @@ -252,18 +256,31 @@ export const secretRotationV2DALFactory = ( db.ref("slug").withSchema(TableName.SecretTag).as("tagSlug"), db.ref("id").withSchema(TableName.ResourceMetadata).as("metadataId"), db.ref("key").withSchema(TableName.ResourceMetadata).as("metadataKey"), - db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue") + db.ref("value").withSchema(TableName.ResourceMetadata).as("metadataValue"), + db.raw(`DENSE_RANK() OVER (ORDER BY ${TableName.SecretRotationV2}."createdAt" DESC) as rank`) ); if (search) { - void extendedQuery.where((query) => { - void query + void subquery.where((qb) => { + void qb .whereILike(`${TableName.SecretV2}.key`, `%${search}%`) .orWhereILike(`${TableName.SecretRotationV2}.name`, `%${search}%`); }); } - const secretRotations = await extendedQuery; + let secretRotations: Awaited; + if (limit !== undefined) { + const rankOffset = offset + 1; + const queryWithLimit = (tx || db) + .with("inner", subquery) + .select("*") + .from("inner") + .where("inner.rank", ">=", rankOffset) + .andWhere("inner.rank", "<", rankOffset + limit); + secretRotations = (await queryWithLimit) as unknown as Awaited; + } else { + secretRotations = await subquery; + } if (!secretRotations.length) return []; diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts index 661a2399a..470a63849 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-enums.ts @@ -8,7 +8,8 @@ export enum SecretRotation { AwsIamUserSecret = "aws-iam-user-secret", LdapPassword = "ldap-password", OktaClientSecret = "okta-client-secret", - RedisCredentials = "redis-credentials" + RedisCredentials = "redis-credentials", + MongoDBCredentials = "mongodb-credentials" } export enum SecretRotationStatus { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts index e4e6a8531..bb774c4be 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-fns.ts @@ -9,6 +9,7 @@ import { AUTH0_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./auth0-client-secret" import { AWS_IAM_USER_SECRET_ROTATION_LIST_OPTION } from "./aws-iam-user-secret"; import { AZURE_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./azure-client-secret"; import { LDAP_PASSWORD_ROTATION_LIST_OPTION, TLdapPasswordRotation } from "./ldap-password"; +import { MONGODB_CREDENTIALS_ROTATION_LIST_OPTION } from "./mongodb-credentials"; import { MSSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mssql-credentials"; import { MYSQL_CREDENTIALS_ROTATION_LIST_OPTION } from "./mysql-credentials"; import { OKTA_CLIENT_SECRET_ROTATION_LIST_OPTION } from "./okta-client-secret"; @@ -37,7 +38,8 @@ const SECRET_ROTATION_LIST_OPTIONS: Record { diff --git a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts index 2087fa195..c2b0714ab 100644 --- a/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts +++ b/backend/src/ee/services/secret-rotation-v2/secret-rotation-v2-maps.ts @@ -11,7 +11,8 @@ export const SECRET_ROTATION_NAME_MAP: Record = { [SecretRotation.AwsIamUserSecret]: "AWS IAM User Secret", [SecretRotation.LdapPassword]: "LDAP Password", [SecretRotation.OktaClientSecret]: "Okta Client Secret", - [SecretRotation.RedisCredentials]: "Redis Credentials" + [SecretRotation.RedisCredentials]: "Redis Credentials", + [SecretRotation.MongoDBCredentials]: "MongoDB Credentials" }; export const SECRET_ROTATION_CONNECTION_MAP: Record = { @@ -24,5 +25,6 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record = async ( callback ) => { - // For SQL, since we get existing users, we change both their passwords - // on issue to invalidate their existing passwords // For SQL, since we get existing users, we change both their passwords // on issue to invalidate their existing passwords const credentialsSet = [ diff --git a/backend/src/lib/api-docs/constants.ts b/backend/src/lib/api-docs/constants.ts index 3c8972248..81b0c0de2 100644 --- a/backend/src/lib/api-docs/constants.ts +++ b/backend/src/lib/api-docs/constants.ts @@ -170,10 +170,13 @@ export const IDENTITIES = { } } as const; +const IDENTITY_AUTH_SUB_ORGANIZATION_NAME = "sub-organization name to scope the token to"; + export const UNIVERSAL_AUTH = { LOGIN: { clientId: "Your Machine Identity Client ID.", - clientSecret: "Your Machine Identity Client Secret." + clientSecret: "Your Machine Identity Client Secret.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -247,7 +250,8 @@ export const LDAP_AUTH = { LOGIN: { identityId: "The ID of the machine identity to login.", username: "The username of the LDAP user to login.", - password: "The password of the LDAP user to login." + password: "The password of the LDAP user to login.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { templateId: "The ID of the identity auth template to attach the configuration onto.", @@ -312,7 +316,8 @@ export const ALICLOUD_AUTH = { Timestamp: "The timestamp of the request in UTC, formatted as 'YYYY-MM-DDTHH:mm:ssZ'.", SignatureVersion: "The signature version. For STS GetCallerIdentity, this should be '1.0'.", SignatureNonce: "A unique random string to prevent replay attacks.", - Signature: "The signature string calculated based on the request parameters and AccessKey Secret." + Signature: "The signature string calculated based on the request parameters and AccessKey Secret.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -340,7 +345,8 @@ export const ALICLOUD_AUTH = { export const TLS_CERT_AUTH = { LOGIN: { - identityId: "The ID of the machine identity to login." + identityId: "The ID of the machine identity to login.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -378,7 +384,8 @@ export const AWS_AUTH = { "The base64-encoded HTTP URL used in the signed request. Most likely, the base64-encoding of https://sts.amazonaws.com/.", iamRequestBody: "The base64-encoded body of the signed request. Most likely, the base64-encoding of Action=GetCallerIdentity&Version=2011-06-15.", - iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request." + iamRequestHeaders: "The base64-encoded headers of the sts:GetCallerIdentity signed request.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -416,7 +423,8 @@ export const OCI_AUTH = { LOGIN: { identityId: "The ID of the machine identity to login.", userOcid: "The OCID of the user attempting login.", - headers: "The headers of the signed request." + headers: "The headers of the signed request.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -448,7 +456,8 @@ export const OCI_AUTH = { export const AZURE_AUTH = { LOGIN: { - identityId: "The ID of the machine identity to login." + identityId: "The ID of the machine identity to login.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -482,7 +491,8 @@ export const AZURE_AUTH = { export const GCP_AUTH = { LOGIN: { - identityId: "The ID of the machine identity to login." + identityId: "The ID of the machine identity to login.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -520,7 +530,8 @@ export const GCP_AUTH = { export const KUBERNETES_AUTH = { LOGIN: { - identityId: "The ID of the machine identity to login." + identityId: "The ID of the machine identity to login.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -600,7 +611,8 @@ export const TOKEN_AUTH = { }, CREATE_TOKEN: { identityId: "The ID of the machine identity to create the token for.", - name: "The name of the token to create." + name: "The name of the token to create.", + subOrganizationName: "The sub organization name to scope the token to." }, UPDATE_TOKEN: { tokenId: "The ID of the token to update metadata for.", @@ -613,7 +625,8 @@ export const TOKEN_AUTH = { export const OIDC_AUTH = { LOGIN: { - identityId: "The ID of the machine identity to login." + identityId: "The ID of the machine identity to login.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -653,7 +666,8 @@ export const OIDC_AUTH = { export const JWT_AUTH = { LOGIN: { - identityId: "The ID of the machine identity to login." + identityId: "The ID of the machine identity to login.", + subOrganizationName: IDENTITY_AUTH_SUB_ORGANIZATION_NAME }, ATTACH: { identityId: "The ID of the machine identity to attach the configuration onto.", @@ -2860,6 +2874,12 @@ export const SecretRotations = { }, REDIS_CREDENTIALS: { permissionScope: "The ACL permission scope to assign to the issued Redis users." + }, + MONGODB_CREDENTIALS: { + username1: + "The username of the first MongoDB user to rotate passwords for. This user must already exist in your database.", + username2: + "The username of the second MongoDB user to rotate passwords for. This user must already exist in your database." } }, SECRETS_MAPPING: { @@ -2890,6 +2910,10 @@ export const SecretRotations = { OKTA_CLIENT_SECRET: { clientId: "The name of the secret that the client ID will be mapped to.", clientSecret: "The name of the secret that the rotated client secret will be mapped to." + }, + MONGODB_CREDENTIALS: { + username: "The name of the secret that the active username will be mapped to.", + password: "The name of the secret that the generated password will be mapped to." } } }; diff --git a/backend/src/lib/config/env.ts b/backend/src/lib/config/env.ts index 21e83c2b7..14eb60192 100644 --- a/backend/src/lib/config/env.ts +++ b/backend/src/lib/config/env.ts @@ -286,6 +286,10 @@ const envSchema = z DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()).default( process.env.INF_APP_CONNECTION_AWS_SECRET_ACCESS_KEY ), + + // PAM AWS credentials (for AWS IAM PAM resource type) + PAM_AWS_ACCESS_KEY_ID: zpStr(z.string().optional()), + PAM_AWS_SECRET_ACCESS_KEY: zpStr(z.string().optional()), /* ----------------------------------------------------------------------------- */ /* App Connections ----------------------------------------------------------------------------- */ diff --git a/backend/src/lib/errors/index.ts b/backend/src/lib/errors/index.ts index dab9d3278..b5497ca21 100644 --- a/backend/src/lib/errors/index.ts +++ b/backend/src/lib/errors/index.ts @@ -183,3 +183,23 @@ export class CryptographyError extends Error { this.error = error; } } + +export class PolicyViolationError extends Error { + name: string; + + error: unknown; + + details?: unknown; + + constructor({ + name, + error, + message, + details + }: { message?: string; name?: string; error?: unknown; details?: unknown } = {}) { + super(message || "A policy is in place for this resource"); + this.name = name || "PolicyViolationError"; + this.error = error; + this.details = details; + } +} diff --git a/backend/src/queue/queue-service.ts b/backend/src/queue/queue-service.ts index c46e9c023..0243c81f2 100644 --- a/backend/src/queue/queue-service.ts +++ b/backend/src/queue/queue-service.ts @@ -83,6 +83,7 @@ export enum QueueName { HealthAlert = "health-alert", CertificateV3AutoRenewal = "certificate-v3-auto-renewal", PamAccountRotation = "pam-account-rotation", + PamSessionExpiration = "pam-session-expiration", PkiAcmeChallengeValidation = "pki-acme-challenge-validation" } @@ -138,6 +139,7 @@ export enum QueueJobs { HealthAlert = "health-alert", CertificateV3DailyAutoRenewal = "certificate-v3-daily-auto-renewal", PamAccountRotation = "pam-account-rotation", + PamSessionExpiration = "pam-session-expiration", PkiAcmeChallengeValidation = "pki-acme-challenge-validation" } @@ -404,6 +406,10 @@ export type TQueueJobTypes = { name: QueueJobs.PamAccountRotation; payload: undefined; }; + [QueueName.PamSessionExpiration]: { + name: QueueJobs.PamSessionExpiration; + payload: { sessionId: string }; + }; [QueueName.PkiAcmeChallengeValidation]: { name: QueueJobs.PkiAcmeChallengeValidation; payload: { challengeId: string }; diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 8008855c6..16c7b1e29 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -8,7 +8,6 @@ import { TScimTokenJwtPayload } from "@app/ee/services/scim/scim-types"; import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto"; import { BadRequestError } from "@app/lib/errors"; -import { slugSchema } from "@app/server/lib/schemas"; import { ActorType, AuthMethod, AuthMode, AuthModeJwtTokenPayload, AuthTokenType } from "@app/services/auth/auth-type"; import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-token/identity-access-token-types"; import { getServerCfg } from "@app/services/super-admin/super-admin-service"; @@ -152,15 +151,10 @@ export const injectIdentity = fp( if (!authMode) return; - const subOrganizationSelector = req.headers?.["x-infisical-org"] as string | undefined; - if (subOrganizationSelector) { - await slugSchema().parseAsync(subOrganizationSelector); - } - switch (authMode) { case AuthMode.JWT: { const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } = - await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector); + await server.services.authToken.fnValidateJwtIdentity(token); requestContext.set("orgId", orgId); requestContext.set("orgName", orgName); requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" }); @@ -180,11 +174,7 @@ export const injectIdentity = fp( break; } case AuthMode.IDENTITY_ACCESS_TOKEN: { - const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken( - token, - req.realIp, - subOrganizationSelector - ); + const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); const serverCfg = await getServerCfg(); requestContext.set("orgId", identity.orgId); requestContext.set("orgName", identity.orgName); @@ -223,9 +213,6 @@ export const injectIdentity = fp( const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); requestContext.set("orgId", serviceToken.orgId); - if (subOrganizationSelector) - throw new BadRequestError({ message: `Service token doesn't support sub organization selector` }); - req.auth = { orgId: serviceToken.orgId, rootOrgId: serviceToken.rootOrgId, @@ -248,9 +235,6 @@ export const injectIdentity = fp( const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); requestContext.set("orgId", orgId); - if (subOrganizationSelector) - throw new BadRequestError({ message: `SCIM token doesn't support sub organization selector` }); - req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, diff --git a/backend/src/server/plugins/error-handler.ts b/backend/src/server/plugins/error-handler.ts index e703df5ef..df1988ce3 100644 --- a/backend/src/server/plugins/error-handler.ts +++ b/backend/src/server/plugins/error-handler.ts @@ -17,6 +17,7 @@ import { NotFoundError, OidcAuthError, PermissionBoundaryError, + PolicyViolationError, RateLimitError, ScimRequestError, UnauthorizedError @@ -255,6 +256,14 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider detail: error.message // TODO: add subproblems if they exist }); + } else if (error instanceof PolicyViolationError) { + void res.status(HttpStatusCodes.Forbidden).send({ + reqId: req.id, + statusCode: HttpStatusCodes.Forbidden, + error: "PolicyViolationError", + message: error.message, + details: error.details + }); } else { void res.status(HttpStatusCodes.InternalServerError).send({ reqId: req.id, diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 914491d3c..6611f4a59 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -159,6 +159,19 @@ import { apiKeyDALFactory } from "@app/services/api-key/api-key-dal"; import { apiKeyServiceFactory } from "@app/services/api-key/api-key-service"; import { appConnectionDALFactory } from "@app/services/app-connection/app-connection-dal"; import { appConnectionServiceFactory } from "@app/services/app-connection/app-connection-service"; +import { + approvalPolicyDALFactory, + approvalPolicyStepApproversDALFactory, + approvalPolicyStepsDALFactory +} from "@app/services/approval-policy/approval-policy-dal"; +import { approvalPolicyServiceFactory } from "@app/services/approval-policy/approval-policy-service"; +import { + approvalRequestApprovalsDALFactory, + approvalRequestDALFactory, + approvalRequestGrantsDALFactory, + approvalRequestStepEligibleApproversDALFactory, + approvalRequestStepsDALFactory +} from "@app/services/approval-policy/approval-request-dal"; import { authDALFactory } from "@app/services/auth/auth-dal"; import { authLoginServiceFactory } from "@app/services/auth/auth-login-service"; import { authPaswordServiceFactory } from "@app/services/auth/auth-password-service"; @@ -279,6 +292,7 @@ import { orgServiceFactory } from "@app/services/org/org-service"; import { orgAdminServiceFactory } from "@app/services/org-admin/org-admin-service"; import { orgMembershipDALFactory } from "@app/services/org-membership/org-membership-dal"; import { pamAccountRotationServiceFactory } from "@app/services/pam-account-rotation/pam-account-rotation-queue"; +import { pamSessionExpirationServiceFactory } from "@app/services/pam-session-expiration/pam-session-expiration-queue"; import { dailyExpiringPkiItemAlertQueueServiceFactory } from "@app/services/pki-alert/expiring-pki-item-alert-queue"; import { pkiAlertDALFactory } from "@app/services/pki-alert/pki-alert-dal"; import { pkiAlertServiceFactory } from "@app/services/pki-alert/pki-alert-service"; @@ -1916,6 +1930,9 @@ export const registerRoutes = async ( identityDAL }); + const approvalRequestDAL = approvalRequestDALFactory(db); + const approvalRequestGrantsDAL = approvalRequestGrantsDALFactory(db); + // DAILY const dailyResourceCleanUp = dailyResourceCleanUpQueueServiceFactory({ scimService, @@ -1931,7 +1948,9 @@ export const registerRoutes = async ( serviceTokenService, orgService, userNotificationDAL, - keyValueStoreDAL + keyValueStoreDAL, + approvalRequestDAL, + approvalRequestGrantsDAL }); const healthAlert = healthAlertServiceFactory({ @@ -2412,6 +2431,12 @@ export const registerRoutes = async ( gatewayV2Service }); + const approvalPolicyDAL = approvalPolicyDALFactory(db); + const pamSessionExpirationService = pamSessionExpirationServiceFactory({ + queueService, + pamSessionDAL + }); + const pamAccountService = pamAccountServiceFactory({ pamAccountDAL, gatewayV2Service, @@ -2423,7 +2448,10 @@ export const registerRoutes = async ( permissionService, projectDAL, userDAL, - auditLogService + auditLogService, + approvalRequestGrantsDAL, + approvalPolicyDAL, + pamSessionExpirationService }); const pamAccountRotation = pamAccountRotationServiceFactory({ @@ -2451,6 +2479,27 @@ export const registerRoutes = async ( auditLogService }); + const approvalPolicyStepsDAL = approvalPolicyStepsDALFactory(db); + const approvalPolicyStepApproversDAL = approvalPolicyStepApproversDALFactory(db); + const approvalRequestStepsDAL = approvalRequestStepsDALFactory(db); + const approvalRequestStepEligibleApproversDAL = approvalRequestStepEligibleApproversDALFactory(db); + const approvalRequestApprovalsDAL = approvalRequestApprovalsDALFactory(db); + + const approvalPolicyService = approvalPolicyServiceFactory({ + approvalPolicyDAL, + approvalPolicyStepsDAL, + approvalPolicyStepApproversDAL, + permissionService, + projectMembershipDAL, + approvalRequestDAL, + approvalRequestStepsDAL, + approvalRequestStepEligibleApproversDAL, + approvalRequestApprovalsDAL, + userGroupMembershipDAL, + notificationService, + approvalRequestGrantsDAL + }); + // setup the communication with license key server await licenseService.init(); @@ -2490,6 +2539,7 @@ export const registerRoutes = async ( await healthAlert.init(); await pkiSyncCleanup.init(); await pamAccountRotation.init(); + await pamSessionExpirationService.init(); await dailyReminderQueueService.startDailyRemindersJob(); await dailyReminderQueueService.startSecretReminderMigrationJob(); await dailyExpiringPkiItemAlert.startSendingAlerts(); @@ -2630,7 +2680,8 @@ export const registerRoutes = async ( additionalPrivilege: additionalPrivilegeService, identityProject: identityProjectService, convertor: convertorService, - pkiAlertV2: pkiAlertV2Service + pkiAlertV2: pkiAlertV2Service, + approvalPolicy: approvalPolicyService }); const cronJobs: CronJob[] = []; diff --git a/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts b/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts index e44b9af4e..0feb1ba55 100644 --- a/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts +++ b/backend/src/server/routes/sanitizedSchema/identitiy-additional-privilege.ts @@ -2,6 +2,8 @@ import { IdentityProjectAdditionalPrivilegeSchema } from "@app/db/schemas"; import { UnpackedPermissionSchema } from "./permission"; -export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.extend({ +export const SanitizedIdentityPrivilegeSchema = IdentityProjectAdditionalPrivilegeSchema.omit({ + projectMembershipId: true +}).extend({ permissions: UnpackedPermissionSchema.array() }); diff --git a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts index 48fdc7c38..072abbadb 100644 --- a/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts +++ b/backend/src/server/routes/v1/app-connection-routers/app-connection-router.ts @@ -87,6 +87,10 @@ import { SanitizedLaravelForgeConnectionSchema } from "@app/services/app-connection/laravel-forge"; import { LdapConnectionListItemSchema, SanitizedLdapConnectionSchema } from "@app/services/app-connection/ldap"; +import { + MongoDBConnectionListItemSchema, + SanitizedMongoDBConnectionSchema +} from "@app/services/app-connection/mongodb"; import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql"; import { MySqlConnectionListItemSchema, SanitizedMySqlConnectionSchema } from "@app/services/app-connection/mysql"; import { @@ -173,6 +177,7 @@ const SanitizedAppConnectionSchema = z.union([ ...SanitizedOktaConnectionSchema.options, ...SanitizedAzureADCSConnectionSchema.options, ...SanitizedRedisConnectionSchema.options, + ...SanitizedMongoDBConnectionSchema.options, ...SanitizedLaravelForgeConnectionSchema.options, ...SanitizedChefConnectionSchema.options, ...SanitizedDNSMadeEasyConnectionSchema.options @@ -219,6 +224,7 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [ OktaConnectionListItemSchema, AzureADCSConnectionListItemSchema, RedisConnectionListItemSchema, + MongoDBConnectionListItemSchema, LaravelForgeConnectionListItemSchema, ChefConnectionListItemSchema, DNSMadeEasyConnectionListItemSchema diff --git a/backend/src/server/routes/v1/app-connection-routers/index.ts b/backend/src/server/routes/v1/app-connection-routers/index.ts index d7a4065fd..0738f0407 100644 --- a/backend/src/server/routes/v1/app-connection-routers/index.ts +++ b/backend/src/server/routes/v1/app-connection-routers/index.ts @@ -16,8 +16,8 @@ import { registerCamundaConnectionRouter } from "./camunda-connection-router"; import { registerChecklyConnectionRouter } from "./checkly-connection-router"; import { registerCloudflareConnectionRouter } from "./cloudflare-connection-router"; import { registerDatabricksConnectionRouter } from "./databricks-connection-router"; -import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router"; import { registerDigitalOceanConnectionRouter } from "./digital-ocean-connection-router"; +import { registerDNSMadeEasyConnectionRouter } from "./dns-made-easy-connection-router"; import { registerFlyioConnectionRouter } from "./flyio-connection-router"; import { registerGcpConnectionRouter } from "./gcp-connection-router"; import { registerGitHubConnectionRouter } from "./github-connection-router"; @@ -28,6 +28,7 @@ import { registerHerokuConnectionRouter } from "./heroku-connection-router"; import { registerHumanitecConnectionRouter } from "./humanitec-connection-router"; import { registerLaravelForgeConnectionRouter } from "./laravel-forge-connection-router"; import { registerLdapConnectionRouter } from "./ldap-connection-router"; +import { registerMongoDBConnectionRouter } from "./mongodb-connection-router"; import { registerMsSqlConnectionRouter } from "./mssql-connection-router"; import { registerMySqlConnectionRouter } from "./mysql-connection-router"; import { registerNetlifyConnectionRouter } from "./netlify-connection-router"; @@ -90,5 +91,6 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record { + registerAppConnectionEndpoints({ + app: AppConnection.MongoDB, + server, + sanitizedResponseSchema: SanitizedMongoDBConnectionSchema, + createSchema: CreateMongoDBConnectionSchema, + updateSchema: UpdateMongoDBConnectionSchema + }); +}; diff --git a/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts b/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts new file mode 100644 index 000000000..91e042542 --- /dev/null +++ b/backend/src/server/routes/v1/approval-policy-routers/approval-policy-endpoints.ts @@ -0,0 +1,625 @@ +import { z } from "zod"; + +import { EventType } from "@app/ee/services/audit-log/audit-log-types"; +import { BadRequestError } from "@app/lib/errors"; +import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; +import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums"; +import { + TApprovalPolicy, + TCreatePolicyDTO, + TCreateRequestDTO, + TUpdatePolicyDTO +} from "@app/services/approval-policy/approval-policy-types"; +import { AuthMode } from "@app/services/auth/auth-type"; + +export const registerApprovalPolicyEndpoints =

({ + server, + policyType, + createPolicySchema, + updatePolicySchema, + policyResponseSchema, + createRequestSchema, + requestResponseSchema, + grantResponseSchema +}: { + server: FastifyZodProvider; + policyType: ApprovalPolicyType; + createPolicySchema: z.ZodType< + TCreatePolicyDTO & { + conditions: P["conditions"]["conditions"]; + constraints: P["constraints"]["constraints"]; + } + >; + updatePolicySchema: z.ZodType< + TUpdatePolicyDTO & { + conditions?: P["conditions"]["conditions"]; + constraints?: P["constraints"]["constraints"]; + } + >; + policyResponseSchema: z.ZodTypeAny; + createRequestSchema: z.ZodType; + requestResponseSchema: z.ZodTypeAny; + grantResponseSchema: z.ZodTypeAny; +}) => { + // Policies + server.route({ + method: "POST", + url: "/", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create approval policy", + body: createPolicySchema, + response: { + 200: z.object({ + policy: policyResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policy } = await server.services.approvalPolicy.create(policyType, req.body, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.body.projectId, + event: { + type: EventType.APPROVAL_POLICY_CREATE, + metadata: { + policyType, + name: req.body.name + } + } + }); + + return { policy }; + } + }); + + server.route({ + method: "GET", + url: "/", + config: { + rateLimit: readLimit + }, + schema: { + description: "List approval policies", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + policies: z.array(policyResponseSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policies } = await server.services.approvalPolicy.list(policyType, req.query.projectId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.query.projectId, + event: { + type: EventType.APPROVAL_POLICY_LIST, + metadata: { + policyType, + count: policies.length + } + } + }); + + return { policies }; + } + }); + + server.route({ + method: "GET", + url: "/:policyId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get approval policy", + params: z.object({ + policyId: z.string().uuid() + }), + response: { + 200: z.object({ + policy: policyResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policy } = await server.services.approvalPolicy.getById(req.params.policyId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: policy.projectId, + event: { + type: EventType.APPROVAL_POLICY_GET, + metadata: { + policyType, + policyId: policy.id, + name: policy.name + } + } + }); + + return { policy }; + } + }); + + server.route({ + method: "PATCH", + url: "/:policyId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Update approval policy", + params: z.object({ + policyId: z.string().uuid() + }), + body: updatePolicySchema, + response: { + 200: z.object({ + policy: policyResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policy } = await server.services.approvalPolicy.updateById(req.params.policyId, req.body, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: policy.projectId, + event: { + type: EventType.APPROVAL_POLICY_UPDATE, + metadata: { + policyType, + policyId: policy.id, + name: policy.name + } + } + }); + + return { policy }; + } + }); + + server.route({ + method: "DELETE", + url: "/:policyId", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Delete approval policy", + params: z.object({ + policyId: z.string().uuid() + }), + response: { + 200: z.object({ + policyId: z.string().uuid() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { policyId, projectId } = await server.services.approvalPolicy.deleteById( + req.params.policyId, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId, + event: { + type: EventType.APPROVAL_POLICY_DELETE, + metadata: { + policyType, + policyId + } + } + }); + + return { policyId }; + } + }); + + // Requests + server.route({ + method: "GET", + url: "/requests", + config: { + rateLimit: readLimit + }, + schema: { + description: "List approval requests", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + requests: z.array(requestResponseSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { requests } = await server.services.approvalPolicy.listRequests( + policyType, + req.query.projectId, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.query.projectId, + event: { + type: EventType.APPROVAL_REQUEST_LIST, + metadata: { + policyType, + count: requests.length + } + } + }); + + return { requests }; + } + }); + + server.route({ + method: "POST", + url: "/requests", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Create approval request", + body: createRequestSchema, + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + // To prevent type errors when accessing req.auth.user + if (req.auth.authMode !== AuthMode.JWT) { + throw new BadRequestError({ message: "You can only request access using JWT auth tokens." }); + } + + const { request } = await server.services.approvalPolicy.createRequest( + policyType, + { + requesterName: `${req.auth.user.firstName ?? ""} ${req.auth.user.lastName ?? ""}`.trim(), + requesterEmail: req.auth.user.email ?? "", + ...req.body + }, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_CREATE, + metadata: { + policyType, + justification: req.body.justification || undefined, + requestDuration: req.body.requestDuration || "infinite" + } + } + }); + + return { request }; + } + }); + + server.route({ + method: "GET", + url: "/requests/:requestId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get approval request", + params: z.object({ + requestId: z.string().uuid() + }), + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.approvalPolicy.getRequestById(req.params.requestId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_GET, + metadata: { + policyType, + requestId: request.id, + status: request.status + } + } + }); + + return { request }; + } + }); + + server.route({ + method: "POST", + url: "/requests/:requestId/approve", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Approve approval request", + params: z.object({ + requestId: z.string().uuid() + }), + body: z.object({ + comment: z.string().optional() + }), + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.approvalPolicy.approveRequest( + req.params.requestId, + req.body, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_APPROVE, + metadata: { + policyType, + requestId: req.params.requestId, + comment: req.body.comment + } + } + }); + + return { request }; + } + }); + + server.route({ + method: "POST", + url: "/requests/:requestId/reject", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Reject approval request", + params: z.object({ + requestId: z.string().uuid() + }), + body: z.object({ + comment: z.string().optional() + }), + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.approvalPolicy.rejectRequest( + req.params.requestId, + req.body, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_REJECT, + metadata: { + policyType, + requestId: req.params.requestId, + comment: req.body.comment + } + } + }); + + return { request }; + } + }); + + server.route({ + method: "POST", + url: "/requests/:requestId/cancel", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Cancel approval request", + params: z.object({ + requestId: z.string().uuid() + }), + response: { + 200: z.object({ + request: requestResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { request } = await server.services.approvalPolicy.cancelRequest(req.params.requestId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: request.projectId, + event: { + type: EventType.APPROVAL_REQUEST_CANCEL, + metadata: { + policyType, + requestId: req.params.requestId + } + } + }); + + return { request }; + } + }); + + // Grants + server.route({ + method: "GET", + url: "/grants", + config: { + rateLimit: readLimit + }, + schema: { + description: "List approval grants", + querystring: z.object({ + projectId: z.string().uuid() + }), + response: { + 200: z.object({ + grants: z.array(grantResponseSchema) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { grants } = await server.services.approvalPolicy.listGrants( + policyType, + req.query.projectId, + req.permission + ); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: req.query.projectId, + event: { + type: EventType.APPROVAL_REQUEST_GRANT_LIST, + metadata: { + policyType, + count: grants.length + } + } + }); + + return { grants }; + } + }); + + server.route({ + method: "GET", + url: "/grants/:grantId", + config: { + rateLimit: readLimit + }, + schema: { + description: "Get approval grant", + params: z.object({ + grantId: z.string().uuid() + }), + response: { + 200: z.object({ + grant: grantResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { grant } = await server.services.approvalPolicy.getGrantById(req.params.grantId, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: grant.projectId, + event: { + type: EventType.APPROVAL_REQUEST_GRANT_GET, + metadata: { + policyType, + grantId: grant.id, + status: grant.status + } + } + }); + + return { grant }; + } + }); + + server.route({ + method: "POST", + url: "/grants/:grantId/revoke", + config: { + rateLimit: writeLimit + }, + schema: { + description: "Revoke approval grant", + params: z.object({ + grantId: z.string().uuid() + }), + body: z.object({ + revocationReason: z.string().optional() + }), + response: { + 200: z.object({ + grant: grantResponseSchema + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const { grant } = await server.services.approvalPolicy.revokeGrant(req.params.grantId, req.body, req.permission); + + await server.services.auditLog.createAuditLog({ + ...req.auditLogInfo, + orgId: req.permission.orgId, + projectId: grant.projectId, + event: { + type: EventType.APPROVAL_REQUEST_GRANT_REVOKE, + metadata: { + policyType, + grantId: grant.id, + revocationReason: req.body.revocationReason + } + } + }); + + return { grant }; + } + }); +}; diff --git a/backend/src/server/routes/v1/approval-policy-routers/index.ts b/backend/src/server/routes/v1/approval-policy-routers/index.ts new file mode 100644 index 000000000..c848e2e47 --- /dev/null +++ b/backend/src/server/routes/v1/approval-policy-routers/index.ts @@ -0,0 +1,29 @@ +import { ApprovalPolicyType } from "@app/services/approval-policy/approval-policy-enums"; +import { + CreatePamAccessPolicySchema, + CreatePamAccessRequestSchema, + PamAccessPolicySchema, + PamAccessRequestGrantSchema, + PamAccessRequestSchema, + UpdatePamAccessPolicySchema +} from "@app/services/approval-policy/pam-access/pam-access-policy-schemas"; + +import { registerApprovalPolicyEndpoints } from "./approval-policy-endpoints"; + +export const APPROVAL_POLICY_REGISTER_ROUTER_MAP: Record< + ApprovalPolicyType, + (server: FastifyZodProvider) => Promise +> = { + [ApprovalPolicyType.PamAccess]: async (server: FastifyZodProvider) => { + registerApprovalPolicyEndpoints({ + server, + policyType: ApprovalPolicyType.PamAccess, + createPolicySchema: CreatePamAccessPolicySchema, + updatePolicySchema: UpdatePamAccessPolicySchema, + policyResponseSchema: PamAccessPolicySchema, + createRequestSchema: CreatePamAccessRequestSchema, + requestResponseSchema: PamAccessRequestSchema, + grantResponseSchema: PamAccessRequestGrantSchema + }); + } +}; diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts index 48939844e..bedc519da 100644 --- a/backend/src/server/routes/v1/auth-router.ts +++ b/backend/src/server/routes/v1/auth-router.ts @@ -81,7 +81,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { response: { 200: z.object({ token: z.string(), - organizationId: z.string().optional() + organizationId: z.string().optional(), + subOrganizationId: z.string().optional() }) } }, @@ -89,14 +90,15 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid); const appCfg = getConfig(); let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME; + if (decodedToken.organizationId) { - const org = await server.services.org.findOrganizationById( - decodedToken.userId, - decodedToken.organizationId, - decodedToken.authMethod, - decodedToken.organizationId, - decodedToken.organizationId - ); + const org = await server.services.org.findOrganizationById({ + userId: decodedToken.userId, + orgId: decodedToken.subOrganizationId ? decodedToken.subOrganizationId : decodedToken.organizationId, + actorAuthMethod: decodedToken.authMethod, + actorOrgId: decodedToken.subOrganizationId ? decodedToken.subOrganizationId : decodedToken.organizationId, + rootOrgId: decodedToken.organizationId + }); if (org && org.userTokenExpiration) { expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); } @@ -110,14 +112,14 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => { tokenVersionId: tokenVersion.id, accessVersion: tokenVersion.accessVersion, organizationId: decodedToken.organizationId, + ...(decodedToken.subOrganizationId && { subOrganizationId: decodedToken.subOrganizationId }), isMfaVerified: decodedToken.isMfaVerified, mfaMethod: decodedToken.mfaMethod }, appCfg.AUTH_SECRET, { expiresIn } ); - - return { token, organizationId: decodedToken.organizationId }; + return { token, organizationId: decodedToken.organizationId, subOrganizationId: decodedToken.subOrganizationId }; } }); }; diff --git a/backend/src/server/routes/v1/certificate-router.ts b/backend/src/server/routes/v1/certificate-router.ts index 397085171..25beb710c 100644 --- a/backend/src/server/routes/v1/certificate-router.ts +++ b/backend/src/server/routes/v1/certificate-router.ts @@ -316,13 +316,11 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { params: z.object({ requestId: z.string().uuid() }), - query: z.object({ - projectId: z.string().uuid() - }), response: { 200: z.object({ status: z.nativeEnum(CertificateRequestStatus), certificate: z.string().nullable(), + certificateId: z.string().nullable(), privateKey: z.string().nullable(), serialNumber: z.string().nullable(), errorMessage: z.string().nullable(), @@ -333,18 +331,17 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { }, onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), handler: async (req) => { - const data = await server.services.certificateRequest.getCertificateFromRequest({ + const { certificateRequest, projectId } = await server.services.certificateRequest.getCertificateFromRequest({ actor: req.permission.type, actorId: req.permission.id, actorAuthMethod: req.permission.authMethod, actorOrgId: req.permission.orgId, - projectId: (req.query as { projectId: string }).projectId, certificateRequestId: req.params.requestId }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, - projectId: (req.query as { projectId: string }).projectId, + projectId, event: { type: EventType.GET_CERTIFICATE_REQUEST, metadata: { @@ -352,7 +349,7 @@ export const registerCertificateRouter = async (server: FastifyZodProvider) => { } } }); - return data; + return certificateRequest; } }); diff --git a/backend/src/server/routes/v1/dashboard-router.ts b/backend/src/server/routes/v1/dashboard-router.ts index 8cf9604a4..7dc763730 100644 --- a/backend/src/server/routes/v1/dashboard-router.ts +++ b/backend/src/server/routes/v1/dashboard-router.ts @@ -624,7 +624,10 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { secretValueHidden: z.boolean(), secretPath: z.string().optional(), secretMetadata: ResourceMetadataSchema.optional(), - tags: SanitizedTagSchema.array().optional() + tags: SanitizedTagSchema.array().optional(), + reminder: RemindersSchema.extend({ + recipients: z.string().array() + }).nullable() }) .nullable() .array() @@ -743,6 +746,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { ReturnType >[number]["secrets"][number] & { isEmpty: boolean; + reminder: Awaited>[string] | null; } > | null)[]; })[] @@ -847,27 +851,38 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { ); if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) { - secretRotations = ( - await server.services.secretRotationV2.getDashboardSecretRotations( - { - projectId, - search, - orderBy, - orderDirection, - environments: [environment], - secretPath, - limit: remainingLimit, - offset: adjustedOffset - }, - req.permission - ) - ).map((rotation) => ({ + const rawSecretRotations = await server.services.secretRotationV2.getDashboardSecretRotations( + { + projectId, + search, + orderBy, + orderDirection, + environments: [environment], + secretPath, + limit: remainingLimit, + offset: adjustedOffset + }, + req.permission + ); + + const allRotationSecretIds = rawSecretRotations + .flatMap((rotation) => rotation.secrets) + .filter((secret) => Boolean(secret)) + .map((secret) => secret.id); + + const rotationReminders = + allRotationSecretIds.length > 0 + ? await server.services.reminder.getRemindersForDashboard(allRotationSecretIds) + : {}; + + secretRotations = rawSecretRotations.map((rotation) => ({ ...rotation, secrets: rotation.secrets.map((secret) => secret ? { ...secret, - isEmpty: !secret.secretValue + isEmpty: !secret.secretValue, + reminder: rotationReminders[secret.id] ?? null } : secret ) @@ -948,7 +963,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { search, tagSlugs: tags, includeTagsInSearch: true, - includeMetadataInSearch: true + includeMetadataInSearch: true, + excludeRotatedSecrets: includeSecretRotations }); if (remainingLimit > 0 && totalSecretCount > adjustedOffset) { @@ -970,7 +986,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => { offset: adjustedOffset, tagSlugs: tags, includeTagsInSearch: true, - includeMetadataInSearch: true + includeMetadataInSearch: true, + excludeRotatedSecrets: includeSecretRotations }) ).secrets; diff --git a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts index 8f64d3b23..817cde667 100644 --- a/backend/src/server/routes/v1/identity-alicloud-auth-router.ts +++ b/backend/src/server/routes/v1/identity-alicloud-auth-router.ts @@ -5,6 +5,7 @@ import { IdentityAlicloudAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ALICLOUD_AUTH, ApiDocsTags } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -38,6 +39,7 @@ export const registerIdentityAliCloudAuthRouter = async (server: FastifyZodProvi message: "AccessKeyId must be alphanumeric" }) .describe(ALICLOUD_AUTH.LOGIN.AccessKeyId), + subOrganizationName: slugSchema().optional().describe(ALICLOUD_AUTH.LOGIN.subOrganizationName), SignatureMethod: z.enum(["HMAC-SHA1"]).describe(ALICLOUD_AUTH.LOGIN.SignatureMethod), Timestamp: z .string() diff --git a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts index 3cfb19895..d9d9cab40 100644 --- a/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts +++ b/backend/src/server/routes/v1/identity-aws-iam-auth-router.ts @@ -4,6 +4,7 @@ import { IdentityAwsAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, AWS_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -28,7 +29,8 @@ export const registerIdentityAwsAuthRouter = async (server: FastifyZodProvider) identityId: z.string().trim().describe(AWS_AUTH.LOGIN.identityId), iamHttpRequestMethod: z.string().default("POST").describe(AWS_AUTH.LOGIN.iamHttpRequestMethod), iamRequestBody: z.string().describe(AWS_AUTH.LOGIN.iamRequestBody), - iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders) + iamRequestHeaders: z.string().describe(AWS_AUTH.LOGIN.iamRequestHeaders), + subOrganizationName: slugSchema().optional().describe(AWS_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-azure-auth-router.ts b/backend/src/server/routes/v1/identity-azure-auth-router.ts index cdab7af02..e8dd4341d 100644 --- a/backend/src/server/routes/v1/identity-azure-auth-router.ts +++ b/backend/src/server/routes/v1/identity-azure-auth-router.ts @@ -4,6 +4,7 @@ import { IdentityAzureAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, AZURE_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -23,7 +24,8 @@ export const registerIdentityAzureAuthRouter = async (server: FastifyZodProvider description: "Login with Azure Auth for machine identity", body: z.object({ identityId: z.string().trim().describe(AZURE_AUTH.LOGIN.identityId), - jwt: z.string() + jwt: z.string(), + subOrganizationName: slugSchema().optional().describe(AZURE_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-gcp-auth-router.ts b/backend/src/server/routes/v1/identity-gcp-auth-router.ts index 474999b2b..9f6d284bd 100644 --- a/backend/src/server/routes/v1/identity-gcp-auth-router.ts +++ b/backend/src/server/routes/v1/identity-gcp-auth-router.ts @@ -4,6 +4,7 @@ import { IdentityGcpAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, GCP_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -23,7 +24,8 @@ export const registerIdentityGcpAuthRouter = async (server: FastifyZodProvider) description: "Login with GCP Auth for machine identity", body: z.object({ identityId: z.string().trim().describe(GCP_AUTH.LOGIN.identityId), - jwt: z.string() + jwt: z.string(), + subOrganizationName: slugSchema().optional().describe(GCP_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-jwt-auth-router.ts b/backend/src/server/routes/v1/identity-jwt-auth-router.ts index 5d71b3781..a9d0b90c6 100644 --- a/backend/src/server/routes/v1/identity-jwt-auth-router.ts +++ b/backend/src/server/routes/v1/identity-jwt-auth-router.ts @@ -4,6 +4,7 @@ import { IdentityJwtAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, JWT_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -99,7 +100,8 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) description: "Login with JWT Auth for machine identity", body: z.object({ identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId), - jwt: z.string().trim() + jwt: z.string().trim(), + subOrganizationName: slugSchema().optional().describe(JWT_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -112,10 +114,7 @@ export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) }, handler: async (req) => { const { identityJwtAuth, accessToken, identityAccessToken, identity } = - await server.services.identityJwtAuth.login({ - identityId: req.body.identityId, - jwt: req.body.jwt - }); + await server.services.identityJwtAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts index 28f611aba..29edf363a 100644 --- a/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts +++ b/backend/src/server/routes/v1/identity-kubernetes-auth-router.ts @@ -5,6 +5,7 @@ import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, KUBERNETES_AUTH } from "@app/lib/api-docs"; import { CharacterType, characterValidator } from "@app/lib/validator/validate-string"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -44,7 +45,8 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide description: "Login with Kubernetes Auth for machine identity", body: z.object({ identityId: z.string().trim().describe(KUBERNETES_AUTH.LOGIN.identityId), - jwt: z.string().trim() + jwt: z.string().trim(), + subOrganizationName: slugSchema().optional().describe(KUBERNETES_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -57,10 +59,7 @@ export const registerIdentityKubernetesRouter = async (server: FastifyZodProvide }, handler: async (req) => { const { identityKubernetesAuth, accessToken, identityAccessToken, identity } = - await server.services.identityKubernetesAuth.login({ - identityId: req.body.identityId, - jwt: req.body.jwt - }); + await server.services.identityKubernetesAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v1/identity-ldap-auth-router.ts b/backend/src/server/routes/v1/identity-ldap-auth-router.ts index dade20ea3..ec5360345 100644 --- a/backend/src/server/routes/v1/identity-ldap-auth-router.ts +++ b/backend/src/server/routes/v1/identity-ldap-auth-router.ts @@ -21,6 +21,7 @@ import { getConfig } from "@app/lib/config/env"; import { UnauthorizedError } from "@app/lib/errors"; import { logger } from "@app/lib/logger"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -124,7 +125,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) body: z.object({ identityId: z.string().trim().describe(LDAP_AUTH.LOGIN.identityId), username: z.string().describe(LDAP_AUTH.LOGIN.username), - password: z.string().describe(LDAP_AUTH.LOGIN.password) + password: z.string().describe(LDAP_AUTH.LOGIN.password), + subOrganizationName: slugSchema().optional().describe(LDAP_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -163,7 +165,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider) const { identityId, user } = req.passportMachineIdentity; const { accessToken, identityLdapAuth, identity } = await server.services.identityLdapAuth.login({ - identityId + identityId, + subOrganizationName: req.body.subOrganizationName }); await server.services.auditLog.createAuditLog({ diff --git a/backend/src/server/routes/v1/identity-oci-auth-router.ts b/backend/src/server/routes/v1/identity-oci-auth-router.ts index 003d9810b..cffba6f41 100644 --- a/backend/src/server/routes/v1/identity-oci-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oci-auth-router.ts @@ -4,6 +4,7 @@ import { IdentityOciAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, OCI_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -40,7 +41,8 @@ export const registerIdentityOciAuthRouter = async (server: FastifyZodProvider) }); } }) - .describe(OCI_AUTH.LOGIN.headers) + .describe(OCI_AUTH.LOGIN.headers), + subOrganizationName: slugSchema().optional().describe(OCI_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-oidc-auth-router.ts b/backend/src/server/routes/v1/identity-oidc-auth-router.ts index 6fad1f400..9b49a65f0 100644 --- a/backend/src/server/routes/v1/identity-oidc-auth-router.ts +++ b/backend/src/server/routes/v1/identity-oidc-auth-router.ts @@ -4,6 +4,7 @@ import { IdentityOidcAuthsSchema } from "@app/db/schemas"; import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, OIDC_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -47,7 +48,8 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) description: "Login with OIDC Auth for machine identity", body: z.object({ identityId: z.string().trim().describe(OIDC_AUTH.LOGIN.identityId), - jwt: z.string().trim() + jwt: z.string().trim(), + subOrganizationName: slugSchema().optional().describe(OIDC_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -60,10 +62,7 @@ export const registerIdentityOidcAuthRouter = async (server: FastifyZodProvider) }, handler: async (req) => { const { identityOidcAuth, accessToken, identityAccessToken, identity, oidcTokenData } = - await server.services.identityOidcAuth.login({ - identityId: req.body.identityId, - jwt: req.body.jwt - }); + await server.services.identityOidcAuth.login(req.body); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts index b7a44c62c..2a6faf896 100644 --- a/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts +++ b/backend/src/server/routes/v1/identity-tls-cert-auth-router.ts @@ -7,6 +7,7 @@ import { getConfig } from "@app/lib/config/env"; import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError } from "@app/lib/errors"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -46,7 +47,8 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid tags: [ApiDocsTags.TlsCertAuth], description: "Login with TLS Certificate Auth for machine identity", body: z.object({ - identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId) + identityId: z.string().trim().describe(TLS_CERT_AUTH.LOGIN.identityId), + subOrganizationName: slugSchema().optional().describe(TLS_CERT_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -66,7 +68,7 @@ export const registerIdentityTlsCertAuthRouter = async (server: FastifyZodProvid const { identityTlsCertAuth, accessToken, identityAccessToken, identity } = await server.services.identityTlsCertAuth.login({ - identityId: req.body.identityId, + ...req.body, clientCertificate: clientCertificate as string }); diff --git a/backend/src/server/routes/v1/identity-token-auth-router.ts b/backend/src/server/routes/v1/identity-token-auth-router.ts index 71f299e43..e6ad10acc 100644 --- a/backend/src/server/routes/v1/identity-token-auth-router.ts +++ b/backend/src/server/routes/v1/identity-token-auth-router.ts @@ -4,6 +4,7 @@ import { IdentityAccessTokensSchema, IdentityTokenAuthsSchema } from "@app/db/sc import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, TOKEN_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -307,7 +308,8 @@ export const registerIdentityTokenAuthRouter = async (server: FastifyZodProvider identityId: z.string().describe(TOKEN_AUTH.CREATE_TOKEN.identityId) }), body: z.object({ - name: z.string().optional().describe(TOKEN_AUTH.CREATE_TOKEN.name) + name: z.string().optional().describe(TOKEN_AUTH.CREATE_TOKEN.name), + subOrganizationName: slugSchema().optional().describe(TOKEN_AUTH.CREATE_TOKEN.subOrganizationName) }), response: { 200: z.object({ diff --git a/backend/src/server/routes/v1/identity-universal-auth-router.ts b/backend/src/server/routes/v1/identity-universal-auth-router.ts index 88a4cb775..64a6cfebe 100644 --- a/backend/src/server/routes/v1/identity-universal-auth-router.ts +++ b/backend/src/server/routes/v1/identity-universal-auth-router.ts @@ -4,6 +4,7 @@ import { IdentityUaClientSecretsSchema, IdentityUniversalAuthsSchema } from "@ap import { EventType } from "@app/ee/services/audit-log/audit-log-types"; import { ApiDocsTags, UNIVERSAL_AUTH } from "@app/lib/api-docs"; import { readLimit, writeLimit } from "@app/server/config/rateLimiter"; +import { slugSchema } from "@app/server/lib/schemas"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; import { TIdentityTrustedIp } from "@app/services/identity/identity-types"; @@ -35,7 +36,8 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { description: "Login with Universal Auth for machine identity", body: z.object({ clientId: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientId), - clientSecret: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientSecret) + clientSecret: z.string().trim().describe(UNIVERSAL_AUTH.LOGIN.clientSecret), + subOrganizationName: slugSchema().optional().describe(UNIVERSAL_AUTH.LOGIN.subOrganizationName) }), response: { 200: z.object({ @@ -55,7 +57,10 @@ export const registerIdentityUaRouter = async (server: FastifyZodProvider) => { identity, accessTokenTTL, accessTokenMaxTTL - } = await server.services.identityUa.login(req.body.clientId, req.body.clientSecret, req.realIp); + } = await server.services.identityUa.login({ + ...req.body, + ip: req.realIp + }); await server.services.auditLog.createAuditLog({ ...req.auditLogInfo, diff --git a/backend/src/server/routes/v1/index.ts b/backend/src/server/routes/v1/index.ts index c27399453..3b4b10b88 100644 --- a/backend/src/server/routes/v1/index.ts +++ b/backend/src/server/routes/v1/index.ts @@ -7,6 +7,7 @@ import { registerDashboardRouter } from "@app/server/routes/v1/dashboard-router" import { registerSecretSyncRouter, SECRET_SYNC_REGISTER_ROUTER_MAP } from "@app/server/routes/v1/secret-sync-routers"; import { registerAdminRouter } from "./admin-router"; +import { APPROVAL_POLICY_REGISTER_ROUTER_MAP } from "./approval-policy-routers"; import { registerAuthRoutes } from "./auth-router"; import { registerProjectBotRouter } from "./bot-router"; import { registerCaRouter } from "./certificate-authority-router"; @@ -275,4 +276,14 @@ export const registerV1Routes = async (server: FastifyZodProvider) => { await server.register(registerEventRouter, { prefix: "/events" }); await server.register(registerUpgradePathRouter, { prefix: "/upgrade-path" }); + + await server.register( + async (approvalPolicyRouter) => { + // Register policy type-specific endpoints + for await (const [type, router] of Object.entries(APPROVAL_POLICY_REGISTER_ROUTER_MAP)) { + await approvalPolicyRouter.register(router, { prefix: `/${type}` }); + } + }, + { prefix: "/approval-policies" } + ); }; diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts index d4b0058f9..a56fdcfed 100644 --- a/backend/src/server/routes/v1/organization-router.ts +++ b/backend/src/server/routes/v1/organization-router.ts @@ -60,26 +60,19 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => { }), response: { 200: z.object({ - organization: sanitizedOrganizationSchema.extend({ - subOrganization: z - .object({ - id: z.string(), - name: z.string() - }) - .optional() - }) + organization: sanitizedOrganizationSchema }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const organization = await server.services.org.findOrganizationById( - req.permission.id, - req.params.organizationId, - req.permission.authMethod, - req.permission.rootOrgId, - req.permission.orgId - ); + const organization = await server.services.org.findOrganizationById({ + userId: req.permission.id, + orgId: req.params.organizationId, + actorAuthMethod: req.permission.authMethod, + rootOrgId: req.permission.rootOrgId, + actorOrgId: req.permission.orgId + }); return { organization }; } }); diff --git a/backend/src/server/routes/v3/login-router.ts b/backend/src/server/routes/v3/login-router.ts index 07923fd6c..0f77c02f6 100644 --- a/backend/src/server/routes/v3/login-router.ts +++ b/backend/src/server/routes/v3/login-router.ts @@ -57,6 +57,7 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => { }, handler: async (req, res) => { const cfg = getConfig(); + const tokens = await server.services.login.selectOrganization({ userAgent: req.body.userAgent ?? req.headers["user-agent"], authJwtToken: req.headers.authorization, diff --git a/backend/src/services/additional-privilege/additional-privilege-service.ts b/backend/src/services/additional-privilege/additional-privilege-service.ts index 2af9e6419..69f103c85 100644 --- a/backend/src/services/additional-privilege/additional-privilege-service.ts +++ b/backend/src/services/additional-privilege/additional-privilege-service.ts @@ -79,7 +79,10 @@ export const additionalPrivilegeServiceFactory = ({ }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; } @@ -103,7 +106,10 @@ export const additionalPrivilegeServiceFactory = ({ }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; @@ -136,7 +142,10 @@ export const additionalPrivilegeServiceFactory = ({ }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; } @@ -158,7 +167,10 @@ export const additionalPrivilegeServiceFactory = ({ }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; @@ -179,7 +191,10 @@ export const additionalPrivilegeServiceFactory = ({ const additionalPrivilege = await additionalPrivilegeDAL.deleteById(existingPrivilege.id); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; @@ -199,7 +214,10 @@ export const additionalPrivilegeServiceFactory = ({ throw new NotFoundError({ message: `Additional privilege with id ${selector.id} doesn't exist` }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; @@ -219,7 +237,10 @@ export const additionalPrivilegeServiceFactory = ({ throw new NotFoundError({ message: `Additional privilege with name ${selector.name} doesn't exist` }); return { - additionalPrivilege: { ...additionalPrivilege, permissions: unpackPermissions(additionalPrivilege.permissions) } + additionalPrivilege: { + ...additionalPrivilege, + permissions: unpackPermissions(additionalPrivilege.permissions) + } }; }; diff --git a/backend/src/services/app-connection/app-connection-enums.ts b/backend/src/services/app-connection/app-connection-enums.ts index 8e0260c01..e7e2bca76 100644 --- a/backend/src/services/app-connection/app-connection-enums.ts +++ b/backend/src/services/app-connection/app-connection-enums.ts @@ -39,6 +39,7 @@ export enum AppConnection { Netlify = "netlify", Okta = "okta", Redis = "redis", + MongoDB = "mongodb", LaravelForge = "laravel-forge", Chef = "chef", Northflank = "northflank" diff --git a/backend/src/services/app-connection/app-connection-fns.ts b/backend/src/services/app-connection/app-connection-fns.ts index d8af3773b..f28508efb 100644 --- a/backend/src/services/app-connection/app-connection-fns.ts +++ b/backend/src/services/app-connection/app-connection-fns.ts @@ -119,6 +119,7 @@ import { validateLaravelForgeConnectionCredentials } from "./laravel-forge"; import { getLdapConnectionListItem, LdapConnectionMethod, validateLdapConnectionCredentials } from "./ldap"; +import { getMongoDBConnectionListItem, MongoDBConnectionMethod, validateMongoDBConnectionCredentials } from "./mongodb"; import { getMsSqlConnectionListItem, MsSqlConnectionMethod } from "./mssql"; import { MySqlConnectionMethod } from "./mysql/mysql-connection-enums"; import { getMySqlConnectionListItem } from "./mysql/mysql-connection-fns"; @@ -224,6 +225,7 @@ export const listAppConnectionOptions = (projectType?: ProjectType) => { getNorthflankConnectionListItem(), getOktaConnectionListItem(), getRedisConnectionListItem(), + getMongoDBConnectionListItem(), getChefConnectionListItem() ] .filter((option) => { @@ -357,7 +359,8 @@ export const validateAppConnectionCredentials = async ( [AppConnection.Northflank]: validateNorthflankConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Okta]: validateOktaConnectionCredentials as TAppConnectionCredentialsValidator, [AppConnection.Chef]: validateChefConnectionCredentials as TAppConnectionCredentialsValidator, - [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator + [AppConnection.Redis]: validateRedisConnectionCredentials as TAppConnectionCredentialsValidator, + [AppConnection.MongoDB]: validateMongoDBConnectionCredentials as TAppConnectionCredentialsValidator }; return VALIDATE_APP_CONNECTION_CREDENTIALS_MAP[appConnection.app](appConnection, gatewayService, gatewayV2Service); @@ -411,6 +414,7 @@ export const getAppConnectionMethodName = (method: TAppConnection["method"]) => case OracleDBConnectionMethod.UsernameAndPassword: case AzureADCSConnectionMethod.UsernamePassword: case RedisConnectionMethod.UsernameAndPassword: + case MongoDBConnectionMethod.UsernameAndPassword: return "Username & Password"; case WindmillConnectionMethod.AccessToken: case HCVaultConnectionMethod.AccessToken: @@ -504,6 +508,7 @@ export const TRANSITION_CONNECTION_CREDENTIALS_TO_PLATFORM: Record< [AppConnection.Northflank]: platformManagedCredentialsNotSupported, [AppConnection.Okta]: platformManagedCredentialsNotSupported, [AppConnection.Redis]: platformManagedCredentialsNotSupported, + [AppConnection.MongoDB]: platformManagedCredentialsNotSupported, [AppConnection.LaravelForge]: platformManagedCredentialsNotSupported, [AppConnection.Chef]: platformManagedCredentialsNotSupported }; diff --git a/backend/src/services/app-connection/app-connection-maps.ts b/backend/src/services/app-connection/app-connection-maps.ts index 27d6a27a8..a41589d12 100644 --- a/backend/src/services/app-connection/app-connection-maps.ts +++ b/backend/src/services/app-connection/app-connection-maps.ts @@ -42,6 +42,7 @@ export const APP_CONNECTION_NAME_MAP: Record = { [AppConnection.Netlify]: "Netlify", [AppConnection.Okta]: "Okta", [AppConnection.Redis]: "Redis", + [AppConnection.MongoDB]: "MongoDB", [AppConnection.Chef]: "Chef", [AppConnection.Northflank]: "Northflank" }; @@ -88,6 +89,7 @@ export const APP_CONNECTION_PLAN_MAP: Record { + return { + name: "MongoDB" as const, + app: AppConnection.MongoDB as const, + methods: Object.values(MongoDBConnectionMethod) as [MongoDBConnectionMethod.UsernameAndPassword], + supportsPlatformManagement: false as const + }; +}; + +export type TMongoDBConnectionCredentials = { + host: string; + port?: number; + database: string; + username: string; + password: string; + tlsEnabled?: boolean; + tlsRejectUnauthorized?: boolean; + tlsCertificate?: string; +}; + +export type TCreateMongoClientOptions = { + authCredentials?: { username: string; password: string }; + validateConnection?: boolean; +}; + +const DEFAULT_CONNECTION_TIMEOUT_MS = 10_000; + +export const createMongoClient = async ( + credentials: TMongoDBConnectionCredentials, + options?: TCreateMongoClientOptions +): Promise => { + const srvRegex = new RE2("^mongodb\\+srv:\\/\\/"); + const protocolRegex = new RE2("^mongodb:\\/\\/"); + + let normalizedHost = credentials.host.trim(); + const isSrvFromHost = srvRegex.test(normalizedHost); + if (isSrvFromHost) { + normalizedHost = srvRegex.replace(normalizedHost, ""); + } else if (protocolRegex.test(normalizedHost)) { + normalizedHost = protocolRegex.replace(normalizedHost, ""); + } + + const [hostIp] = await verifyHostInputValidity(normalizedHost); + + const isSrv = !credentials.port || isSrvFromHost; + const uri = isSrv ? `mongodb+srv://${hostIp}` : `mongodb://${hostIp}:${credentials.port}`; + + const authCredentials = options?.authCredentials ?? { + username: credentials.username, + password: credentials.password + }; + + const clientOptions: { + auth?: { username: string; password?: string }; + authSource?: string; + tls?: boolean; + tlsInsecure?: boolean; + ca?: string; + directConnection?: boolean; + connectTimeoutMS?: number; + serverSelectionTimeoutMS?: number; + socketTimeoutMS?: number; + } = { + auth: { + username: authCredentials.username, + password: authCredentials.password + }, + authSource: isSrv ? undefined : credentials.database, + directConnection: !isSrv, + connectTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS, + serverSelectionTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS, + socketTimeoutMS: DEFAULT_CONNECTION_TIMEOUT_MS + }; + + if (credentials.tlsEnabled) { + clientOptions.tls = true; + clientOptions.tlsInsecure = !credentials.tlsRejectUnauthorized; + if (credentials.tlsCertificate) { + clientOptions.ca = credentials.tlsCertificate; + } + } + + const client = new MongoClient(uri, clientOptions); + + if (options?.validateConnection) { + await client + .db(credentials.database) + .command({ ping: 1 }) + .then(() => true); + } + + return client; +}; + +export const validateMongoDBConnectionCredentials = async (config: TMongoDBConnectionConfig) => { + let client: MongoClient | null = null; + try { + client = await createMongoClient(config.credentials, { validateConnection: true }); + + if (client) await client.close(); + + return config.credentials; + } catch (err) { + if (err instanceof BadRequestError) { + throw err; + } + throw new BadRequestError({ + message: `Unable to validate connection: ${(err as Error)?.message || "verify credentials"}` + }); + } finally { + if (client) await client.close(); + } +}; diff --git a/backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts b/backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts new file mode 100644 index 000000000..c934a0741 --- /dev/null +++ b/backend/src/services/app-connection/mongodb/mongodb-connection-schemas.ts @@ -0,0 +1,89 @@ +import z from "zod"; + +import { AppConnections } from "@app/lib/api-docs"; +import { + BaseAppConnectionSchema, + GenericCreateAppConnectionFieldsSchema, + GenericUpdateAppConnectionFieldsSchema +} from "@app/services/app-connection/app-connection-schemas"; + +import { AppConnection } from "../app-connection-enums"; +import { MongoDBConnectionMethod } from "./mongodb-connection-enums"; + +export const BaseMongoDBUsernameAndPasswordConnectionSchema = z.object({ + host: z.string().toLowerCase().min(1), + port: z.coerce.number(), + username: z.string().min(1), + password: z.string().min(1), + database: z.string().min(1).trim(), + + tlsRejectUnauthorized: z.boolean(), + tlsEnabled: z.boolean(), + tlsCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() +}); + +export const MongoDBConnectionAccessTokenCredentialsSchema = BaseMongoDBUsernameAndPasswordConnectionSchema; + +const BaseMongoDBConnectionSchema = BaseAppConnectionSchema.extend({ app: z.literal(AppConnection.MongoDB) }); + +export const MongoDBConnectionSchema = BaseMongoDBConnectionSchema.extend({ + method: z.literal(MongoDBConnectionMethod.UsernameAndPassword), + credentials: MongoDBConnectionAccessTokenCredentialsSchema +}); + +export const SanitizedMongoDBConnectionSchema = z.discriminatedUnion("method", [ + BaseMongoDBConnectionSchema.extend({ + method: z.literal(MongoDBConnectionMethod.UsernameAndPassword), + credentials: MongoDBConnectionAccessTokenCredentialsSchema.pick({ + host: true, + port: true, + username: true, + database: true, + tlsEnabled: true, + tlsRejectUnauthorized: true, + tlsCertificate: true + }) + }) +]); + +export const ValidateMongoDBConnectionCredentialsSchema = z.discriminatedUnion("method", [ + z.object({ + method: z + .literal(MongoDBConnectionMethod.UsernameAndPassword) + .describe(AppConnections.CREATE(AppConnection.MongoDB).method), + credentials: MongoDBConnectionAccessTokenCredentialsSchema.describe( + AppConnections.CREATE(AppConnection.MongoDB).credentials + ) + }) +]); + +export const CreateMongoDBConnectionSchema = ValidateMongoDBConnectionCredentialsSchema.and( + GenericCreateAppConnectionFieldsSchema(AppConnection.MongoDB, { + supportsPlatformManagedCredentials: false, + supportsGateways: false + }) +); + +export const UpdateMongoDBConnectionSchema = z + .object({ + credentials: MongoDBConnectionAccessTokenCredentialsSchema.optional().describe( + AppConnections.UPDATE(AppConnection.MongoDB).credentials + ) + }) + .and( + GenericUpdateAppConnectionFieldsSchema(AppConnection.MongoDB, { + supportsPlatformManagedCredentials: false, + supportsGateways: false + }) + ); + +export const MongoDBConnectionListItemSchema = z.object({ + name: z.literal("MongoDB"), + app: z.literal(AppConnection.MongoDB), + methods: z.nativeEnum(MongoDBConnectionMethod).array(), + supportsPlatformManagement: z.literal(false) +}); diff --git a/backend/src/services/app-connection/mongodb/mongodb-connection-types.ts b/backend/src/services/app-connection/mongodb/mongodb-connection-types.ts new file mode 100644 index 000000000..52212545a --- /dev/null +++ b/backend/src/services/app-connection/mongodb/mongodb-connection-types.ts @@ -0,0 +1,22 @@ +import z from "zod"; + +import { DiscriminativePick } from "@app/lib/types"; + +import { AppConnection } from "../app-connection-enums"; +import { + CreateMongoDBConnectionSchema, + MongoDBConnectionSchema, + ValidateMongoDBConnectionCredentialsSchema +} from "./mongodb-connection-schemas"; + +export type TMongoDBConnection = z.infer; + +export type TMongoDBConnectionInput = z.infer & { + app: AppConnection.MongoDB; +}; + +export type TValidateMongoDBConnectionCredentialsSchema = typeof ValidateMongoDBConnectionCredentialsSchema; + +export type TMongoDBConnectionConfig = DiscriminativePick & { + orgId: string; +}; diff --git a/backend/src/services/approval-policy/approval-policy-dal.ts b/backend/src/services/approval-policy/approval-policy-dal.ts new file mode 100644 index 000000000..67dd7a521 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-dal.ts @@ -0,0 +1,150 @@ +import { TDbClient } from "@app/db"; +import { TableName } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +import { ApprovalPolicyType, ApproverType } from "./approval-policy-enums"; +import { ApprovalPolicyStep } from "./approval-policy-types"; + +// Approval Policy +export type TApprovalPolicyDALFactory = ReturnType; +export const approvalPolicyDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalPolicies); + + const findStepsByPolicyId = async (policyId: string) => { + try { + const dbInstance = db.replicaNode(); + const steps = await dbInstance(TableName.ApprovalPolicySteps).where({ policyId }).orderBy("stepNumber", "asc"); + + if (!steps.length) { + return []; + } + + const stepIds = steps.map((step) => step.id); + + const approvers = await dbInstance(TableName.ApprovalPolicyStepApprovers) + .whereIn("policyStepId", stepIds) + .select("policyStepId", "userId", "groupId"); + + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.policyStepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.policyStepId] = stepApprovers; + return acc; + }, + {} + ); + + return steps.map((step) => { + const stepApprovers = approversByStepId[step.id] || []; + + const formattedStep: ApprovalPolicyStep = { + requiredApprovals: step.requiredApprovals, + approvers: stepApprovers + }; + + if (step.name) { + formattedStep.name = step.name; + } + if (typeof step.notifyApprovers === "boolean") { + formattedStep.notifyApprovers = step.notifyApprovers; + } + + return formattedStep; + }); + } catch (error) { + throw new DatabaseError({ error, name: "Find approval policy steps" }); + } + }; + + const findByProjectId = async (policyType: ApprovalPolicyType, projectId: string) => { + try { + const dbInstance = db.replicaNode(); + const policies = await dbInstance(TableName.ApprovalPolicies).where({ type: policyType, projectId }); + + if (!policies.length) { + return []; + } + + const policyIds = policies.map((p) => p.id); + + const steps = await dbInstance(TableName.ApprovalPolicySteps) + .whereIn("policyId", policyIds) + .orderBy("stepNumber", "asc"); + + const stepsByPolicyId: Record = {}; + + if (steps.length) { + const stepIds = steps.map((step) => step.id); + + const approvers = await dbInstance(TableName.ApprovalPolicyStepApprovers) + .whereIn("policyStepId", stepIds) + .select("policyStepId", "userId", "groupId"); + + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.policyStepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.policyStepId] = stepApprovers; + return acc; + }, + {} + ); + + steps.forEach((step) => { + const stepApprovers = approversByStepId[step.id] || []; + const formattedStep: ApprovalPolicyStep = { + requiredApprovals: step.requiredApprovals, + approvers: stepApprovers + }; + + if (step.name) { + formattedStep.name = step.name; + } + if (typeof step.notifyApprovers === "boolean") { + formattedStep.notifyApprovers = step.notifyApprovers; + } + + if (!stepsByPolicyId[step.policyId]) { + stepsByPolicyId[step.policyId] = []; + } + stepsByPolicyId[step.policyId].push(formattedStep); + }); + } + + return policies.map((policy) => ({ + ...policy, + steps: stepsByPolicyId[policy.id] || [] + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find approval policies by project id" }); + } + }; + + return { + ...orm, + findStepsByPolicyId, + findByProjectId + }; +}; + +// Approval Policy Steps +export type TApprovalPolicyStepsDALFactory = ReturnType; +export const approvalPolicyStepsDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalPolicySteps); + return orm; +}; + +// Approval Policy Step Approvers +export type TApprovalPolicyStepApproversDALFactory = ReturnType; +export const approvalPolicyStepApproversDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalPolicyStepApprovers); + return orm; +}; diff --git a/backend/src/services/approval-policy/approval-policy-enums.ts b/backend/src/services/approval-policy/approval-policy-enums.ts new file mode 100644 index 000000000..bc57801f3 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-enums.ts @@ -0,0 +1,33 @@ +export enum ApprovalPolicyType { + PamAccess = "pam-access" +} + +export enum ApproverType { + Group = "group", + User = "user" +} + +export enum ApprovalRequestStatus { + Pending = "pending", + Approved = "approved", + Rejected = "rejected", + Expired = "expired", + Cancelled = "cancelled" +} + +export enum ApprovalRequestStepStatus { + Pending = "pending", + InProgress = "in-progress", + Completed = "completed" +} + +export enum ApprovalRequestApprovalDecision { + Approved = "approved", + Rejected = "rejected" +} + +export enum ApprovalRequestGrantStatus { + Active = "active", + Expired = "expired", + Revoked = "revoked" +} diff --git a/backend/src/services/approval-policy/approval-policy-factory.ts b/backend/src/services/approval-policy/approval-policy-factory.ts new file mode 100644 index 000000000..42ef11ac7 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-factory.ts @@ -0,0 +1,18 @@ +import { ApprovalPolicyType } from "./approval-policy-enums"; +import { + TApprovalPolicy, + TApprovalPolicyInputs, + TApprovalRequestData, + TApprovalResourceFactory +} from "./approval-policy-types"; +import { pamAccessPolicyFactory } from "./pam-access/pam-access-policy-factory"; + +type TApprovalPolicyFactoryImplementation = TApprovalResourceFactory< + TApprovalPolicyInputs, + TApprovalPolicy, + TApprovalRequestData +>; + +export const APPROVAL_POLICY_FACTORY_MAP: Record = { + [ApprovalPolicyType.PamAccess]: pamAccessPolicyFactory as TApprovalPolicyFactoryImplementation +}; diff --git a/backend/src/services/approval-policy/approval-policy-schemas.ts b/backend/src/services/approval-policy/approval-policy-schemas.ts new file mode 100644 index 000000000..d912e3245 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-schemas.ts @@ -0,0 +1,95 @@ +import { z } from "zod"; + +import { + ApprovalPoliciesSchema, + ApprovalRequestApprovalsSchema, + ApprovalRequestGrantsSchema, + ApprovalRequestsSchema, + ApprovalRequestStepsSchema +} from "@app/db/schemas"; +import { ms } from "@app/lib/ms"; + +import { ApproverType } from "./approval-policy-enums"; + +const ApprovalPolicyStepSchema = z.object({ + name: z.string().min(1).max(128).nullable().optional(), + requiredApprovals: z.number().min(1).max(100), + notifyApprovers: z.boolean().nullable().optional(), + approvers: z + .object({ + type: z.nativeEnum(ApproverType), + id: z.string().uuid() + }) + .array() +}); + +const MaxRequestTtlSchema = z.string().refine( + (val) => { + const duration = ms(val) / 1000; + + // 1 hour to 30 days + return duration >= 3600 && duration <= 2592000; + }, + { message: "Duration must be between 1 hour and 30 days" } +); + +// Policy +export const BaseApprovalPolicySchema = ApprovalPoliciesSchema.extend({ + steps: ApprovalPolicyStepSchema.array() +}); + +export const BaseCreateApprovalPolicySchema = z.object({ + projectId: z.string().uuid(), + name: z.string().min(1).max(128), + maxRequestTtl: MaxRequestTtlSchema.nullable().optional(), + steps: ApprovalPolicyStepSchema.array() +}); + +export const BaseUpdateApprovalPolicySchema = z.object({ + name: z.string().min(1).max(128).optional(), + maxRequestTtl: MaxRequestTtlSchema.nullable().optional(), + steps: ApprovalPolicyStepSchema.array().optional() +}); + +// Request +const ApprovalRequestStepSchema = ApprovalRequestStepsSchema.extend({ + name: z.string().min(1).max(128).nullable().optional(), + requiredApprovals: z.number().min(1).max(100), + notifyApprovers: z.boolean().nullable().optional(), + stepNumber: z.number(), + status: z.string(), + startedAt: z.date().nullable().optional(), + completedAt: z.date().nullable().optional(), + approvers: z + .object({ + type: z.nativeEnum(ApproverType), + id: z.string().uuid() + }) + .array(), + approvals: ApprovalRequestApprovalsSchema.array() +}); + +export const BaseApprovalRequestSchema = ApprovalRequestsSchema.extend({ + steps: ApprovalRequestStepSchema.array() +}); + +export const BaseCreateApprovalRequestSchema = z.object({ + projectId: z.string().uuid(), + justification: z.string().max(256).nullable().optional(), + requestDuration: z + .string() + .refine( + (val) => { + const duration = ms(val) / 1000; + + // 1 minute to 30 days + return duration >= 60 && duration <= 2592000; + }, + { message: "Duration must be between 1 minute and 30 days" } + ) + .nullable() + .optional() +}); + +// Grants +export const BaseApprovalRequestGrantSchema = ApprovalRequestGrantsSchema; diff --git a/backend/src/services/approval-policy/approval-policy-service.ts b/backend/src/services/approval-policy/approval-policy-service.ts new file mode 100644 index 000000000..80fa820ac --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-service.ts @@ -0,0 +1,902 @@ +import { ForbiddenError } from "@casl/ability"; + +import { ActionProjectType, ProjectMembershipRole, TApprovalPolicies, TApprovalRequests } from "@app/db/schemas"; +import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; +import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; +import { + ProjectPermissionApprovalRequestActions, + ProjectPermissionApprovalRequestGrantActions, + ProjectPermissionSub +} from "@app/ee/services/permission/project-permission"; +import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { ms } from "@app/lib/ms"; +import { OrgServiceActor } from "@app/lib/types"; +import { TNotificationServiceFactory } from "@app/services/notification/notification-service"; +import { NotificationType } from "@app/services/notification/notification-types"; + +import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; +import { + TApprovalPolicyDALFactory, + TApprovalPolicyStepApproversDALFactory, + TApprovalPolicyStepsDALFactory +} from "./approval-policy-dal"; +import { + ApprovalPolicyType, + ApprovalRequestApprovalDecision, + ApprovalRequestGrantStatus, + ApprovalRequestStatus, + ApprovalRequestStepStatus, + ApproverType +} from "./approval-policy-enums"; +import { APPROVAL_POLICY_FACTORY_MAP } from "./approval-policy-factory"; +import { + ApprovalPolicyStep, + TApprovalRequest, + TCreatePolicyDTO, + TCreateRequestDTO, + TUpdatePolicyDTO +} from "./approval-policy-types"; +import { + TApprovalRequestApprovalsDALFactory, + TApprovalRequestDALFactory, + TApprovalRequestGrantsDALFactory, + TApprovalRequestStepEligibleApproversDALFactory, + TApprovalRequestStepsDALFactory +} from "./approval-request-dal"; + +type TApprovalPolicyServiceFactoryDep = { + approvalPolicyDAL: TApprovalPolicyDALFactory; + approvalPolicyStepsDAL: TApprovalPolicyStepsDALFactory; + approvalPolicyStepApproversDAL: TApprovalPolicyStepApproversDALFactory; + approvalRequestApprovalsDAL: TApprovalRequestApprovalsDALFactory; + approvalRequestDAL: TApprovalRequestDALFactory; + approvalRequestStepsDAL: TApprovalRequestStepsDALFactory; + approvalRequestStepEligibleApproversDAL: TApprovalRequestStepEligibleApproversDALFactory; + approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory; + userGroupMembershipDAL: TUserGroupMembershipDALFactory; + notificationService: TNotificationServiceFactory; + permissionService: Pick; + projectMembershipDAL: Pick; +}; +export type TApprovalPolicyServiceFactory = ReturnType; + +export const approvalPolicyServiceFactory = ({ + approvalPolicyDAL, + approvalPolicyStepsDAL, + approvalPolicyStepApproversDAL, + approvalRequestApprovalsDAL, + approvalRequestDAL, + approvalRequestStepsDAL, + approvalRequestStepEligibleApproversDAL, + approvalRequestGrantsDAL, + userGroupMembershipDAL, + notificationService, + permissionService, + projectMembershipDAL +}: TApprovalPolicyServiceFactoryDep) => { + const $notifyApproversForStep = async (step: ApprovalPolicyStep, request: TApprovalRequests) => { + if (!step.notifyApprovers) return; + + const userIdsToNotify = new Set(); + + for await (const approver of step.approvers) { + if (approver.type === ApproverType.User) { + userIdsToNotify.add(approver.id); + } else if (approver.type === ApproverType.Group) { + const members = await userGroupMembershipDAL.find({ groupId: approver.id }); + members.forEach((member) => userIdsToNotify.add(member.userId)); + } + } + + if (userIdsToNotify.size === 0) return; + + // TODO: Potentially link to requests in the future to support click redirects + await notificationService.createUserNotifications( + Array.from(userIdsToNotify).map((userId) => ({ + userId, + orgId: request.organizationId, + type: NotificationType.APPROVAL_REQUIRED, + title: "Approval Required", + body: `You have a new approval request for ${request.type} from ${request.requesterName}.` + })) + ); + }; + + const $verifyProjectUserMembership = async (userIds: string[], orgId: string, projectId: string) => { + const uniqueUserIds = [...new Set(userIds)]; + if (uniqueUserIds.length === 0) return; + + const allMemberships = await projectMembershipDAL.findProjectMembershipsByUserIds(orgId, uniqueUserIds); + const projectMemberships = allMemberships.filter((membership) => membership.projectId === projectId); + + if (projectMemberships.length !== uniqueUserIds.length) { + const projectMemberUserIds = new Set(projectMemberships.map((membership) => membership.userId)); + const userIdsNotInProject = uniqueUserIds.filter((id) => !projectMemberUserIds.has(id)); + throw new BadRequestError({ + message: `Some users are not members of the project: ${userIdsNotInProject.join(", ")}` + }); + } + }; + + const create = async ( + policyType: ApprovalPolicyType, + { projectId, name, maxRequestTtl, conditions, constraints, steps }: TCreatePolicyDTO, + actor: OrgServiceActor + ) => { + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + // Verify all users are part of project + const approverUserIds = steps + .flatMap((step) => step.approvers ?? []) + .filter((approver) => approver.type === ApproverType.User) + .map((approver) => approver.id); + await $verifyProjectUserMembership(approverUserIds, actor.orgId, projectId); + + const policy = await approvalPolicyDAL.transaction(async (tx) => { + const newPolicy = await approvalPolicyDAL.create( + { + projectId, + organizationId: actor.orgId, + name, + maxRequestTtl, + conditions: { version: 1, conditions }, + constraints: { version: 1, constraints }, + type: policyType + }, + tx + ); + + // Create policy steps and their approvers + await Promise.all( + steps.map(async (step, i) => { + const newStep = await approvalPolicyStepsDAL.create( + { + policyId: newPolicy.id, + requiredApprovals: step.requiredApprovals, + stepNumber: i + 1, + name: step.name, + notifyApprovers: step.notifyApprovers + }, + tx + ); + + if (step.approvers?.length) { + await Promise.all( + step.approvers.map((approver) => + approvalPolicyStepApproversDAL.create( + { + policyStepId: newStep.id, + userId: approver.type === ApproverType.User ? approver.id : null, + groupId: approver.type === ApproverType.Group ? approver.id : null + }, + tx + ) + ) + ); + } + }) + ); + + return newPolicy; + }); + + return { + policy: { ...policy, steps } + }; + }; + + const list = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + const policies = await approvalPolicyDAL.findByProjectId(policyType, projectId); + + return { policies }; + }; + + const getById = async (policyId: string, actor: OrgServiceActor) => { + const policy = await approvalPolicyDAL.findById(policyId); + if (!policy) { + throw new ForbiddenRequestError({ message: "Policy not found" }); + } + + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: policy.projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + const steps = await approvalPolicyDAL.findStepsByPolicyId(policyId); + + return { policy: { ...policy, steps } }; + }; + + const updateById = async ( + policyId: string, + { name, maxRequestTtl, conditions, constraints, steps }: TUpdatePolicyDTO, + actor: OrgServiceActor + ) => { + const policy = await approvalPolicyDAL.findById(policyId); + if (!policy) { + throw new ForbiddenRequestError({ message: "Policy not found" }); + } + + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: policy.projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + if (steps !== undefined) { + // Verify all users are part of project + const approverUserIds = steps + .flatMap((step) => step.approvers ?? []) + .filter((approver) => approver.type === ApproverType.User) + .map((approver) => approver.id); + await $verifyProjectUserMembership(approverUserIds, actor.orgId, policy.projectId); + } + + const updatedPolicy = await approvalPolicyDAL.transaction(async (tx) => { + const updateDoc: Partial = {}; + + if (name !== undefined) { + updateDoc.name = name; + } + + if (maxRequestTtl !== undefined) { + updateDoc.maxRequestTtl = maxRequestTtl; + } + + if (conditions !== undefined) { + updateDoc.conditions = { version: 1, conditions }; + } + + if (constraints !== undefined) { + updateDoc.constraints = { version: 1, constraints }; + } + + const updated = await approvalPolicyDAL.updateById(policyId, updateDoc, tx); + + if (steps !== undefined) { + await approvalPolicyStepsDAL.delete({ policyId }, tx); + + await Promise.all( + steps.map(async (step, i) => { + const newStep = await approvalPolicyStepsDAL.create( + { + policyId, + requiredApprovals: step.requiredApprovals, + stepNumber: i + 1, + name: step.name, + notifyApprovers: step.notifyApprovers + }, + tx + ); + + if (step.approvers?.length) { + await Promise.all( + step.approvers.map((approver) => + approvalPolicyStepApproversDAL.create( + { + policyStepId: newStep.id, + userId: approver.type === ApproverType.User ? approver.id : null, + groupId: approver.type === ApproverType.Group ? approver.id : null + }, + tx + ) + ) + ); + } + }) + ); + } + return updated; + }); + + const fetchedSteps = await approvalPolicyDAL.findStepsByPolicyId(policyId); + + return { + policy: { ...updatedPolicy, steps: fetchedSteps } + }; + }; + + const deleteById = async (policyId: string, actor: OrgServiceActor) => { + const policy = await approvalPolicyDAL.findById(policyId); + if (!policy) { + throw new ForbiddenRequestError({ message: "Policy not found" }); + } + + const { hasRole } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: policy.projectId, + actionProjectType: ActionProjectType.Any + }); + + if (!hasRole(ProjectMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "User has insufficient privileges" }); + } + + await approvalPolicyDAL.deleteById(policyId); + + return { + policyId, + projectId: policy.projectId + }; + }; + + const createRequest = async ( + policyType: ApprovalPolicyType, + { + projectId, + requestData, + requestDuration, + justification, + requesterName, + requesterEmail + }: TCreateRequestDTO & { + requesterName: string; + requesterEmail: string; + }, + actor: OrgServiceActor + ) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestActions.Create, + ProjectPermissionSub.ApprovalRequests + ); + + const fac = APPROVAL_POLICY_FACTORY_MAP[policyType](policyType); + + const policy = await fac.matchPolicy(approvalPolicyDAL, projectId, requestData); + + if (!policy) { + throw new ForbiddenRequestError({ + message: "No policies match the requested resource, you can access it without a request" + }); + } + + const constraintValidation = fac.validateConstraints(policy, requestData); + if (!constraintValidation.valid) { + const errorMessage = constraintValidation.errors + ? `Policy constraints not met: ${constraintValidation.errors.join("; ")}` + : "Policy constraints not met"; + throw new ForbiddenRequestError({ message: errorMessage }); + } + + let expiresAt: Date | undefined; + + if (requestDuration) { + const ttlMs = ms(requestDuration); + + expiresAt = new Date(Date.now() + ttlMs); + + if (policy.maxRequestTtl) { + const maxTtlMs = ms(policy.maxRequestTtl); + if (ttlMs > maxTtlMs) { + throw new BadRequestError({ + message: `Expiration time exceeds the maximum allowed TTL of ${policy.maxRequestTtl}` + }); + } + } + } + + const { request, steps } = await approvalRequestDAL.transaction(async (tx) => { + const newRequest = await approvalRequestDAL.create( + { + projectId, + organizationId: actor.orgId, + policyId: policy.id, + requesterId: actor.id, + requesterName, + requesterEmail, + type: policyType, + status: ApprovalRequestStatus.Pending, + justification, + currentStep: 1, + requestData: { version: 1, requestData }, + expiresAt + }, + tx + ); + + const newSteps = await Promise.all( + policy.steps.map(async (step, i) => { + const stepNum = i + 1; + const newStep = await approvalRequestStepsDAL.create( + { + requestId: newRequest.id, + stepNumber: stepNum, + name: step.name, + status: stepNum === 1 ? ApprovalRequestStepStatus.InProgress : ApprovalRequestStepStatus.Pending, + requiredApprovals: step.requiredApprovals, + notifyApprovers: step.notifyApprovers, + startedAt: stepNum === 1 ? new Date() : null + }, + tx + ); + + await Promise.all( + step.approvers.map((approver) => + approvalRequestStepEligibleApproversDAL.create( + { + stepId: newStep.id, + userId: approver.type === ApproverType.User ? approver.id : null, + groupId: approver.type === ApproverType.Group ? approver.id : null + }, + tx + ) + ) + ); + + return { + ...newStep, + approvers: step.approvers, + approvals: [] + }; + }) + ); + + return { request: newRequest, steps: newSteps }; + }); + + if (steps.length > 0) { + await $notifyApproversForStep(steps[0], request); + } + + return { + request: { ...request, steps } + }; + }; + + const getRequestById = async (requestId: string, actor: OrgServiceActor) => { + const request = await approvalRequestDAL.findById(requestId); + if (!request) { + throw new ForbiddenRequestError({ message: "Request not found" }); + } + + const steps = await approvalRequestDAL.findStepsByRequestId(requestId); + + const isRequester = request.requesterId === actor.id; + + // Check if user is an eligible approver for any step + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const isApprover = steps.some((step) => + step.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ) + ); + + // If user is requester or approver, allow access regardless of role permission + if (!isRequester && !isApprover) { + // Otherwise, check role permission + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: request.projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestActions.Read, + ProjectPermissionSub.ApprovalRequests + ); + } + + return { + request: { ...request, steps } + }; + }; + + const approveRequest = async (requestId: string, { comment }: { comment?: string }, actor: OrgServiceActor) => { + const request = await approvalRequestDAL.findById(requestId); + if (!request) { + throw new ForbiddenRequestError({ message: "Request not found" }); + } + + if (request.status !== ApprovalRequestStatus.Pending) { + throw new BadRequestError({ message: "Request is not pending" }); + } + + if (request.expiresAt && new Date(request.expiresAt) < new Date()) { + await approvalRequestDAL.updateById(requestId, { status: ApprovalRequestStatus.Expired }); + throw new BadRequestError({ message: "Request has expired" }); + } + + const steps = await approvalRequestDAL.findStepsByRequestId(requestId); + const currentStepIndex = steps.findIndex((s) => s.stepNumber === request.currentStep); + if (currentStepIndex === -1) { + throw new BadRequestError({ message: "Current step not found" }); + } + + const currentStep = steps[currentStepIndex]; + + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const isEligible = currentStep.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ); + + if (!isEligible) { + throw new ForbiddenRequestError({ message: "You are not an eligible approver for this step" }); + } + + const hasApproved = currentStep.approvals.some((a) => a.approverUserId === actor.id); + if (hasApproved) { + throw new BadRequestError({ message: "You have already approved this request" }); + } + + const { updatedRequest, nextStepToNotify } = await approvalRequestDAL.transaction(async (tx) => { + let nextStepToNotifyInner = null; + + // Create approval + await approvalRequestApprovalsDAL.create( + { + stepId: currentStep.id, + approverUserId: actor.id, + decision: ApprovalRequestApprovalDecision.Approved, + comment + }, + tx + ); + + const newApprovalCount = currentStep.approvals.length + 1; + if (newApprovalCount >= currentStep.requiredApprovals) { + // Step completed + await approvalRequestStepsDAL.updateById( + currentStep.id, + { + status: ApprovalRequestStepStatus.Completed, + completedAt: new Date() + }, + tx + ); + + const nextStep = steps[currentStepIndex + 1]; + if (nextStep) { + // Move to next step + await approvalRequestDAL.updateById( + requestId, + { + currentStep: request.currentStep + 1 + }, + tx + ); + + await approvalRequestStepsDAL.updateById( + nextStep.id, + { + status: ApprovalRequestStepStatus.InProgress, + startedAt: new Date() + }, + tx + ); + + if (nextStep.notifyApprovers) { + nextStepToNotifyInner = nextStep; + } + } else { + // All steps completed + const completedReq = await approvalRequestDAL.updateById( + requestId, + { + status: ApprovalRequestStatus.Approved + }, + tx + ); + + return { updatedRequest: completedReq, nextStepToNotify: null }; + } + } + + return { updatedRequest: request, nextStepToNotify: nextStepToNotifyInner }; + }); + + if (nextStepToNotify) { + await $notifyApproversForStep(nextStepToNotify, updatedRequest); + } + + // Fetch fresh state + const finalSteps = await approvalRequestDAL.findStepsByRequestId(requestId); + const finalRequest = await approvalRequestDAL.findById(requestId); + + const newRequest = { ...finalRequest, steps: finalSteps }; + + if (updatedRequest.status === ApprovalRequestStatus.Approved) { + const fac = APPROVAL_POLICY_FACTORY_MAP[updatedRequest.type as ApprovalPolicyType]( + updatedRequest.type as ApprovalPolicyType + ); + await fac.postApprovalRoutine(approvalRequestGrantsDAL, newRequest as TApprovalRequest); + } + + return { request: newRequest }; + }; + + const rejectRequest = async (requestId: string, { comment }: { comment?: string }, actor: OrgServiceActor) => { + const request = await approvalRequestDAL.findById(requestId); + if (!request) { + throw new ForbiddenRequestError({ message: "Request not found" }); + } + + if (request.status !== ApprovalRequestStatus.Pending) { + throw new BadRequestError({ message: "Request is not pending" }); + } + + if (request.expiresAt && new Date(request.expiresAt) < new Date()) { + await approvalRequestDAL.updateById(requestId, { status: ApprovalRequestStatus.Expired }); + throw new BadRequestError({ message: "Request has expired" }); + } + + const steps = await approvalRequestDAL.findStepsByRequestId(requestId); + const currentStep = steps.find((s) => s.stepNumber === request.currentStep); + + if (!currentStep) { + throw new BadRequestError({ message: "Current step not found" }); + } + + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const isEligible = currentStep.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ); + + if (!isEligible) { + throw new ForbiddenRequestError({ message: "You are not an eligible approver for this step" }); + } + + await approvalRequestDAL.transaction(async (tx) => { + await approvalRequestApprovalsDAL.create( + { + stepId: currentStep.id, + approverUserId: actor.id, + decision: ApprovalRequestApprovalDecision.Rejected, + comment + }, + tx + ); + + await approvalRequestDAL.updateById( + requestId, + { + status: ApprovalRequestStatus.Rejected + }, + tx + ); + }); + + const finalSteps = await approvalRequestDAL.findStepsByRequestId(requestId); + const finalRequest = await approvalRequestDAL.findById(requestId); + + return { request: { ...finalRequest, steps: finalSteps } }; + }; + + const listRequests = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + const hasReadPermission = permission.can( + ProjectPermissionApprovalRequestActions.Read, + ProjectPermissionSub.ApprovalRequests + ); + + const requests = await approvalRequestDAL.findByProjectId(policyType, projectId); + + // If user has read permission, return all requests + if (hasReadPermission) { + return { requests }; + } + + // Otherwise, filter to only requests where user is requester or approver + const userGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(actor.id, actor.orgId); + const userGroupIds = new Set(userGroups.map((g) => g.groupId)); + + const filteredRequests = []; + for (const request of requests) { + const isRequester = request.requesterId === actor.id; + + if (isRequester) { + filteredRequests.push(request); + // eslint-disable-next-line no-continue + continue; + } + + // Check if user is an eligible approver for any step + const isApprover = request.steps.some((step) => + step.approvers.some( + (approver) => + (approver.type === ApproverType.User && approver.id === actor.id) || + (approver.type === ApproverType.Group && userGroupIds.has(approver.id)) + ) + ); + + if (isApprover) { + filteredRequests.push(request); + } + } + + return { requests: filteredRequests }; + }; + + const cancelRequest = async (requestId: string, actor: OrgServiceActor) => { + const request = await approvalRequestDAL.findById(requestId); + if (!request) { + throw new ForbiddenRequestError({ message: "Request not found" }); + } + + if (request.status !== ApprovalRequestStatus.Pending) { + throw new BadRequestError({ message: "Request is not pending" }); + } + + if (request.requesterId !== actor.id) { + throw new ForbiddenRequestError({ message: "You are not the requester of this request" }); + } + + const updatedRequest = await approvalRequestDAL.updateById(requestId, { + status: ApprovalRequestStatus.Cancelled + }); + + const steps = await approvalRequestDAL.findStepsByRequestId(requestId); + + return { request: { ...updatedRequest, steps } }; + }; + + const listGrants = async (policyType: ApprovalPolicyType, projectId: string, actor: OrgServiceActor) => { + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestGrantActions.Read, + ProjectPermissionSub.ApprovalRequestGrants + ); + + const grants = await approvalRequestGrantsDAL.find({ projectId, type: policyType }); + return { grants }; + }; + + const getGrantById = async (grantId: string, actor: OrgServiceActor) => { + const grant = await approvalRequestGrantsDAL.findById(grantId); + if (!grant) { + throw new NotFoundError({ message: "Grant not found" }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: grant.projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestGrantActions.Read, + ProjectPermissionSub.ApprovalRequestGrants + ); + + return { grant }; + }; + + const revokeGrant = async ( + grantId: string, + { revocationReason }: { revocationReason?: string }, + actor: OrgServiceActor + ) => { + const grant = await approvalRequestGrantsDAL.findById(grantId); + if (!grant) { + throw new NotFoundError({ message: "Grant not found" }); + } + + const { permission } = await permissionService.getProjectPermission({ + actor: actor.type, + actorAuthMethod: actor.authMethod, + actorId: actor.id, + actorOrgId: actor.orgId, + projectId: grant.projectId, + actionProjectType: ActionProjectType.Any + }); + + ForbiddenError.from(permission).throwUnlessCan( + ProjectPermissionApprovalRequestGrantActions.Revoke, + ProjectPermissionSub.ApprovalRequestGrants + ); + + if (grant.status !== ApprovalRequestGrantStatus.Active) { + throw new BadRequestError({ message: "Grant is not active" }); + } + + const updatedGrant = await approvalRequestGrantsDAL.updateById(grantId, { + status: ApprovalRequestGrantStatus.Revoked, + revokedAt: new Date(), + revokedByUserId: actor.id, + revocationReason + }); + + return { grant: updatedGrant }; + }; + + return { + create, + list, + getById, + updateById, + deleteById, + createRequest, + listRequests, + getRequestById, + approveRequest, + rejectRequest, + cancelRequest, + listGrants, + getGrantById, + revokeGrant + }; +}; diff --git a/backend/src/services/approval-policy/approval-policy-types.ts b/backend/src/services/approval-policy/approval-policy-types.ts new file mode 100644 index 000000000..8dccc1453 --- /dev/null +++ b/backend/src/services/approval-policy/approval-policy-types.ts @@ -0,0 +1,88 @@ +import { TApprovalPolicyDALFactory } from "@app/services/approval-policy/approval-policy-dal"; +import { TApprovalRequestGrantsDALFactory } from "@app/services/approval-policy/approval-request-dal"; + +import { ApprovalPolicyType, ApproverType } from "./approval-policy-enums"; +import { + TPamAccessPolicy, + TPamAccessPolicyConditions, + TPamAccessPolicyConstraints, + TPamAccessPolicyInputs, + TPamAccessRequest, + TPamAccessRequestData +} from "./pam-access/pam-access-policy-types"; + +export type TApprovalPolicy = TPamAccessPolicy; +export type TApprovalPolicyInputs = TPamAccessPolicyInputs; +export type TApprovalPolicyConditions = TPamAccessPolicyConditions; +export type TApprovalPolicyConstraints = TPamAccessPolicyConstraints; + +export type TApprovalRequest = TPamAccessRequest; +export type TApprovalRequestData = TPamAccessRequestData; + +export interface ApprovalPolicyStep { + name?: string | null; + requiredApprovals: number; + notifyApprovers?: boolean | null; + approvers: { + type: ApproverType; + id: string; + }[]; +} + +// Policy DTOs +export interface TCreatePolicyDTO { + projectId: TApprovalPolicy["projectId"]; + name: TApprovalPolicy["name"]; + maxRequestTtl?: TApprovalPolicy["maxRequestTtl"]; + conditions: TApprovalPolicy["conditions"]["conditions"]; + constraints: TApprovalPolicy["constraints"]["constraints"]; + steps: ApprovalPolicyStep[]; +} + +export interface TUpdatePolicyDTO { + name?: TApprovalPolicy["name"]; + maxRequestTtl?: TApprovalPolicy["maxRequestTtl"]; + conditions?: TApprovalPolicy["conditions"]["conditions"]; + constraints?: TApprovalPolicy["constraints"]["constraints"]; + steps?: ApprovalPolicyStep[]; +} + +// Request DTOs +export interface TCreateRequestDTO { + projectId: TApprovalRequest["projectId"]; + requestData: TApprovalRequest["requestData"]["requestData"]; + justification?: TApprovalRequest["justification"]; + requestDuration?: string | null; +} + +// Factory +export type TApprovalRequestFactoryMatchPolicy = ( + approvalPolicyDAL: TApprovalPolicyDALFactory, + projectId: string, + inputs: I +) => Promise

; +export type TApprovalRequestFactoryCanAccess = ( + approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory, + projectId: string, + userId: string, + inputs: I +) => Promise; +export type TApprovalRequestFactoryValidateConstraints

= ( + policy: P, + inputs: R +) => { valid: boolean; errors?: string[] }; +export type TApprovalRequestFactoryPostApprovalRoutine = ( + approvalRequestGrantsDAL: TApprovalRequestGrantsDALFactory, + request: TApprovalRequest +) => Promise; + +export type TApprovalResourceFactory< + I extends TApprovalPolicyInputs, + P extends TApprovalPolicy, + R extends TApprovalRequestData +> = (policyType: ApprovalPolicyType) => { + matchPolicy: TApprovalRequestFactoryMatchPolicy; + canAccess: TApprovalRequestFactoryCanAccess; + validateConstraints: TApprovalRequestFactoryValidateConstraints; + postApprovalRoutine: TApprovalRequestFactoryPostApprovalRoutine; +}; diff --git a/backend/src/services/approval-policy/approval-request-dal.ts b/backend/src/services/approval-policy/approval-request-dal.ts new file mode 100644 index 000000000..ced8bea41 --- /dev/null +++ b/backend/src/services/approval-policy/approval-request-dal.ts @@ -0,0 +1,199 @@ +import { TDbClient } from "@app/db"; +import { TableName, TApprovalRequestApprovals } from "@app/db/schemas"; +import { DatabaseError } from "@app/lib/errors"; +import { ormify } from "@app/lib/knex"; + +import { + ApprovalPolicyType, + ApprovalRequestGrantStatus, + ApprovalRequestStatus, + ApproverType +} from "./approval-policy-enums"; +import { ApprovalPolicyStep } from "./approval-policy-types"; + +// Approval Request +export type TApprovalRequestDALFactory = ReturnType; +export const approvalRequestDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequests); + + const findStepsByRequestId = async (requestId: string) => { + try { + const dbInstance = db.replicaNode(); + const steps = await dbInstance(TableName.ApprovalRequestSteps).where({ requestId }).orderBy("stepNumber", "asc"); + + if (!steps.length) { + return []; + } + + const stepIds = steps.map((step) => step.id); + + const [approvers, approvals] = await Promise.all([ + dbInstance(TableName.ApprovalRequestStepEligibleApprovers) + .whereIn("stepId", stepIds) + .select("stepId", "userId", "groupId"), + dbInstance(TableName.ApprovalRequestApprovals).whereIn("stepId", stepIds) + ]); + + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.stepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.stepId] = stepApprovers; + return acc; + }, + {} + ); + + const approvalsByStepId = approvals.reduce>((acc, approval) => { + const stepApprovals = acc[approval.stepId] || []; + stepApprovals.push(approval); + acc[approval.stepId] = stepApprovals; + return acc; + }, {}); + + return steps.map((step) => { + return { + ...step, + approvers: approversByStepId[step.id] || [], + approvals: approvalsByStepId[step.id] || [] + }; + }); + } catch (error) { + throw new DatabaseError({ error, name: "Find approval request steps" }); + } + }; + + const findByProjectId = async (policyType: ApprovalPolicyType, projectId: string) => { + try { + const dbInstance = db.replicaNode(); + const requests = await dbInstance(TableName.ApprovalRequests).where({ type: policyType, projectId }); + + if (!requests.length) { + return []; + } + + const requestIds = requests.map((req) => req.id); + + const steps = await dbInstance(TableName.ApprovalRequestSteps) + .whereIn("requestId", requestIds) + .orderBy("stepNumber", "asc"); + + const stepsByRequestId: Record = {}; + + if (steps.length) { + const stepIds = steps.map((step) => step.id); + + const [approvers, approvals] = await Promise.all([ + dbInstance(TableName.ApprovalRequestStepEligibleApprovers) + .whereIn("stepId", stepIds) + .select("stepId", "userId", "groupId"), + dbInstance(TableName.ApprovalRequestApprovals).whereIn("stepId", stepIds) + ]); + + const approversByStepId = approvers.reduce>( + (acc, approver) => { + const stepApprovers = acc[approver.stepId] || []; + stepApprovers.push({ + type: approver.userId ? ApproverType.User : ApproverType.Group, + id: (approver.userId || approver.groupId) as string + }); + acc[approver.stepId] = stepApprovers; + return acc; + }, + {} + ); + + const approvalsByStepId = approvals.reduce>((acc, approval) => { + const stepApprovals = acc[approval.stepId] || []; + stepApprovals.push(approval); + acc[approval.stepId] = stepApprovals; + return acc; + }, {}); + + steps.forEach((step) => { + const formattedStep = { + ...step, + approvers: approversByStepId[step.id] || [], + approvals: approvalsByStepId[step.id] || [] + }; + + if (!stepsByRequestId[step.requestId]) { + stepsByRequestId[step.requestId] = []; + } + stepsByRequestId[step.requestId].push(formattedStep); + }); + } + + return requests.map((req) => ({ + ...req, + steps: stepsByRequestId[req.id] || [] + })); + } catch (error) { + throw new DatabaseError({ error, name: "Find approval requests by project id" }); + } + }; + + const markExpiredRequests = async () => { + try { + const result = await db(TableName.ApprovalRequests) + .where("status", ApprovalRequestStatus.Pending) + .whereNotNull("expiresAt") + .where("expiresAt", "<", new Date()) + .update({ status: ApprovalRequestStatus.Expired }); + + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Mark expired approval requests" }); + } + }; + + return { ...orm, findStepsByRequestId, findByProjectId, markExpiredRequests }; +}; + +// Approval Request Steps +export type TApprovalRequestStepsDALFactory = ReturnType; +export const approvalRequestStepsDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequestSteps); + return orm; +}; + +// Approval Request Step Eligible Approvers +export type TApprovalRequestStepEligibleApproversDALFactory = ReturnType< + typeof approvalRequestStepEligibleApproversDALFactory +>; +export const approvalRequestStepEligibleApproversDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequestStepEligibleApprovers); + return orm; +}; + +// Approval Request Grants +export type TApprovalRequestGrantsDALFactory = ReturnType; +export const approvalRequestGrantsDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequestGrants); + + const markExpiredGrants = async () => { + try { + const result = await db(TableName.ApprovalRequestGrants) + .where("status", ApprovalRequestGrantStatus.Active) + .whereNotNull("expiresAt") + .where("expiresAt", "<", new Date()) + .update({ status: ApprovalRequestGrantStatus.Expired }); + + return result; + } catch (error) { + throw new DatabaseError({ error, name: "Mark expired approval grants" }); + } + }; + + return { ...orm, markExpiredGrants }; +}; + +// Approval Request Approvals +export type TApprovalRequestApprovalsDALFactory = ReturnType; +export const approvalRequestApprovalsDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.ApprovalRequestApprovals); + return orm; +}; diff --git a/backend/src/services/approval-policy/pam-access/pam-access-policy-factory.ts b/backend/src/services/approval-policy/pam-access/pam-access-policy-factory.ts new file mode 100644 index 000000000..21b97e0d1 --- /dev/null +++ b/backend/src/services/approval-policy/pam-access/pam-access-policy-factory.ts @@ -0,0 +1,127 @@ +import picomatch from "picomatch"; + +import { ms } from "@app/lib/ms"; + +import { ApprovalRequestGrantStatus } from "../approval-policy-enums"; +import { + TApprovalRequestFactoryCanAccess, + TApprovalRequestFactoryMatchPolicy, + TApprovalRequestFactoryPostApprovalRoutine, + TApprovalRequestFactoryValidateConstraints, + TApprovalResourceFactory +} from "../approval-policy-types"; +import { TPamAccessPolicy, TPamAccessPolicyInputs, TPamAccessRequestData } from "./pam-access-policy-types"; + +export const pamAccessPolicyFactory: TApprovalResourceFactory< + TPamAccessPolicyInputs, + TPamAccessPolicy, + TPamAccessRequestData +> = (policyType) => { + const matchPolicy: TApprovalRequestFactoryMatchPolicy = async ( + approvalPolicyDAL, + projectId, + inputs + ) => { + const policies = await approvalPolicyDAL.findByProjectId(policyType, projectId); + + let bestMatch: { policy: TPamAccessPolicy; wildcardCount: number; pathLength: number } | null = null; + + for (const policy of policies) { + const p = policy as TPamAccessPolicy; + for (const c of p.conditions.conditions) { + // Find the most specific path pattern + // TODO(andrey): Make matching logic more advanced by accounting for wildcard positions + for (const pathPattern of c.accountPaths) { + if (picomatch(pathPattern)(inputs.accountPath)) { + const wildcardCount = (pathPattern.match(/\*/g) || []).length; + const pathLength = pathPattern.length; + + if ( + !bestMatch || + wildcardCount < bestMatch.wildcardCount || + (wildcardCount === bestMatch.wildcardCount && pathLength > bestMatch.pathLength) + ) { + bestMatch = { policy: p, wildcardCount, pathLength }; + } + } + } + } + } + + return bestMatch?.policy || null; + }; + + const canAccess: TApprovalRequestFactoryCanAccess = async ( + approvalRequestGrantsDAL, + projectId, + userId, + inputs + ) => { + const grants = await approvalRequestGrantsDAL.find({ + granteeUserId: userId, + type: policyType, + status: ApprovalRequestGrantStatus.Active, + projectId, + revokedAt: null + }); + + // TODO(andrey): Move some of this check to be part of SQL query + return grants.some((grant) => { + const grantAttributes = grant.attributes as TPamAccessPolicyInputs; + const isMatch = picomatch(grantAttributes.accountPath); + return isMatch(inputs.accountPath) && (!grant.expiresAt || grant.expiresAt > new Date()); + }); + }; + + const validateConstraints: TApprovalRequestFactoryValidateConstraints = ( + policy, + inputs + ) => { + const reqDuration = ms(inputs.accessDuration); + const durationConstraint = policy.constraints.constraints.accessDuration; + const minDuration = ms(durationConstraint.min); + const maxDuration = ms(durationConstraint.max); + + const errors: string[] = []; + + if (reqDuration < minDuration) { + errors.push( + `Access duration ${inputs.accessDuration} is below the minimum allowed duration of ${durationConstraint.min}` + ); + } + + if (reqDuration > maxDuration) { + errors.push( + `Access duration ${inputs.accessDuration} exceeds the maximum allowed duration of ${durationConstraint.max}` + ); + } + + return { + valid: errors.length === 0, + errors: errors.length > 0 ? errors : undefined + }; + }; + + const postApprovalRoutine: TApprovalRequestFactoryPostApprovalRoutine = async (approvalRequestGrantsDAL, request) => { + const inputs = request.requestData.requestData; + const durationMs = ms(inputs.accessDuration); + const expiresAt = new Date(Date.now() + durationMs); + + await approvalRequestGrantsDAL.create({ + projectId: request.projectId, + requestId: request.id, + granteeUserId: request.requesterId, + status: ApprovalRequestGrantStatus.Active, + type: request.type, + attributes: inputs, + expiresAt + }); + }; + + return { + matchPolicy, + canAccess, + validateConstraints, + postApprovalRoutine + }; +}; diff --git a/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts b/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts new file mode 100644 index 000000000..caa2cfa67 --- /dev/null +++ b/backend/src/services/approval-policy/pam-access/pam-access-policy-schemas.ts @@ -0,0 +1,101 @@ +import picomatch from "picomatch"; +import { z } from "zod"; + +import { ms } from "@app/lib/ms"; + +import { + BaseApprovalPolicySchema, + BaseApprovalRequestGrantSchema, + BaseApprovalRequestSchema, + BaseCreateApprovalPolicySchema, + BaseCreateApprovalRequestSchema, + BaseUpdateApprovalPolicySchema +} from "../approval-policy-schemas"; + +// Inputs +export const PamAccessPolicyInputsSchema = z.object({ + accountPath: z.string() +}); + +// Conditions +export const PamAccessPolicyConditionsSchema = z + .object({ + accountPaths: z + .string() + .refine( + (el) => { + try { + picomatch.parse([el]); + return true; + } catch { + return false; + } + }, + { message: "Invalid glob pattern" } + ) + .array() + }) + .array(); + +const DurationSchema = z.string().refine( + (val) => { + const duration = ms(val) / 1000; + + // 30 seconds to 7 days + return duration >= 30 && duration <= 604800; + }, + { message: "Duration must be between 30 seconds and 7 days" } +); + +// Constraints +export const PamAccessPolicyConstraintsSchema = z.object({ + accessDuration: z.object({ + min: DurationSchema, + max: DurationSchema + }) +}); + +// Request Data +export const PamAccessPolicyRequestDataSchema = z.object({ + accountPath: z.string(), + accessDuration: DurationSchema +}); + +// Policy +export const PamAccessPolicySchema = BaseApprovalPolicySchema.extend({ + conditions: z.object({ + version: z.literal(1), + conditions: PamAccessPolicyConditionsSchema + }), + constraints: z.object({ + version: z.literal(1), + constraints: PamAccessPolicyConstraintsSchema + }) +}); + +export const CreatePamAccessPolicySchema = BaseCreateApprovalPolicySchema.extend({ + conditions: PamAccessPolicyConditionsSchema, + constraints: PamAccessPolicyConstraintsSchema +}); + +export const UpdatePamAccessPolicySchema = BaseUpdateApprovalPolicySchema.extend({ + conditions: PamAccessPolicyConditionsSchema.optional(), + constraints: PamAccessPolicyConstraintsSchema.optional() +}); + +// Request +export const PamAccessRequestSchema = BaseApprovalRequestSchema.extend({ + requestData: z.object({ + version: z.literal(1), + requestData: PamAccessPolicyRequestDataSchema + }) +}); + +export const CreatePamAccessRequestSchema = BaseCreateApprovalRequestSchema.extend({ + requestData: PamAccessPolicyRequestDataSchema +}); + +// Grants +export const PamAccessRequestGrantSchema = BaseApprovalRequestGrantSchema.extend({ + attributes: PamAccessPolicyRequestDataSchema +}); diff --git a/backend/src/services/approval-policy/pam-access/pam-access-policy-types.ts b/backend/src/services/approval-policy/pam-access/pam-access-policy-types.ts new file mode 100644 index 000000000..78118fcb0 --- /dev/null +++ b/backend/src/services/approval-policy/pam-access/pam-access-policy-types.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +import { + PamAccessPolicyConditionsSchema, + PamAccessPolicyConstraintsSchema, + PamAccessPolicyInputsSchema, + PamAccessPolicyRequestDataSchema, + PamAccessPolicySchema, + PamAccessRequestSchema +} from "./pam-access-policy-schemas"; + +// Policy +export type TPamAccessPolicy = z.infer; +export type TPamAccessPolicyInputs = z.infer; +export type TPamAccessPolicyConditions = z.infer; +export type TPamAccessPolicyConstraints = z.infer; + +// Request +export type TPamAccessRequest = z.infer; +export type TPamAccessRequestData = z.infer; diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index fb7213109..c090f02de 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -196,7 +196,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD }; // to parse jwt identity in inject identity plugin - const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, subOrganizationSelector?: string) => { + const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => { const session = await tokenDAL.findOneTokenSession({ id: token.tokenVersionId, userId: token.userId @@ -214,13 +214,17 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD let rootOrgId = ""; let parentOrgId = ""; if (token.organizationId) { - if (subOrganizationSelector) { + // Check if token has sub-organization scope + if (token.subOrganizationId) { const subOrganization = await orgDAL.findOne({ - rootOrgId: token.organizationId, - slug: subOrganizationSelector + id: token.subOrganizationId }); if (!subOrganization) - throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); + throw new BadRequestError({ message: `Sub organization ${token.subOrganizationId} not found` }); + // Verify the sub-org belongs to the token's root organization + if (subOrganization.rootOrgId !== token.organizationId && subOrganization.id !== token.organizationId) { + throw new ForbiddenRequestError({ message: "Sub-organization does not belong to the token's organization" }); + } const orgMembership = await membershipUserDAL.findOne({ actorUserId: user.id, diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 8e0f654a3..9f13d0ddd 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -13,7 +13,13 @@ import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { getConfig } from "@app/lib/config/env"; import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { getUserPrivateKey } from "@app/lib/crypto/srp"; -import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; +import { + BadRequestError, + DatabaseError, + ForbiddenRequestError, + NotFoundError, + UnauthorizedError +} from "@app/lib/errors"; import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn"; import { logger } from "@app/lib/logger"; import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; @@ -142,6 +148,7 @@ export const authLoginServiceFactory = ({ ip, userAgent, organizationId, + subOrganizationId, authMethod, isMfaVerified, mfaMethod @@ -150,6 +157,7 @@ export const authLoginServiceFactory = ({ ip: string; userAgent: string; organizationId?: string; + subOrganizationId?: string; authMethod: AuthMethod; isMfaVerified?: boolean; mfaMethod?: MfaMethod; @@ -193,6 +201,7 @@ export const authLoginServiceFactory = ({ tokenVersionId: tokenSession.id, accessVersion: tokenSession.accessVersion, organizationId, + subOrganizationId, isMfaVerified, mfaMethod }, @@ -208,6 +217,7 @@ export const authLoginServiceFactory = ({ tokenVersionId: tokenSession.id, refreshVersion: tokenSession.refreshVersion, organizationId, + subOrganizationId, isMfaVerified, mfaMethod }, @@ -526,33 +536,73 @@ export const authLoginServiceFactory = ({ const user = await userDAL.findUserEncKeyByUserId(decodedToken.userId); if (!user) throw new BadRequestError({ message: "User not found", name: "Find user from token" }); - // Check if the user actually has access to the specified organization. - const userOrgs = await orgDAL.findAllOrgsByUserId(user.id); + // Check user membership in the sub-organization + const orgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: organizationId, + scope: AccessScope.Organization, + status: OrgMembershipStatus.Accepted + }); - const selectedOrgMembership = userOrgs.find((org) => org.id === organizationId && org.userStatus !== "invited"); - - const selectedOrg = await orgDAL.findById(organizationId); - - if (!selectedOrgMembership) { + if (!orgMembership) { throw new ForbiddenRequestError({ - message: `User does not have access to the organization named ${selectedOrg?.name}` + message: `User does not have access to the organization with ID ${organizationId}` }); } - // Check if authEnforced is true and the current auth method is not an enforced method + const selectedOrg = await orgDAL.findById(organizationId); + if (!selectedOrg) { + throw new NotFoundError({ message: `Organization with ID '${organizationId}' not found` }); + } + + const isSubOrganization = Boolean(selectedOrg.rootOrgId && selectedOrg.id !== selectedOrg.rootOrgId); + + const membershipRole = (await membershipRoleDAL.findOne({ membershipId: orgMembership.id })).role; + + let rootOrg = selectedOrg; + + if (isSubOrganization) { + if (!selectedOrg.rootOrgId) { + throw new BadRequestError({ + message: "Invalid sub-organization" + }); + } + + rootOrg = await orgDAL.findById(selectedOrg.rootOrgId); + if (!rootOrg) { + throw new BadRequestError({ + message: "Invalid sub-organization" + }); + } + + // Check user membership in the root organization + const rootOrgMembership = await membershipUserDAL.findOne({ + actorUserId: user.id, + scopeOrgId: selectedOrg.rootOrgId, + scope: AccessScope.Organization, + status: OrgMembershipStatus.Accepted + }); + + if (!rootOrgMembership) { + throw new ForbiddenRequestError({ + message: "User does not have access to the root organization" + }); + } + } + if ( - selectedOrg.authEnforced && + rootOrg.authEnforced && !isAuthMethodSaml(decodedToken.authMethod) && decodedToken.authMethod !== AuthMethod.OIDC && - !(selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin) + !(rootOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin) ) { throw new BadRequestError({ message: "Login with the auth method required by your organization." }); } - if (selectedOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) { - const canBypass = selectedOrg.bypassOrgAuthEnabled && selectedOrgMembership.userRole === OrgMembershipRole.Admin; + if (rootOrg.googleSsoAuthEnforced && decodedToken.authMethod !== AuthMethod.GOOGLE) { + const canBypass = rootOrg.bypassOrgAuthEnabled && membershipRole === OrgMembershipRole.Admin; if (!canBypass) { throw new ForbiddenRequestError({ @@ -563,13 +613,13 @@ export const authLoginServiceFactory = ({ } if (decodedToken.authMethod === AuthMethod.GOOGLE) { - await orgDAL.updateById(selectedOrg.id, { + await orgDAL.updateById(rootOrg.id, { googleSsoAuthLastUsed: new Date() }); } - const shouldCheckMfa = selectedOrg.enforceMfa || user.isMfaEnabled; - const orgMfaMethod = selectedOrg.enforceMfa ? (selectedOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; + const shouldCheckMfa = rootOrg.enforceMfa || user.isMfaEnabled; + const orgMfaMethod = rootOrg.enforceMfa ? (rootOrg.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; const userMfaMethod = user.isMfaEnabled ? (user.selectedMfaMethod ?? MfaMethod.EMAIL) : undefined; const mfaMethod = orgMfaMethod ?? userMfaMethod; @@ -603,15 +653,16 @@ export const authLoginServiceFactory = ({ user, userAgent, ip: ipAddress, - organizationId, + organizationId: isSubOrganization ? rootOrg.id : organizationId, + subOrganizationId: isSubOrganization ? organizationId : undefined, isMfaVerified: decodedToken.isMfaVerified, mfaMethod: decodedToken.mfaMethod }); // In the event of this being a break-glass request (non-saml / non-oidc, when either is enforced) if ( - selectedOrg.authEnforced && - selectedOrg.bypassOrgAuthEnabled && + rootOrg.authEnforced && + rootOrg.bypassOrgAuthEnabled && !isAuthMethodSaml(decodedToken.authMethod) && decodedToken.authMethod !== AuthMethod.OIDC && decodedToken.authMethod !== AuthMethod.GOOGLE @@ -671,29 +722,55 @@ export const authLoginServiceFactory = ({ } } - await auditLogService.createAuditLog({ - orgId: organizationId, - ipAddress, - userAgent, - userAgentType: getUserAgentType(userAgent), - actor: { - type: ActorType.USER, - metadata: { - email: user.email, - userId: user.id, - username: user.username, - authMethod: decodedToken.authMethod + // Create audit log for organization selection + if (isSubOrganization) { + await auditLogService.createAuditLog({ + orgId: organizationId, + ipAddress, + userAgent, + userAgentType: getUserAgentType(userAgent), + actor: { + type: ActorType.USER, + metadata: { + email: user.email, + userId: user.id, + username: user.username, + authMethod: decodedToken.authMethod + } + }, + event: { + type: EventType.SELECT_SUB_ORGANIZATION, + metadata: { + organizationId, + organizationName: selectedOrg.name, + rootOrganizationId: selectedOrg.rootOrgId || "" + } } - }, - event: { - type: EventType.SELECT_ORGANIZATION, - metadata: { - organizationId, - organizationName: selectedOrg.name + }); + } else { + await auditLogService.createAuditLog({ + orgId: organizationId, + ipAddress, + userAgent, + userAgentType: getUserAgentType(userAgent), + actor: { + type: ActorType.USER, + metadata: { + email: user.email, + userId: user.id, + username: user.username, + authMethod: decodedToken.authMethod + } + }, + event: { + type: EventType.SELECT_ORGANIZATION, + metadata: { + organizationId, + organizationName: selectedOrg.name + } } - } - }); - + }); + } return { ...tokens, user, diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts index 14f4387b9..ae27b113d 100644 --- a/backend/src/services/auth/auth-signup-service.ts +++ b/backend/src/services/auth/auth-signup-service.ts @@ -258,13 +258,13 @@ export const authSignupServiceFactory = ({ let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME; if (organizationId) { - const org = await orgService.findOrganizationById( - user.id, - organizationId, - authMethod, - organizationId, - organizationId - ); + const org = await orgService.findOrganizationById({ + userId: user.id, + orgId: organizationId, + actorAuthMethod: authMethod, + actorOrgId: organizationId, + rootOrgId: organizationId + }); if (org && org.userTokenExpiration) { tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration); refreshTokenExpiresIn = org.userTokenExpiration; diff --git a/backend/src/services/auth/auth-type.ts b/backend/src/services/auth/auth-type.ts index ef54ac0be..9e692dcd3 100644 --- a/backend/src/services/auth/auth-type.ts +++ b/backend/src/services/auth/auth-type.ts @@ -55,6 +55,7 @@ export type AuthModeJwtTokenPayload = { tokenVersionId: string; accessVersion: number; organizationId?: string; + subOrganizationId?: string; isMfaVerified?: boolean; mfaMethod?: MfaMethod; }; @@ -74,6 +75,7 @@ export type AuthModeRefreshJwtTokenPayload = { tokenVersionId: string; refreshVersion: number; organizationId?: string; + subOrganizationId?: string; isMfaVerified?: boolean; mfaMethod?: MfaMethod; }; diff --git a/backend/src/services/certificate-request/certificate-request-service.test.ts b/backend/src/services/certificate-request/certificate-request-service.test.ts index 10c8b73cf..812d11e73 100644 --- a/backend/src/services/certificate-request/certificate-request-service.test.ts +++ b/backend/src/services/certificate-request/certificate-request-service.test.ts @@ -258,7 +258,7 @@ describe("CertificateRequestService", () => { (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); (mockCertificateService.getCertPrivateKey as any).mockResolvedValue(mockPrivateKey); - const result = await service.getCertificateFromRequest(mockGetData); + const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData); expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( "550e8400-e29b-41d4-a716-446655440005" @@ -277,8 +277,9 @@ describe("CertificateRequestService", () => { actorAuthMethod: AuthMethod.EMAIL, actorOrgId: "550e8400-e29b-41d4-a716-446655440002" }); - expect(result).toEqual({ + expect(certificateRequest).toEqual({ status: CertificateRequestStatus.ISSUED, + certificateId: "550e8400-e29b-41d4-a716-446655440006", certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", privateKey: "-----BEGIN PRIVATE KEY-----\nMOCK_KEY_PEM\n-----END PRIVATE KEY-----", serialNumber: "123456", @@ -286,6 +287,7 @@ describe("CertificateRequestService", () => { createdAt: mockRequestWithCert.createdAt, updatedAt: mockRequestWithCert.updatedAt }); + expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003"); }); it("should get certificate from request successfully when no certificate is attached", async () => { @@ -310,10 +312,11 @@ describe("CertificateRequestService", () => { (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithoutCert); - const result = await service.getCertificateFromRequest(mockGetData); + const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData); - expect(result).toEqual({ + expect(certificateRequest).toEqual({ status: CertificateRequestStatus.PENDING, + certificateId: null, certificate: null, privateKey: null, serialNumber: null, @@ -321,6 +324,7 @@ describe("CertificateRequestService", () => { createdAt: mockRequestWithoutCert.createdAt, updatedAt: mockRequestWithoutCert.updatedAt }); + expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003"); }); it("should get certificate from request successfully when user lacks private key permission", async () => { @@ -354,7 +358,7 @@ describe("CertificateRequestService", () => { (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockRequestWithCert); (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); - const result = await service.getCertificateFromRequest(mockGetData); + const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData); expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( "550e8400-e29b-41d4-a716-446655440005" @@ -367,8 +371,9 @@ describe("CertificateRequestService", () => { actorOrgId: "550e8400-e29b-41d4-a716-446655440002" }); expect(mockCertificateService.getCertPrivateKey).not.toHaveBeenCalled(); - expect(result).toEqual({ + expect(certificateRequest).toEqual({ status: CertificateRequestStatus.ISSUED, + certificateId: "550e8400-e29b-41d4-a716-446655440008", certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", privateKey: null, serialNumber: "123456", @@ -376,6 +381,7 @@ describe("CertificateRequestService", () => { createdAt: mockRequestWithCert.createdAt, updatedAt: mockRequestWithCert.updatedAt }); + expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003"); }); it("should get certificate from request successfully when user has private key permission but key retrieval fails", async () => { @@ -414,7 +420,7 @@ describe("CertificateRequestService", () => { (mockCertificateService.getCertBody as any).mockResolvedValue(mockCertBody); (mockCertificateService.getCertPrivateKey as any).mockRejectedValue(new Error("Private key not found")); - const result = await service.getCertificateFromRequest(mockGetData); + const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData); expect(mockCertificateRequestDAL.findByIdWithCertificate).toHaveBeenCalledWith( "550e8400-e29b-41d4-a716-446655440005" @@ -433,8 +439,9 @@ describe("CertificateRequestService", () => { actorAuthMethod: AuthMethod.EMAIL, actorOrgId: "550e8400-e29b-41d4-a716-446655440002" }); - expect(result).toEqual({ + expect(certificateRequest).toEqual({ status: CertificateRequestStatus.ISSUED, + certificateId: "550e8400-e29b-41d4-a716-446655440009", certificate: "-----BEGIN CERTIFICATE-----\nMOCK_CERT_PEM\n-----END CERTIFICATE-----", privateKey: null, serialNumber: "123456", @@ -442,6 +449,7 @@ describe("CertificateRequestService", () => { createdAt: mockRequestWithCert.createdAt, updatedAt: mockRequestWithCert.updatedAt }); + expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003"); }); it("should get certificate from request with error message when failed", async () => { @@ -466,17 +474,19 @@ describe("CertificateRequestService", () => { (mockPermissionService.getProjectPermission as any).mockResolvedValue(mockPermission); (mockCertificateRequestDAL.findByIdWithCertificate as any).mockResolvedValue(mockFailedRequest); - const result = await service.getCertificateFromRequest(mockGetData); + const { certificateRequest, projectId } = await service.getCertificateFromRequest(mockGetData); - expect(result).toEqual({ + expect(certificateRequest).toEqual({ status: CertificateRequestStatus.FAILED, certificate: null, + certificateId: null, privateKey: null, serialNumber: null, errorMessage: "Certificate issuance failed", createdAt: mockFailedRequest.createdAt, updatedAt: mockFailedRequest.updatedAt }); + expect(projectId).toEqual("550e8400-e29b-41d4-a716-446655440003"); }); it("should throw NotFoundError when certificate request does not exist", async () => { diff --git a/backend/src/services/certificate-request/certificate-request-service.ts b/backend/src/services/certificate-request/certificate-request-service.ts index 46cd49476..3bce23de8 100644 --- a/backend/src/services/certificate-request/certificate-request-service.ts +++ b/backend/src/services/certificate-request/certificate-request-service.ts @@ -170,13 +170,17 @@ export const certificateRequestServiceFactory = ({ actorId, actorAuthMethod, actorOrgId, - projectId, certificateRequestId }: TGetCertificateFromRequestDTO) => { + const certificateRequest = await certificateRequestDAL.findByIdWithCertificate(certificateRequestId); + if (!certificateRequest) { + throw new NotFoundError({ message: "Certificate request not found" }); + } + const { permission } = await permissionService.getProjectPermission({ actor, actorId, - projectId, + projectId: certificateRequest.projectId, actorAuthMethod, actorOrgId, actionProjectType: ActionProjectType.CertificateManager @@ -187,25 +191,20 @@ export const certificateRequestServiceFactory = ({ ProjectPermissionSub.Certificates ); - const certificateRequest = await certificateRequestDAL.findByIdWithCertificate(certificateRequestId); - if (!certificateRequest) { - throw new NotFoundError({ message: "Certificate request not found" }); - } - - if (certificateRequest.projectId !== projectId) { - throw new NotFoundError({ message: "Certificate request not found" }); - } - // If no certificate is attached, return basic info if (!certificateRequest.certificate) { return { - status: certificateRequest.status as CertificateRequestStatus, - certificate: null, - privateKey: null, - serialNumber: null, - errorMessage: certificateRequest.errorMessage || null, - createdAt: certificateRequest.createdAt, - updatedAt: certificateRequest.updatedAt + certificateRequest: { + status: certificateRequest.status as CertificateRequestStatus, + certificate: null, + certificateId: null, + privateKey: null, + serialNumber: null, + errorMessage: certificateRequest.errorMessage || null, + createdAt: certificateRequest.createdAt, + updatedAt: certificateRequest.updatedAt + }, + projectId: certificateRequest.projectId }; } @@ -240,13 +239,17 @@ export const certificateRequestServiceFactory = ({ } return { - status: certificateRequest.status as CertificateRequestStatus, - certificate: certBody.certificate, - privateKey, - serialNumber: certificateRequest.certificate.serialNumber, - errorMessage: certificateRequest.errorMessage || null, - createdAt: certificateRequest.createdAt, - updatedAt: certificateRequest.updatedAt + certificateRequest: { + status: certificateRequest.status as CertificateRequestStatus, + certificate: certBody.certificate, + certificateId: certificateRequest.certificate.id, + privateKey, + serialNumber: certificateRequest.certificate.serialNumber, + errorMessage: certificateRequest.errorMessage || null, + createdAt: certificateRequest.createdAt, + updatedAt: certificateRequest.updatedAt + }, + projectId: certificateRequest.projectId }; }; diff --git a/backend/src/services/certificate-request/certificate-request-types.ts b/backend/src/services/certificate-request/certificate-request-types.ts index c8a00de7e..0c62973b2 100644 --- a/backend/src/services/certificate-request/certificate-request-types.ts +++ b/backend/src/services/certificate-request/certificate-request-types.ts @@ -27,7 +27,7 @@ export type TGetCertificateRequestDTO = TProjectPermission & { certificateRequestId: string; }; -export type TGetCertificateFromRequestDTO = TProjectPermission & { +export type TGetCertificateFromRequestDTO = Omit & { certificateRequestId: string; }; diff --git a/backend/src/services/certificate-v3/certificate-v3-service.test.ts b/backend/src/services/certificate-v3/certificate-v3-service.test.ts index 9d8d1aebb..3cb1746a4 100644 --- a/backend/src/services/certificate-v3/certificate-v3-service.test.ts +++ b/backend/src/services/certificate-v3/certificate-v3-service.test.ts @@ -19,8 +19,8 @@ import { CertExtendedKeyUsageType, CertIncludeType, CertKeyUsageType, - CertSubjectAttributeType, - CertSubjectAlternativeNameType + CertSubjectAlternativeNameType, + CertSubjectAttributeType } from "@app/services/certificate-common/certificate-constants"; import { TCertificateProfileDALFactory } from "@app/services/certificate-profile/certificate-profile-dal"; import { EnrollmentType, IssuerType } from "@app/services/certificate-profile/certificate-profile-types"; diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index 2721596d2..2a67866d3 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -18,7 +18,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { .where(filter) .join(TableName.Identity, `${TableName.Identity}.id`, `${TableName.IdentityAccessToken}.identityId`) .select(selectAllTableCols(TableName.IdentityAccessToken)) - .select(db.ref("orgId").withSchema(TableName.Identity).as("identityScopeOrgId")) + .select(db.ref("orgId").withSchema(TableName.Identity).as("identityOrgId")) + .select(db.ref("subOrganizationId").withSchema(TableName.IdentityAccessToken).as("subOrganizationId")) .select(db.ref("name").withSchema(TableName.Identity).as("identityName")) .first(); diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 244e98908..4aaff748c 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -184,11 +184,7 @@ export const identityAccessTokenServiceFactory = ({ return { revokedToken }; }; - const fnValidateIdentityAccessToken = async ( - token: TIdentityAccessTokenJwtPayload, - ipAddress?: string, - subOrganizationSelector?: string - ) => { + const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => { const identityAccessToken = await identityAccessTokenDAL.findOne({ [`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId, isAccessTokenRevoked: false @@ -209,46 +205,30 @@ export const identityAccessTokenServiceFactory = ({ trustedIps: trustedIps as TIp[] }); } - let orgId = ""; - let orgName = ""; - let parentOrgId = ""; - const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId }); - const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id; - if (subOrganizationSelector) { - const subOrganization = await orgDAL.findOne({ rootOrgId, slug: subOrganizationSelector }); - if (!subOrganization) - throw new BadRequestError({ message: `Sub organization ${subOrganizationSelector} not found` }); + const scopeOrgId = identityAccessToken.subOrganizationId || identityAccessToken.identityOrgId; - const identityOrgMembership = await membershipIdentityDAL.findOne({ - scope: AccessScope.Organization, - actorIdentityId: identityAccessToken.identityId, - scopeOrgId: subOrganization.id - }); + const identityOrgDetails = await orgDAL.findOne({ id: scopeOrgId }); - if (!identityOrgMembership) { - throw new BadRequestError({ message: "Identity does not belong to this organization" }); - } - orgId = subOrganization.id; - orgName = subOrganization.name; + const isSubOrg = Boolean(identityOrgDetails.rootOrgId); - parentOrgId = subOrganization.parentOrgId as string; - } else { - const identityOrgMembership = await membershipIdentityDAL.findOne({ - scope: AccessScope.Organization, - actorIdentityId: identityAccessToken.identityId, - scopeOrgId: identityOrgDetails.id - }); + const rootOrgId = isSubOrg ? identityOrgDetails.rootOrgId || identityOrgDetails.id : identityOrgDetails.id; - if (!identityOrgMembership) { - throw new BadRequestError({ message: "Identity does not belong to this organization" }); - } + // Verify identity membership in the organization + const identityOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identityAccessToken.identityId, + scopeOrgId: identityOrgDetails.id + }); - orgId = identityOrgDetails.id; - orgName = identityOrgDetails.name; - parentOrgId = rootOrgId; + if (!identityOrgMembership) { + throw new BadRequestError({ message: "Identity does not belong to this organization" }); } + const orgId = identityOrgDetails.id; + const orgName = identityOrgDetails.name; + const parentOrgId = identityOrgDetails.parentOrgId || rootOrgId; + let { accessTokenNumUses } = identityAccessToken; const tokenStatusInCache = await accessTokenQueue.getIdentityTokenDetailsInCache(identityAccessToken.id); if (tokenStatusInCache) { diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index fba7fee98..79b6ef525 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -53,7 +53,7 @@ type TIdentityAliCloudAuthServiceFactoryDep = { membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityAliCloudAuthServiceFactory = ReturnType; @@ -67,7 +67,7 @@ export const identityAliCloudAuthServiceFactory = ({ permissionService, orgDAL }: TIdentityAliCloudAuthServiceFactoryDep) => { - const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => { + const login = async ({ identityId, subOrganizationName, ...params }: TLoginAliCloudAuthDTO) => { const appCfg = getConfig(); const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId }); if (!identityAliCloudAuth) { @@ -80,6 +80,10 @@ export const identityAliCloudAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; try { const requestUrl = new URL("https://sts.aliyuncs.com"); @@ -103,6 +107,30 @@ export const identityAliCloudAuthServiceFactory = ({ }); } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + // Generate the token const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( @@ -132,7 +160,8 @@ export const identityAliCloudAuthServiceFactory = ({ accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.ALICLOUD_AUTH + authMethod: IdentityAuthMethod.ALICLOUD_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-types.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-types.ts index 86133491e..575341dd3 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-types.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-types.ts @@ -11,6 +11,7 @@ export type TLoginAliCloudAuthDTO = { SignatureVersion: string; SignatureNonce: string; Signature: string; + subOrganizationName?: string; }; export type TAttachAliCloudAuthDTO = { diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index b3b6bbfce..b102362b3 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -53,7 +53,7 @@ type TIdentityAwsAuthServiceFactoryDep = { membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityAwsAuthServiceFactory = ReturnType; @@ -101,7 +101,13 @@ export const identityAwsAuthServiceFactory = ({ permissionService, orgDAL }: TIdentityAwsAuthServiceFactoryDep) => { - const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => { + const login = async ({ + identityId, + iamHttpRequestMethod, + iamRequestBody, + iamRequestHeaders, + subOrganizationName + }: TLoginAwsAuthDTO) => { const appCfg = getConfig(); const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId }); if (!identityAwsAuth) { @@ -112,6 +118,11 @@ export const identityAwsAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; + try { const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); const body: string = Buffer.from(iamRequestBody, "base64").toString(); @@ -179,6 +190,30 @@ export const identityAwsAuthServiceFactory = ({ } } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( identity.projectId @@ -207,7 +242,8 @@ export const identityAwsAuthServiceFactory = ({ accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.AWS_AUTH + authMethod: IdentityAuthMethod.AWS_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts index 9844c8a63..4570932f0 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-types.ts @@ -5,6 +5,7 @@ export type TLoginAwsAuthDTO = { iamHttpRequestMethod: string; iamRequestBody: string; iamRequestHeaders: string; + subOrganizationName?: string; }; export type TAttachAwsAuthDTO = { diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index 3c05511d1..c205d76c8 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -49,7 +49,7 @@ type TIdentityAzureAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityAzureAuthServiceFactory = ReturnType; @@ -63,7 +63,7 @@ export const identityAzureAuthServiceFactory = ({ licenseService, orgDAL }: TIdentityAzureAuthServiceFactoryDep) => { - const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => { + const login = async ({ identityId, jwt: azureJwt, subOrganizationName }: TLoginAzureAuthDTO) => { const appCfg = getConfig(); const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); if (!identityAzureAuth) { @@ -74,6 +74,10 @@ export const identityAzureAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; try { const azureIdentity = await validateAzureIdentity({ @@ -98,6 +102,30 @@ export const identityAzureAuthServiceFactory = ({ } } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( identity.projectId @@ -126,7 +154,8 @@ export const identityAzureAuthServiceFactory = ({ accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.AZURE_AUTH + authMethod: IdentityAuthMethod.AZURE_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts index 485753b6f..78d99dd55 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-types.ts @@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TLoginAzureAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; export type TAttachAzureAuthDTO = { diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index 847abd81f..471f5a542 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -47,7 +47,7 @@ type TIdentityGcpAuthServiceFactoryDep = { identityAccessTokenDAL: Pick; permissionService: Pick; licenseService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityGcpAuthServiceFactory = ReturnType; @@ -61,7 +61,7 @@ export const identityGcpAuthServiceFactory = ({ licenseService, orgDAL }: TIdentityGcpAuthServiceFactoryDep) => { - const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => { + const login = async ({ identityId, jwt: gcpJwt, subOrganizationName }: TLoginGcpAuthDTO) => { const appCfg = getConfig(); const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); if (!identityGcpAuth) { @@ -72,6 +72,11 @@ export const identityGcpAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; + try { let gcpIdentityDetails: TGcpIdentityDetails; switch (identityGcpAuth.type) { @@ -138,6 +143,30 @@ export const identityGcpAuthServiceFactory = ({ }); } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( identity.projectId @@ -166,7 +195,8 @@ export const identityGcpAuthServiceFactory = ({ accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.GCP_AUTH + authMethod: IdentityAuthMethod.GCP_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts index 063630c73..b26fa6539 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-types.ts @@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TLoginGcpAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; export type TAttachGcpAuthDTO = { diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index 82935e4a4..2d68f10dd 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -60,7 +60,7 @@ type TIdentityJwtAuthServiceFactoryDep = { permissionService: Pick; licenseService: Pick; kmsService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityJwtAuthServiceFactory = ReturnType; @@ -75,7 +75,7 @@ export const identityJwtAuthServiceFactory = ({ kmsService, orgDAL }: TIdentityJwtAuthServiceFactoryDep) => { - const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => { + const login = async ({ identityId, jwt: jwtValue, subOrganizationName }: TLoginJwtAuthDTO) => { const appCfg = getConfig(); const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); if (!identityJwtAuth) { @@ -86,6 +86,11 @@ export const identityJwtAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; + try { const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -218,6 +223,30 @@ export const identityJwtAuthServiceFactory = ({ }); } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( identity.projectId @@ -246,7 +275,8 @@ export const identityJwtAuthServiceFactory = ({ accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.JWT_AUTH + authMethod: IdentityAuthMethod.JWT_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts index bc19aba83..54d9a928d 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-types.ts @@ -49,4 +49,5 @@ export type TRevokeJwtAuthDTO = { export type TLoginJwtAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 212cb0894..5d4021fef 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -78,7 +78,7 @@ type TIdentityKubernetesAuthServiceFactoryDep = { gatewayV2Service: TGatewayV2ServiceFactory; gatewayDAL: Pick; gatewayV2DAL: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityKubernetesAuthServiceFactory = ReturnType; @@ -185,7 +185,7 @@ export const identityKubernetesAuthServiceFactory = ({ return callbackResult; }; - const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { + const login = async ({ identityId, jwt: serviceAccountJwt, subOrganizationName }: TLoginKubernetesAuthDTO) => { const appCfg = getConfig(); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); if (!identityKubernetesAuth) { @@ -198,6 +198,10 @@ export const identityKubernetesAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; try { const { decryptor } = await kmsService.createCipherPairWithDataKey({ @@ -459,6 +463,30 @@ export const identityKubernetesAuthServiceFactory = ({ }); } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( identity.projectId @@ -487,7 +515,8 @@ export const identityKubernetesAuthServiceFactory = ({ accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.KUBERNETES_AUTH + authMethod: IdentityAuthMethod.KUBERNETES_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts index 269fa19e0..decb2957e 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-types.ts @@ -3,6 +3,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TLoginKubernetesAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; export enum IdentityKubernetesAuthTokenReviewMode { diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 455b52412..92b754e5a 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -70,7 +70,7 @@ type TIdentityLdapAuthServiceFactoryDep = { TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" >; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityLdapAuthServiceFactory = ReturnType; @@ -153,7 +153,7 @@ export const identityLdapAuthServiceFactory = ({ return { opts, ldapConfig }; }; - const login = async ({ identityId }: TLoginLdapAuthDTO) => { + const login = async ({ identityId, subOrganizationName }: TLoginLdapAuthDTO) => { const appCfg = getConfig(); const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); @@ -167,6 +167,11 @@ export const identityLdapAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; + const plan = await licenseService.getPlan(identity.orgId); if (!plan.ldap) { throw new BadRequestError({ @@ -174,6 +179,29 @@ export const identityLdapAuthServiceFactory = ({ "Failed to login to identity due to plan restriction. Upgrade plan to login to use LDAP authentication." }); } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } try { const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { @@ -204,7 +232,8 @@ export const identityLdapAuthServiceFactory = ({ accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.LDAP_AUTH + authMethod: IdentityAuthMethod.LDAP_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts index a4aea7573..b438c937f 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-types.ts @@ -59,6 +59,7 @@ export type TGetLdapAuthDTO = { export type TLoginLdapAuthDTO = { identityId: string; + subOrganizationName?: string; }; export type TRevokeLdapAuthDTO = { diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index 05e56c77d..18f01e83e 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -51,7 +51,7 @@ type TIdentityOciAuthServiceFactoryDep = { membershipIdentityDAL: Pick; licenseService: Pick; permissionService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityOciAuthServiceFactory = ReturnType; @@ -65,7 +65,7 @@ export const identityOciAuthServiceFactory = ({ permissionService, orgDAL }: TIdentityOciAuthServiceFactoryDep) => { - const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => { + const login = async ({ identityId, headers, userOcid, subOrganizationName }: TLoginOciAuthDTO) => { const appCfg = getConfig(); const identityOciAuth = await identityOciAuthDAL.findOne({ identityId }); if (!identityOciAuth) { @@ -76,6 +76,11 @@ export const identityOciAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; + try { // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { @@ -108,6 +113,30 @@ export const identityOciAuthServiceFactory = ({ }); } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + // Generate the token const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( @@ -137,7 +166,8 @@ export const identityOciAuthServiceFactory = ({ accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.OCI_AUTH + authMethod: IdentityAuthMethod.OCI_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts index 8eb33a866..0ac043ab8 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-types.ts @@ -9,6 +9,7 @@ export type TLoginOciAuthDTO = { "x-date"?: string; date?: string; }; + subOrganizationName?: string; }; export type TAttachOciAuthDTO = { diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index a5178f36d..1e033d662 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -61,7 +61,7 @@ type TIdentityOidcAuthServiceFactoryDep = { permissionService: Pick; licenseService: Pick; kmsService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityOidcAuthServiceFactory = ReturnType; @@ -76,7 +76,7 @@ export const identityOidcAuthServiceFactory = ({ kmsService, orgDAL }: TIdentityOidcAuthServiceFactoryDep) => { - const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { + const login = async ({ identityId, jwt: oidcJwt, subOrganizationName }: TLoginOidcAuthDTO) => { const appCfg = getConfig(); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); if (!identityOidcAuth) { @@ -87,6 +87,11 @@ export const identityOidcAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; + try { const { decryptor } = await kmsService.createCipherPairWithDataKey({ type: KmsDataKey.Organization, @@ -286,6 +291,30 @@ export const identityOidcAuthServiceFactory = ({ }); } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( identity.projectId @@ -314,7 +343,8 @@ export const identityOidcAuthServiceFactory = ({ accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.OIDC_AUTH + authMethod: IdentityAuthMethod.OIDC_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts index fc5da3e27..5d06d778c 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-types.ts @@ -38,6 +38,7 @@ export type TGetOidcAuthDTO = { export type TLoginOidcAuthDTO = { identityId: string; jwt: string; + subOrganizationName?: string; }; export type TRevokeOidcAuthDTO = { diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts index 630638a06..bfa4db14d 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -46,7 +46,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = { licenseService: Pick; permissionService: Pick; kmsService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; const parseSubjectDetails = (data: string) => { @@ -68,7 +68,11 @@ export const identityTlsCertAuthServiceFactory = ({ kmsService, orgDAL }: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => { - const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => { + const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ + identityId, + clientCertificate, + subOrganizationName + }) => { const appCfg = getConfig(); const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId }); if (!identityTlsCertAuth) { @@ -81,6 +85,10 @@ export const identityTlsCertAuthServiceFactory = ({ if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; try { const { decryptor } = await kmsService.createCipherPairWithDataKey({ @@ -128,6 +136,30 @@ export const identityTlsCertAuthServiceFactory = ({ } } + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + // Generate the token const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( @@ -157,7 +189,8 @@ export const identityTlsCertAuthServiceFactory = ({ accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL, accessTokenNumUses: 0, accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.TLS_CERT_AUTH + authMethod: IdentityAuthMethod.TLS_CERT_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts index cf35bb5ee..77932042b 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-types.ts @@ -4,6 +4,7 @@ import { TProjectPermission } from "@app/lib/types"; export type TLoginTlsCertAuthDTO = { identityId: string; clientCertificate: string; + subOrganizationName?: string; }; export type TAttachTlsCertAuthDTO = { diff --git a/backend/src/services/identity-token-auth/identity-token-auth-service.ts b/backend/src/services/identity-token-auth/identity-token-auth-service.ts index 1a20b1192..bda66e6e1 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-service.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-service.ts @@ -59,7 +59,7 @@ type TIdentityTokenAuthServiceFactoryDep = { >; permissionService: Pick; licenseService: Pick; - orgDAL: Pick; + orgDAL: Pick; }; export type TIdentityTokenAuthServiceFactory = ReturnType; @@ -424,7 +424,8 @@ export const identityTokenAuthServiceFactory = ({ actorAuthMethod, actorOrgId, name, - isActorSuperAdmin + isActorSuperAdmin, + subOrganizationName }: TCreateTokenAuthTokenDTO) => { await validateIdentityUpdateForSuperAdminPrivileges(identityId, isActorSuperAdmin); @@ -503,6 +504,36 @@ export const identityTokenAuthServiceFactory = ({ const identity = await identityDAL.findById(identityTokenAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); + const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; + + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + const identityAccessToken = await identityTokenAuthDAL.transaction(async (tx) => { await membershipIdentityDAL.update( identity.projectId @@ -529,7 +560,8 @@ export const identityTokenAuthServiceFactory = ({ accessTokenNumUses: 0, accessTokenNumUsesLimit: identityTokenAuth.accessTokenNumUsesLimit, name, - authMethod: IdentityAuthMethod.TOKEN_AUTH + authMethod: IdentityAuthMethod.TOKEN_AUTH, + subOrganizationId }, tx ); diff --git a/backend/src/services/identity-token-auth/identity-token-auth-types.ts b/backend/src/services/identity-token-auth/identity-token-auth-types.ts index 6be2c5fe0..14fc95fa9 100644 --- a/backend/src/services/identity-token-auth/identity-token-auth-types.ts +++ b/backend/src/services/identity-token-auth/identity-token-auth-types.ts @@ -30,6 +30,7 @@ export type TRevokeTokenAuthDTO = { export type TCreateTokenAuthTokenDTO = { identityId: string; name?: string; + subOrganizationName?: string; isActorSuperAdmin?: boolean; } & Omit; diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 5ea5c4a6e..d47b93049 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -41,6 +41,7 @@ import { TGetUaClientSecretsDTO, TGetUaDTO, TGetUniversalAuthClientSecretByIdDTO, + TLoginUaDTO, TRevokeUaClientSecretDTO, TRevokeUaDTO, TUpdateUaDTO @@ -54,7 +55,7 @@ type TIdentityUaServiceFactoryDep = { membershipIdentityDAL: TMembershipIdentityDALFactory; permissionService: Pick; licenseService: Pick; - orgDAL: Pick; + orgDAL: Pick; keyStore: Pick< TKeyStoreFactory, "setItemWithExpiry" | "getItem" | "deleteItem" | "getKeysByPattern" | "deleteItems" | "acquireLock" @@ -79,7 +80,7 @@ export const identityUaServiceFactory = ({ keyStore, identityDAL }: TIdentityUaServiceFactoryDep) => { - const login = async (clientId: string, clientSecret: string, ip: string) => { + const login = async ({ clientId, clientSecret, ip, subOrganizationName }: TLoginUaDTO) => { const appCfg = getConfig(); const identityUa = await identityUaDAL.findOne({ clientId }); if (!identityUa) { @@ -90,6 +91,10 @@ export const identityUaServiceFactory = ({ const identity = await identityDAL.findById(identityUa.identityId); const org = await orgDAL.findById(identity.orgId); + const isSubOrgIdentity = Boolean(org.rootOrgId); + + // If the identity is a sub-org identity, then the scope is always the org.id, and if it's a root org identity, then we need to resolve the scope if a subOrganizationName is specified + let subOrganizationId = isSubOrgIdentity ? org.id : null; try { checkIPAgainstBlocklist({ @@ -229,6 +234,30 @@ export const identityUaServiceFactory = ({ accessTokenMaxTTL: 1000000000 }; + if (subOrganizationName) { + if (!isSubOrgIdentity) { + const subOrg = await orgDAL.findOne({ rootOrgId: org.id, slug: subOrganizationName }); + + if (!subOrg) { + throw new NotFoundError({ message: `Sub organization with name ${subOrganizationName} not found` }); + } + + const subOrgMembership = await membershipIdentityDAL.findOne({ + scope: AccessScope.Organization, + actorIdentityId: identity.id, + scopeOrgId: subOrg.id + }); + + if (!subOrgMembership) { + throw new UnauthorizedError({ + message: `Identity not authorized to access sub organization ${subOrganizationName}` + }); + } + + subOrganizationId = subOrg.id; + } + } + const identityAccessToken = await identityUaDAL.transaction(async (tx) => { const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); await membershipIdentityDAL.update( @@ -259,6 +288,7 @@ export const identityUaServiceFactory = ({ accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit, accessTokenPeriod: identityUa.accessTokenPeriod, authMethod: IdentityAuthMethod.UNIVERSAL_AUTH, + subOrganizationId, ...accessTokenTTLParams }, tx diff --git a/backend/src/services/identity-ua/identity-ua-types.ts b/backend/src/services/identity-ua/identity-ua-types.ts index 8e7644b58..3ff8f5cc5 100644 --- a/backend/src/services/identity-ua/identity-ua-types.ts +++ b/backend/src/services/identity-ua/identity-ua-types.ts @@ -1,5 +1,12 @@ import { TProjectPermission } from "@app/lib/types"; +export type TLoginUaDTO = { + clientId: string; + clientSecret: string; + ip: string; + subOrganizationName?: string; +}; + export type TAttachUaDTO = { identityId: string; accessTokenTTL: number; diff --git a/backend/src/services/membership-user/membership-user-service.ts b/backend/src/services/membership-user/membership-user-service.ts index 4b14ee771..59772a603 100644 --- a/backend/src/services/membership-user/membership-user-service.ts +++ b/backend/src/services/membership-user/membership-user-service.ts @@ -188,16 +188,29 @@ export const membershipUserServiceFactory = ({ }); if (existingMemberships.length === users.length) return { memberships: [] }; + const orgDetails = await orgDAL.findById(dto.permission.orgId); + const isSubOrganization = Boolean(orgDetails.rootOrgId); const newMembershipUsers = users.filter((user) => !existingMemberships?.find((el) => el.actorUserId === user.id)); await factory.onCreateMembershipUserGuard(dto, newMembershipUsers); - const newMemberships = newMembershipUsers.map((user) => ({ - scope: scopeData.scope, - ...scopeDatabaseFields, - actorUserId: user.id, - status: scopeData.scope === AccessScope.Organization ? OrgMembershipStatus.Invited : undefined, - inviteEmail: scopeData.scope === AccessScope.Organization ? user.email : undefined - })); + const newMemberships = newMembershipUsers.map((user) => { + let status: OrgMembershipStatus | undefined; + if (scopeData.scope === AccessScope.Organization) { + if (isSubOrganization) { + status = OrgMembershipStatus.Accepted; + } else { + status = OrgMembershipStatus.Invited; + } + } + + return { + scope: scopeData.scope, + ...scopeDatabaseFields, + actorUserId: user.id, + status, + inviteEmail: status === OrgMembershipStatus.Invited ? user.email : undefined + }; + }); const customInputRoles = data.roles.filter((el) => factory.isCustomRole(el.role)); const hasCustomRole = customInputRoles.length > 0; diff --git a/backend/src/services/membership-user/org/org-membership-user-factory.ts b/backend/src/services/membership-user/org/org-membership-user-factory.ts index deb819c7a..2668ed665 100644 --- a/backend/src/services/membership-user/org/org-membership-user-factory.ts +++ b/backend/src/services/membership-user/org/org-membership-user-factory.ts @@ -1,6 +1,6 @@ import { ForbiddenError } from "@casl/ability"; -import { AccessScope, OrganizationActionScope } from "@app/db/schemas"; +import { AccessScope, OrganizationActionScope, OrgMembershipStatus } from "@app/db/schemas"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; @@ -122,16 +122,33 @@ export const newOrgMembershipUserFactory = ({ const signUpTokens: { email: string; link: string }[] = []; const orgDetails = await orgDAL.findById(dto.permission.orgId); if (orgDetails.rootOrgId) { - const emails = newUsers.map((el) => el.email).filter(Boolean); - await smtpService.sendMail({ - template: SmtpTemplates.SubOrgInvite, - subjectLine: "Infisical sub-organization invitation", - recipients: emails as string[], - substitutions: { - subOrganizationName: orgDetails.slug, - callback_url: `${appCfg.SITE_URL}/organizations/${dto.permission.orgId}/projects?subOrganization=${orgDetails.slug}` + // checking if the users have accepted the invitation in the root organization to send the email + const orgMembershipAccepted = await membershipUserDAL.find({ + scope: AccessScope.Organization, + scopeOrgId: orgDetails.rootOrgId, + status: OrgMembershipStatus.Accepted, + $in: { + actorUserId: newUsers.map((el) => el.id) } }); + + const orgMembershipAcceptedUserIds = orgMembershipAccepted.map((el) => el.actorUserId as string); + + const emails = newUsers + .filter((el) => Boolean(el?.email) && orgMembershipAcceptedUserIds.includes(el.id)) + .map((el) => el?.email as string); + + if (emails.length) { + await smtpService.sendMail({ + template: SmtpTemplates.SubOrgInvite, + subjectLine: "Infisical sub-organization invitation", + recipients: emails, + substitutions: { + subOrganizationName: orgDetails.slug, + callback_url: `${appCfg.SITE_URL}/organizations/${dto.permission.orgId}/projects` + } + }); + } } else { await Promise.allSettled( newUsers.map(async (el) => { diff --git a/backend/src/services/notification/notification-types.ts b/backend/src/services/notification/notification-types.ts index 84cf35a50..56045174a 100644 --- a/backend/src/services/notification/notification-types.ts +++ b/backend/src/services/notification/notification-types.ts @@ -17,7 +17,8 @@ export enum NotificationType { PROJECT_INVITATION = "project-invitation", SECRET_SYNC_FAILED = "secret-sync-failed", GATEWAY_HEALTH_ALERT = "gateway-health-alert", - RELAY_HEALTH_ALERT = "relay-health-alert" + RELAY_HEALTH_ALERT = "relay-health-alert", + APPROVAL_REQUIRED = "approval-required" } export interface TCreateUserNotificationDTO { diff --git a/backend/src/services/org/org-schema.ts b/backend/src/services/org/org-schema.ts index be5c300b5..dbdd04586 100644 --- a/backend/src/services/org/org-schema.ts +++ b/backend/src/services/org/org-schema.ts @@ -28,5 +28,7 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({ shareSecretsProductEnabled: true, maxSharedSecretLifetime: true, maxSharedSecretViewLimit: true, - blockDuplicateSecretSyncDestinations: true + blockDuplicateSecretSyncDestinations: true, + rootOrgId: true, + parentOrgId: true }); diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts index b0bbd9a2a..abcdebf51 100644 --- a/backend/src/services/org/org-service.ts +++ b/backend/src/services/org/org-service.ts @@ -150,36 +150,47 @@ export const orgServiceFactory = ({ /* * Get organization details by the organization id * */ - const findOrganizationById = async ( - userId: string, - orgId: string, - actorAuthMethod: ActorAuthMethod, - rootOrgId: string, - actorOrgId: string - ) => { + const findOrganizationById = async ({ + userId, + orgId, + actorAuthMethod, + rootOrgId, + actorOrgId + }: { + userId: string; + orgId: string; + actorAuthMethod: ActorAuthMethod; + rootOrgId: string; + actorOrgId: string; + }) => { await permissionService.getOrgPermission({ actor: ActorType.USER, actorId: userId, orgId, actorAuthMethod, - actorOrgId: rootOrgId, + actorOrgId, scope: OrganizationActionScope.Any }); const appCfg = getConfig(); - const org = await orgDAL.findOrgById(orgId); - if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` }); + const hasSubOrg = rootOrgId !== actorOrgId; + + const org = await orgDAL.findOrgById(rootOrgId); + if (!org) throw new NotFoundError({ message: `Organization with ID '${rootOrgId}' not found` }); - const hasSubOrg = actorOrgId !== rootOrgId; let subOrg; if (hasSubOrg) { subOrg = await orgDAL.findOne({ rootOrgId, id: actorOrgId }); + + if (!subOrg) throw new NotFoundError({ message: `Sub-organization with ID '${actorOrgId}' not found` }); } - if (!org.userTokenExpiration) { - return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME, subOrganization: subOrg }; + const data = hasSubOrg && subOrg ? subOrg : org; + if (!data.userTokenExpiration) { + return { ...data, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME }; } - return { ...org, subOrganization: subOrg }; + return data; }; + /* * Get all organization a user part of * */ diff --git a/backend/src/services/pam-session-expiration/pam-session-expiration-queue.ts b/backend/src/services/pam-session-expiration/pam-session-expiration-queue.ts new file mode 100644 index 000000000..f14da58f0 --- /dev/null +++ b/backend/src/services/pam-session-expiration/pam-session-expiration-queue.ts @@ -0,0 +1,81 @@ +import { TPamSessionDALFactory } from "@app/ee/services/pam-session/pam-session-dal"; +import { getConfig } from "@app/lib/config/env"; +import { logger } from "@app/lib/logger"; +import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; + +type TPamSessionExpirationServiceFactoryDep = { + queueService: TQueueServiceFactory; + pamSessionDAL: Pick; +}; + +export type TPamSessionExpirationServiceFactory = ReturnType; + +export const pamSessionExpirationServiceFactory = ({ + queueService, + pamSessionDAL +}: TPamSessionExpirationServiceFactoryDep) => { + const appCfg = getConfig(); + + const init = async () => { + if (appCfg.isSecondaryInstance) { + return; + } + + await queueService.startPg( + QueueJobs.PamSessionExpiration, + async (jobs) => { + await Promise.all( + jobs.map(async (job) => { + const { sessionId } = job.data; + try { + logger.info({ sessionId }, `${QueueName.PamSessionExpiration}: expiring session`); + const updated = await pamSessionDAL.expireSessionById(sessionId); + if (updated > 0) { + logger.info({ sessionId }, `${QueueName.PamSessionExpiration}: session expired successfully`); + } else { + logger.info( + { sessionId }, + `${QueueName.PamSessionExpiration}: session not expired (already ended or not found)` + ); + } + } catch (error) { + logger.error(error, `${QueueName.PamSessionExpiration}: failed to expire session ${sessionId}`); + throw error; + } + }) + ); + }, + { + batchSize: 1, + workerCount: 1, + pollingIntervalSeconds: 30 + } + ); + }; + + // Schedule a session expiration job to run at the session's expiresAt time + const scheduleSessionExpiration = async (sessionId: string, expiresAt: Date) => { + const now = new Date(); + const delayMs = Math.max(0, expiresAt.getTime() - now.getTime()); + const startAfter = new Date(now.getTime() + delayMs); + + await queueService.queuePg( + QueueJobs.PamSessionExpiration, + { sessionId }, + { + startAfter, + singletonKey: `pam-session-expiration-${sessionId}` + } + ); + + logger.info( + { sessionId, expiresAt: expiresAt.toISOString(), scheduledFor: startAfter.toISOString() }, + `${QueueName.PamSessionExpiration}: scheduled session expiration` + ); + }; + + return { + init, + scheduleSessionExpiration + }; +}; diff --git a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts index 60310765b..68d261193 100644 --- a/backend/src/services/resource-cleanup/resource-cleanup-queue.ts +++ b/backend/src/services/resource-cleanup/resource-cleanup-queue.ts @@ -7,6 +7,7 @@ import { logger } from "@app/lib/logger"; import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue"; import { TUserNotificationDALFactory } from "@app/services/notification/user-notification-dal"; +import { TApprovalRequestDALFactory, TApprovalRequestGrantsDALFactory } from "../approval-policy/approval-request-dal"; import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal"; import { TIdentityUaClientSecretDALFactory } from "../identity-ua/identity-ua-client-secret-dal"; import { TOrgServiceFactory } from "../org/org-service"; @@ -31,6 +32,8 @@ type TDailyResourceCleanUpQueueServiceFactoryDep = { userNotificationDAL: Pick; keyValueStoreDAL: Pick; scimService: Pick; + approvalRequestDAL: Pick; + approvalRequestGrantsDAL: Pick; }; export type TDailyResourceCleanUpQueueServiceFactory = ReturnType; @@ -49,7 +52,9 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ scimService, orgService, userNotificationDAL, - keyValueStoreDAL + keyValueStoreDAL, + approvalRequestDAL, + approvalRequestGrantsDAL }: TDailyResourceCleanUpQueueServiceFactoryDep) => { const appCfg = getConfig(); @@ -94,6 +99,8 @@ export const dailyResourceCleanUpQueueServiceFactory = ({ await auditLogDAL.pruneAuditLog(); await userNotificationDAL.pruneNotifications(); await keyValueStoreDAL.pruneExpiredKeys(); + await approvalRequestDAL.markExpiredRequests(); + await approvalRequestGrantsDAL.markExpiredGrants(); logger.info(`${QueueName.DailyResourceCleanUp}: queue task completed`); } catch (error) { logger.error(error, `${QueueName.DailyResourceCleanUp}: resource cleanup failed`); diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts index d8220e4f5..0753f9640 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-dal.ts @@ -416,6 +416,7 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { tagSlugs?: string[]; includeTagsInSearch?: boolean; includeMetadataInSearch?: boolean; + excludeRotatedSecrets?: boolean; } ) => { try { @@ -481,6 +482,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { ); } + if (filters?.excludeRotatedSecrets) { + void query.whereNull(`${TableName.SecretRotationV2SecretMapping}.secretId`); + } + const secrets = await query; // @ts-expect-error not inferred by knex @@ -594,6 +599,11 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { void bd.whereIn(`${TableName.SecretTag}.slug`, slugs); } }) + .where((bd) => { + if (filters?.excludeRotatedSecrets) { + void bd.whereNull(`${TableName.SecretRotationV2SecretMapping}.secretId`); + } + }) .orderBy( filters?.orderBy === SecretsOrderBy.Name ? "key" : "id", filters?.orderDirection ?? OrderByDirection.ASC diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts index d42a26eff..d8d06bd46 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-service.ts @@ -483,8 +483,8 @@ export const secretV2BridgeServiceFactory = ({ }); if (!sharedSecretToModify) throw new NotFoundError({ message: `Secret with name ${inputSecret.secretName} not found` }); - if (sharedSecretToModify.isRotatedSecret && (inputSecret.newSecretName || inputSecret.secretValue)) - throw new BadRequestError({ message: "Cannot update rotated secret name or value" }); + if (sharedSecretToModify.isRotatedSecret && inputSecret.newSecretName) + throw new BadRequestError({ message: "Cannot update rotated secret name" }); secretId = sharedSecretToModify.id; secret = sharedSecretToModify; } @@ -888,6 +888,7 @@ export const secretV2BridgeServiceFactory = ({ | "tagSlugs" | "environment" | "search" + | "excludeRotatedSecrets" >) => { const { permission } = await permissionService.getProjectPermission({ actor, @@ -1934,8 +1935,14 @@ export const secretV2BridgeServiceFactory = ({ if (el.isRotatedSecret) { const input = secretsToUpdateGroupByPath[secretPath].find((i) => i.secretKey === el.key); - if (input && (input.newSecretName || input.secretValue)) - throw new BadRequestError({ message: `Cannot update rotated secret name or value: ${el.key}` }); + if (input) { + if (input.newSecretName) { + delete input.newSecretName; + } + if (input.secretValue !== undefined) { + delete input.secretValue; + } + } } }); @@ -2061,8 +2068,11 @@ export const secretV2BridgeServiceFactory = ({ commitChanges, inputSecrets: secretsToUpdate.map((el) => { const originalSecret = secretsToUpdateInDBGroupedByKey[el.secretKey][0]; + const shouldUpdateValue = !originalSecret.isRotatedSecret && typeof el.secretValue !== "undefined"; + const shouldUpdateName = !originalSecret.isRotatedSecret && el.newSecretName; + const encryptedValue = - typeof el.secretValue !== "undefined" + shouldUpdateValue && el.secretValue !== undefined ? { encryptedValue: secretManagerEncryptor({ plainText: Buffer.from(el.secretValue) }).cipherTextBlob, references: secretReferencesGroupByInputSecretKey[el.secretKey]?.nestedReferences @@ -2077,7 +2087,7 @@ export const secretV2BridgeServiceFactory = ({ (value) => secretManagerEncryptor({ plainText: Buffer.from(value) }).cipherTextBlob ), skipMultilineEncoding: el.skipMultilineEncoding, - key: el.newSecretName || el.secretKey, + key: shouldUpdateName ? el.newSecretName : el.secretKey, tags: el.tagIds, secretMetadata: el.secretMetadata, ...encryptedValue diff --git a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts index 5e2ffc1a0..f8613f57a 100644 --- a/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts +++ b/backend/src/services/secret-v2-bridge/secret-v2-bridge-types.ts @@ -50,6 +50,7 @@ export type TGetSecretsDTO = { limit?: number; search?: string; keys?: string[]; + excludeRotatedSecrets?: boolean; } & TProjectPermission; export type TGetSecretsMissingReadValuePermissionDTO = Omit< @@ -362,6 +363,7 @@ export type TFindSecretsByFolderIdsFilter = { includeTagsInSearch?: boolean; includeMetadataInSearch?: boolean; keys?: string[]; + excludeRotatedSecrets?: boolean; }; export type TGetSecretsRawByFolderMappingsDTO = { diff --git a/backend/src/services/secret/secret-service.ts b/backend/src/services/secret/secret-service.ts index 9ba1df47d..27690249d 100644 --- a/backend/src/services/secret/secret-service.ts +++ b/backend/src/services/secret/secret-service.ts @@ -1154,6 +1154,7 @@ export const secretServiceFactory = ({ | "search" | "includeTagsInSearch" | "includeMetadataInSearch" + | "excludeRotatedSecrets" >) => { const { shouldUseSecretV2Bridge } = await projectBotService.getBotKey(projectId); diff --git a/backend/src/services/secret/secret-types.ts b/backend/src/services/secret/secret-types.ts index d8c778d7e..be2c8b214 100644 --- a/backend/src/services/secret/secret-types.ts +++ b/backend/src/services/secret/secret-types.ts @@ -214,6 +214,7 @@ export type TGetSecretsRawDTO = { keys?: string[]; includeTagsInSearch?: boolean; includeMetadataInSearch?: boolean; + excludeRotatedSecrets?: boolean; } & TProjectPermission; export type TGetSecretAccessListDTO = { diff --git a/backend/src/services/super-admin/super-admin-service.ts b/backend/src/services/super-admin/super-admin-service.ts index 63bab2666..e7fdf20b4 100644 --- a/backend/src/services/super-admin/super-admin-service.ts +++ b/backend/src/services/super-admin/super-admin-service.ts @@ -640,7 +640,8 @@ export const superAdminServiceFactory = ({ accessTokenNumUses: 0, accessTokenNumUsesLimit: tokenAuth.accessTokenNumUsesLimit, name: "Instance Admin Token", - authMethod: IdentityAuthMethod.TOKEN_AUTH + authMethod: IdentityAuthMethod.TOKEN_AUTH, + subOrganizationId: organization.id }, tx ); diff --git a/docs/docs.json b/docs/docs.json index 5a3d965fd..39ebf31a9 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -130,6 +130,7 @@ "integrations/app-connections/laravel-forge", "integrations/app-connections/ldap", "integrations/app-connections/mssql", + "integrations/app-connections/mongodb", "integrations/app-connections/mysql", "integrations/app-connections/netlify", "integrations/app-connections/northflank", @@ -444,6 +445,7 @@ "documentation/platform/secret-rotation/aws-iam-user-secret", "documentation/platform/secret-rotation/azure-client-secret", "documentation/platform/secret-rotation/ldap-password", + "documentation/platform/secret-rotation/mongodb-credentials", "documentation/platform/secret-rotation/mssql-credentials", "documentation/platform/secret-rotation/mysql-credentials", "documentation/platform/secret-rotation/okta-client-secret", @@ -1393,6 +1395,18 @@ "api-reference/endpoints/app-connections/mssql/delete" ] }, + { + "group": "MongoDB", + "pages": [ + "api-reference/endpoints/app-connections/mongodb/list", + "api-reference/endpoints/app-connections/mongodb/available", + "api-reference/endpoints/app-connections/mongodb/get-by-id", + "api-reference/endpoints/app-connections/mongodb/get-by-name", + "api-reference/endpoints/app-connections/mongodb/create", + "api-reference/endpoints/app-connections/mongodb/update", + "api-reference/endpoints/app-connections/mongodb/delete" + ] + }, { "group": "MySQL", "pages": [ @@ -1929,6 +1943,19 @@ "api-reference/endpoints/secret-rotations/mssql-credentials/update" ] }, + { + "group": "MongoDB Credentials", + "pages": [ + "api-reference/endpoints/secret-rotations/mongodb-credentials/create", + "api-reference/endpoints/secret-rotations/mongodb-credentials/delete", + "api-reference/endpoints/secret-rotations/mongodb-credentials/get-by-id", + "api-reference/endpoints/secret-rotations/mongodb-credentials/get-by-name", + "api-reference/endpoints/secret-rotations/mongodb-credentials/get-generated-credentials-by-id", + "api-reference/endpoints/secret-rotations/mongodb-credentials/list", + "api-reference/endpoints/secret-rotations/mongodb-credentials/rotate-secrets", + "api-reference/endpoints/secret-rotations/mongodb-credentials/update" + ] + }, { "group": "MySQL Credentials", "pages": [ diff --git a/docs/documentation/platform/pam/resources/aws-iam.mdx b/docs/documentation/platform/pam/resources/aws-iam.mdx new file mode 100644 index 000000000..662d76b5e --- /dev/null +++ b/docs/documentation/platform/pam/resources/aws-iam.mdx @@ -0,0 +1,258 @@ +--- +title: "AWS IAM" +sidebarTitle: "AWS IAM" +description: "Learn how to configure AWS Management Console access through Infisical PAM for secure, audited, and just-in-time access to AWS." +--- + +Infisical PAM supports secure, just-in-time access to the **AWS Management Console** through federated sign-in. This allows your team to access AWS without sharing long-lived credentials, while maintaining a complete audit trail of who accessed what and when. + +## How It Works + +Unlike database or SSH resources that require a Gateway for network connectivity, AWS Console access works differently. Infisical uses AWS STS (Security Token Service) to assume roles on your behalf and generates temporary federated sign-in URLs. + +```mermaid +sequenceDiagram + participant User + participant Infisical + participant Resource Role as Resource Role
(Your AWS Account) + participant Target Role as Target Role
(Your AWS Account) + participant Console as AWS Console + + User->>Infisical: Request AWS Console access + Infisical->>Resource Role: AssumeRole (with ExternalId) + Resource Role-->>Infisical: Temporary credentials + Infisical->>Target Role: AssumeRole (role chaining) + Target Role-->>Infisical: Session credentials + Infisical->>Console: Generate federation URL + Console-->>Infisical: Signed console URL + Infisical-->>User: Return console URL + User->>Console: Open AWS Console (federated) +``` + +### Key Concepts + +1. **Resource Role**: An IAM role in your AWS account that trusts Infisical. This is the "bridge" role that Infisical assumes first. + +2. **Target Role**: The IAM role that end users will actually use in the AWS Console. The Resource Role assumes this role on behalf of the user. + +3. **Role Chaining**: Infisical uses AWS role chaining - it first assumes the Resource Role, then uses those credentials to assume the Target Role. This provides an additional layer of security and audit capability. + +4. **External ID**: A unique identifier (your Infisical Project ID) used in the trust policy to prevent [confused deputy attacks](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html). + +## Session Behavior + +### Session Duration + +The session duration is set when creating the account and applies to all access requests. You can specify the duration using human-readable formats like `15m`, `30m`, or `1h`. Due to AWS role chaining limitations: + +- **Minimum**: 15 minutes (`15m`) +- **Maximum**: 1 hour (`1h`) + +### Session Tracking + +Infisical tracks: +- When the session was created +- Who accessed which role +- When the session expires + + + **Important**: AWS Console sessions cannot be terminated early. Once a federated URL is generated, the session remains valid until the configured duration expires. However, you can [revoke active sessions](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_revoke-sessions.html) by modifying the role's trust policy. + + +### CloudTrail Integration + +All actions performed in the AWS Console are logged in [AWS CloudTrail](https://console.aws.amazon.com/cloudtrail). The session is identified by the `RoleSessionName`, which includes the user's email address for attribution: + +``` +arn:aws:sts::123456789012:assumed-role/pam-readonly/user@example.com +``` + +This allows you to correlate Infisical PAM sessions with CloudTrail logs for complete audit visibility. + +## Prerequisites + +Before configuring AWS Console access in Infisical PAM, you need to set up two IAM roles in your AWS account: + +1. **Resource Role** - Trusted by Infisical, can assume target roles +2. **Target Role(s)** - The actual roles users will use in the console + + + **No Gateway Required**: Unlike database or SSH resources, AWS Console access does not require an Infisical Gateway. Infisical communicates directly with AWS APIs. + + +## Create the PAM Resource + +The PAM Resource represents the connection between Infisical and your AWS account. It contains the Resource Role that Infisical will assume. + + + + First, create an IAM policy that allows the Resource Role to assume your target roles. For simplicity, you can use a wildcard to allow assuming any role in your account: + + ```json + { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Action": "sts:AssumeRole", + "Resource": "arn:aws:iam:::role/*" + }] + } + ``` + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/resource-role-policy.png) + + + **For more granular control**: If you want to restrict which roles the Resource Role can assume, replace the wildcard (`/*`) with a more specific pattern. For example: + - `arn:aws:iam:::role/pam-*` to only allow roles with the `pam-` prefix + - `arn:aws:iam:::role/infisical-*` to only allow roles with the `infisical-` prefix + + This allows you to limit the blast radius of the Resource Role's permissions. + + + + + Create an IAM role (e.g., `InfisicalResourceRole`) with: + - The permissions policy from the previous step attached + - The following trust policy: + + ```json + { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam:::root" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "" + } + } + }] + } + ``` + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/resource-role-trust-policy.png) + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/resource-role-attach-policy.png) + + + **Security Best Practice**: Always use the External ID condition. This prevents confused deputy attacks where another Infisical customer could potentially trick Infisical into assuming your role. + + + **Infisical AWS Account IDs:** + | Region | Account ID | + |--------|------------| + | US | `381492033652` | + | EU | `345594589636` | + + + **For Dedicated Instances**: Your AWS account ID differs from the ones listed above. Please contact Infisical support to obtain your dedicated AWS account ID. + + + + **For Self-Hosted Instances**: Use the AWS account ID where your Infisical instance is deployed. This is the account that hosts your Infisical infrastructure and will be assuming the Resource Role. + + + + + 1. Navigate to your PAM project and go to the **Resources** tab + 2. Click **Add Resource** and select **AWS IAM** + 3. Enter a name for the resource (e.g., `production-aws`) + 4. Enter the **Resource Role ARN** - the ARN of the role you created in the previous step + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/create-resource.png) + + Clicking **Create Resource** will validate that Infisical can assume the Resource Role. If the connection fails, verify: + - The trust policy has the correct Infisical AWS account ID + - The External ID matches your project ID + - The role ARN is correct + + + +## Create PAM Accounts + +A PAM Account represents a specific Target Role that users can request access to. You can create multiple accounts per resource, each pointing to a different target role with different permission levels. + + + + Each target role needs a trust policy that allows your Resource Role to assume it: + + ```json + { + "Version": "2012-10-17", + "Statement": [{ + "Effect": "Allow", + "Principal": { + "AWS": "arn:aws:iam:::role/InfisicalResourceRole" + }, + "Action": "sts:AssumeRole", + "Condition": { + "StringEquals": { + "sts:ExternalId": "" + } + } + }] + } + ``` + + ![Create AWS IAM Resource](/images/pam/resources/aws-iam/target-role-trust-policy.png) + + + + 1. Navigate to the **Accounts** tab in your PAM project + 2. Click **Add Account** and select the AWS IAM resource you created + 3. Fill in the account details: + + ![Create AWS IAM Account](/images/pam/resources/aws-iam/create-account.png) + + + A friendly name for this account (e.g., `readonly`, `admin`, `developer`) + + + + Optional description of what this account is used for + + + + The ARN of the IAM role users will assume (e.g., `arn:aws:iam::123456789012:role/pam-readonly`) + + + + Session duration using human-readable format (e.g., `15m`, `30m`, `1h`). Minimum 15 minutes, maximum 1 hour. + + + Due to AWS role chaining limitations, the maximum session duration is **1 hour**, regardless of the target role's configured maximum session duration. + + + + + +## Access the AWS Console + +Once your resource and accounts are configured, users can request access through Infisical: + +![Create AWS IAM Resource](/images/pam/resources/aws-iam/access-account.png) + + + + Go to the **Accounts** tab in your PAM project. + + + + Find the AWS Console account you want to access. + + + + Click the **Access** button. + + Infisical will: + 1. Assume the Resource Role using your project's External ID + 2. Assume the Target Role using role chaining + 3. Generate a federated sign-in URL + 4. Open the AWS Console in a new browser tab + + The user will be signed into the AWS Console with the permissions of the Target Role. + + \ No newline at end of file diff --git a/docs/documentation/platform/pam/resources/kubernetes.mdx b/docs/documentation/platform/pam/resources/kubernetes.mdx new file mode 100644 index 000000000..a92ec51c7 --- /dev/null +++ b/docs/documentation/platform/pam/resources/kubernetes.mdx @@ -0,0 +1,224 @@ +--- +title: "Kubernetes" +sidebarTitle: "Kubernetes" +description: "Learn how to configure Kubernetes cluster access through Infisical PAM for secure, audited, and just-in-time access to your Kubernetes clusters." +--- + +Infisical PAM supports secure, just-in-time access to Kubernetes clusters through service account token authentication. This allows your team to access Kubernetes clusters without sharing long-lived credentials, while maintaining a complete audit trail of who accessed what and when. + +## How It Works + +Kubernetes access in Infisical PAM uses an Infisical Gateway to securely proxy connections to your Kubernetes API server. When a user requests access, Infisical generates a temporary kubeconfig that routes traffic through the Gateway, enabling secure access without exposing your cluster directly. + +```mermaid +sequenceDiagram + participant User + participant CLI as Infisical CLI + participant Infisical + participant Gateway as Infisical Gateway + participant K8s as Kubernetes API Server + + User->>CLI: Request Kubernetes access + CLI->>Infisical: Authenticate & request session + Infisical-->>CLI: Session credentials & Gateway info + CLI->>CLI: Start local proxy + CLI->>Gateway: Establish secure tunnel + User->>CLI: kubectl commands + CLI->>Gateway: Proxy kubectl requests + Gateway->>K8s: Forward with SA token + K8s-->>Gateway: Response + Gateway-->>CLI: Return response + CLI-->>User: kubectl output +``` + +### Key Concepts + +1. **Gateway**: An Infisical Gateway deployed in your network that can reach the Kubernetes API server. The Gateway handles secure communication between users and your cluster. + +2. **Service Account Token**: A Kubernetes service account token that grants access to the cluster. This token is stored securely in Infisical and used by the Gateway to authenticate with the Kubernetes API. + +3. **Local Proxy**: The Infisical CLI starts a local proxy on your machine that intercepts kubectl commands and routes them securely through the Gateway to your cluster. + +4. **Session Tracking**: All access sessions are logged, including when the session was created, who accessed the cluster, session duration, and when it ended. + +### Session Tracking + +Infisical tracks: +- When the session was created +- Who accessed which cluster +- Session duration +- All kubectl commands executed during the session +- When the session ended + + + **Session Logs**: After ending a session (by stopping the proxy), you can view detailed session logs in the Sessions page, including all commands executed during the session. + + +## Prerequisites + +Before configuring Kubernetes access in Infisical PAM, you need: + +1. **Infisical Gateway** - A Gateway deployed in your network with access to the Kubernetes API server +2. **Service Account** - A Kubernetes service account with appropriate RBAC permissions +3. **Infisical CLI** - The Infisical CLI installed on user machines + + + **Gateway Required**: Unlike AWS Console access, Kubernetes access requires an Infisical Gateway to be deployed and registered with your Infisical instance. The Gateway must have network connectivity to your Kubernetes API server. + + +## Create the PAM Resource + +The PAM Resource represents the connection between Infisical and your Kubernetes cluster. + + + + Before creating the resource, ensure you have an Infisical Gateway running and registered with your Infisical instance. The Gateway must have network access to your Kubernetes API server. + + + + 1. Navigate to your PAM project and go to the **Resources** tab + 2. Click **Add Resource** and select **Kubernetes** + 3. Enter a name for the resource (e.g., `production-k8s`, `staging-cluster`) + 4. Enter the **Kubernetes API Server URL** - the URL to your Kubernetes API endpoint (e.g.`https://kubernetes.example.com:6443`) + 5. Select the **Gateway** that has access to this cluster + 6. Configure SSL verification options if needed + + + **SSL Verification**: You may need to disable SSL verification if your Kubernetes API server uses a self-signed certificate or if the certificate's hostname doesn't match the URL you're using to access it. + + + + +## Create a Service Account + +Infisical PAM currently supports service account token authentication for Kubernetes. You'll need to create a service account with appropriate permissions in your cluster. + + + + Create a file named `sa.yaml` with the following content: + + ```yaml sa.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: infisical-pam-sa + namespace: kube-system + --- + # Bind the ServiceAccount to the desired ClusterRole + # This example uses cluster-admin - adjust based on your needs + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: infisical-pam-binding + subjects: + - kind: ServiceAccount + name: infisical-pam-sa + namespace: kube-system + roleRef: + kind: ClusterRole + name: cluster-admin # Change this to a more restrictive role as needed + apiGroup: rbac.authorization.k8s.io + --- + # Create a static, non-expiring token for the ServiceAccount + apiVersion: v1 + kind: Secret + metadata: + name: infisical-pam-sa-token + namespace: kube-system + annotations: + kubernetes.io/service-account.name: infisical-pam-sa + type: kubernetes.io/service-account-token + ``` + + + **Security Best Practice**: The example above uses `cluster-admin` for simplicity. In production environments, you should create custom ClusterRoles or Roles with the minimum permissions required for each use case. + + + + + Apply the configuration to your cluster: + + ```bash + kubectl apply -f sa.yaml + ``` + + This creates: + - A ServiceAccount named `infisical-pam-sa` in the `kube-system` namespace + - A ClusterRoleBinding that grants the service account its permissions + - A Secret containing a static, non-expiring token for the service account + + + + Get the service account token that you'll use when creating the PAM account: + + ```bash + kubectl -n kube-system get secret infisical-pam-sa-token -o jsonpath='{.data.token}' | base64 -d + ``` + + Copy this token - you'll need it in the next step. + + + +## Create PAM Accounts + +Once you have configured the PAM resource, you'll need to configure a PAM account for your Kubernetes resource. +A PAM Account represents a specific service account that users can request access to. You can create multiple accounts per resource, each with different permission levels. + + + + Go to the **Accounts** tab in your PAM project. + + + + Click **Add Account** and select the Kubernetes resource you created. + + + + Fill in the account details and paste the service account token you retrieved earlier. + + + +## Access Kubernetes Cluster + +Once your resource and accounts are configured, users can request access through the Infisical CLI: + + + + 1. Navigate to the **Accounts** tab in your PAM project + 2. Find the Kubernetes account you want to access + 3. Click the **Access** button + 4. Copy the provided CLI command + + + + + Run the copied command in your terminal. + + The CLI will: + 1. Authenticate with Infisical + 2. Establish a secure connection through the Gateway + 3. Start a local proxy on your machine + 4. Configure kubectl to use the proxy + + + + Once the proxy is running, you can use `kubectl` commands as normal: + + ```bash + kubectl get pods + kubectl get namespaces + kubectl describe deployment my-app + ``` + + All commands are routed securely through the Infisical Gateway to your cluster. + + + + When you're done, stop the proxy by pressing `Ctrl+C` in the terminal where it's running. This will: + - Close the secure tunnel + - End the session + - Log the session details to Infisical + + You can view session logs in the **Sessions** page of your PAM project. + + diff --git a/docs/documentation/platform/secret-rotation/mongodb-credentials.mdx b/docs/documentation/platform/secret-rotation/mongodb-credentials.mdx new file mode 100644 index 000000000..5b3808126 --- /dev/null +++ b/docs/documentation/platform/secret-rotation/mongodb-credentials.mdx @@ -0,0 +1,177 @@ +--- +title: "MongoDB Credentials Rotation" +description: "Learn how to automatically rotate MongoDB credentials." +--- + +## Prerequisites + +1. Create a [MongoDB Connection](/integrations/app-connections/mongodb) with the required **Secret Rotation** permissions +2. Create two designated database users for Infisical to rotate the credentials for. Be sure to grant each user login permissions for the desired database with the necessary privileges their use case will require. + + An example creation statement might look like: + ```bash + // Switch to the target database + use my_database + + // Create first user + db.createUser({ + user: "infisical_user_1", + pwd: "temporary_password", + roles: [] + }) + + // Create second user + db.createUser({ + user: "infisical_user_2", + pwd: "temporary_password", + roles: [] + }) + + // Grant necessary permissions to both users + db.grantRolesToUser("infisical_user_1", [ + { role: "readWrite", db: "my_database" } + ]) + + db.grantRolesToUser("infisical_user_2", [ + { role: "readWrite", db: "my_database" } + ]) + ``` + + + To learn more about MongoDB's permission system, please visit their [documentation](https://www.mongodb.com/docs/manual/core/security-built-in-roles/). + + +3. Ensure your network security policies allow incoming requests from Infisical to this rotation provider, if network restrictions apply. + +## Create a MongoDB Credentials Rotation in Infisical + + + + 1. Navigate to your Secret Manager Project's Dashboard and select **Add Secret Rotation** from the actions dropdown. + ![Secret Manager Dashboard](/images/secret-rotations-v2/generic/add-secret-rotation.png) + + 2. Select the **MongoDB Credentials** option. + ![Select MongoDB Credentials](/images/secret-rotations-v2/mongodb-credentials/select-mongodb-credentials-option.png) + + 3. Select the **MongoDB Connection** to use and configure the rotation behavior. Then click **Next**. + ![Rotation Configuration](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-configuration.png) + + - **MongoDB Connection** - the connection that will perform the rotation of the configured database user credentials. + - **Rotation Interval** - the interval, in days, that once elapsed will trigger a rotation. + - **Rotate At** - the local time of day when rotation should occur once the interval has elapsed. + - **Auto-Rotation Enabled** - whether secrets should automatically be rotated once the rotation interval has elapsed. Disable this option to manually rotate secrets or pause secret rotation. + + 4. Input the usernames of the database users created above that will be used for rotation. Then click **Next**. + ![Rotation Parameters](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-parameters.png) + + - **Database Username 1** - the username of the first user that will be used for rotation. + - **Database Username 2** - the username of the second user that will be used for rotation. + + 5. Specify the secret names that the active credentials should be mapped to. Then click **Next**. + ![Rotation Secrets Mapping](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-secrets-mapping.png) + + - **Username** - the name of the secret that the active username will be mapped to. + - **Password** - the name of the secret that the active password will be mapped to. + + 6. Give your rotation a name and description (optional). Then click **Next**. + ![Rotation Details](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-details.png) + + - **Name** - the name of the secret rotation configuration. Must be slug-friendly. + - **Description** (optional) - a description of this rotation configuration. + + 7. Review your configuration, then click **Create Secret Rotation**. + ![Rotation Review](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-confirm.png) + + 8. Your **MongoDB Credentials** are now available for use via the mapped secrets. + ![Rotation Created](/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-created.png) + + + To create a MongoDB Credentials Rotation, make an API request to the [Create MongoDB + Credentials Rotation](/api-reference/endpoints/secret-rotations/mongodb-credentials/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://us.infisical.com/api/v2/secret-rotations/mongodb-credentials \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-mongodb-rotation", + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "description": "my database credentials rotation", + "connectionId": "11c76f38-cd13-4137-b1a3-ecd6a429952c", + "environment": "dev", + "secretPath": "/", + "isAutoRotationEnabled": true, + "rotationInterval": 30, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "parameters": { + "username1": "infisical_user_1", + "username2": "infisical_user_2" + }, + "secretsMapping": { + "username": "MONGODB_DB_USERNAME", + "password": "MONGODB_DB_PASSWORD" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "secretRotation": { + "id": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "name": "my-mongodb-rotation", + "description": "my database credentials rotation", + "secretsMapping": { + "username": "MONGODB_DB_USERNAME", + "password": "MONGODB_DB_PASSWORD" + }, + "isAutoRotationEnabled": true, + "activeIndex": 0, + "folderId": "b3257e1f-8d32-4e86-8bfd-b1f1bc1bf2c3", + "connectionId": "11c76f38-cd13-4137-b1a3-ecd6a429952c", + "createdAt": "2023-11-07T05:31:56Z", + "updatedAt": "2023-11-07T05:31:56Z", + "rotationInterval": 30, + "rotationStatus": "success", + "lastRotationAttemptedAt": "2023-11-07T05:31:56Z", + "lastRotatedAt": "2023-11-07T05:31:56Z", + "lastRotationJobId": null, + "nextRotationAt": "2023-11-07T05:31:56Z", + "isLastRotationManual": true, + "connection": { + "app": "mongodb", + "name": "my-mongodb-connection", + "id": "11c76f38-cd13-4137-b1a3-ecd6a429952c" + }, + "environment": { + "slug": "dev", + "name": "Development", + "id": "170a40f1-1b48-4cc7-addf-e563aa9fbe37" + }, + "projectId": "3c90c3cc-0d44-4b50-8888-8dd25736052a", + "folder": { + "id": "b3257e1f-8d32-4e86-8bfd-b1f1bc1bf2c3", + "path": "/" + }, + "rotateAtUtc": { + "hours": 0, + "minutes": 0 + }, + "lastRotationMessage": null, + "type": "mongodb-credentials", + "parameters": { + "username1": "infisical_user_1", + "username2": "infisical_user_2" + } + } + } + ``` + + + diff --git a/docs/images/app-connections/general/add-connection.png b/docs/images/app-connections/general/add-connection.png index b6dce69ac..5ebadae09 100644 Binary files a/docs/images/app-connections/general/add-connection.png and b/docs/images/app-connections/general/add-connection.png differ diff --git a/docs/images/app-connections/mongodb/mongodb-app-connection-form.png b/docs/images/app-connections/mongodb/mongodb-app-connection-form.png new file mode 100644 index 000000000..f57ca1aa5 Binary files /dev/null and b/docs/images/app-connections/mongodb/mongodb-app-connection-form.png differ diff --git a/docs/images/app-connections/mongodb/mongodb-app-connection-generated.png b/docs/images/app-connections/mongodb/mongodb-app-connection-generated.png new file mode 100644 index 000000000..eed7d01af Binary files /dev/null and b/docs/images/app-connections/mongodb/mongodb-app-connection-generated.png differ diff --git a/docs/images/app-connections/mongodb/mongodb-app-connection-option.png b/docs/images/app-connections/mongodb/mongodb-app-connection-option.png new file mode 100644 index 000000000..0999decb3 Binary files /dev/null and b/docs/images/app-connections/mongodb/mongodb-app-connection-option.png differ diff --git a/docs/images/pam/resources/aws-iam/access-account.png b/docs/images/pam/resources/aws-iam/access-account.png new file mode 100644 index 000000000..6e6a57e1e Binary files /dev/null and b/docs/images/pam/resources/aws-iam/access-account.png differ diff --git a/docs/images/pam/resources/aws-iam/create-account.png b/docs/images/pam/resources/aws-iam/create-account.png new file mode 100644 index 000000000..4d2203667 Binary files /dev/null and b/docs/images/pam/resources/aws-iam/create-account.png differ diff --git a/docs/images/pam/resources/aws-iam/create-resource.png b/docs/images/pam/resources/aws-iam/create-resource.png new file mode 100644 index 000000000..766b86ffc Binary files /dev/null and b/docs/images/pam/resources/aws-iam/create-resource.png differ diff --git a/docs/images/pam/resources/aws-iam/resource-role-attach-policy.png b/docs/images/pam/resources/aws-iam/resource-role-attach-policy.png new file mode 100644 index 000000000..39201c7cd Binary files /dev/null and b/docs/images/pam/resources/aws-iam/resource-role-attach-policy.png differ diff --git a/docs/images/pam/resources/aws-iam/resource-role-policy.png b/docs/images/pam/resources/aws-iam/resource-role-policy.png new file mode 100644 index 000000000..1c52aaebb Binary files /dev/null and b/docs/images/pam/resources/aws-iam/resource-role-policy.png differ diff --git a/docs/images/pam/resources/aws-iam/resource-role-trust-policy.png b/docs/images/pam/resources/aws-iam/resource-role-trust-policy.png new file mode 100644 index 000000000..7c018f985 Binary files /dev/null and b/docs/images/pam/resources/aws-iam/resource-role-trust-policy.png differ diff --git a/docs/images/pam/resources/aws-iam/target-role-trust-policy.png b/docs/images/pam/resources/aws-iam/target-role-trust-policy.png new file mode 100644 index 000000000..b91dd0030 Binary files /dev/null and b/docs/images/pam/resources/aws-iam/target-role-trust-policy.png differ diff --git a/docs/images/secret-rotations-v2/generic/add-secret-rotation.png b/docs/images/secret-rotations-v2/generic/add-secret-rotation.png index 86b84001b..4b1b626ae 100644 Binary files a/docs/images/secret-rotations-v2/generic/add-secret-rotation.png and b/docs/images/secret-rotations-v2/generic/add-secret-rotation.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-configuration.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-configuration.png new file mode 100644 index 000000000..8d0db7d03 Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-configuration.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-confirm.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-confirm.png new file mode 100644 index 000000000..d568e2d5d Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-confirm.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-created.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-created.png new file mode 100644 index 000000000..e18ef7b0f Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-created.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-details.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-details.png new file mode 100644 index 000000000..949ff40be Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-details.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-parameters.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-parameters.png new file mode 100644 index 000000000..14723c0a6 Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-parameters.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-secrets-mapping.png b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-secrets-mapping.png new file mode 100644 index 000000000..aceb38a8d Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/mongodb-credentials-secrets-mapping.png differ diff --git a/docs/images/secret-rotations-v2/mongodb-credentials/select-mongodb-credentials-option.png b/docs/images/secret-rotations-v2/mongodb-credentials/select-mongodb-credentials-option.png new file mode 100644 index 000000000..4de950114 Binary files /dev/null and b/docs/images/secret-rotations-v2/mongodb-credentials/select-mongodb-credentials-option.png differ diff --git a/docs/integrations/app-connections/mongodb.mdx b/docs/integrations/app-connections/mongodb.mdx new file mode 100644 index 000000000..e677eaa4e --- /dev/null +++ b/docs/integrations/app-connections/mongodb.mdx @@ -0,0 +1,141 @@ +--- +title: "MongoDB Connection" +description: "Learn how to configure a MongoDB Connection for Infisical." +--- + +Infisical supports the use of Username & Password authentication to connect with MongoDB databases. + +## Configure a MongoDB user for Infisical + + + + Infisical recommends creating a designated user in your MongoDB database for your connection. + + ```bash + use [TARGET-DATABASE] + db.createUser({ + user: "infisical_manager", + pwd: "[ENTER-YOUR-USER-PASSWORD]", + roles: [] + }) + ``` + + + + Depending on how you intend to use your MongoDB connection, you'll need to grant one or more of the following permissions. + + + To learn more about MongoDB's permission system, please visit their [documentation](https://www.mongodb.com/docs/manual/core/security-built-in-roles/). + + + + + For Secret Rotations, your Infisical user will require the ability to create, update, and delete users in the target database: + + ```bash + use [TARGET-DATABASE] + db.grantRolesToUser("infisical_manager", [ + { role: "userAdmin", db: "[TARGET-DATABASE]" } + ]) + ``` + + + The `userAdmin` role allows managing users (create, update passwords, delete) within the specified database. + + + + + + + +## Create MongoDB Connection in Infisical + + + + + + In your Infisical dashboard, navigate to the **App Connections** page in the desired project. + + ![App Connections Tab](/images/app-connections/general/add-connection.png) + + + Click the **+ Add Connection** button and select the **MongoDB Connection** option from the available integrations. + + ![Select MongoDB Connection](/images/app-connections/mongodb/mongodb-app-connection-option.png) + + + Complete the MongoDB Connection form by entering: + - A descriptive name for the connection + - An optional description for future reference + - The MongoDB host URL for your database + - The MongoDB port for your database + - The MongoDB username for your database + - The MongoDB password for your database + - The MongoDB database name to connect to + + You can optionally configure SSL/TLS for your MongoDB connection in the **SSL** section. + + ![MongoDB Connection Modal](/images/app-connections/mongodb/mongodb-app-connection-form.png) + + + After clicking Create, your **MongoDB Connection** is established and ready to use with your Infisical project. + + ![MongoDB Connection Created](/images/app-connections/mongodb/mongodb-app-connection-generated.png) + + + + + To create a MongoDB Connection, make an API request to the [Create MongoDB Connection](/api-reference/endpoints/app-connections/mongodb/create) API endpoint. + + ### Sample request + + ```bash Request + curl --request POST \ + --url https://app.infisical.com/api/v1/app-connections/mongodb \ + --header 'Content-Type: application/json' \ + --data '{ + "name": "my-mongodb-connection", + "method": "username-and-password", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", + "credentials": { + "host": "[MONGODB HOST]", + "port": 27017, + "username": "[MONGODB USERNAME]", + "password": "[MONGODB PASSWORD]", + "database": "[MONGODB DATABASE]" + } + }' + ``` + + ### Sample response + + ```bash Response + { + "appConnection": { + "id": "e5d18aca-86f7-4026-a95e-efb8aeb0d8e6", + "name": "my-mongodb-connection", + "projectId": "7ffbb072-2575-495a-b5b0-127f88caef78", + "description": null, + "version": 1, + "orgId": "6f03caa1-a5de-43ce-b127-95a145d3464c", + "createdAt": "2025-04-23T19:46:34.831Z", + "updatedAt": "2025-04-23T19:46:34.831Z", + "isPlatformManagedCredentials": false, + "credentialsHash": "d41d8cd98f00b204e9800998ecf8427e", + "app": "mongodb", + "method": "username-and-password", + "credentials": { + "host": "[MONGODB HOST]", + "port": 27017, + "username": "[MONGODB USERNAME]", + "database": "[MONGODB DATABASE]", + "sslEnabled": false, + "sslRejectUnauthorized": false, + "sslCertificate": "" + } + } + } + ``` + + + diff --git a/docs/snippets/AppConnectionsBrowser.jsx b/docs/snippets/AppConnectionsBrowser.jsx index d7001d6eb..c79ef366b 100644 --- a/docs/snippets/AppConnectionsBrowser.jsx +++ b/docs/snippets/AppConnectionsBrowser.jsx @@ -362,6 +362,13 @@ export const AppConnectionsBrowser = () => { "Learn how to connect your Northflank projects to pull secrets from Infisical.", category: "Hosting", }, + { + name: "MongoDB", + slug: "mongodb", + path: "/integrations/app-connections/mongodb", + description: "Learn how to connect your MongoDB to pull secrets from Infisical.", + category: "Databases" + } ].sort(function (a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); diff --git a/docs/snippets/RotationsBrowser.jsx b/docs/snippets/RotationsBrowser.jsx index 3dbede698..0666237b3 100644 --- a/docs/snippets/RotationsBrowser.jsx +++ b/docs/snippets/RotationsBrowser.jsx @@ -16,7 +16,8 @@ export const RotationsBrowser = () => { {"name": "PostgreSQL", "slug": "postgres-credentials", "path": "/documentation/platform/secret-rotation/postgres-credentials", "description": "Learn how to automatically rotate PostgreSQL database credentials.", "category": "Databases"}, {"name": "Redis", "slug": "redis-credentials", "path": "/documentation/platform/secret-rotation/redis-credentials", "description": "Learn how to automatically rotate Redis database credentials.", "category": "Databases"}, {"name": "Microsoft SQL Server", "slug": "mssql-credentials", "path": "/documentation/platform/secret-rotation/mssql-credentials", "description": "Learn how to automatically rotate Microsoft SQL Server credentials.", "category": "Databases"}, - {"name": "Oracle Database", "slug": "oracledb-credentials", "path": "/documentation/platform/secret-rotation/oracledb-credentials", "description": "Learn how to automatically rotate Oracle Database credentials.", "category": "Databases"} + {"name": "Oracle Database", "slug": "oracledb-credentials", "path": "/documentation/platform/secret-rotation/oracledb-credentials", "description": "Learn how to automatically rotate Oracle Database credentials.", "category": "Databases"}, + {"name": "MongoDB Credentials", "slug": "mongodb-credentials", "path": "/documentation/platform/secret-rotation/mongodb-credentials", "description": "Learn how to automatically rotate MongoDB credentials.", "category": "Databases"} ].sort(function(a, b) { return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); }); diff --git a/frontend/.storybook/decorators/RouterDecorator.tsx b/frontend/.storybook/decorators/RouterDecorator.tsx index a559c5cd1..c44ee6b12 100644 --- a/frontend/.storybook/decorators/RouterDecorator.tsx +++ b/frontend/.storybook/decorators/RouterDecorator.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import type { Decorator } from "@storybook/react-vite"; import { createRootRoute, createRouter, RouterProvider } from "@tanstack/react-router"; -export const RouterDecorator: Decorator = (Story) => { +export const RouterDecorator: Decorator = (Story, params) => { const router = useMemo(() => { const routeTree = createRootRoute({ component: Story @@ -11,7 +11,7 @@ export const RouterDecorator: Decorator = (Story) => { return createRouter({ routeTree }); - }, [Story]); + }, [Story, params]); return ; }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index e8fdda096..bbb72bfa7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -42,6 +42,7 @@ "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.1.3", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", @@ -136,6 +137,7 @@ "prettier": "3.4.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.14", + "tw-animate-css": "^1.4.0", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", "vite": "^6.2.0", @@ -3353,6 +3355,85 @@ } } }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", @@ -14777,6 +14858,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tw-animate-css": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz", + "integrity": "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/tweetnacl": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 8009c2118..075807caa 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -51,6 +51,7 @@ "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.1.3", + "@radix-ui/react-separator": "^1.1.8", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-switch": "^1.1.2", "@radix-ui/react-tabs": "^1.1.2", @@ -145,6 +146,7 @@ "prettier": "3.4.2", "prettier-plugin-tailwindcss": "^0.6.14", "tailwindcss": "^4.1.14", + "tw-animate-css": "^1.4.0", "typescript": "~5.6.2", "typescript-eslint": "^8.15.0", "vite": "^6.2.0", diff --git a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx index e8553f6d9..01904b852 100644 --- a/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx +++ b/frontend/src/components/secret-rotations-v2/ViewSecretRotationV2GeneratedCredentials/ViewSecretRotationV2GeneratedCredentials.tsx @@ -67,6 +67,7 @@ const Content = ({ secretRotation }: ContentProps) => { case SecretRotation.MySqlCredentials: case SecretRotation.MsSqlCredentials: case SecretRotation.OracleDBCredentials: + case SecretRotation.MongoDBCredentials: Component = ( = { [SecretRotation.LdapPassword]: LdapPasswordRotationParametersFields, [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationParametersFields, [SecretRotation.OktaClientSecret]: OktaClientSecretRotationParametersFields, - [SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields + [SecretRotation.RedisCredentials]: RedisCredentialsRotationParametersFields, + [SecretRotation.MongoDBCredentials]: SqlCredentialsRotationParametersFields }; export const SecretRotationV2ParametersFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx index e484a64b1..d48c69cca 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2ReviewFields/SecretRotationReviewFields.tsx @@ -24,7 +24,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.LdapPassword]: LdapPasswordRotationReviewFields, [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationReviewFields, [SecretRotation.OktaClientSecret]: OktaClientSecretRotationReviewFields, - [SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields + [SecretRotation.RedisCredentials]: RedisCredentialsRotationReviewFields, + [SecretRotation.MongoDBCredentials]: SqlCredentialsRotationReviewFields }; export const SecretRotationV2ReviewFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx index e05fd31f5..a211abff8 100644 --- a/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx +++ b/frontend/src/components/secret-rotations-v2/forms/SecretRotationV2SecretsMappingFields/SecretRotationV2SecretsMappingFields.tsx @@ -21,7 +21,8 @@ const COMPONENT_MAP: Record = { [SecretRotation.LdapPassword]: LdapPasswordRotationSecretsMappingFields, [SecretRotation.AwsIamUserSecret]: AwsIamUserSecretRotationSecretsMappingFields, [SecretRotation.OktaClientSecret]: OktaClientSecretRotationSecretsMappingFields, - [SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields + [SecretRotation.RedisCredentials]: RedisCredentialsRotationSecretsMappingFields, + [SecretRotation.MongoDBCredentials]: SqlCredentialsRotationSecretsMappingFields }; export const SecretRotationV2SecretsMappingFields = () => { diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts index 199036a8f..3ea0f75ed 100644 --- a/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/index.ts @@ -4,6 +4,7 @@ import { Auth0ClientSecretRotationSchema } from "@app/components/secret-rotation import { AwsIamUserSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/aws-iam-user-secret-rotation-schema"; import { AzureClientSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/azure-client-secret-rotation-schema"; import { LdapPasswordRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/ldap-password-rotation-schema"; +import { MongoDBCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mongodb-credentials-rotation-schema"; import { MsSqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mssql-credentials-rotation-schema"; import { MySqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/mysql-credentials-rotation-schema"; import { PostgresCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/postgres-credentials-rotation-schema"; @@ -27,7 +28,8 @@ export const SecretRotationV2FormSchema = (isUpdate: boolean) => LdapPasswordRotationSchema, AwsIamUserSecretRotationSchema, OktaClientSecretRotationSchema, - RedisCredentialsRotationSchema + RedisCredentialsRotationSchema, + MongoDBCredentialsRotationSchema ]), z.object({ id: z.string().optional() }) ) diff --git a/frontend/src/components/secret-rotations-v2/forms/schemas/mongodb-credentials-rotation-schema.ts b/frontend/src/components/secret-rotations-v2/forms/schemas/mongodb-credentials-rotation-schema.ts new file mode 100644 index 000000000..24c22cfed --- /dev/null +++ b/frontend/src/components/secret-rotations-v2/forms/schemas/mongodb-credentials-rotation-schema.ts @@ -0,0 +1,12 @@ +import { z } from "zod"; + +import { BaseSecretRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/base-secret-rotation-v2-schema"; +import { SqlCredentialsRotationSchema } from "@app/components/secret-rotations-v2/forms/schemas/shared"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; + +export const MongoDBCredentialsRotationSchema = z + .object({ + type: z.literal(SecretRotation.MongoDBCredentials) + }) + .merge(SqlCredentialsRotationSchema) + .merge(BaseSecretRotationSchema); diff --git a/frontend/src/components/v2/Input/Input.tsx b/frontend/src/components/v2/Input/Input.tsx index 30cd4ce8e..9227d57b3 100644 --- a/frontend/src/components/v2/Input/Input.tsx +++ b/frontend/src/components/v2/Input/Input.tsx @@ -65,6 +65,12 @@ const inputParentContainerVariants = cva("inline-flex font-inter items-center bo } }); +const data1pIgnore = (autoComplete?: string) => { + if (!autoComplete) return true; + + return !autoComplete.match(/(email|password|username)/i); +}; + export type InputProps = Omit, "size"> & VariantProps & Props; @@ -86,6 +92,7 @@ export const Input = forwardRef( isReadOnly, autoCapitalization, warning, + autoComplete, ...props }, ref @@ -116,6 +123,8 @@ export const Input = forwardRef( readOnly={isReadOnly} disabled={isDisabled} onInput={handleInput} + autoComplete={autoComplete} + data-1p-ignore={data1pIgnore(autoComplete)} className={twMerge( leftIcon ? "pl-10" : "pl-2.5", rightIcon || warning ? "pr-10" : "pr-2.5", diff --git a/frontend/src/components/v2/PageHeader/PageHeader.tsx b/frontend/src/components/v2/PageHeader/PageHeader.tsx index 5a2ba2e12..05e0c131b 100644 --- a/frontend/src/components/v2/PageHeader/PageHeader.tsx +++ b/frontend/src/components/v2/PageHeader/PageHeader.tsx @@ -32,7 +32,7 @@ export const PageHeader = ({ title, description, children, className, scope }: P

{children}

-
{description}
+
{description}
); diff --git a/frontend/src/components/v3/generic/Accordion/Accordion.tsx b/frontend/src/components/v3/generic/Accordion/Accordion.tsx new file mode 100644 index 000000000..dcf4d5559 --- /dev/null +++ b/frontend/src/components/v3/generic/Accordion/Accordion.tsx @@ -0,0 +1,79 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDownIcon } from "lucide-react"; + +import { cn } from "../../utils"; + +function UnstableAccordion({ ...props }: React.ComponentProps) { + return ( + + ); +} + +function UnstableAccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function UnstableAccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + "cursor-pointer hover:bg-foreground/5", + "data-[state=open]:bg-foreground/5", + className + )} + {...props} + > + + {children} + + + ); +} + +function UnstableAccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ); +} + +export { + UnstableAccordion, + UnstableAccordionContent, + UnstableAccordionItem, + UnstableAccordionTrigger +}; diff --git a/frontend/src/components/v3/generic/Accordion/index.ts b/frontend/src/components/v3/generic/Accordion/index.ts new file mode 100644 index 000000000..16e0243c2 --- /dev/null +++ b/frontend/src/components/v3/generic/Accordion/index.ts @@ -0,0 +1 @@ +export * from "./Accordion"; diff --git a/frontend/src/components/v3/generic/Alert/Alert.tsx b/frontend/src/components/v3/generic/Alert/Alert.tsx new file mode 100644 index 000000000..a5de2cb91 --- /dev/null +++ b/frontend/src/components/v3/generic/Alert/Alert.tsx @@ -0,0 +1,62 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; +import { cva, type VariantProps } from "cva"; + +import { cn } from "../../utils"; + +const alertVariants = cva( + "relative w-full border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", + { + variants: { + variant: { + default: "bg-container text-card-foreground", + info: "bg-info/10 text-info border-info/20", + org: "bg-org/10 text-org border-org/20" + } + }, + defaultVariants: { + variant: "default" + } + } +); + +function UnstableAlert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function UnstableAlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableAlertDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { UnstableAlert, UnstableAlertDescription, UnstableAlertTitle }; diff --git a/frontend/src/components/v3/generic/Alert/index.ts b/frontend/src/components/v3/generic/Alert/index.ts new file mode 100644 index 000000000..b8e17a03c --- /dev/null +++ b/frontend/src/components/v3/generic/Alert/index.ts @@ -0,0 +1 @@ +export * from "./Alert"; diff --git a/frontend/src/components/v3/generic/Badge/Badge.stories.tsx b/frontend/src/components/v3/generic/Badge/Badge.stories.tsx index 4cbf955f9..244bd8108 100644 --- a/frontend/src/components/v3/generic/Badge/Badge.stories.tsx +++ b/frontend/src/components/v3/generic/Badge/Badge.stories.tsx @@ -16,6 +16,7 @@ import { } from "lucide-react"; import { OrgIcon, ProjectIcon, SubOrgIcon } from "../../platform"; +import { UnstableButtonGroup } from "../ButtonGroup"; import { Badge } from "./Badge"; /** @@ -33,13 +34,34 @@ const meta = { argTypes: { variant: { control: "select", - options: ["neutral", "success", "info", "warning", "danger", "project", "org", "sub-org"] + options: [ + "default", + "outline", + "neutral", + "success", + "info", + "warning", + "danger", + "project", + "org", + "sub-org" + ] }, isTruncatable: { table: { disable: true } }, + isFullWidth: { + table: { + disable: true + } + }, + isSquare: { + table: { + disable: true + } + }, asChild: { table: { disable: true @@ -57,6 +79,38 @@ const meta = { export default meta; type Story = StoryObj; +export const Default: Story = { + name: "Variant: Default", + args: { + variant: "default", + children: <>Default + }, + parameters: { + docs: { + description: { + story: + "Use this variant when other badge variants are not applicable or as the key when displaying key-value pairs with ButtonGroup." + } + } + } +}; + +export const Outline: Story = { + name: "Variant: Outline", + args: { + variant: "outline", + children: <>Outline + }, + parameters: { + docs: { + description: { + story: + "Use this variant when other badge variants are not applicable or as the value when displaying key-value pairs with ButtonGroup." + } + } + } +}; + export const Neutral: Story = { name: "Variant: Neutral", args: { @@ -71,8 +125,7 @@ export const Neutral: Story = { parameters: { docs: { description: { - story: - "Use this variant when indicating neutral or disabled states or when linking to external documents." + story: "Use this variant when indicating neutral or disabled states." } } } @@ -133,7 +186,8 @@ export const Info: Story = { parameters: { docs: { description: { - story: "Use this variant when indicating informational states." + story: + "Use this variant when indicating informational states or when linking to external documentation." } } } @@ -374,3 +428,22 @@ export const IsFullWidth: Story = {
) }; + +export const KeyValuePair: Story = { + name: "Example: Key-Value Pair", + args: {}, + parameters: { + docs: { + description: { + story: + "Use a default and outline badge in conjunction with the `` component to display key-value pairs." + } + } + }, + decorators: () => ( + + Key + Value + + ) +}; diff --git a/frontend/src/components/v3/generic/Badge/Badge.tsx b/frontend/src/components/v3/generic/Badge/Badge.tsx index f94bff5f2..a31f279f2 100644 --- a/frontend/src/components/v3/generic/Badge/Badge.tsx +++ b/frontend/src/components/v3/generic/Badge/Badge.tsx @@ -6,7 +6,7 @@ import { cn } from "@app/components/v3/utils"; const badgeVariants = cva( [ - "select-none items-center align-middle rounded-sm h-4.5 px-1.5 text-xs", + "select-none border items-center align-middle rounded-sm h-4.5 px-1.5 text-xs", "gap-x-1 [a&,button&]:cursor-pointer inline-flex font-normal", "[&>svg]:pointer-events-none [&>svg]:shrink-0 [&>svg]:stroke-[2.25] [&_svg:not([class*='size-'])]:size-3", "transition duration-200 ease-in-out" @@ -24,19 +24,22 @@ const badgeVariants = cva( true: "w-4.5 justify-center px-0.5" }, variant: { - ghost: "text-mineshaft-200 gap-x-2", - neutral: "bg-neutral/25 text-neutral [a&,button&]:hover:bg-neutral/35", - success: "bg-success/25 text-success [a&,button&]:hover:bg-success/35", - info: "bg-info/25 text-info [a&,button&]:hover:bg-info/35", - warning: "bg-warning/25 text-warning [a&,button&]:hover:bg-warning/35", - danger: "bg-danger/25 text-danger [a&,button&]:hover:bg-danger/35", - project: "bg-project/25 text-project [a&,button&]:hover:bg-project/35", - org: "bg-org/25 text-org [a&,button&]:hover:bg-org/35", - "sub-org": "bg-sub-org/25 text-sub-org [a&,button&]:hover:bg-sub-org/35" + ghost: "text-foreground border-none", + default: "bg-label text-background border-label [a&,button&]:hover:bg-primary/35", + outline: "text-label border-label border", + neutral: "bg-neutral/15 border-neutral/10 text-neutral [a&,button&]:hover:bg-neutral/35", + success: "bg-success/15 border-success/10 text-success [a&,button&]:hover:bg-success/35", + info: "bg-info/15 border-info/10 border text-info [a&,button&]:hover:bg-info/35", + warning: "bg-warning/15 border-warning/10 text-warning [a&,button&]:hover:bg-warning/35", + danger: "bg-danger/15 border-danger/10 text-danger border [a&,button&]:hover:bg-danger/35", + project: + "bg-project/15 text-project border-project/10 border [a&,button&]:hover:bg-project/35", + org: "bg-org/15 border border-org/10 text-org [a&,button&]:hover:bg-org/35", + "sub-org": "bg-sub-org/15 border-sub-org/10 text-sub-org [a&,button&]:hover:bg-sub-org/35" } }, defaultVariants: { - variant: "neutral" + variant: "default" } } ); @@ -44,7 +47,6 @@ const badgeVariants = cva( type TBadgeProps = VariantProps & React.ComponentProps<"span"> & { asChild?: boolean; - variant: NonNullable["variant"]>; // TODO: REMOVE }; const Badge = forwardRef( diff --git a/frontend/src/components/v3/generic/Button/Button.stories.tsx b/frontend/src/components/v3/generic/Button/Button.stories.tsx new file mode 100644 index 000000000..0f975e70d --- /dev/null +++ b/frontend/src/components/v3/generic/Button/Button.stories.tsx @@ -0,0 +1,357 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { + AsteriskIcon, + BanIcon, + CheckIcon, + CircleXIcon, + ExternalLinkIcon, + InfoIcon, + RadarIcon, + TriangleAlertIcon, + UserIcon +} from "lucide-react"; + +import { OrgIcon, ProjectIcon, SubOrgIcon } from "../../platform"; +import { UnstableButton } from "./Button"; + +/** + * Buttons act as an indicator that can optionally be made interactable. + * You can place text and icons inside a Button. + * Buttons are often used for the indication of a status, state or scope. + */ +const meta = { + title: "Generic/Button", + component: UnstableButton, + parameters: { + layout: "centered" + }, + tags: ["autodocs"], + argTypes: { + variant: { + control: "select", + options: [ + "default", + "outline", + "neutral", + "success", + "info", + "warning", + "danger", + "project", + "org", + "sub-org" + ] + }, + size: { + control: "select", + options: ["xs", "sm", "md", "lg"] + }, + isPending: { + control: "boolean" + }, + isFullWidth: { + control: "boolean" + }, + isDisabled: { + control: "boolean" + }, + as: { + table: { + disable: true + } + }, + children: { + table: { + disable: true + } + } + }, + args: { children: "Button", isPending: false, isDisabled: false, isFullWidth: false, size: "md" } +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + name: "Variant: Default", + args: { + variant: "default", + children: <>Default + }, + parameters: { + docs: { + description: { + story: + "Use this variant when other Button variants are not applicable or as the key when displaying key-value pairs with ButtonGroup." + } + } + } +}; + +export const Outline: Story = { + name: "Variant: Outline", + args: { + variant: "outline", + children: <>Outline + }, + parameters: { + docs: { + description: { + story: + "Use this variant when other Button variants are not applicable or as the value when displaying key-value pairs with ButtonGroup." + } + } + } +}; + +export const Neutral: Story = { + name: "Variant: Neutral", + args: { + variant: "neutral", + children: ( + <> + + Disabled + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating neutral or disabled states." + } + } + } +}; + +export const Ghost: Story = { + name: "Variant: Ghost", + args: { + variant: "ghost", + children: ( + <> + + User + + ) + }, + parameters: { + docs: { + description: { + story: + "Use this variant when indicating a configuration or property value. Avoid using this variant as an interactive element as it is not intuitive to interact with." + } + } + } +}; + +export const Success: Story = { + name: "Variant: Success", + args: { + variant: "success", + children: ( + <> + + Success + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating successful or healthy states." + } + } + } +}; + +export const Info: Story = { + name: "Variant: Info", + args: { + variant: "info", + children: ( + <> + + Info + + ) + }, + parameters: { + docs: { + description: { + story: + "Use this variant when indicating informational states or when linking to external documentation." + } + } + } +}; + +export const Warning: Story = { + name: "Variant: Warning", + args: { + variant: "warning", + children: ( + <> + + Warning + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating activity or attention warranting states." + } + } + } +}; + +export const Danger: Story = { + name: "Variant: Danger", + args: { + variant: "danger", + children: ( + <> + + Danger + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating destructive or error states." + } + } + } +}; + +export const Organization: Story = { + name: "Variant: Organization", + args: { + variant: "org", + children: ( + <> + + Organization + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating organization scope or links." + } + } + } +}; + +export const SubOrganization: Story = { + name: "Variant: Sub-Organization", + args: { + variant: "sub-org", + children: ( + <> + + Sub-Organization + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating sub-organization scope or links." + } + } + } +}; + +export const Project: Story = { + name: "Variant: Project", + args: { + variant: "project", + children: ( + <> + + Project + + ) + }, + parameters: { + docs: { + description: { + story: "Use this variant when indicating project scope or links." + } + } + } +}; + +export const AsExternalLink: Story = { + name: "Example: As External Link", + args: { + variant: "info", + as: "a", + href: "https://www.infisical.com", + children: ( + <> + Link + + ) + }, + parameters: { + docs: { + description: { + story: 'Use the `as="a"` prop to use a Button as an external `a` tag component.' + } + } + } +}; + +export const AsRouterLink: Story = { + name: "Example: As Router Link", + args: { + variant: "project", + as: "link", + children: ( + <> + + Secret Scanning + + ) + }, + parameters: { + docs: { + description: { + story: 'Use the `as="link"` prop to use a Button as an internal `Link` component.' + } + } + } +}; + +export const IsFullWidth: Story = { + name: "Example: isFullWidth", + args: { + variant: "neutral", + isFullWidth: true, + + children: ( + <> + + Secret Value + + ) + }, + parameters: { + docs: { + description: { + story: + "Use the `isFullWidth` prop to expand the Buttons width to fill it's parent container." + } + } + }, + decorators: (Story) => ( +
+ +
+ ) +}; diff --git a/frontend/src/components/v3/generic/Button/Button.tsx b/frontend/src/components/v3/generic/Button/Button.tsx new file mode 100644 index 000000000..4c065b1d0 --- /dev/null +++ b/frontend/src/components/v3/generic/Button/Button.tsx @@ -0,0 +1,139 @@ +import * as React from "react"; +import { forwardRef } from "react"; +import { Link, LinkProps } from "@tanstack/react-router"; +import { cva, type VariantProps } from "cva"; + +import { Lottie } from "@app/components/v2"; +import { cn } from "@app/components/v3/utils"; + +const buttonVariants = cva( + cn( + "inline-flex items-center active:scale-[0.95] justify-center border cursor-pointer whitespace-nowrap", + " text-sm transition-all disabled:pointer-events-none disabled:opacity-75 shrink-0", + "[&>svg]:pointer-events-none [&>svg]:shrink-0", + "focus-visible:ring-ring outline-0 focus-visible:ring-2 select-none" + ), + { + variants: { + variant: { + default: + "border-foreground bg-foreground text-background hover:bg-foreground/90 hover:border-foreground/90", + neutral: + "border-neutral/10 bg-neutral/40 text-foreground hover:bg-neutral/50 hover:border-neutral/20", + outline: "text-foreground hover:bg-foreground/10 border-border hover:border-foreground/20", + ghost: "text-foreground hover:bg-foreground/10 border-transparent", + project: + "border-project/25 bg-project/15 text-foreground hover:bg-project/30 hover:border-project/30", + org: "border-org/25 bg-org/15 text-foreground hover:bg-org/30 hover:border-org/30", + "sub-org": + "border-sub-org/25 bg-sub-org/15 text-foreground hover:bg-sub-org/30 hover:border-sub-org/30", + success: + "border-success/25 bg-success/15 text-foreground hover:bg-success/30 hover:border-success/30", + info: "border-info/25 bg-info/15 text-foreground hover:bg-info/30 hover:border-info/30", + warning: + "border-warning/25 bg-warning/15 text-foreground hover:bg-warning/30 hover:border-warning/30", + danger: + "border-danger/25 bg-danger/15 text-foreground hover:bg-danger/30 hover:border-danger/30" + }, + size: { + xs: "h-7 px-2 rounded-[3px] text-xs [&>svg]:size-3 gap-1.5", + sm: "h-8 px-2.5 rounded-[4px] text-sm [&>svg]:size-3 gap-1.5", + md: "h-9 px-3 rounded-[5px] text-sm [&>svg]:size-3.5 gap-1.5", + lg: "h-10 px-3 rounded-[6px] text-sm [&>svg]:size-3.5 gap-1.5" + }, + isPending: { + true: "text-transparent" + }, + isFullWidth: { + true: "w-full", + false: "w-fit" + } + }, + defaultVariants: { + variant: "default", + size: "md" + } + } +); + +type UnstableButtonProps = (VariantProps & { + isPending?: boolean; + isFullWidth?: boolean; + isDisabled?: boolean; +}) & + ( + | ({ as?: "button" | undefined } & React.ComponentProps<"button">) + | ({ as: "link"; className?: string } & LinkProps) + | ({ as: "a" } & React.ComponentProps<"a">) + ); + +const UnstableButton = forwardRef( + ( + { + className, + variant = "default", + size = "md", + isPending = false, + isFullWidth = false, + isDisabled = false, + children, + ...props + }, + ref + ): JSX.Element => { + const sharedProps = { + "data-slot": "button", + className: cn(buttonVariants({ variant, size, isPending, isFullWidth }), className) + }; + + const child = ( + <> + {children} + {isPending && ( + + )} + + ); + + switch (props.as) { + case "a": + return ( + } + target="_blank" + rel="noopener noreferrer" + {...props} + {...sharedProps} + > + {child} + + ); + case "link": + return ( + } {...props} {...sharedProps}> + {child} + + ); + default: + return ( + + ); + } + } +); + +UnstableButton.displayName = "Button"; + +export { buttonVariants, UnstableButton, type UnstableButtonProps }; diff --git a/frontend/src/components/v3/generic/Button/index.ts b/frontend/src/components/v3/generic/Button/index.ts new file mode 100644 index 000000000..e22c29adc --- /dev/null +++ b/frontend/src/components/v3/generic/Button/index.ts @@ -0,0 +1 @@ +export * from "./Button"; diff --git a/frontend/src/components/v3/generic/ButtonGroup/ButtonGroup.tsx b/frontend/src/components/v3/generic/ButtonGroup/ButtonGroup.tsx new file mode 100644 index 000000000..753d28a31 --- /dev/null +++ b/frontend/src/components/v3/generic/ButtonGroup/ButtonGroup.tsx @@ -0,0 +1,83 @@ +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "cva"; + +import { cn } from "../../utils"; +import { UnstableSeparator } from "../Separator"; + +const buttonGroupVariants = cva( + "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", + { + variants: { + orientation: { + horizontal: + "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", + vertical: + "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none" + } + }, + defaultVariants: { + orientation: "horizontal" + } + } +); + +function UnstableButtonGroup({ + className, + orientation, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ); +} + +function UnstableButtonGroupText({ + className, + asChild = false, + ...props +}: React.ComponentProps<"div"> & { + asChild?: boolean; +}) { + const Comp = asChild ? Slot : "div"; + + return ( + + ); +} + +function UnstableButtonGroupSeparator({ + className, + orientation = "vertical", + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { + buttonGroupVariants, + UnstableButtonGroup, + UnstableButtonGroupSeparator, + UnstableButtonGroupText +}; diff --git a/frontend/src/components/v3/generic/ButtonGroup/index.ts b/frontend/src/components/v3/generic/ButtonGroup/index.ts new file mode 100644 index 000000000..d22eaf4c2 --- /dev/null +++ b/frontend/src/components/v3/generic/ButtonGroup/index.ts @@ -0,0 +1 @@ +export * from "./ButtonGroup"; diff --git a/frontend/src/components/v3/generic/Card/Card.tsx b/frontend/src/components/v3/generic/Card/Card.tsx new file mode 100644 index 000000000..f38930526 --- /dev/null +++ b/frontend/src/components/v3/generic/Card/Card.tsx @@ -0,0 +1,82 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; + +import { cn } from "../../utils"; + +function UnstableCard({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableCardHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableCardTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function CardDescription({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableCardAction({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableCardContent({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function UnstableCardFooter({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + UnstableCard, + UnstableCardAction, + UnstableCardContent, + CardDescription as UnstableCardDescription, + UnstableCardFooter, + UnstableCardHeader, + UnstableCardTitle +}; diff --git a/frontend/src/components/v3/generic/Card/index.ts b/frontend/src/components/v3/generic/Card/index.ts new file mode 100644 index 000000000..24d321246 --- /dev/null +++ b/frontend/src/components/v3/generic/Card/index.ts @@ -0,0 +1 @@ +export * from "./Card"; diff --git a/frontend/src/components/v3/generic/Detail/Detail.tsx b/frontend/src/components/v3/generic/Detail/Detail.tsx new file mode 100644 index 000000000..16aa8d0d4 --- /dev/null +++ b/frontend/src/components/v3/generic/Detail/Detail.tsx @@ -0,0 +1,23 @@ +import { cn } from "../../utils"; + +function Detail({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function DetailLabel({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function DetailValue({ className, ...props }: React.ComponentProps<"div">) { + return
; +} + +function DetailGroup({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { Detail, DetailGroup, DetailLabel, DetailValue }; diff --git a/frontend/src/components/v3/generic/Detail/index.ts b/frontend/src/components/v3/generic/Detail/index.ts new file mode 100644 index 000000000..f511dd353 --- /dev/null +++ b/frontend/src/components/v3/generic/Detail/index.ts @@ -0,0 +1 @@ +export * from "./Detail"; diff --git a/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx b/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx new file mode 100644 index 000000000..5a93c3fb6 --- /dev/null +++ b/frontend/src/components/v3/generic/Dropdown/Dropdown.tsx @@ -0,0 +1,255 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"; + +import { cn } from "@app/components/v3/utils"; + +function UnstableDropdownMenu({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuPortal({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuTrigger({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuContent({ + className, + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + + ); +} + +function UnstableDropdownMenuGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuItem({ + className, + inset, + variant = "default", + isDisabled, + ...props +}: Omit, "disabled"> & { + inset?: boolean; + variant?: "default" | "danger"; + isDisabled?: boolean; +}) { + return ( + + ); +} + +function UnstableDropdownMenuCheckboxItem({ + className, + children, + checked, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function UnstableDropdownMenuRadioGroup({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuRadioItem({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + + + + + + {children} + + ); +} + +function UnstableDropdownMenuLabel({ + className, + inset, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + ); +} + +function UnstableDropdownMenuSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function UnstableDropdownMenuShortcut({ className, ...props }: React.ComponentProps<"span">) { + return ( + + ); +} + +function UnstableDropdownMenuSub({ + ...props +}: React.ComponentProps) { + return ; +} + +function UnstableDropdownMenuSubTrigger({ + className, + inset, + children, + ...props +}: React.ComponentProps & { + inset?: boolean; +}) { + return ( + + {children} + + + ); +} + +function UnstableDropdownMenuSubContent({ + className, + sideOffset = 8, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +type UnstableDropdownMenuChecked = DropdownMenuPrimitive.DropdownMenuCheckboxItemProps["checked"]; + +export { + UnstableDropdownMenu, + UnstableDropdownMenuCheckboxItem, + type UnstableDropdownMenuChecked, + UnstableDropdownMenuContent, + UnstableDropdownMenuGroup, + UnstableDropdownMenuItem, + UnstableDropdownMenuLabel, + UnstableDropdownMenuPortal, + UnstableDropdownMenuRadioGroup, + UnstableDropdownMenuRadioItem, + UnstableDropdownMenuSeparator, + UnstableDropdownMenuShortcut, + UnstableDropdownMenuSub, + UnstableDropdownMenuSubContent, + UnstableDropdownMenuSubTrigger, + UnstableDropdownMenuTrigger +}; diff --git a/frontend/src/components/v3/generic/Dropdown/index.ts b/frontend/src/components/v3/generic/Dropdown/index.ts new file mode 100644 index 000000000..f024a9e9a --- /dev/null +++ b/frontend/src/components/v3/generic/Dropdown/index.ts @@ -0,0 +1 @@ +export * from "./Dropdown"; diff --git a/frontend/src/components/v3/generic/Empty/Empty.tsx b/frontend/src/components/v3/generic/Empty/Empty.tsx new file mode 100644 index 000000000..3b86bcc88 --- /dev/null +++ b/frontend/src/components/v3/generic/Empty/Empty.tsx @@ -0,0 +1,100 @@ +import { cn } from "../../utils"; + +function UnstableEmpty({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableEmptyHeader({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +// scott: TODO + +// const emptyMediaVariants = cva( +// "flex shrink-0 items-center justify-center mb-2 [&_svg]:pointer-events-none [&_svg]:shrink-0", +// { +// variants: { +// variant: { +// default: "bg-transparent", +// icon: "bg-bunker-900 rounded text-foreground flex size-10 shrink-0 items-center justify-center [&_svg:not([class*='size-'])]:size-6" +// } +// }, +// defaultVariants: { +// variant: "default" +// } +// } +// ); + +// function EmptyMedia({ +// className, +// variant = "default", +// ...props +// }: React.ComponentProps<"div"> & VariantProps) { +// return ( +//
+// ); +// } + +function UnstableEmptyTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +function UnstableEmptyDescription({ className, ...props }: React.ComponentProps<"p">) { + return ( +
a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-project", + className + )} + {...props} + /> + ); +} + +function UnstableEmptyContent({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ); +} + +export { + UnstableEmpty, + UnstableEmptyContent, + UnstableEmptyDescription, + UnstableEmptyHeader, + UnstableEmptyTitle +}; diff --git a/frontend/src/components/v3/generic/Empty/index.ts b/frontend/src/components/v3/generic/Empty/index.ts new file mode 100644 index 000000000..7aa85b1b7 --- /dev/null +++ b/frontend/src/components/v3/generic/Empty/index.ts @@ -0,0 +1 @@ +export * from "./Empty"; diff --git a/frontend/src/components/v3/generic/IconButton/IconButton.tsx b/frontend/src/components/v3/generic/IconButton/IconButton.tsx new file mode 100644 index 000000000..d6fdfb4ce --- /dev/null +++ b/frontend/src/components/v3/generic/IconButton/IconButton.tsx @@ -0,0 +1,111 @@ +import * as React from "react"; +import { forwardRef } from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "cva"; +import { twMerge } from "tailwind-merge"; + +import { Lottie } from "@app/components/v2"; +import { cn } from "@app/components/v3/utils"; + +const iconButtonVariants = cva( + cn( + "inline-flex items-center active:scale-[0.99] justify-center border cursor-pointer whitespace-nowrap rounded-[4px] text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-75 [&_svg]:pointer-events-none shrink-0 [&>svg]:shrink-0", + "focus-visible:ring-ring outline-0 focus-visible:ring-2" + ), + { + variants: { + variant: { + default: + "border-foreground bg-foreground text-background hover:bg-foreground/90 hover:border-foreground/90", + accent: + "border-accent/10 bg-accent/40 text-foreground hover:bg-accent/50 hover:border-accent/20", + outline: "text-foreground hover:bg-foreground/20 border-border hover:border-foreground/50", + ghost: "text-foreground hover:bg-foreground/40 border-transparent", + project: + "border-project/75 bg-project/40 text-foreground hover:bg-project/50 hover:border-kms", + org: "border-org/75 bg-org/40 text-foreground hover:bg-org/50 hover:border-org", + "sub-org": + "border-sub-org/75 bg-sub-org/40 text-foreground hover:bg-sub-org/50 hover:border-namespace", + success: + "border-success/75 bg-success/40 text-foreground hover:bg-success/50 hover:border-success", + info: "border-info/75 bg-info/40 text-foreground hover:bg-info/50 hover:border-info", + warning: + "border-warning/75 bg-warning/40 text-foreground hover:bg-warning/50 hover:border-warning", + danger: + "border-danger/75 bg-danger/40 text-foreground hover:bg-danger/50 hover:border-danger" + }, + size: { + xs: "h-7 w-7 [&>svg]:size-3.5 [&>svg]:stroke-[1.75]", + sm: "h-8 w-8 [&>svg]:size-4 [&>svg]:stroke-[1.5]", + md: "h-9 w-9 [&>svg]:size-6 [&>svg]:stroke-[1.5]", + lg: "h-10 w-10 [&>svg]:size-7 [&>svg]:stroke-[1.5]" + }, + isPending: { + true: "text-transparent" + }, + isFullWidth: { + true: "w-full", + false: "w-fit" + } + }, + defaultVariants: { + variant: "default", + size: "md" + } + } +); + +type UnstableIconButtonProps = React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean; + isPending?: boolean; + isDisabled?: boolean; + }; + +const UnstableIconButton = forwardRef( + ( + { + className, + variant = "default", + size = "md", + asChild = false, + isPending = false, + disabled = false, + isDisabled = false, + children, + ...props + }, + ref + ): JSX.Element => { + const Comp = asChild ? Slot : "button"; + + return ( + + {children} + {isPending && ( + + )} + + ); + } +); + +UnstableIconButton.displayName = "IconButton"; + +export { iconButtonVariants, UnstableIconButton, type UnstableIconButtonProps }; diff --git a/frontend/src/components/v3/generic/IconButton/index.ts b/frontend/src/components/v3/generic/IconButton/index.ts new file mode 100644 index 000000000..53185101d --- /dev/null +++ b/frontend/src/components/v3/generic/IconButton/index.ts @@ -0,0 +1 @@ +export * from "./IconButton"; diff --git a/frontend/src/components/v3/generic/PageLoader/PageLoader.tsx b/frontend/src/components/v3/generic/PageLoader/PageLoader.tsx new file mode 100644 index 000000000..346eead3a --- /dev/null +++ b/frontend/src/components/v3/generic/PageLoader/PageLoader.tsx @@ -0,0 +1,9 @@ +import { Lottie } from "@app/components/v2"; + +export function UnstablePageLoader() { + return ( +
+ +
+ ); +} diff --git a/frontend/src/components/v3/generic/PageLoader/index.ts b/frontend/src/components/v3/generic/PageLoader/index.ts new file mode 100644 index 000000000..70c6707ce --- /dev/null +++ b/frontend/src/components/v3/generic/PageLoader/index.ts @@ -0,0 +1 @@ +export * from "./PageLoader"; diff --git a/frontend/src/components/v3/generic/Separator/Separator.tsx b/frontend/src/components/v3/generic/Separator/Separator.tsx new file mode 100644 index 000000000..0ebfe6745 --- /dev/null +++ b/frontend/src/components/v3/generic/Separator/Separator.tsx @@ -0,0 +1,28 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "../../utils"; + +function UnstableSeparator({ + className, + orientation = "horizontal", + decorative = true, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export { UnstableSeparator }; diff --git a/frontend/src/components/v3/generic/Separator/index.ts b/frontend/src/components/v3/generic/Separator/index.ts new file mode 100644 index 000000000..4060cb5ec --- /dev/null +++ b/frontend/src/components/v3/generic/Separator/index.ts @@ -0,0 +1 @@ +export * from "./Separator"; diff --git a/frontend/src/components/v3/generic/Table/Table.stories.tsx b/frontend/src/components/v3/generic/Table/Table.stories.tsx new file mode 100644 index 000000000..09098795b --- /dev/null +++ b/frontend/src/components/v3/generic/Table/Table.stories.tsx @@ -0,0 +1,138 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { CopyIcon, EditIcon, MoreHorizontalIcon, TrashIcon } from "lucide-react"; + +import { + Badge, + UnstableDropdownMenu, + UnstableDropdownMenuContent, + UnstableDropdownMenuItem, + UnstableDropdownMenuTrigger, + UnstableIconButton +} from "@app/components/v3/generic"; +import { ProjectIcon } from "@app/components/v3/platform"; + +import { + UnstableTable, + UnstableTableBody, + UnstableTableCell, + UnstableTableHead, + UnstableTableHeader, + UnstableTableRow +} from "./Table"; + +const identities: { + name: string; + role: string; + managedBy?: { scope: "org" | "namespace"; name: string }; +}[] = [ + { + name: "machine-one", + role: "Admin", + managedBy: { + scope: "org", + name: "infisical" + } + }, + { + name: "machine-two", + role: "Viewer", + managedBy: { + scope: "namespace", + name: "engineering" + } + }, + { + name: "machine-three", + role: "Developer" + }, + { + name: "machine-four", + role: "Admin", + managedBy: { + scope: "namespace", + name: "dev-ops" + } + }, + { + name: "machine-five", + role: "Viewer", + managedBy: { + scope: "org", + name: "infisical" + } + }, + { + name: "machine-six", + role: "Developer" + } +]; + +function TableDemo() { + return ( + + + + Name + Role + Managed By + + + + + {identities.map((identity) => ( + + {identity.name} + {identity.role} + + + + Project + + + + + + + + + + + + + Copy ID + + + + Edit Identity + + + + Delete Identity + + + + + + ))} + + + ); +} + +const meta = { + title: "Generic/Table", + component: TableDemo, + parameters: { + layout: "centered" + }, + tags: ["autodocs"], + argTypes: {} +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const KitchenSInk: Story = { + name: "Example: Kitchen Sink", + args: {} +}; diff --git a/frontend/src/components/v3/generic/Table/Table.tsx b/frontend/src/components/v3/generic/Table/Table.tsx new file mode 100644 index 000000000..ffd25b766 --- /dev/null +++ b/frontend/src/components/v3/generic/Table/Table.tsx @@ -0,0 +1,106 @@ +/* eslint-disable react/prop-types */ + +import * as React from "react"; + +import { cn } from "@app/components/v3/utils"; + +function UnstableTable({ className, ...props }: React.ComponentProps<"table">) { + return ( +
+ + + ); +} + +function UnstableTableHeader({ className, ...props }: React.ComponentProps<"thead">) { + return ( + + ); +} + +function UnstableTableBody({ className, ...props }: React.ComponentProps<"tbody">) { + return ( + tr]:last:border-b-0", className)} {...props} /> + ); +} + +function UnstableTableFooter({ className, ...props }: React.ComponentProps<"tfoot">) { + return ( + tr]:last:border-b-0", className)} + {...props} + /> + ); +} + +function UnstableTableRow({ className, ...props }: React.ComponentProps<"tr">) { + return ( + + ); +} + +function UnstableTableHead({ className, ...props }: React.ComponentProps<"th">) { + return ( +
[role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ); +} + +function UnstableTableCell({ className, ...props }: React.ComponentProps<"td">) { + return ( + [role=checkbox]]:translate-y-[2px]", + className + )} + {...props} + /> + ); +} + +function UnstableTableCaption({ className, ...props }: React.ComponentProps<"caption">) { + return ( +
+ ); +} + +export { + UnstableTable, + UnstableTableBody, + UnstableTableCaption, + UnstableTableCell, + UnstableTableFooter, + UnstableTableHead, + UnstableTableHeader, + UnstableTableRow +}; diff --git a/frontend/src/components/v3/generic/Table/index.ts b/frontend/src/components/v3/generic/Table/index.ts new file mode 100644 index 000000000..e40efa476 --- /dev/null +++ b/frontend/src/components/v3/generic/Table/index.ts @@ -0,0 +1 @@ +export * from "./Table"; diff --git a/frontend/src/components/v3/generic/index.ts b/frontend/src/components/v3/generic/index.ts index ae21190ba..43ddf08b0 100644 --- a/frontend/src/components/v3/generic/index.ts +++ b/frontend/src/components/v3/generic/index.ts @@ -1 +1,13 @@ +export * from "./Accordion"; +export * from "./Alert"; export * from "./Badge"; +export * from "./Button"; +export * from "./ButtonGroup"; +export * from "./Card"; +export * from "./Detail"; +export * from "./Dropdown"; +export * from "./Empty"; +export * from "./IconButton"; +export * from "./PageLoader"; +export * from "./Separator"; +export * from "./Table"; diff --git a/frontend/src/components/v3/platform/ScopeIcons.tsx b/frontend/src/components/v3/platform/ScopeIcons.tsx index 8f8712319..9f21ce250 100644 --- a/frontend/src/components/v3/platform/ScopeIcons.tsx +++ b/frontend/src/components/v3/platform/ScopeIcons.tsx @@ -1,8 +1,8 @@ import { BoxesIcon, BoxIcon, Building2Icon, ServerIcon } from "lucide-react"; -const InstanceIcon = ServerIcon; -const OrgIcon = Building2Icon; -const SubOrgIcon = BoxesIcon; -const ProjectIcon = BoxIcon; - -export { InstanceIcon, OrgIcon, ProjectIcon, SubOrgIcon }; +export { + ServerIcon as InstanceIcon, + Building2Icon as OrgIcon, + BoxIcon as ProjectIcon, + BoxesIcon as SubOrgIcon +}; diff --git a/frontend/src/config/request.ts b/frontend/src/config/request.ts index 16b8b4f45..a37b38cb6 100644 --- a/frontend/src/config/request.ts +++ b/frontend/src/config/request.ts @@ -24,8 +24,6 @@ apiRequest.interceptors.request.use((config) => { const token = getAuthToken(); const providerAuthToken = SecurityClient.getProviderAuthToken(); - const params = new URLSearchParams(window.location.search); - if (config.headers) { if (signupTempToken) { // eslint-disable-next-line no-param-reassign @@ -40,17 +38,6 @@ apiRequest.interceptors.request.use((config) => { // eslint-disable-next-line no-param-reassign config.headers.Authorization = `Bearer ${providerAuthToken}`; } - - const rootOrgHeader = config.headers.get("x-root-org"); - - if (rootOrgHeader) { - config.headers.delete("x-root-org"); - } else { - const subOrganization = params.get("subOrganization"); - if (subOrganization) { - config.headers.set("x-infisical-org", subOrganization); - } - } } return config; diff --git a/frontend/src/const.ts b/frontend/src/const.ts index af52b68af..806755f38 100644 --- a/frontend/src/const.ts +++ b/frontend/src/const.ts @@ -61,7 +61,8 @@ export const leaveConfirmDefaultMessage = export enum SessionStorageKeys { CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN", ORG_LOGIN_SUCCESS_REDIRECT_URL = "ORG_LOGIN_SUCCESS_REDIRECT_URL", - AUTH_CONSENT = "AUTH_CONSENT" + AUTH_CONSENT = "AUTH_CONSENT", + MFA_TEMP_TOKEN = "MFA_TEMP_TOKEN" } export const secretTagsColors = [ diff --git a/frontend/src/const/routes.ts b/frontend/src/const/routes.ts index 6046c8fc8..ee922f9e6 100644 --- a/frontend/src/const/routes.ts +++ b/frontend/src/const/routes.ts @@ -359,6 +359,10 @@ export const ROUTE_PATHS = Object.freeze({ "/organizations/$orgId/projects/pam/$projectId/sessions", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/" ), + ApprovalRequestDetailPage: setRoute( + "/organizations/$orgId/projects/pam/$projectId/approval-requests/$approvalRequestId", + "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/approval-requests/$approvalRequestId" + ), PamSessionByIDPage: setRoute( "/organizations/$orgId/projects/pam/$projectId/sessions/$sessionId", "/_authenticate/_inject-org-details/_org-layout/organizations/$orgId/projects/pam/$projectId/_pam-layout/sessions/$sessionId" diff --git a/frontend/src/context/OrganizationContext/OrganizationContext.tsx b/frontend/src/context/OrganizationContext/OrganizationContext.tsx index bfc17d143..ef5edcc0f 100644 --- a/frontend/src/context/OrganizationContext/OrganizationContext.tsx +++ b/frontend/src/context/OrganizationContext/OrganizationContext.tsx @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { useSuspenseQuery } from "@tanstack/react-query"; -import { useRouteContext, useSearch } from "@tanstack/react-router"; +import { useRouteContext } from "@tanstack/react-router"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; @@ -10,28 +10,23 @@ export const useOrganization = () => { select: (el) => el.organizationId }); - const subOrganization = useSearch({ - strict: false, - select: (el) => el?.subOrganization - }); - const { data: currentOrg } = useSuspenseQuery({ - queryKey: organizationKeys.getOrgById(organizationId, subOrganization || "root"), + queryKey: organizationKeys.getOrgById(organizationId), queryFn: () => fetchOrganizationById(organizationId), staleTime: Infinity }); + const isSubOrganization = currentOrg.id !== currentOrg.rootOrgId && Boolean(currentOrg.rootOrgId); const org = useMemo( () => ({ currentOrg: { ...currentOrg, - id: currentOrg?.subOrganization?.id || currentOrg?.id, - parentOrgId: currentOrg.id + parentOrgId: isSubOrganization ? currentOrg?.parentOrgId : null }, - isSubOrganization: Boolean(currentOrg.subOrganization), - isRootOrganization: !currentOrg.subOrganization + isSubOrganization, + isRootOrganization: !isSubOrganization }), - [currentOrg, subOrganization] + [currentOrg] ); return org; diff --git a/frontend/src/context/ProjectPermissionContext/types.ts b/frontend/src/context/ProjectPermissionContext/types.ts index 83eb2fecd..0838b0c36 100644 --- a/frontend/src/context/ProjectPermissionContext/types.ts +++ b/frontend/src/context/ProjectPermissionContext/types.ts @@ -228,6 +228,16 @@ export enum ProjectPermissionPamSessionActions { // Terminate = "terminate" } +export enum ProjectPermissionApprovalRequestActions { + Read = "read", + Create = "create" +} + +export enum ProjectPermissionApprovalRequestGrantActions { + Read = "read", + Revoke = "revoke" +} + export type IdentityManagementSubjectFields = { identityId: string; }; @@ -339,7 +349,9 @@ export enum ProjectPermissionSub { PamFolders = "pam-folders", PamResources = "pam-resources", PamAccounts = "pam-accounts", - PamSessions = "pam-sessions" + PamSessions = "pam-sessions", + ApprovalRequests = "approval-requests", + ApprovalRequestGrants = "approval-request-grants" } export type SecretSubjectFields = { @@ -577,6 +589,8 @@ export type ProjectPermissionSet = | (ForcedSubject & PamAccountSubjectFields) ) ] - | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions]; + | [ProjectPermissionPamSessionActions, ProjectPermissionSub.PamSessions] + | [ProjectPermissionApprovalRequestActions, ProjectPermissionSub.ApprovalRequests] + | [ProjectPermissionApprovalRequestGrantActions, ProjectPermissionSub.ApprovalRequestGrants]; export type TProjectPermission = MongoAbility; diff --git a/frontend/src/helpers/appConnections.ts b/frontend/src/helpers/appConnections.ts index 7f02bd64e..e8857e022 100644 --- a/frontend/src/helpers/appConnections.ts +++ b/frontend/src/helpers/appConnections.ts @@ -31,6 +31,7 @@ import { HCVaultConnectionMethod, HumanitecConnectionMethod, LdapConnectionMethod, + MongoDBConnectionMethod, MsSqlConnectionMethod, MySqlConnectionMethod, OktaConnectionMethod, @@ -129,6 +130,7 @@ export const APP_CONNECTION_MAP: Record< [AppConnection.Northflank]: { name: "Northflank", image: "Northflank.png" }, [AppConnection.Okta]: { name: "Okta", image: "Okta.png" }, [AppConnection.Redis]: { name: "Redis", image: "Redis.png" }, + [AppConnection.MongoDB]: { name: "MongoDB", image: "MongoDB.png" }, [AppConnection.LaravelForge]: { name: "Laravel Forge", image: "Laravel Forge.png", @@ -181,6 +183,7 @@ export const getAppConnectionMethodDetails = (method: TAppConnection["method"]) case OracleDBConnectionMethod.UsernameAndPassword: case AzureADCSConnectionMethod.UsernamePassword: case RedisConnectionMethod.UsernameAndPassword: + case MongoDBConnectionMethod.UsernameAndPassword: return { name: "Username & Password", icon: faLock }; case HCVaultConnectionMethod.AccessToken: case TeamCityConnectionMethod.AccessToken: diff --git a/frontend/src/helpers/secretRotationsV2.ts b/frontend/src/helpers/secretRotationsV2.ts index d3bb83f19..187177c68 100644 --- a/frontend/src/helpers/secretRotationsV2.ts +++ b/frontend/src/helpers/secretRotationsV2.ts @@ -54,6 +54,11 @@ export const SECRET_ROTATION_MAP: Record< name: "Redis Credentials", image: "Redis.png", size: 50 + }, + [SecretRotation.MongoDBCredentials]: { + name: "MongoDB Credentials", + image: "MongoDB.png", + size: 50 } }; @@ -67,7 +72,8 @@ export const SECRET_ROTATION_CONNECTION_MAP: Record = { [SecretRotation.LdapPassword]: false, [SecretRotation.AwsIamUserSecret]: true, [SecretRotation.OktaClientSecret]: true, - [SecretRotation.RedisCredentials]: true + [SecretRotation.RedisCredentials]: true, + [SecretRotation.MongoDBCredentials]: true }; export const getRotateAtLocal = ({ hours, minutes }: TSecretRotationV2["rotateAtUtc"]) => { diff --git a/frontend/src/hooks/api/appConnections/enums.ts b/frontend/src/hooks/api/appConnections/enums.ts index 9535e8348..dbe6c7367 100644 --- a/frontend/src/hooks/api/appConnections/enums.ts +++ b/frontend/src/hooks/api/appConnections/enums.ts @@ -40,6 +40,7 @@ export enum AppConnection { Northflank = "northflank", Okta = "okta", Redis = "redis", + MongoDB = "mongodb", LaravelForge = "laravel-forge", Chef = "chef" } diff --git a/frontend/src/hooks/api/appConnections/types/app-options.ts b/frontend/src/hooks/api/appConnections/types/app-options.ts index d1c991f34..9c0f78a0c 100644 --- a/frontend/src/hooks/api/appConnections/types/app-options.ts +++ b/frontend/src/hooks/api/appConnections/types/app-options.ts @@ -184,6 +184,10 @@ export type TRedisConnectionOption = TAppConnectionOptionBase & { app: AppConnection.Redis; }; +export type TMongoDBConnectionOption = TAppConnectionOptionBase & { + app: AppConnection.MongoDB; +}; + export type TDNSMadeEasyConnectionOption = TAppConnectionOptionBase & { app: AppConnection.DNSMadeEasy; }; @@ -229,6 +233,8 @@ export type TAppConnectionOption = | TOktaConnectionOption | TAzureAdCsConnectionOption | TLaravelForgeConnectionOption + | TRedisConnectionOption + | TMongoDBConnectionOption | TChefConnectionOption | TDNSMadeEasyConnectionOption; @@ -274,6 +280,7 @@ export type TAppConnectionOptionMap = { [AppConnection.Okta]: TOktaConnectionOption; [AppConnection.AzureADCS]: TAzureAdCsConnectionOption; [AppConnection.Redis]: TRedisConnectionOption; + [AppConnection.MongoDB]: TMongoDBConnectionOption; [AppConnection.LaravelForge]: TLaravelForgeConnectionOption; [AppConnection.Chef]: TChefConnectionOption; }; diff --git a/frontend/src/hooks/api/appConnections/types/index.ts b/frontend/src/hooks/api/appConnections/types/index.ts index a272b48cd..c78d2aed2 100644 --- a/frontend/src/hooks/api/appConnections/types/index.ts +++ b/frontend/src/hooks/api/appConnections/types/index.ts @@ -26,6 +26,7 @@ import { THerokuConnection } from "./heroku-connection"; import { THumanitecConnection } from "./humanitec-connection"; import { TLaravelForgeConnection } from "./laravel-forge-connection"; import { TLdapConnection } from "./ldap-connection"; +import { TMongoDBConnection } from "./mongodb-connection"; import { TMsSqlConnection } from "./mssql-connection"; import { TMySqlConnection } from "./mysql-connection"; import { TNetlifyConnection } from "./netlify-connection"; @@ -69,6 +70,7 @@ export * from "./heroku-connection"; export * from "./humanitec-connection"; export * from "./laravel-forge-connection"; export * from "./ldap-connection"; +export * from "./mongodb-connection"; export * from "./mssql-connection"; export * from "./mysql-connection"; export * from "./netlify-connection"; @@ -129,6 +131,7 @@ export type TAppConnection = | TNorthflankConnection | TOktaConnection | TRedisConnection + | TMongoDBConnection | TChefConnection | TDNSMadeEasyConnection; diff --git a/frontend/src/hooks/api/appConnections/types/mongodb-connection.ts b/frontend/src/hooks/api/appConnections/types/mongodb-connection.ts new file mode 100644 index 000000000..5c8e72cb5 --- /dev/null +++ b/frontend/src/hooks/api/appConnections/types/mongodb-connection.ts @@ -0,0 +1,22 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { TRootAppConnection } from "@app/hooks/api/appConnections/types/root-connection"; + +export enum MongoDBConnectionMethod { + UsernameAndPassword = "username-and-password" +} + +export type TMongoDBConnectionCredentials = { + host: string; + port: number; + username: string; + password: string; + database: string; + tlsEnabled: boolean; + tlsRejectUnauthorized: boolean; + tlsCertificate?: string; +}; + +export type TMongoDBConnection = TRootAppConnection & { app: AppConnection.MongoDB } & { + method: MongoDBConnectionMethod.UsernameAndPassword; + credentials: TMongoDBConnectionCredentials; +}; diff --git a/frontend/src/hooks/api/approvalGrants/index.tsx b/frontend/src/hooks/api/approvalGrants/index.tsx new file mode 100644 index 000000000..140fb4bac --- /dev/null +++ b/frontend/src/hooks/api/approvalGrants/index.tsx @@ -0,0 +1,10 @@ +export { useRevokeApprovalGrant } from "./mutations"; +export { approvalGrantQuery } from "./queries"; +export { + ApprovalGrantStatus, + type PamAccessGrantAttributes, + type TApprovalGrant, + type TGetApprovalGrantByIdDTO, + type TListApprovalGrantsDTO, + type TRevokeApprovalGrantDTO +} from "./types"; diff --git a/frontend/src/hooks/api/approvalGrants/mutations.tsx b/frontend/src/hooks/api/approvalGrants/mutations.tsx new file mode 100644 index 000000000..b28ff45f9 --- /dev/null +++ b/frontend/src/hooks/api/approvalGrants/mutations.tsx @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { approvalGrantQuery } from "./queries"; +import { TApprovalGrant, TRevokeApprovalGrantDTO } from "./types"; + +export const useRevokeApprovalGrant = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ policyType, grantId, revocationReason }: TRevokeApprovalGrantDTO) => { + const { data } = await apiRequest.post<{ grant: TApprovalGrant }>( + `/api/v1/approval-policies/${policyType}/grants/${grantId}/revoke`, + { revocationReason } + ); + return data.grant; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalGrantQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/approvalGrants/queries.tsx b/frontend/src/hooks/api/approvalGrants/queries.tsx new file mode 100644 index 000000000..ca9334ca7 --- /dev/null +++ b/frontend/src/hooks/api/approvalGrants/queries.tsx @@ -0,0 +1,37 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TApprovalGrant, TGetApprovalGrantByIdDTO, TListApprovalGrantsDTO } from "./types"; + +export const approvalGrantQuery = { + allKey: () => ["approval-grants"] as const, + getByIdKey: (params: TGetApprovalGrantByIdDTO) => + [...approvalGrantQuery.allKey(), "by-id", params] as const, + listKey: (params: TListApprovalGrantsDTO) => + [...approvalGrantQuery.allKey(), "list", params] as const, + getById: (params: TGetApprovalGrantByIdDTO) => + queryOptions({ + queryKey: approvalGrantQuery.getByIdKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ grant: TApprovalGrant }>( + `/api/v1/approval-policies/${params.policyType}/grants/${params.grantId}` + ); + return data.grant; + } + }), + list: (params: TListApprovalGrantsDTO) => + queryOptions({ + queryKey: approvalGrantQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ + grants: TApprovalGrant[]; + }>(`/api/v1/approval-policies/${params.policyType}/grants`, { + params: { + projectId: params.projectId + } + }); + return data.grants; + } + }) +}; diff --git a/frontend/src/hooks/api/approvalGrants/types.ts b/frontend/src/hooks/api/approvalGrants/types.ts new file mode 100644 index 000000000..852529259 --- /dev/null +++ b/frontend/src/hooks/api/approvalGrants/types.ts @@ -0,0 +1,46 @@ +import { ApprovalPolicyType } from "../approvalPolicies"; + +export enum ApprovalGrantStatus { + Active = "active", + Expired = "expired", + Revoked = "revoked" +} + +// PAM Access Grant Attributes +export type PamAccessGrantAttributes = { + accountPath: string; + accessDuration: string; +}; + +// Base Grant Type +export type TApprovalGrant = { + id: string; + projectId: string; + requestId: string | null; + granteeUserId: string | null; + revokedByUserId: string | null; + revocationReason: string | null; + status: ApprovalGrantStatus; + type: ApprovalPolicyType; + attributes: PamAccessGrantAttributes; + createdAt: string; + expiresAt: string | null; + revokedAt: string | null; +}; + +// DTOs +export type TListApprovalGrantsDTO = { + policyType: ApprovalPolicyType; + projectId: string; +}; + +export type TGetApprovalGrantByIdDTO = { + policyType: ApprovalPolicyType; + grantId: string; +}; + +export type TRevokeApprovalGrantDTO = { + policyType: ApprovalPolicyType; + grantId: string; + revocationReason?: string; +}; diff --git a/frontend/src/hooks/api/approvalPolicies/index.tsx b/frontend/src/hooks/api/approvalPolicies/index.tsx new file mode 100644 index 000000000..303a09844 --- /dev/null +++ b/frontend/src/hooks/api/approvalPolicies/index.tsx @@ -0,0 +1,19 @@ +export { + useCreateApprovalPolicy, + useDeleteApprovalPolicy, + useUpdateApprovalPolicy +} from "./mutations"; +export { approvalPolicyQuery } from "./queries"; +export { + type ApprovalPolicyStep, + ApprovalPolicyType, + ApproverType, + type PamAccessPolicyConditions, + type PamAccessPolicyConstraints, + type TApprovalPolicy, + type TCreateApprovalPolicyDTO, + type TDeleteApprovalPolicyDTO, + type TGetApprovalPolicyByIdDTO, + type TListApprovalPoliciesDTO, + type TUpdateApprovalPolicyDTO +} from "./types"; diff --git a/frontend/src/hooks/api/approvalPolicies/mutations.tsx b/frontend/src/hooks/api/approvalPolicies/mutations.tsx new file mode 100644 index 000000000..ca9aae0fc --- /dev/null +++ b/frontend/src/hooks/api/approvalPolicies/mutations.tsx @@ -0,0 +1,58 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { approvalPolicyQuery } from "./queries"; +import { + TApprovalPolicy, + TCreateApprovalPolicyDTO, + TDeleteApprovalPolicyDTO, + TUpdateApprovalPolicyDTO +} from "./types"; + +export const useCreateApprovalPolicy = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, ...dto }: TCreateApprovalPolicyDTO) => { + const { data } = await apiRequest.post<{ policy: TApprovalPolicy }>( + `/api/v1/approval-policies/${policyType}`, + dto + ); + return data.policy; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalPolicyQuery.allKey() }); + } + }); +}; + +export const useUpdateApprovalPolicy = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, policyId, ...updates }: TUpdateApprovalPolicyDTO) => { + const { data } = await apiRequest.patch<{ policy: TApprovalPolicy }>( + `/api/v1/approval-policies/${policyType}/${policyId}`, + updates + ); + return data.policy; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalPolicyQuery.allKey() }); + } + }); +}; + +export const useDeleteApprovalPolicy = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, policyId }: TDeleteApprovalPolicyDTO) => { + const { data } = await apiRequest.delete<{ policyId: string }>( + `/api/v1/approval-policies/${policyType}/${policyId}` + ); + return data.policyId; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalPolicyQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/approvalPolicies/queries.tsx b/frontend/src/hooks/api/approvalPolicies/queries.tsx new file mode 100644 index 000000000..1c524ddac --- /dev/null +++ b/frontend/src/hooks/api/approvalPolicies/queries.tsx @@ -0,0 +1,37 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TApprovalPolicy, TGetApprovalPolicyByIdDTO, TListApprovalPoliciesDTO } from "./types"; + +export const approvalPolicyQuery = { + allKey: () => ["approval-policies"] as const, + getByIdKey: (params: TGetApprovalPolicyByIdDTO) => + [...approvalPolicyQuery.allKey(), "by-id", params] as const, + listKey: (params: TListApprovalPoliciesDTO) => + [...approvalPolicyQuery.allKey(), "list", params] as const, + getById: (params: TGetApprovalPolicyByIdDTO) => + queryOptions({ + queryKey: approvalPolicyQuery.getByIdKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ policy: TApprovalPolicy }>( + `/api/v1/approval-policies/${params.policyType}/${params.policyId}` + ); + return data.policy; + } + }), + list: (params: TListApprovalPoliciesDTO) => + queryOptions({ + queryKey: approvalPolicyQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ + policies: TApprovalPolicy[]; + }>(`/api/v1/approval-policies/${params.policyType}`, { + params: { + projectId: params.projectId + } + }); + return data.policies; + } + }) +}; diff --git a/frontend/src/hooks/api/approvalPolicies/types.ts b/frontend/src/hooks/api/approvalPolicies/types.ts new file mode 100644 index 000000000..8fb461814 --- /dev/null +++ b/frontend/src/hooks/api/approvalPolicies/types.ts @@ -0,0 +1,83 @@ +export enum ApprovalPolicyType { + PamAccess = "pam-access" +} + +export enum ApproverType { + Group = "group", + User = "user" +} + +export type ApprovalPolicyStep = { + name?: string | null; + requiredApprovals: number; + notifyApprovers?: boolean; + approvers: { + type: ApproverType; + id: string; + }[]; +}; + +export type PamAccessPolicyConditions = { + accountPaths: string[]; +}[]; + +export type PamAccessPolicyConstraints = { + accessDuration: { + min: string; + max: string; + }; +}; + +export type TApprovalPolicy = { + id: string; + projectId: string; + name: string; + maxRequestTtl?: string | null; + type: ApprovalPolicyType; + conditions: { + version: number; + conditions: PamAccessPolicyConditions; + }; + constraints: { + version: number; + constraints: PamAccessPolicyConstraints; + }; + steps: ApprovalPolicyStep[]; + createdAt: string; + updatedAt: string; +}; + +export type TCreateApprovalPolicyDTO = { + policyType: ApprovalPolicyType; + projectId: string; + name: string; + maxRequestTtl?: string | null; + conditions: PamAccessPolicyConditions; + constraints: PamAccessPolicyConstraints; + steps: ApprovalPolicyStep[]; +}; + +export type TUpdateApprovalPolicyDTO = { + policyType: ApprovalPolicyType; + policyId: string; + name?: string; + maxRequestTtl?: string | null; + conditions?: PamAccessPolicyConditions; + constraints?: PamAccessPolicyConstraints; + steps?: ApprovalPolicyStep[]; +}; + +export type TGetApprovalPolicyByIdDTO = { + policyType: ApprovalPolicyType; + policyId: string; +}; + +export type TListApprovalPoliciesDTO = { + policyType: ApprovalPolicyType; + projectId: string; +}; + +export type TDeleteApprovalPolicyDTO = { + policyType: ApprovalPolicyType; + policyId: string; +}; diff --git a/frontend/src/hooks/api/approvalRequests/index.tsx b/frontend/src/hooks/api/approvalRequests/index.tsx new file mode 100644 index 000000000..2b146836a --- /dev/null +++ b/frontend/src/hooks/api/approvalRequests/index.tsx @@ -0,0 +1,20 @@ +export { + useApproveApprovalRequest, + useCancelApprovalRequest, + useCreateApprovalRequest, + useRejectApprovalRequest +} from "./mutations"; +export { approvalRequestQuery } from "./queries"; +export { + type ApprovalRequestApproval, + ApprovalRequestStatus, + type ApprovalRequestStep, + ApprovalRequestStepStatus, + type PamAccessRequestData, + type TApprovalRequest, + type TApproveApprovalRequestDTO, + type TCreateApprovalRequestDTO, + type TGetApprovalRequestByIdDTO, + type TListApprovalRequestsDTO, + type TRejectApprovalRequestDTO +} from "./types"; diff --git a/frontend/src/hooks/api/approvalRequests/mutations.tsx b/frontend/src/hooks/api/approvalRequests/mutations.tsx new file mode 100644 index 000000000..81e14b311 --- /dev/null +++ b/frontend/src/hooks/api/approvalRequests/mutations.tsx @@ -0,0 +1,75 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { approvalRequestQuery } from "./queries"; +import { + TApprovalRequest, + TApproveApprovalRequestDTO, + TCancelApprovalRequestDTO, + TCreateApprovalRequestDTO, + TRejectApprovalRequestDTO +} from "./types"; + +export const useCreateApprovalRequest = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, ...dto }: TCreateApprovalRequestDTO) => { + const { data } = await apiRequest.post<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${policyType}/requests`, + dto + ); + return data.request; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() }); + } + }); +}; + +export const useApproveApprovalRequest = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, requestId, comment }: TApproveApprovalRequestDTO) => { + const { data } = await apiRequest.post<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${policyType}/requests/${requestId}/approve`, + { comment } + ); + return data.request; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() }); + } + }); +}; + +export const useRejectApprovalRequest = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, requestId, comment }: TRejectApprovalRequestDTO) => { + const { data } = await apiRequest.post<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${policyType}/requests/${requestId}/reject`, + { comment } + ); + return data.request; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() }); + } + }); +}; + +export const useCancelApprovalRequest = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ policyType, requestId }: TCancelApprovalRequestDTO) => { + const { data } = await apiRequest.post<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${policyType}/requests/${requestId}/cancel` + ); + return data.request; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: approvalRequestQuery.allKey() }); + } + }); +}; diff --git a/frontend/src/hooks/api/approvalRequests/queries.tsx b/frontend/src/hooks/api/approvalRequests/queries.tsx new file mode 100644 index 000000000..3b4abddcc --- /dev/null +++ b/frontend/src/hooks/api/approvalRequests/queries.tsx @@ -0,0 +1,37 @@ +import { queryOptions } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TApprovalRequest, TGetApprovalRequestByIdDTO, TListApprovalRequestsDTO } from "./types"; + +export const approvalRequestQuery = { + allKey: () => ["approval-requests"] as const, + getByIdKey: (params: TGetApprovalRequestByIdDTO) => + [...approvalRequestQuery.allKey(), "by-id", params] as const, + listKey: (params: TListApprovalRequestsDTO) => + [...approvalRequestQuery.allKey(), "list", params] as const, + getById: (params: TGetApprovalRequestByIdDTO) => + queryOptions({ + queryKey: approvalRequestQuery.getByIdKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ request: TApprovalRequest }>( + `/api/v1/approval-policies/${params.policyType}/requests/${params.requestId}` + ); + return data.request; + } + }), + list: (params: TListApprovalRequestsDTO) => + queryOptions({ + queryKey: approvalRequestQuery.listKey(params), + queryFn: async () => { + const { data } = await apiRequest.get<{ + requests: TApprovalRequest[]; + }>(`/api/v1/approval-policies/${params.policyType}/requests`, { + params: { + projectId: params.projectId + } + }); + return data.requests; + } + }) +}; diff --git a/frontend/src/hooks/api/approvalRequests/types.ts b/frontend/src/hooks/api/approvalRequests/types.ts new file mode 100644 index 000000000..2270d4988 --- /dev/null +++ b/frontend/src/hooks/api/approvalRequests/types.ts @@ -0,0 +1,110 @@ +import { ApprovalPolicyType, ApproverType } from "../approvalPolicies"; + +export enum ApprovalRequestStatus { + Pending = "pending", + Approved = "approved", + Rejected = "rejected", + Expired = "expired", + Cancelled = "cancelled" +} + +export enum ApprovalRequestStepStatus { + Pending = "pending", + InProgress = "in-progress", + Approved = "approved", + Rejected = "rejected" +} + +export enum ApprovalRequestApprovalDecision { + Approved = "approved", + Rejected = "rejected" +} + +export type ApprovalRequestApproval = { + id: string; + stepId: string; + approverUserId: string; + decision: ApprovalRequestApprovalDecision.Approved; + comment?: string | null; + createdAt: string; + updatedAt: string; +}; + +export type ApprovalRequestStep = { + id: string; + requestId: string; + name?: string | null; + requiredApprovals: number; + notifyApprovers?: boolean | null; + stepNumber: number; + status: ApprovalRequestStepStatus; + startedAt?: string | null; + completedAt?: string | null; + approvers: { + type: ApproverType; + id: string; + }[]; + approvals: ApprovalRequestApproval[]; + createdAt: string; + updatedAt: string; +}; + +export type PamAccessRequestData = { + accountPath: string; + accessDuration: string; +}; + +export type TApprovalRequest = { + id: string; + projectId: string; + policyId: string; + type: ApprovalPolicyType; + status: ApprovalRequestStatus; + requesterId: string; + requesterName: string; + requesterEmail: string; + justification?: string | null; + expiresAt?: string | null; + requestData: { + version: number; + requestData: PamAccessRequestData; + }; + steps: ApprovalRequestStep[]; + createdAt: string; + updatedAt: string; +}; + +export type TCreateApprovalRequestDTO = { + policyType: ApprovalPolicyType; + projectId: string; + justification?: string | null; + requestDuration?: string | null; + requestData: PamAccessRequestData; +}; + +export type TGetApprovalRequestByIdDTO = { + policyType: ApprovalPolicyType; + requestId: string; +}; + +export type TListApprovalRequestsDTO = { + policyType: ApprovalPolicyType; + projectId: string; +}; + +export type TApproveApprovalRequestDTO = { + policyType: ApprovalPolicyType; + requestId: string; + comment?: string; +}; + +export type TRejectApprovalRequestDTO = { + policyType: ApprovalPolicyType; + requestId: string; + comment?: string; +}; + +export type TCancelApprovalRequestDTO = { + policyType: ApprovalPolicyType; + requestId: string; +}; diff --git a/frontend/src/hooks/api/auditLogs/constants.tsx b/frontend/src/hooks/api/auditLogs/constants.tsx index a11504193..3ace3f841 100644 --- a/frontend/src/hooks/api/auditLogs/constants.tsx +++ b/frontend/src/hooks/api/auditLogs/constants.tsx @@ -291,7 +291,22 @@ export const eventToNameMap: { [K in EventType]: string } = { [EventType.SIGN_CERTIFICATE_FROM_PROFILE]: "Sign Certificate From Profile", [EventType.ORDER_CERTIFICATE_FROM_PROFILE]: "Order Certificate From Profile", [EventType.GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE]: - "Get Certificate Profile Latest Active Bundle" + "Get Certificate Profile Latest Active Bundle", + + [EventType.APPROVAL_POLICY_CREATE]: "Create Approval Policy", + [EventType.APPROVAL_POLICY_UPDATE]: "Update Approval Policy", + [EventType.APPROVAL_POLICY_DELETE]: "Delete Approval Policy", + [EventType.APPROVAL_POLICY_LIST]: "List Approval Policies", + [EventType.APPROVAL_POLICY_GET]: "Get Approval Policy", + [EventType.APPROVAL_REQUEST_GET]: "Get Approval Request", + [EventType.APPROVAL_REQUEST_LIST]: "List Approval Requests", + [EventType.APPROVAL_REQUEST_CREATE]: "Create Approval Request", + [EventType.APPROVAL_REQUEST_APPROVE]: "Approve Approval Request", + [EventType.APPROVAL_REQUEST_REJECT]: "Reject Approval Request", + [EventType.APPROVAL_REQUEST_CANCEL]: "Cancel Approval Request", + [EventType.APPROVAL_REQUEST_GRANT_LIST]: "List Approval Request Grants", + [EventType.APPROVAL_REQUEST_GRANT_GET]: "Get Approval Request Grant", + [EventType.APPROVAL_REQUEST_GRANT_REVOKE]: "Revoke Approval Request Grant" }; export const userAgentTypeToNameMap: { [K in UserAgentType]: string } = { @@ -309,7 +324,21 @@ const sharedProjectEvents = [ EventType.REMOVE_PROJECT_MEMBER, EventType.CREATE_PROJECT_ROLE, EventType.UPDATE_PROJECT_ROLE, - EventType.DELETE_PROJECT_ROLE + EventType.DELETE_PROJECT_ROLE, + EventType.APPROVAL_POLICY_CREATE, + EventType.APPROVAL_POLICY_UPDATE, + EventType.APPROVAL_POLICY_DELETE, + EventType.APPROVAL_POLICY_LIST, + EventType.APPROVAL_POLICY_GET, + EventType.APPROVAL_REQUEST_GET, + EventType.APPROVAL_REQUEST_LIST, + EventType.APPROVAL_REQUEST_CREATE, + EventType.APPROVAL_REQUEST_APPROVE, + EventType.APPROVAL_REQUEST_REJECT, + EventType.APPROVAL_REQUEST_CANCEL, + EventType.APPROVAL_REQUEST_GRANT_LIST, + EventType.APPROVAL_REQUEST_GRANT_GET, + EventType.APPROVAL_REQUEST_GRANT_REVOKE ]; export const projectToEventsMap: Partial> = { diff --git a/frontend/src/hooks/api/auditLogs/enums.tsx b/frontend/src/hooks/api/auditLogs/enums.tsx index bde306450..b3be5a6fd 100644 --- a/frontend/src/hooks/api/auditLogs/enums.tsx +++ b/frontend/src/hooks/api/auditLogs/enums.tsx @@ -282,5 +282,20 @@ export enum EventType { ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile", SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile", ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile", - GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle" + GET_CERTIFICATE_PROFILE_LATEST_ACTIVE_BUNDLE = "get-certificate-profile-latest-active-bundle", + + APPROVAL_POLICY_CREATE = "approval-policy-create", + APPROVAL_POLICY_UPDATE = "approval-policy-update", + APPROVAL_POLICY_DELETE = "approval-policy-delete", + APPROVAL_POLICY_LIST = "approval-policy-list", + APPROVAL_POLICY_GET = "approval-policy-get", + APPROVAL_REQUEST_GET = "approval-request-get", + APPROVAL_REQUEST_LIST = "approval-request-list", + APPROVAL_REQUEST_CREATE = "approval-request-create", + APPROVAL_REQUEST_APPROVE = "approval-request-approve", + APPROVAL_REQUEST_REJECT = "approval-request-reject", + APPROVAL_REQUEST_CANCEL = "approval-request-cancel", + APPROVAL_REQUEST_GRANT_LIST = "approval-request-grant-list", + APPROVAL_REQUEST_GRANT_GET = "approval-request-grant-get", + APPROVAL_REQUEST_GRANT_REVOKE = "approval-request-grant-revoke" } diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 207980017..e4355ddc7 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -58,10 +58,12 @@ export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => { return data; }; -export const selectOrganization = async (data: { +export type SelectOrganizationParams = { organizationId: string; userAgent?: UserAgentType; -}) => { +}; + +export const selectOrganization = async (data: SelectOrganizationParams) => { const { data: res } = await apiRequest.post<{ token: string; isMfaEnabled: boolean; @@ -73,7 +75,7 @@ export const selectOrganization = async (data: { export const useSelectOrganization = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => { + mutationFn: async (details: SelectOrganizationParams) => { const data = await selectOrganization(details); // If a custom user agent is set, then this session is meant for another consuming application, not the web application. diff --git a/frontend/src/hooks/api/auth/types.ts b/frontend/src/hooks/api/auth/types.ts index fd9d6fb5c..d0ad86526 100644 --- a/frontend/src/hooks/api/auth/types.ts +++ b/frontend/src/hooks/api/auth/types.ts @@ -1,6 +1,7 @@ export type GetAuthTokenAPI = { token: string; organizationId?: string; + subOrganizationId?: string; }; export enum UserEncryptionVersion { diff --git a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx index 1e070e2bb..dbdbe7c3b 100644 --- a/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx +++ b/frontend/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx @@ -5,7 +5,6 @@ export enum IdentityProjectAdditionalPrivilegeTemporaryMode { } export type TIdentityProjectPrivilege = { - projectMembershipId: string; slug: string; id: string; createdAt: Date; diff --git a/frontend/src/hooks/api/index.tsx b/frontend/src/hooks/api/index.tsx index 33eacd18d..acbf1d9f5 100644 --- a/frontend/src/hooks/api/index.tsx +++ b/frontend/src/hooks/api/index.tsx @@ -1,6 +1,9 @@ export * from "./accessApproval"; export * from "./admin"; export * from "./apiKeys"; +export * from "./approvalGrants"; +export * from "./approvalPolicies"; +export * from "./approvalRequests"; export * from "./assumePrivileges"; export * from "./auditLogs"; export * from "./auditLogStreams"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 9e2413b28..857358ffe 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -42,7 +42,7 @@ export const organizationKeys = { [...organizationKeys.getOrgIdentityMemberships(orgId), params] as const, getOrgGroups: (orgId: string) => [{ orgId }, "organization-groups"] as const, getOrgIntegrationAuths: (orgId: string) => [{ orgId }, "integration-auths"] as const, - getOrgById: (orgId: string, subOrg?: string) => ["organization", { orgId, subOrg }], + getOrgById: (orgId: string) => ["organization", { orgId }], getAvailableIdentities: () => ["available-identities"], getAvailableUsers: () => ["available-users"] }; @@ -67,7 +67,7 @@ export const fetchOrganizationById = async (id: string) => { const { data: { organization } } = await apiRequest.get<{ - organization: Organization & { subOrganization?: { id: string; name: string } }; + organization: Organization; }>(`/api/v1/organization/${id}`); return organization; }; diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index b366277f1..45870176f 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -30,6 +30,8 @@ export type Organization = { maxSharedSecretLifetime: number; maxSharedSecretViewLimit: number | null; blockDuplicateSecretSyncDestinations: boolean; + parentOrgId: string | null; + rootOrgId: string | null; }; export type UpdateOrgDTO = { diff --git a/frontend/src/hooks/api/pam/enums.ts b/frontend/src/hooks/api/pam/enums.ts index 2c86d9921..c6dfcd70c 100644 --- a/frontend/src/hooks/api/pam/enums.ts +++ b/frontend/src/hooks/api/pam/enums.ts @@ -16,7 +16,8 @@ export enum PamResourceType { CockroachDB = "cockroachdb", Elasticsearch = "elasticsearch", Snowflake = "snowflake", - DynamoDB = "dynamodb" + DynamoDB = "dynamodb", + AwsIam = "aws-iam" } export enum PamResourceOrderBy { diff --git a/frontend/src/hooks/api/pam/maps.ts b/frontend/src/hooks/api/pam/maps.ts index 90286a05d..e42eb7748 100644 --- a/frontend/src/hooks/api/pam/maps.ts +++ b/frontend/src/hooks/api/pam/maps.ts @@ -20,5 +20,6 @@ export const PAM_RESOURCE_TYPE_MAP: Record< [PamResourceType.CockroachDB]: { name: "CockroachDB", image: "CockroachDB.png" }, [PamResourceType.Elasticsearch]: { name: "Elasticsearch", image: "Elastic.png" }, [PamResourceType.Snowflake]: { name: "Snowflake", image: "Snowflake.png" }, - [PamResourceType.DynamoDB]: { name: "DynamoDB", image: "DynamoDB.png", size: 55 } + [PamResourceType.DynamoDB]: { name: "DynamoDB", image: "DynamoDB.png", size: 55 }, + [PamResourceType.AwsIam]: { name: "AWS IAM", image: "Amazon Web Services.png" } }; diff --git a/frontend/src/hooks/api/pam/mutations.tsx b/frontend/src/hooks/api/pam/mutations.tsx index c5d6ff05b..ce92de0fe 100644 --- a/frontend/src/hooks/api/pam/mutations.tsx +++ b/frontend/src/hooks/api/pam/mutations.tsx @@ -120,6 +120,45 @@ export const useDeletePamAccount = () => { }); }; +export type TAccessPamAccountDTO = { + accountId: string; + accountPath: string; + projectId: string; + duration: string; +}; + +export type TAccessPamAccountResponse = { + sessionId: string; + resourceType: string; + consoleUrl?: string; + metadata?: Record; + relayClientCertificate?: string; + relayClientPrivateKey?: string; + relayServerCertificateChain?: string; + gatewayClientCertificate?: string; + gatewayClientPrivateKey?: string; + gatewayServerCertificateChain?: string; + relayHost?: string; +}; + +export const useAccessPamAccount = () => { + return useMutation({ + mutationFn: async ({ accountId, accountPath, projectId, duration }: TAccessPamAccountDTO) => { + const { data } = await apiRequest.post( + "/api/v1/pam/accounts/access", + { + accountId, + accountPath, + projectId, + duration + } + ); + + return data; + } + }); +}; + // Folders export const useCreatePamFolder = () => { const queryClient = useQueryClient(); diff --git a/frontend/src/hooks/api/pam/types/aws-iam-resource.ts b/frontend/src/hooks/api/pam/types/aws-iam-resource.ts new file mode 100644 index 000000000..8cb51a0ec --- /dev/null +++ b/frontend/src/hooks/api/pam/types/aws-iam-resource.ts @@ -0,0 +1,25 @@ +import { PamResourceType } from "../enums"; +import { TBasePamAccount } from "./base-account"; +import { TBasePamResource } from "./base-resource"; + +export type TAwsIamConnectionDetails = { + roleArn: string; +}; + +export type TAwsIamCredentials = { + targetRoleArn: string; + defaultSessionDuration: number; +}; + +export type TAwsIamResource = Omit & { + resourceType: PamResourceType.AwsIam; + gatewayId?: string | null; + connectionDetails: TAwsIamConnectionDetails; +}; + +export type TAwsIamAccount = Omit< + TBasePamAccount, + "rotationEnabled" | "rotationIntervalSeconds" | "lastRotatedAt" +> & { + credentials: TAwsIamCredentials; +}; diff --git a/frontend/src/hooks/api/pam/types/index.ts b/frontend/src/hooks/api/pam/types/index.ts index 255aaf834..332c985a9 100644 --- a/frontend/src/hooks/api/pam/types/index.ts +++ b/frontend/src/hooks/api/pam/types/index.ts @@ -6,19 +6,31 @@ import { PamResourceType, PamSessionStatus } from "../enums"; +import { TAwsIamAccount, TAwsIamResource } from "./aws-iam-resource"; import { TKubernetesAccount, TKubernetesResource } from "./kubernetes-resource"; import { TMySQLAccount, TMySQLResource } from "./mysql-resource"; import { TPostgresAccount, TPostgresResource } from "./postgres-resource"; import { TSSHAccount, TSSHResource } from "./ssh-resource"; +export * from "./aws-iam-resource"; export * from "./kubernetes-resource"; export * from "./mysql-resource"; export * from "./postgres-resource"; export * from "./ssh-resource"; -export type TPamResource = TPostgresResource | TMySQLResource | TSSHResource | TKubernetesResource; +export type TPamResource = + | TPostgresResource + | TMySQLResource + | TSSHResource + | TAwsIamResource + | TKubernetesResource; -export type TPamAccount = TPostgresAccount | TMySQLAccount | TSSHAccount | TKubernetesAccount; +export type TPamAccount = + | TPostgresAccount + | TMySQLAccount + | TSSHAccount + | TAwsIamAccount + | TKubernetesAccount; export type TPamFolder = { id: string; diff --git a/frontend/src/hooks/api/projects/query-keys.tsx b/frontend/src/hooks/api/projects/query-keys.tsx index 48830f9e2..6f74bf6f3 100644 --- a/frontend/src/hooks/api/projects/query-keys.tsx +++ b/frontend/src/hooks/api/projects/query-keys.tsx @@ -3,14 +3,16 @@ import { WorkflowIntegrationPlatform } from "../workflowIntegrations/types"; import { TListProjectIdentitiesDTO, TSearchProjectsDTO } from "./types"; export const projectKeys = { - getProjectById: (projectId: string) => ["projects", { projectId }] as const, + allProjectQueries: () => ["projects"] as const, + getProjectById: (projectId: string) => + [...projectKeys.allProjectQueries(), { projectId }] as const, getProjectSecrets: (projectId: string) => [{ projectId }, "project-secrets"] as const, getProjectIndexStatus: (projectId: string) => [{ projectId }, "project-index-status"] as const, getProjectUpgradeStatus: (projectId: string) => [{ projectId }, "project-upgrade-status"], getProjectMemberships: (orgId: string) => [{ orgId }, "project-memberships"], getProjectAuthorization: (projectId: string) => [{ projectId }, "project-authorizations"], getProjectIntegrations: (projectId: string) => [{ projectId }, "project-integrations"], - getAllUserProjects: () => ["projects"] as const, + getAllUserProjects: () => [...projectKeys.allProjectQueries()] as const, getProjectAuditLogs: (projectId: string) => [{ projectId }, "project-audit-logs"] as const, getProjectUsers: ( projectId: string, @@ -28,7 +30,8 @@ export const projectKeys = { // allows invalidation using above key without knowing params getProjectIdentityMembershipsWithParams: ({ projectId, ...params }: TListProjectIdentitiesDTO) => [...projectKeys.getProjectIdentityMemberships(projectId), params] as const, - searchProject: (dto: TSearchProjectsDTO) => ["search-projects", dto] as const, + searchProject: (dto: TSearchProjectsDTO) => + [...projectKeys.allProjectQueries(), "search-projects", dto] as const, getProjectGroupMemberships: (projectId: string) => [{ projectId }, "project-groups"] as const, getProjectGroupMembershipDetails: (projectId: string, groupId: string) => [{ projectId, groupId }, "project-group-membership-details"] as const, diff --git a/frontend/src/hooks/api/secretRotationsV2/enums.ts b/frontend/src/hooks/api/secretRotationsV2/enums.ts index 264a6a4a4..d52de16fc 100644 --- a/frontend/src/hooks/api/secretRotationsV2/enums.ts +++ b/frontend/src/hooks/api/secretRotationsV2/enums.ts @@ -8,7 +8,8 @@ export enum SecretRotation { LdapPassword = "ldap-password", AwsIamUserSecret = "aws-iam-user-secret", OktaClientSecret = "okta-client-secret", - RedisCredentials = "redis-credentials" + RedisCredentials = "redis-credentials", + MongoDBCredentials = "mongodb-credentials" } export enum SecretRotationStatus { diff --git a/frontend/src/hooks/api/secretRotationsV2/types/index.ts b/frontend/src/hooks/api/secretRotationsV2/types/index.ts index a04b0e020..cc938ae05 100644 --- a/frontend/src/hooks/api/secretRotationsV2/types/index.ts +++ b/frontend/src/hooks/api/secretRotationsV2/types/index.ts @@ -31,6 +31,11 @@ import { TSqlCredentialsRotationOption } from "@app/hooks/api/secretRotationsV2/ import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { DiscriminativePick } from "@app/types"; +import { + TMongoDBCredentialsRotation, + TMongoDBCredentialsRotationGeneratedCredentialsResponse, + TMongoDBCredentialsRotationOption +} from "./mongodb-credentials-rotation"; import { TMySqlCredentialsRotation, TMySqlCredentialsRotationGeneratedCredentialsResponse @@ -61,6 +66,7 @@ export type TSecretRotationV2 = ( | TAwsIamUserSecretRotation | TOktaClientSecretRotation | TRedisCredentialsRotation + | TMongoDBCredentialsRotation ) & { secrets: (SecretV3RawSanitized | null)[]; }; @@ -72,7 +78,8 @@ export type TSecretRotationV2Option = | TLdapPasswordRotationOption | TAwsIamUserSecretRotationOption | TOktaClientSecretRotationOption - | TRedisCredentialsRotationOption; + | TRedisCredentialsRotationOption + | TMongoDBCredentialsRotationOption; export type TListSecretRotationV2Options = { secretRotationOptions: TSecretRotationV2Option[] }; @@ -88,7 +95,8 @@ export type TViewSecretRotationGeneratedCredentialsResponse = | TLdapPasswordRotationGeneratedCredentialsResponse | TAwsIamUserSecretRotationGeneratedCredentialsResponse | TOktaClientSecretRotationGeneratedCredentialsResponse - | TRedisCredentialsRotationGeneratedCredentialsResponse; + | TRedisCredentialsRotationGeneratedCredentialsResponse + | TMongoDBCredentialsRotationGeneratedCredentialsResponse; export type TCreateSecretRotationV2DTO = DiscriminativePick< TSecretRotationV2, @@ -142,6 +150,7 @@ export type TSecretRotationOptionMap = { [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationOption; [SecretRotation.OktaClientSecret]: TOktaClientSecretRotationOption; [SecretRotation.RedisCredentials]: TRedisCredentialsRotationOption; + [SecretRotation.MongoDBCredentials]: TMongoDBCredentialsRotationOption; }; export type TSecretRotationGeneratedCredentialsResponseMap = { @@ -155,4 +164,5 @@ export type TSecretRotationGeneratedCredentialsResponseMap = { [SecretRotation.AwsIamUserSecret]: TAwsIamUserSecretRotationGeneratedCredentialsResponse; [SecretRotation.OktaClientSecret]: TOktaClientSecretRotationGeneratedCredentialsResponse; [SecretRotation.RedisCredentials]: TRedisCredentialsRotationGeneratedCredentialsResponse; + [SecretRotation.MongoDBCredentials]: TMongoDBCredentialsRotationGeneratedCredentialsResponse; }; diff --git a/frontend/src/hooks/api/secretRotationsV2/types/mongodb-credentials-rotation.ts b/frontend/src/hooks/api/secretRotationsV2/types/mongodb-credentials-rotation.ts new file mode 100644 index 000000000..425357d54 --- /dev/null +++ b/frontend/src/hooks/api/secretRotationsV2/types/mongodb-credentials-rotation.ts @@ -0,0 +1,28 @@ +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { SecretRotation } from "@app/hooks/api/secretRotationsV2"; +import { + TSecretRotationV2Base, + TSecretRotationV2GeneratedCredentialsResponseBase, + TSqlCredentialsRotationGeneratedCredentials, + TSqlCredentialsRotationProperties +} from "@app/hooks/api/secretRotationsV2/types/shared"; + +export type TMongoDBCredentialsRotation = TSecretRotationV2Base & { + type: SecretRotation.MongoDBCredentials; +} & TSqlCredentialsRotationProperties; + +export type TMongoDBCredentialsRotationGeneratedCredentialsResponse = + TSecretRotationV2GeneratedCredentialsResponseBase< + SecretRotation.MongoDBCredentials, + TSqlCredentialsRotationGeneratedCredentials + >; + +export type TMongoDBCredentialsRotationOption = { + name: string; + type: SecretRotation.MongoDBCredentials; + connection: AppConnection.MongoDB; + template: { + createUserStatement: string; + secretsMapping: TMongoDBCredentialsRotation["secretsMapping"]; + }; +}; diff --git a/frontend/src/hooks/api/shared/types.ts b/frontend/src/hooks/api/shared/types.ts index c57daf3fd..8a07ff4e6 100644 --- a/frontend/src/hooks/api/shared/types.ts +++ b/frontend/src/hooks/api/shared/types.ts @@ -18,7 +18,7 @@ export type TIdentity = { updatedAt: string; hasDeleteProtection: boolean; authMethods: IdentityAuthMethod[]; - activeLockoutAuthMethods: string[]; + activeLockoutAuthMethods: IdentityAuthMethod[]; metadata?: Array; }; diff --git a/frontend/src/hooks/api/subOrganizations/mutations.tsx b/frontend/src/hooks/api/subOrganizations/mutations.tsx index 81369a62e..f2b9ac7a8 100644 --- a/frontend/src/hooks/api/subOrganizations/mutations.tsx +++ b/frontend/src/hooks/api/subOrganizations/mutations.tsx @@ -11,10 +11,7 @@ export const useCreateSubOrganization = () => { mutationFn: async (dto: TCreateSubOrganizationDTO) => { const { data } = await apiRequest.post<{ organization: TSubOrganization }>( "/api/v1/sub-organizations", - dto, - { - headers: { "x-root-org": "discard" } // akhi/scott: this just tells the request to use the root org ID header - } + dto ); return data; }, diff --git a/frontend/src/hooks/api/users/types.ts b/frontend/src/hooks/api/users/types.ts index b7ceb4a1b..727fafbf6 100644 --- a/frontend/src/hooks/api/users/types.ts +++ b/frontend/src/hooks/api/users/types.ts @@ -78,6 +78,7 @@ export type TUserMembership = { scope: string; scopeOrgId: string; actorUserId: string; + actorGroupId: string; }; export type TProjectMembership = { diff --git a/frontend/src/index.css b/frontend/src/index.css index baa613b36..c1e564d92 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import "tw-animate-css"; @source not "../public"; @@ -39,7 +40,7 @@ /* Colors v2 */ --color-background: #19191c; - --color-foreground: white; + --color-foreground: #ebebeb; --color-success: #2ecc71; --color-info: #63b0bd; --color-warning: #f1c40f; @@ -48,6 +49,14 @@ --color-sub-org: #96ff59; --color-project: #e0ed34; --color-neutral: #adaeb0; + --color-border: #2b2c30; + --color-label: #adaeb0; + --color-muted: #707174; + --color-popover: #141617; + --color-ring: #2d2f33; + --color-card: #16181a; + --color-accent: #7d7f80; + --color-container: #1a1c1e; /*legacy color schema */ --color-org-v1: #30b3ff; diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx index dfc2fb603..4cef73f46 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/Navbar.tsx @@ -80,7 +80,11 @@ const getPlan = (subscription: SubscriptionPlan) => { return "Free"; }; -const getFormattedSupportEmailLink = (variables: { org_id: string; domain: string }) => { +const getFormattedSupportEmailLink = (variables: { + org_id: string; + domain: string; + root_org_id?: string; +}) => { const email = "support@infisical.com"; const body = `Hello Infisical Support Team, @@ -94,6 +98,7 @@ Issue Details: Account Info: - Organization ID: ${variables.org_id} +${variables.root_org_id ? `- Root Organization ID: ${variables.root_org_id}` : ""} - Domain: ${variables.domain} Thank you, @@ -169,6 +174,10 @@ export const Navbar = () => { const isModalIntrusive = Boolean(!isBillingPage && isCardDeclinedMoreThan30Days); + const rootOrg = isSubOrganization + ? orgs?.find((org) => org.id === currentOrg.rootOrgId) || currentOrg + : currentOrg; + useEffect(() => { if (isModalIntrusive) { setShowCardDeclinedModal(true); @@ -182,10 +191,20 @@ export const Navbar = () => { } }, [subscription, isBillingPage, isModalIntrusive]); - const handleOrgChange = async (orgId: string) => { - const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ - organizationId: orgId - }); + const handleOrgSelection = async ({ + organizationId, + navigateTo, + onSuccess + }: { + organizationId?: string; + navigateTo?: string; + onSuccess?: () => void | Promise; + }) => { + if (!organizationId) return; + + if (organizationId === currentOrg.id) return; + + const { token, isMfaEnabled, mfaMethod } = await selectOrganization({ organizationId }); if (isMfaEnabled) { SecurityClient.setMfaToken(token); @@ -193,14 +212,58 @@ export const Navbar = () => { setRequiredMfaMethod(mfaMethod); } toggleShowMfa.on(); - setMfaSuccessCallback(() => () => handleOrgChange(orgId)); + setMfaSuccessCallback(() => async () => { + await handleOrgSelection({ organizationId, onSuccess }); + }); return; } - await router.invalidate(); - await navigateUserToOrg(navigate, orgId); - queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); + + SecurityClient.setToken(token); + SecurityClient.setProviderAuthToken(""); queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); - queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() }); + queryClient.removeQueries({ queryKey: subOrgQuery.queryKey }); + + await queryClient.refetchQueries({ queryKey: authKeys.getAuthToken }); + + await navigateUserToOrg({ navigate, organizationId, navigateTo }); + queryClient.removeQueries({ queryKey: projectKeys.allProjectQueries() }); + + if (onSuccess) { + await onSuccess(); + } + }; + + const handleNavigateToRootOrgBilling = async () => { + const navigateToBilling = () => { + navigate({ + to: "/organizations/$orgId/billing", + params: { orgId: rootOrg.id } + }); + }; + + const onSuccess = () => { + setShowCardDeclinedModal(false); + }; + + if (isSubOrganization) { + await handleOrgSelection({ organizationId: rootOrg.id, onSuccess }); + } else { + await navigateToBilling(); + } + }; + + const handleNavigateToAdminConsole = async () => { + const navigateToAdminConsole = () => { + navigate({ + to: "/admin" + }); + }; + + if (isSubOrganization) { + await handleOrgSelection({ organizationId: rootOrg.id, navigateTo: "/admin" }); + } else { + navigateToAdminConsole(); + } }; const { mutateAsync } = useGetOrgTrialUrl(); @@ -272,7 +335,7 @@ export const Navbar = () => { return; } - handleOrgChange(org?.id); + handleOrgSelection({ organizationId: org?.id }); }; return ( @@ -314,17 +377,20 @@ export const Navbar = () => { className="flex cursor-pointer items-center gap-x-2 truncate whitespace-nowrap" type="button" onClick={async () => { - navigate({ - to: "/organizations/$orgId/projects", - params: { orgId: currentOrg.id } - }); if (isSubOrganization) { - await router.invalidate({ sync: true }).catch(() => null); + await handleOrgSelection({ + organizationId: currentOrg.rootOrgId as string + }); + } else { + navigate({ + to: "/organizations/$orgId/projects", + params: { orgId: currentOrg.id } + }); } }} > - {currentOrg?.name} + {rootOrg?.name} Organization @@ -397,13 +463,7 @@ export const Navbar = () => { {subOrganizations.map((subOrg) => ( { - navigate({ - to: "/organizations/$orgId/projects", - params: { orgId: subOrg.id } - }); - await router.invalidate({ sync: true }).catch(() => null); - }} + onClick={() => handleOrgSelection({ organizationId: subOrg.id })} className="cursor-pointer font-normal" key={subOrg.id} > @@ -458,79 +518,93 @@ export const Navbar = () => { - {currentOrg.subOrganization && ( + {isSubOrganization && ( <> -

/

- - svg]:!text-sub-org" - )} - > - - - {currentOrg.subOrganization.name} - - - -
- - - + +
+ {/* scott: the below is used to hide the top border from the org nav bar */} + {!isProjectScope && isSubOrganization && ( +
+
- - -
- Sub-Organizations -
- {subOrganizations.map((subOrg) => ( - +
+ +
+ +
+ + + +
+
+ - New Sub-Organization -
-
- +
+ Sub-Organizations +
+ {subOrganizations.map((subOrg) => ( + handleOrgSelection({ organizationId: subOrg.id })} + className="cursor-pointer font-normal" + key={subOrg.id} + > +
+ {currentOrg?.id === subOrg.id && ( + + )} +

{subOrg.name}

+
+
+ ))} + {Boolean(subOrganizations.length) && ( +
+ )} + } + onClick={() => setShowSubOrgForm(true)} + > + New Sub-Organization + + + +
)} {isProjectScope && ( @@ -550,11 +624,11 @@ export const Navbar = () => { className="mr-2 border-mineshaft-500 px-2.5 py-1.5 whitespace-nowrap text-mineshaft-200 hover:bg-mineshaft-600" leftIcon={} onClick={async () => { - if (!subscription || !currentOrg) return; + if (!subscription || !rootOrg) return; // direct user to start pro trial const url = await mutateAsync({ - orgId: currentOrg.id, + orgId: rootOrg.id, success_url: window.location.href }); @@ -575,6 +649,7 @@ export const Navbar = () => { Server Console @@ -612,7 +687,8 @@ export const Navbar = () => { text === "Email Support" ? getUrl({ org_id: currentOrg.id, - domain: window.location.origin + domain: window.location.origin, + ...(isSubOrganization && { root_org_id: rootOrg.id }) }) : getUrl(); @@ -772,19 +848,13 @@ export const Navbar = () => {
- - - + Update Payment Method + {!isModalIntrusive && (
diff --git a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx index dbaa6d833..0b803bd53 100644 --- a/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/NavBar/NewSubOrganizationForm.tsx @@ -1,15 +1,18 @@ import { Controller, useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useNavigate, useRouter } from "@tanstack/react-router"; import { z } from "zod"; import { createNotification } from "@app/components/notifications"; +import SecurityClient from "@app/components/utilities/SecurityClient"; import { Button, FormControl, Input } from "@app/components/v2"; +import { useOrganization } from "@app/context"; import { useCreateSubOrganization } from "@app/hooks/api"; +import { selectOrganization } from "@app/hooks/api/auth/queries"; import { slugSchema } from "@app/lib/schemas"; type ContentProps = { onClose: () => void; + handleOrgSelection: (params: { organizationId: string }) => void; }; const AddOrgSchema = z.object({ @@ -18,7 +21,8 @@ const AddOrgSchema = z.object({ type FormData = z.infer; -export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { +export const NewSubOrganizationForm = ({ onClose, handleOrgSelection }: ContentProps) => { + const { currentOrg, isSubOrganization } = useOrganization(); const createSubOrg = useCreateSubOrganization(); const { @@ -32,10 +36,16 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { resolver: zodResolver(AddOrgSchema) }); - const navigate = useNavigate(); - const router = useRouter(); - const onSubmit = async ({ name }: FormData) => { + if (isSubOrganization && currentOrg.rootOrgId) { + const { token } = await selectOrganization({ + organizationId: currentOrg.rootOrgId + }); + + SecurityClient.setToken(token); + SecurityClient.setProviderAuthToken(""); + } + const { organization } = await createSubOrg.mutateAsync({ name }); @@ -46,11 +56,7 @@ export const NewSubOrganizationForm = ({ onClose }: ContentProps) => { }); onClose(); - navigate({ - to: "/organizations/$orgId/projects", - params: { orgId: organization.id } - }); - await router.invalidate({ sync: true }).catch(() => null); + await handleOrgSelection({ organizationId: organization.id }); }; return ( diff --git a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx index e56c02cee..09859b10f 100644 --- a/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx +++ b/frontend/src/layouts/OrganizationLayout/components/OrgNavBar/OrgNavBar.tsx @@ -1,5 +1,6 @@ import { Link, useLocation } from "@tanstack/react-router"; import { motion } from "framer-motion"; +import { twMerge } from "tailwind-merge"; import { CreateOrgModal } from "@app/components/organization/CreateOrgModal"; import { Tab, TabList, Tabs } from "@app/components/v2"; @@ -21,7 +22,14 @@ export const OrgNavBar = ({ isHidden }: Props) => { return (
{!isHidden && ( -
+
{ > {({ isActive }) => Sessions} + + {({ isActive }) => ( + + Approvals + + )} + { - const { isSubOrganization, currentOrg } = useOrganization(); + const { currentOrg } = useOrganization(); const { currentProject } = useProject(); const exitAssumePrivilegeMode = useRemoveAssumeProjectPrivilege(); const { assumedPrivilegeDetails } = useProjectPermission(); @@ -37,7 +37,7 @@ export const AssumePrivilegeModeBanner = () => { }, { onSuccess: () => { - const url = `${getProjectHomePage(currentProject.type, currentProject.environments)}${isSubOrganization ? `?subOrganization=${currentOrg.slug}` : ""}`; + const url = getProjectHomePage(currentProject.type, currentProject.environments); window.location.assign( url.replace("$orgId", currentOrg.id).replace("$projectId", currentProject.id) ); diff --git a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx index e701863eb..2febc1887 100644 --- a/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx +++ b/frontend/src/layouts/ProjectLayout/components/ProjectSelect/ProjectSelect.tsx @@ -157,20 +157,11 @@ const ProjectSelectInner = () => { params: { projectId: workspace.id, orgId: workspace.orgId - }, - search: { - subOrganization: currentOrg?.subOrganization?.name } }); const urlInstance = new URL( `${window.location.origin}${url.to.replaceAll("$orgId", url.params.orgId).replaceAll("$projectId", url.params.projectId)}` ); - if (currentOrg?.subOrganization) { - urlInstance.searchParams.set( - "subOrganization", - currentOrg.subOrganization.name - ); - } window.location.assign(urlInstance); }} icon={ diff --git a/frontend/src/pages/auth/LoginPage/Login.utils.tsx b/frontend/src/pages/auth/LoginPage/Login.utils.tsx index 06a897b5a..8f2c63f83 100644 --- a/frontend/src/pages/auth/LoginPage/Login.utils.tsx +++ b/frontend/src/pages/auth/LoginPage/Login.utils.tsx @@ -5,7 +5,17 @@ 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) => { +type NavigateUserToOrgParams = { + navigate: NavigateFn; + organizationId?: string; + navigateTo?: string; +}; + +export const navigateUserToOrg = async ({ + navigate, + organizationId, + navigateTo +}: NavigateUserToOrgParams) => { const userOrgs = await fetchOrganizations(); const nonAuthEnforcedOrgs = userOrgs.filter((org) => !org.authEnforced); @@ -13,7 +23,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s if (organizationId) { localStorage.setItem("orgData.id", organizationId); navigate({ - to: "/organizations/$orgId/projects", + to: navigateTo || "/organizations/$orgId/projects", params: { orgId: organizationId } }); return; @@ -24,7 +34,7 @@ export const navigateUserToOrg = async (navigate: NavigateFn, organizationId?: s const userOrg = nonAuthEnforcedOrgs[0] && nonAuthEnforcedOrgs[0].id; localStorage.setItem("orgData.id", userOrg); navigate({ - to: "/organizations/$orgId/projects", + to: navigateTo || "/organizations/$orgId/projects", params: { orgId: userOrg } }); } else { diff --git a/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx b/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx index 3e13d6d08..4e8f2ba2c 100644 --- a/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx +++ b/frontend/src/pages/auth/LoginPage/components/PasswordStep/PasswordStep.tsx @@ -115,7 +115,7 @@ export const PasswordStep = ({ return; } - await navigateUserToOrg(navigate, organizationId); + await navigateUserToOrg({ navigate, organizationId }); }; await finishWithOrgWorkflow(); @@ -131,7 +131,7 @@ export const PasswordStep = ({ } // case: no orgs found, so we navigate the user to create an org else { - await navigateUserToOrg(navigate); + await navigateUserToOrg({ navigate }); } } } catch (err: any) { @@ -233,7 +233,7 @@ export const PasswordStep = ({ } // case: no orgs found, so we navigate the user to create an org else { - await navigateUserToOrg(navigate); + await navigateUserToOrg({ navigate }); } } } else { @@ -254,7 +254,7 @@ export const PasswordStep = ({ // case: organization ID is present from the provider auth token -- navigate directly to the org if (organizationId) { - await navigateUserToOrg(navigate, 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 @@ -264,7 +264,7 @@ export const PasswordStep = ({ if (userOrgs.length > 0) { navigateToSelectOrganization(undefined, isAdminLogin); } else { - await navigateUserToOrg(navigate); + await navigateUserToOrg({ navigate }); } } } @@ -316,7 +316,7 @@ export const PasswordStep = ({ return ( - navigateUserToOrg(navigate, organizationId).catch(() => + navigateUserToOrg({ navigate, organizationId }).catch(() => createNotification({ text: "Failed to navigate user", type: "error" }) ) } diff --git a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx index 2a3e9c554..98633e390 100644 --- a/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/SelectOrgSection.tsx @@ -47,6 +47,7 @@ export const SelectOrganizationSection = () => { const orgId = queryParams.get("org_id"); const callbackPort = queryParams.get("callback_port"); const isAdminLogin = queryParams.get("is_admin_login") === "true"; + const mfaPending = queryParams.get("mfa_pending") === "true"; const defaultSelectedOrg = organizations.data?.find((org) => org.id === orgId); const logout = useLogoutUser(true); @@ -188,7 +189,7 @@ export const SelectOrganizationSection = () => { navigate({ to: "/cli-redirect" }); // cli page } else { - navigateUserToOrg(navigate, organization.id); + navigateUserToOrg({ navigate, organizationId: organization.id }); } }, [selectOrg] @@ -201,7 +202,7 @@ export const SelectOrganizationSection = () => { const decodedJwt = jwtDecode(authToken) as any; if (decodedJwt?.organizationId) { - navigateUserToOrg(navigate, decodedJwt.organizationId); + navigateUserToOrg({ navigate, organizationId: decodedJwt.organizationId }); } } @@ -236,10 +237,22 @@ export const SelectOrganizationSection = () => { }, [organizations.isPending, organizations.data]); useEffect(() => { + if (mfaPending && defaultSelectedOrg) { + const storedMfaToken = sessionStorage.getItem(SessionStorageKeys.MFA_TEMP_TOKEN); + if (storedMfaToken) { + sessionStorage.removeItem(SessionStorageKeys.MFA_TEMP_TOKEN); + SecurityClient.setMfaToken(storedMfaToken); + setIsInitialOrgCheckLoading(false); + toggleShowMfa.on(); + setMfaSuccessCallback(() => () => handleSelectOrganization(defaultSelectedOrg)); + return; + } + } + if (defaultSelectedOrg) { handleSelectOrganization(defaultSelectedOrg); } - }, [defaultSelectedOrg]); + }, [defaultSelectedOrg, mfaPending]); if ( userLoading || diff --git a/frontend/src/pages/auth/SelectOrgPage/route.tsx b/frontend/src/pages/auth/SelectOrgPage/route.tsx index 27ad4bb94..f20fda1f3 100644 --- a/frontend/src/pages/auth/SelectOrgPage/route.tsx +++ b/frontend/src/pages/auth/SelectOrgPage/route.tsx @@ -8,7 +8,8 @@ export const SelectOrganizationPageQueryParams = z.object({ org_id: z.string().optional().catch(""), callback_port: z.coerce.number().optional().catch(undefined), is_admin_login: z.boolean().optional().catch(false), - force: z.boolean().optional() + force: z.boolean().optional(), + mfa_pending: z.boolean().optional().catch(false) }); export const Route = createFileRoute("/_restrict-login-signup/login/select-organization")({ @@ -16,7 +17,12 @@ export const Route = createFileRoute("/_restrict-login-signup/login/select-organ validateSearch: zodValidator(SelectOrganizationPageQueryParams), search: { middlewares: [ - stripSearchParams({ org_id: "", callback_port: undefined, is_admin_login: false }) + stripSearchParams({ + org_id: "", + callback_port: undefined, + is_admin_login: false, + mfa_pending: false + }) ] } }); diff --git a/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx b/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx index 81eb4e170..0a6111cce 100644 --- a/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/cert-manager/SettingsPage/SettingsPage.tsx @@ -18,7 +18,7 @@ const tabs = [ export const SettingsPage = () => { const { t } = useTranslation(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); return (
@@ -34,7 +34,8 @@ export const SettingsPage = () => { }} className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" > - Looking for organization settings? + Looking for {isSubOrganization ? "sub-" : ""}organization + settings? diff --git a/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx b/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx index 3a0771302..857f8d446 100644 --- a/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx +++ b/frontend/src/pages/kms/SettingsPage/SettingsPage.tsx @@ -19,7 +19,7 @@ const tabs = [ export const SettingsPage = () => { const { t } = useTranslation(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); return (
@@ -39,7 +39,8 @@ export const SettingsPage = () => { }} className="flex items-center gap-x-1.5 text-xs whitespace-nowrap text-neutral hover:underline" > - Looking for organization settings? + Looking for {isSubOrganization ? "sub-" : ""}organization + settings? diff --git a/frontend/src/pages/middlewares/authenticate.tsx b/frontend/src/pages/middlewares/authenticate.tsx index 3998dde63..cfc531f0d 100644 --- a/frontend/src/pages/middlewares/authenticate.tsx +++ b/frontend/src/pages/middlewares/authenticate.tsx @@ -73,6 +73,11 @@ export const Route = createFileRoute("/_authenticate")({ }); }); - return { organizationId: data.organizationId as string, isAuthenticated: true, user }; + const isSubOrganization = !!data.subOrganizationId; + return { + organizationId: isSubOrganization ? data.subOrganizationId : (data.organizationId as string), + isAuthenticated: true, + user + }; } }); diff --git a/frontend/src/pages/middlewares/inject-org-details.tsx b/frontend/src/pages/middlewares/inject-org-details.tsx index 35d60a537..698718184 100644 --- a/frontend/src/pages/middlewares/inject-org-details.tsx +++ b/frontend/src/pages/middlewares/inject-org-details.tsx @@ -1,7 +1,12 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, isRedirect, redirect } from "@tanstack/react-router"; +import SecurityClient from "@app/components/utilities/SecurityClient"; +import { SessionStorageKeys } from "@app/const"; +import { authKeys, fetchAuthToken, selectOrganization } from "@app/hooks/api/auth/queries"; import { fetchOrganizationById, organizationKeys } from "@app/hooks/api/organization/queries"; +import { projectKeys } from "@app/hooks/api/projects"; import { fetchUserOrgPermissions, roleQueryKeys } from "@app/hooks/api/roles/queries"; +import { subOrganizationsQuery } from "@app/hooks/api/subOrganizations"; import { fetchOrgSubscription, subscriptionQueryKeys } from "@app/hooks/api/subscriptions/queries"; // Route context to fill in organization's data like details, subscription etc @@ -15,6 +20,44 @@ export const Route = createFileRoute("/_authenticate/_inject-org-details")({ organizationId = context.organizationId!; } + if ((params as { orgId?: string })?.orgId && context.organizationId) { + const urlOrgId = (params as { orgId: string }).orgId; + const currentTokenOrgId = context.organizationId; + + if (urlOrgId !== currentTokenOrgId) { + try { + const { token, isMfaEnabled } = await selectOrganization({ organizationId: urlOrgId }); + + if (isMfaEnabled) { + sessionStorage.setItem(SessionStorageKeys.MFA_TEMP_TOKEN, token); + throw redirect({ + to: "/login/select-organization", + search: { org_id: urlOrgId, mfa_pending: true } + }); + } + + if (!isMfaEnabled && token) { + SecurityClient.setToken(token); + SecurityClient.setProviderAuthToken(""); + + context.queryClient.removeQueries({ queryKey: authKeys.getAuthToken }); + context.queryClient.removeQueries({ queryKey: projectKeys.getAllUserProjects() }); + context.queryClient.removeQueries({ queryKey: subOrganizationsQuery.allKey() }); + + await context.queryClient.fetchQuery({ + queryKey: authKeys.getAuthToken, + queryFn: fetchAuthToken + }); + } + } catch (error) { + if (isRedirect(error)) { + throw error; + } + console.warn("Failed to automatically exchange token for organization:", error); + } + } + } + await context.queryClient.ensureQueryData({ queryKey: organizationKeys.getOrgById(organizationId), queryFn: () => fetchOrganizationById(organizationId) diff --git a/frontend/src/pages/middlewares/restrict-login-signup.tsx b/frontend/src/pages/middlewares/restrict-login-signup.tsx index ec1958d14..582b6331b 100644 --- a/frontend/src/pages/middlewares/restrict-login-signup.tsx +++ b/frontend/src/pages/middlewares/restrict-login-signup.tsx @@ -117,9 +117,10 @@ export const Route = createFileRoute("/_restrict-login-signup")({ return; throw redirect({ to: "/login/select-organization" }); } + const orgId = data.subOrganizationId || data.organizationId; throw redirect({ to: "/organizations/$orgId/projects", - params: { orgId: data.organizationId } + params: { orgId } }); }, component: AuthConsentWrapper diff --git a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx index 0d5456f6d..cde27cb3c 100644 --- a/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/AccessManagementPage.tsx @@ -3,7 +3,8 @@ import { Helmet } from "react-helmet"; import { useTranslation } from "react-i18next"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useNavigate, useSearch } from "@tanstack/react-router"; +import { Link, useNavigate, useSearch } from "@tanstack/react-router"; +import { InfoIcon } from "lucide-react"; import { OrgPermissionGuardBanner } from "@app/components/permissions/OrgPermissionCan"; import { Button, PageHeader, Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; @@ -84,8 +85,20 @@ export const AccessManagementPage = () => { + description={`Manage fine-grained access for users, groups, roles, and machine identities within your ${isSubOrganization ? "sub-" : ""}organization resources.`} + > + {isSubOrganization && ( + + Looking for root organization access control? + + )} + {!currentOrg.shouldUseNewPrivilegeSystem && (
diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx index 2f66c6644..4d6a45da3 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsSection.tsx @@ -6,7 +6,12 @@ import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; import { Button, DeleteActionModal } from "@app/components/v2"; import { DocumentationLinkBadge } from "@app/components/v3"; -import { OrgPermissionGroupActions, OrgPermissionSubjects, useSubscription } from "@app/context"; +import { + OrgPermissionGroupActions, + OrgPermissionSubjects, + useOrganization, + useSubscription +} from "@app/context"; import { useDeleteGroup } from "@app/hooks/api"; import { usePopUp } from "@app/hooks/usePopUp"; @@ -15,6 +20,7 @@ import { OrgGroupsTable } from "./OrgGroupsTable"; export const OrgGroupsSection = () => { const { subscription } = useSubscription(); + const { isSubOrganization } = useOrganization(); const { mutateAsync: deleteMutateAsync } = useDeleteGroup(); const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ @@ -51,7 +57,9 @@ export const OrgGroupsSection = () => {
-

Organization Groups

+

+ {isSubOrganization ? "Sub-" : ""}Organization Groups +

@@ -63,7 +71,7 @@ export const OrgGroupsSection = () => { onClick={() => handleAddGroupModal()} isDisabled={!isAllowed} > - Create Organization Group + Create {isSubOrganization ? "Sub-" : ""}Organization Group )} diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx index e0cd09d83..3f77466a3 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgGroupsTab/components/OrgGroupsSection/OrgGroupsTable.tsx @@ -71,7 +71,7 @@ enum GroupsOrderBy { export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { const navigate = useNavigate(); - const { currentOrg } = useOrganization(); + const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; const { isPending, data: groups = [] } = useGetOrganizationGroups(orgId); const { mutateAsync: updateMutateAsync } = useUpdateGroup(); @@ -159,7 +159,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => { value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search organization groups..." + placeholder={`Search ${isSubOrganization ? "sub-" : ""}organization groups...`} /> @@ -205,7 +205,7 @@ export const OrgGroupsTable = ({ handlePopUpOpen }: Props) => {
- Organization Role + {isSubOrganization ? "Sub-" : ""}Organization Role { diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx index c71dc8055..6b6086b0b 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentitySection.tsx @@ -1,13 +1,13 @@ import { useState } from "react"; import { faPlus } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { AnimatePresence, motion } from "framer-motion"; -import { LinkIcon, PlusIcon } from "lucide-react"; +import { InfoIcon } from "lucide-react"; +import { twMerge } from "tailwind-merge"; import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; import { createNotification } from "@app/components/notifications"; import { OrgPermissionCan } from "@app/components/permissions"; -import { Button, DeleteActionModal, Modal, ModalContent } from "@app/components/v2"; +import { Button, DeleteActionModal, Modal, ModalContent, Tooltip } from "@app/components/v2"; import { DocumentationLinkBadge } from "@app/components/v3"; import { OrgPermissionIdentityActions, @@ -30,9 +30,8 @@ import { OrgIdentityLinkForm } from "./OrgIdentityLinkForm"; import { OrgIdentityModal } from "./OrgIdentityModal"; enum IdentityWizardSteps { - SelectAction = "select-action", - LinkIdentity = "link-identity", - OrganizationIdentity = "project-identity" + CreateIdentity = "create-identity", + LinkIdentity = "link-identity" } export const IdentitySection = withPermission( @@ -41,7 +40,7 @@ export const IdentitySection = withPermission( const { currentOrg, isSubOrganization } = useOrganization(); const orgId = currentOrg?.id || ""; - const [wizardStep, setWizardStep] = useState(IdentityWizardSteps.SelectAction); + const [wizardStep, setWizardStep] = useState(IdentityWizardSteps.CreateIdentity); const { mutateAsync: deleteMutateAsync } = useDeleteOrgIdentity(); const { mutateAsync: deleteTemplateMutateAsync } = useDeleteIdentityAuthTemplate(); @@ -100,7 +99,7 @@ export const IdentitySection = withPermission(

- Organization Machine Identities + {isSubOrganization ? "Sub-" : ""}Organization Machine Identities

@@ -124,7 +123,7 @@ export const IdentitySection = withPermission( } if (!isSubOrganization) { - setWizardStep(IdentityWizardSteps.OrganizationIdentity); + setWizardStep(IdentityWizardSteps.CreateIdentity); } handlePopUpOpen("identity"); @@ -197,7 +196,7 @@ export const IdentitySection = withPermission( onOpenChange={(open) => { handlePopUpToggle("identity", open); if (!open) { - setWizardStep(IdentityWizardSteps.SelectAction); + setWizardStep(IdentityWizardSteps.CreateIdentity); } }} > @@ -214,80 +213,84 @@ export const IdentitySection = withPermission( : undefined } > - - {wizardStep === IdentityWizardSteps.SelectAction && ( - -
setWizardStep(IdentityWizardSteps.OrganizationIdentity)} - onKeyDown={(e) => { - if (e.key === "Enter") { - setWizardStep(IdentityWizardSteps.OrganizationIdentity); - } + {isSubOrganization && ( +
+
+
-
setWizardStep(IdentityWizardSteps.LinkIdentity)} - onKeyDown={(e) => { - if (e.key === "Enter") { - setWizardStep(IdentityWizardSteps.LinkIdentity); - } + Create New + +
- - )} - {wizardStep === IdentityWizardSteps.OrganizationIdentity && ( - +
+ +

+ You can add machine identities to your sub-organization in one of two ways: +

+
    +
  • + Create New - + Create a new machine identity specifically for this sub-organization. This + machine identity will be managed at the sub-organization level. +

    + This method is recommended for autonomous teams that need to manage + machine identity authentication. +

    +
  • +
  • + + Assign Existing + {" "} + Assign an existing machine identity from your parent organization. The + machine identity will continue to be managed at its original scope. +

    + This method is recommended for organizations that need to maintain + centralized control. +

    +
  • +
+ + } > - - - )} - {wizardStep === IdentityWizardSteps.LinkIdentity && ( - - handlePopUpClose("identity")} /> - - )} - + +
+
+ )} + {wizardStep === IdentityWizardSteps.CreateIdentity && ( + + )} + {wizardStep === IdentityWizardSteps.LinkIdentity && ( + handlePopUpClose("identity")} /> + )} { - Filter Organization Machine Identities by Role + Filter {isSubOrganization ? "Sub-" : ""}Organization Machine Identities by Role {roles?.map(({ id, slug, name }) => ( { value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search machine identities by name..." + placeholder={`Search ${isSubOrganization ? "sub-organization" : "organization"} machine identities by name...`} />
@@ -258,7 +258,7 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => {
- Organization Role + {isSubOrganization ? "Sub-" : ""}Organization Role { to: "/organizations/$orgId/identities/$identityId", params: { identityId: id, - orgId + orgId: currentOrg.id } }) } @@ -455,8 +455,8 @@ export const IdentityTable = ({ handlePopUpOpen }: Props) => { 0 || filter.roles?.length > 0 - ? "No machine identities match search filter" - : "No machine identities have been created in this organization" + ? `No ${isSubOrganization ? "sub-" : ""}organization machine identities match search filter` + : `No machine identities have been created in this ${isSubOrganization ? "sub-" : ""}organization` } icon={faServer} /> diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx index 6a4d93141..926032ecc 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersSection.tsx @@ -205,7 +205,9 @@ export const OrgMembersSection = () => {
-

Organization Users

+

+ {isSubOrganization ? "Sub-" : ""}Organization Users +

@@ -242,7 +244,7 @@ export const OrgMembersSection = () => { isOpen={popUp.addMemberToSubOrg.isOpen} onOpenChange={(isOpen) => handlePopUpToggle("addMemberToSubOrg", isOpen)} > - + handlePopUpClose("addMemberToSubOrg")} /> diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx index 55b593ffe..6238e1308 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgMembersTab/components/OrgMembersSection/OrgMembersTable.tsx @@ -336,7 +336,7 @@ export const OrgMembersTable = ({ - Filter Organization Users by Role + Filter {isSubOrganization ? "Sub-" : ""}Organization Users by Role {roles?.map(({ id, slug, name }) => ( setSearch(e.target.value)} leftIcon={} - placeholder="Search organization users..." + placeholder={`Search ${isSubOrganization ? "sub-" : ""}organization users...`} />
@@ -434,7 +434,7 @@ export const OrgMembersTable = ({
- Organization Role + {isSubOrganization ? "Sub-" : ""}Organization Role diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx index 63b4284f1..890196348 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgRoleTabSection/OrgRoleTable.tsx @@ -207,7 +207,7 @@ export const OrgRoleTable = () => { }} isDisabled={!isAllowed} > - Add Organization Role + Add {isSubOrganization ? "Sub-" : ""}Organization Role )} @@ -216,7 +216,7 @@ export const OrgRoleTable = () => { value={search} onChange={(e) => setSearch(e.target.value)} leftIcon={} - placeholder="Search organization roles..." + placeholder={`Search ${isSubOrganization ? "sub-" : ""}organization roles...`} className="flex-1" containerClassName="mb-4" /> diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx index af26a940d..6fa3d8854 100644 --- a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/AppConnectionForm.tsx @@ -35,6 +35,7 @@ import { HerokuConnectionForm } from "./HerokuAppConnectionForm"; import { HumanitecConnectionForm } from "./HumanitecConnectionForm"; import { LaravelForgeConnectionForm } from "./LaravelForgeConnectionForm"; import { LdapConnectionForm } from "./LdapConnectionForm"; +import { MongoDBConnectionForm } from "./MongoDBConnectionForm"; import { MsSqlConnectionForm } from "./MsSqlConnectionForm"; import { MySqlConnectionForm } from "./MySqlConnectionForm"; import { NetlifyConnectionForm } from "./NetlifyConnectionForm"; @@ -173,6 +174,8 @@ const CreateForm = ({ app, onComplete, projectId }: CreateFormProps) => { return ; case AppConnection.Redis: return ; + case AppConnection.MongoDB: + return ; default: throw new Error(`Unhandled App ${app}`); } @@ -331,6 +334,8 @@ const UpdateForm = ({ appConnection, onComplete }: UpdateFormProps) => { return ; case AppConnection.Redis: return ; + case AppConnection.MongoDB: + return ; default: throw new Error(`Unhandled App ${(appConnection as TAppConnection).app}`); } diff --git a/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx new file mode 100644 index 000000000..72e359e0a --- /dev/null +++ b/frontend/src/pages/organization/AppConnections/AppConnectionsPage/components/AppConnectionForm/MongoDBConnectionForm.tsx @@ -0,0 +1,326 @@ +import { useState } from "react"; +import { Controller, FormProvider, useForm } from "react-hook-form"; +import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Tab } from "@headlessui/react"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { + Button, + FormControl, + Input, + ModalClose, + SecretInput, + Select, + SelectItem, + Switch, + TextArea, + Tooltip +} from "@app/components/v2"; +import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections"; +import { MongoDBConnectionMethod, TMongoDBConnection } from "@app/hooks/api/appConnections"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; + +import { + genericAppConnectionFieldsSchema, + GenericAppConnectionsFields +} from "./GenericAppConnectionFields"; + +type Props = { + appConnection?: TMongoDBConnection; + onSubmit: (formData: FormData) => Promise; +}; + +const rootSchema = genericAppConnectionFieldsSchema.extend({ + app: z.literal(AppConnection.MongoDB) +}); + +const formSchema = z.discriminatedUnion("method", [ + rootSchema.extend({ + method: z.literal(MongoDBConnectionMethod.UsernameAndPassword), + credentials: z.object({ + host: z.string().trim().min(1, "Host required"), + port: z.coerce.number().default(27017), + username: z.string().trim().min(1, "Username required"), + password: z.string().trim().min(1, "Password required"), + database: z.string().trim().min(1, "Database required"), + tlsEnabled: z.boolean().default(false), + tlsRejectUnauthorized: z.boolean().default(true), + tlsCertificate: z + .string() + .trim() + .transform((value) => value || undefined) + .optional() + }) + }) +]); + +type FormData = z.infer; + +export const MongoDBConnectionForm = ({ appConnection, onSubmit }: Props) => { + const isUpdate = Boolean(appConnection); + const [selectedTabIndex, setSelectedTabIndex] = useState(0); + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: appConnection ?? { + app: AppConnection.MongoDB, + method: MongoDBConnectionMethod.UsernameAndPassword, + credentials: { + host: "", + port: 27017, + username: "", + password: "", + database: "", + tlsEnabled: false, + tlsRejectUnauthorized: true, + tlsCertificate: undefined + } + } + }); + + const { + handleSubmit, + watch, + control, + formState: { isSubmitting, isDirty } + } = form; + + const tlsEnabled = watch("credentials.tlsEnabled"); + + return ( + +
+ {!isUpdate && } + ( + + + + )} + /> + + + + + `-mb-[0.14rem] px-4 py-2 text-sm font-medium whitespace-nowrap outline-hidden disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + Configuration + + + `-mb-[0.14rem] px-4 py-2 text-sm font-medium whitespace-nowrap outline-hidden disabled:opacity-60 ${ + selected + ? "border-b-2 border-mineshaft-300 text-mineshaft-200" + : "text-bunker-300" + }` + } + > + TLS ({tlsEnabled ? "Enabled" : "Disabled"}) + + + + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> +
+
+ ( + + + + )} + /> + ( + + onChange(e.target.value)} + /> + + )} + /> +
+
+ + ( + + + Enable TLS + + + )} + /> + ( + +