diff --git a/frontend-v2/package-lock.json b/frontend-v2/package-lock.json
index 3260b3720..7fc2dda8d 100644
--- a/frontend-v2/package-lock.json
+++ b/frontend-v2/package-lock.json
@@ -24,10 +24,31 @@
"@hookform/resolvers": "^3.9.1",
"@octokit/rest": "^21.0.2",
"@peculiar/x509": "^1.12.3",
+ "@radix-ui/react-accordion": "^1.2.2",
+ "@radix-ui/react-alert-dialog": "^1.1.3",
+ "@radix-ui/react-checkbox": "^1.1.3",
+ "@radix-ui/react-collapsible": "^1.1.2",
+ "@radix-ui/react-dialog": "^1.1.3",
+ "@radix-ui/react-dropdown-menu": "^2.1.3",
+ "@radix-ui/react-hover-card": "^1.1.3",
+ "@radix-ui/react-label": "^2.1.1",
+ "@radix-ui/react-popover": "^1.1.3",
+ "@radix-ui/react-popper": "^1.2.1",
+ "@radix-ui/react-progress": "^1.1.1",
+ "@radix-ui/react-radio-group": "^1.2.2",
+ "@radix-ui/react-select": "^2.1.3",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.2",
+ "@radix-ui/react-toast": "^1.2.3",
+ "@radix-ui/react-tooltip": "^1.1.5",
+ "@sindresorhus/slugify": "^2.2.1",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-router": "^1.87.9",
+ "@ucast/mongo2js": "^1.3.4",
"argon2-browser": "^1.18.0",
+ "axios": "^1.7.9",
"classnames": "^2.5.1",
+ "cva": "npm:class-variance-authority@^0.7.1",
"date-fns": "^4.1.0",
"file-saver": "^2.0.5",
"framer-motion": "^11.14.1",
@@ -1200,6 +1221,19 @@
"@floating-ui/utils": "^0.2.8"
}
},
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
+ "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
"node_modules/@floating-ui/utils": {
"version": "0.2.8",
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz",
@@ -1856,6 +1890,1036 @@
"url": "https://opencollective.com/unts"
}
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
+ "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/primitive": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz",
+ "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==",
+ "license": "MIT"
+ },
+ "node_modules/@radix-ui/react-accordion": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accordion/-/react-accordion-1.2.2.tgz",
+ "integrity": "sha512-b1oh54x4DMCdGsB4/7ahiSrViXxaBwRPotiZNnYXjLha9vfuURSAZErki6qjDoSIV0eXx5v57XnTGVtGwnfp2g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-collapsible": "1.1.2",
+ "@radix-ui/react-collection": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "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-alert-dialog": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.3.tgz",
+ "integrity": "sha512-5xzWppXTNZe6zFrTTwAJIoMJeZmdFe0l8ZqQrPGKAVvhdyOWR4r53/G7SZqx6/uf1J441oxK7GzmTkrrWDroHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dialog": "1.1.3",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-slot": "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-arrow": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.1.tgz",
+ "integrity": "sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.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-checkbox": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.3.tgz",
+ "integrity": "sha512-HD7/ocp8f1B3e6OHygH0n7ZKjONkhciy1Nh0yuBgObqThc3oyx+vuMfFHKAknXRHHWVE9XvXStxJFyjUmB8PIw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-previous": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0"
+ },
+ "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.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz",
+ "integrity": "sha512-PliMB63vxz7vggcyq0IxNYk8vGDrLXVWw4+W4B8YnwI1s18x7YZYqlG9PLX7XxAJUi0g2DxP4XKJMFHh/iVh9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz",
+ "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-slot": "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-compose-refs": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz",
+ "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz",
+ "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==",
+ "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-dialog": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.3.tgz",
+ "integrity": "sha512-ujGvqQNkZ0J7caQyl8XuZRj2/TIrYcOGwqz5TeD1OMcCdfBuEMP0D12ve+8J5F9XuNUth3FAKFWo/wt0E/GJrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.2",
+ "@radix-ui/react-focus-guards": "1.1.1",
+ "@radix-ui/react-focus-scope": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-slot": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.6.0"
+ },
+ "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-direction": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz",
+ "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==",
+ "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.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.2.tgz",
+ "integrity": "sha512-kEHnlhv7wUggvhuJPkyw4qspXLJOdYoAP4dO2c8ngGuXTq1w/HZp1YeVB+NQ2KbH1iEG+pvOCGYSqh9HZOz6hg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-escape-keydown": "1.1.0"
+ },
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.3.tgz",
+ "integrity": "sha512-eKyAfA9e4HOavzyGJC6kiDIlHMPzAU0zqSqTg+VwS0Okvb9nkTo7L4TugkCUqM3I06ciSpdtYQ73cgB7tyUgVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-menu": "2.1.3",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz",
+ "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==",
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.1.tgz",
+ "integrity": "sha512-01omzJAYRxXdG2/he/+xy+c8a8gCydoQ1yOxnWNcRhrrBW5W+RQJ22EK1SaO8tb3WoUsuEw7mJjBozPzihDFjA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "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-hover-card": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.3.tgz",
+ "integrity": "sha512-D+o67Fd7fjkW10ycdsse1sYuGV9dNQKOhoVii7ksSfUYqQiTPxz9bP/Vu1g6huJ1651/2j8q7JGGWSIBIuGO1Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.2",
+ "@radix-ui/react-popper": "1.2.1",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "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.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz",
+ "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "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-label": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.1.tgz",
+ "integrity": "sha512-UUw5E4e/2+4kFMH7+YxORXGWggtY6sM8WIwh5RZchhLuUg2H1hc98Py+pr8HMz6rdaYrK2t296ZEjYLOCO5uUw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.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-menu": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.3.tgz",
+ "integrity": "sha512-wY5SY6yCiJYP+DMIy7RrjF4shoFpB9LJltliVwejBm8T2yepWDJgKBhIFYOGWYR/lFHOCtbstN9duZFu6gmveQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-collection": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-dismissable-layer": "1.1.2",
+ "@radix-ui/react-focus-guards": "1.1.1",
+ "@radix-ui/react-focus-scope": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.1",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-roving-focus": "1.1.1",
+ "@radix-ui/react-slot": "1.1.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.6.0"
+ },
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.3.tgz",
+ "integrity": "sha512-MBDKFwRe6fi0LT8m/Jl4V8J3WbS/UfXJtsgg8Ym5w5AyPG3XfHH4zhBp1P8HmZK83T8J7UzVm6/JpDE3WMl1Dw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.2",
+ "@radix-ui/react-focus-guards": "1.1.1",
+ "@radix-ui/react-focus-scope": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.1",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-slot": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.6.0"
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.1.tgz",
+ "integrity": "sha512-3kn5Me69L+jv82EKRuQCXdYyf1DqHwD2U/sxoNgBGCB7K9TRc3bQamQ+5EPM9EvyPdli0W41sROd+ZU1dTCztw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-use-rect": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0",
+ "@radix-ui/rect": "1.1.0"
+ },
+ "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.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.3.tgz",
+ "integrity": "sha512-NciRqhXnGojhT93RPyDaMPfLH3ZSl4jjIFbZQ1b/vxvZEdHsBZ49wP9w8L3HzUQwep01LcWtkUvm0OVB5JAHTw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "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.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz",
+ "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "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.0.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz",
+ "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "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-progress": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.1.tgz",
+ "integrity": "sha512-6diOawA84f/eMxFHcWut0aE1C2kyE9dOyCTQOMRR2C/qPiXz/X0SaiA/RLbapQaXUCmy0/hLMf9meSccD1N0pA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.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-radio-group": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.2.tgz",
+ "integrity": "sha512-E0MLLGfOP0l8P/NxgVzfXJ8w3Ch8cdO6UDzJfDChu4EJDy+/WdO5LqpdY8PYnCErkmZH3gZhDL1K7kQ41fAHuQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-roving-focus": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-previous": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0"
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
+ "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-collection": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "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-select": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.3.tgz",
+ "integrity": "sha512-tlLwaewTfrKetiex8iW9wwME/qrYlzlH0qcgYmos7xS54MO00SiPHasLoAykg/yVrjf41GQptPPi4oXzrP+sgg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.0",
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-collection": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-dismissable-layer": "1.1.2",
+ "@radix-ui/react-focus-guards": "1.1.1",
+ "@radix-ui/react-focus-scope": "1.1.1",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.1",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-slot": "1.1.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-use-previous": "1.1.0",
+ "@radix-ui/react-visually-hidden": "1.1.1",
+ "aria-hidden": "^1.1.1",
+ "react-remove-scroll": "2.6.0"
+ },
+ "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.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz",
+ "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "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-switch": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.2.tgz",
+ "integrity": "sha512-zGukiWHjEdBCRyXvKR6iXAQG6qXm2esuAD6kDOi9Cn+1X6ev3ASo4+CsYaD6Fov9r/AQFekqnD/7+V0Cs6/98g==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-previous": "1.1.0",
+ "@radix-ui/react-use-size": "1.1.0"
+ },
+ "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-tabs": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",
+ "integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-direction": "1.1.0",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-roving-focus": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0"
+ },
+ "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-toast": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.3.tgz",
+ "integrity": "sha512-oB8irs7CGAml6zWbum7MNySTH/sR7PM1ZQyLV8reO946u73sU83yZUKijrMLNbm4hTOrJY4tE8Oa/XUKrOr2Wg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-collection": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.2",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-use-callback-ref": "1.1.0",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.0",
+ "@radix-ui/react-visually-hidden": "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-tooltip": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.5.tgz",
+ "integrity": "sha512-IucoQPcK5nwUuztaxBQvudvYwH58wtRcJlv1qvaMSyIbL9dEBfFN0vRf/D8xDbu6HmAJLlNGty4z8Na+vIqe9Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.1",
+ "@radix-ui/react-compose-refs": "1.1.1",
+ "@radix-ui/react-context": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.2",
+ "@radix-ui/react-id": "1.1.0",
+ "@radix-ui/react-popper": "1.2.1",
+ "@radix-ui/react-portal": "1.1.3",
+ "@radix-ui/react-presence": "1.1.2",
+ "@radix-ui/react-primitive": "2.0.1",
+ "@radix-ui/react-slot": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.1.0",
+ "@radix-ui/react-visually-hidden": "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-use-callback-ref": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
+ "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==",
+ "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.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz",
+ "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "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.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz",
+ "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-callback-ref": "1.1.0"
+ },
+ "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.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz",
+ "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==",
+ "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-previous": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz",
+ "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==",
+ "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.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz",
+ "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.0"
+ },
+ "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.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz",
+ "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.0"
+ },
+ "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.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.1.tgz",
+ "integrity": "sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.0.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/rect": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz",
+ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==",
+ "license": "MIT"
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
@@ -2129,6 +3193,61 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@sindresorhus/slugify": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz",
+ "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==",
+ "license": "MIT",
+ "dependencies": {
+ "@sindresorhus/transliterate": "^1.0.0",
+ "escape-string-regexp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sindresorhus/slugify/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sindresorhus/transliterate": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz",
+ "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "escape-string-regexp": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@stylistic/eslint-plugin": {
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.12.1.tgz",
@@ -2822,7 +3941,7 @@
"version": "18.3.5",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
- "dev": true,
+ "devOptional": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^18.0.0"
@@ -3216,6 +4335,18 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/aria-hidden": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz",
+ "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@@ -3409,6 +4540,12 @@
"license": "MIT",
"peer": true
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/atob": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
@@ -3486,6 +4623,17 @@
"node": ">=4"
}
},
+ "node_modules/axios": {
+ "version": "1.7.9",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
+ "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -3908,6 +5056,18 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -4041,6 +5201,19 @@
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/cva": {
+ "name": "class-variance-authority",
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
+ "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "clsx": "^2.1.1"
+ },
+ "funding": {
+ "url": "https://polar.sh/cva"
+ }
+ },
"node_modules/damerau-levenshtein": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@@ -4182,6 +5355,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-node-es": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
+ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
+ "license": "MIT"
+ },
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -5287,6 +6475,26 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@@ -5314,6 +6522,20 @@
"url": "https://github.com/sponsors/isaacs"
}
},
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -5459,6 +6681,15 @@
"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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/get-symbol-description": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
@@ -5846,6 +7077,15 @@
"node": ">= 0.4"
}
},
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -6676,6 +7916,27 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -7484,6 +8745,12 @@
"react-is": "^16.13.1"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -7721,6 +8988,53 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
+ "node_modules/react-remove-scroll": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz",
+ "integrity": "sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==",
+ "license": "MIT",
+ "dependencies": {
+ "react-remove-scroll-bar": "^2.3.6",
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.1.0",
+ "use-callback-ref": "^1.3.0",
+ "use-sidecar": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/react-remove-scroll-bar": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz",
+ "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==",
+ "license": "MIT",
+ "dependencies": {
+ "react-style-singleton": "^2.2.1",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-select": {
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.9.0.tgz",
@@ -7742,6 +9056,29 @@
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
+ "node_modules/react-style-singleton": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
+ "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==",
+ "license": "MIT",
+ "dependencies": {
+ "get-nonce": "^1.0.0",
+ "invariant": "^2.2.4",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-toastify": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz",
@@ -9586,6 +10923,27 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-callback-ref": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz",
+ "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-isomorphic-layout-effect": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz",
@@ -9600,6 +10958,28 @@
}
}
},
+ "node_modules/use-sidecar": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz",
+ "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==",
+ "license": "MIT",
+ "dependencies": {
+ "detect-node-es": "^1.1.0",
+ "tslib": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0",
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/use-sync-external-store": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz",
diff --git a/frontend-v2/package.json b/frontend-v2/package.json
index 0a124f349..e89c1b45e 100644
--- a/frontend-v2/package.json
+++ b/frontend-v2/package.json
@@ -9,7 +9,7 @@
"preview": "vite preview",
"lint": "eslint ./src",
"lint:fix": "eslint --fix ./src",
- "type:check": "tsc --project tsconfig.json --noEmit"
+ "type:check": "tsc --noEmit --project ./tsconfig.app.json"
},
"dependencies": {
"@casl/ability": "^6.7.2",
@@ -28,10 +28,31 @@
"@hookform/resolvers": "^3.9.1",
"@octokit/rest": "^21.0.2",
"@peculiar/x509": "^1.12.3",
+ "@radix-ui/react-accordion": "^1.2.2",
+ "@radix-ui/react-alert-dialog": "^1.1.3",
+ "@radix-ui/react-checkbox": "^1.1.3",
+ "@radix-ui/react-collapsible": "^1.1.2",
+ "@radix-ui/react-dialog": "^1.1.3",
+ "@radix-ui/react-dropdown-menu": "^2.1.3",
+ "@radix-ui/react-hover-card": "^1.1.3",
+ "@radix-ui/react-label": "^2.1.1",
+ "@radix-ui/react-popover": "^1.1.3",
+ "@radix-ui/react-popper": "^1.2.1",
+ "@radix-ui/react-progress": "^1.1.1",
+ "@radix-ui/react-radio-group": "^1.2.2",
+ "@radix-ui/react-select": "^2.1.3",
+ "@radix-ui/react-switch": "^1.1.2",
+ "@radix-ui/react-tabs": "^1.1.2",
+ "@radix-ui/react-toast": "^1.2.3",
+ "@radix-ui/react-tooltip": "^1.1.5",
+ "@sindresorhus/slugify": "^2.2.1",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-router": "^1.87.9",
+ "@ucast/mongo2js": "^1.3.4",
"argon2-browser": "^1.18.0",
+ "axios": "^1.7.9",
"classnames": "^2.5.1",
+ "cva": "npm:class-variance-authority@^0.7.1",
"date-fns": "^4.1.0",
"file-saver": "^2.0.5",
"framer-motion": "^11.14.1",
diff --git a/frontend-v2/src/components/analytics/posthog.ts b/frontend-v2/src/components/analytics/posthog.ts
new file mode 100644
index 000000000..f9285012e
--- /dev/null
+++ b/frontend-v2/src/components/analytics/posthog.ts
@@ -0,0 +1,26 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+/* eslint-disable no-undef */
+import posthog from "posthog-js";
+
+import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from "../utilities/config";
+
+export const initPostHog = () => {
+ // @ts-ignore
+ console.log("Hi there 👋");
+ try {
+ if (typeof window !== "undefined") {
+ // @ts-ignore
+ if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) {
+ posthog.init(POSTHOG_API_KEY, {
+ api_host: POSTHOG_HOST
+ });
+ }
+ }
+
+ return posthog;
+ } catch (e) {
+ console.log("posthog err", e);
+ }
+
+ return undefined;
+};
diff --git a/frontend-v2/src/components/basic/Error.tsx b/frontend-v2/src/components/basic/Error.tsx
new file mode 100644
index 000000000..182dcadcb
--- /dev/null
+++ b/frontend-v2/src/components/basic/Error.tsx
@@ -0,0 +1,13 @@
+import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+const Error = ({ text }: { text: string }): JSX.Element => {
+ return (
+
+ );
+};
+
+export default Error;
diff --git a/frontend-v2/src/components/basic/InputField.tsx b/frontend-v2/src/components/basic/InputField.tsx
new file mode 100644
index 000000000..6c0103b53
--- /dev/null
+++ b/frontend-v2/src/components/basic/InputField.tsx
@@ -0,0 +1,129 @@
+import { memo, useState } from "react";
+import { faCircle, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import guidGenerator from "../utilities/randomId";
+
+interface InputFieldProps {
+ isStatic?: boolean;
+ label: string;
+ type: string;
+ value: string;
+ placeholder?: string;
+ isRequired: boolean;
+ error?: boolean;
+ text?: string;
+ name?: string;
+ blurred?: boolean;
+ errorText?: string;
+ onChangeHandler: (value: string) => void;
+}
+
+const InputField = ({
+ isRequired,
+ label,
+ onChangeHandler,
+ type,
+ value,
+ autoComplete,
+ blurred,
+ error,
+ errorText,
+ id,
+ name,
+ placeholder,
+ isStatic,
+ text
+}: InputFieldProps & Pick) => {
+ const [passwordVisible, setPasswordVisible] = useState(false);
+
+ if (isStatic === true) {
+ return (
+
+
{label}
+ {text &&
{text}
}
+
onChangeHandler(e.target.value)}
+ type={type}
+ placeholder={placeholder}
+ value={value}
+ required={isRequired}
+ className="text-md w-full min-w-16 rounded-md border border-gray-600 bg-bunker-800 p-2 text-gray-400 outline-none"
+ name={name}
+ readOnly
+ autoComplete={autoComplete}
+ id={id}
+ />
+
+ );
+ }
+ return (
+
+
+
+
onChangeHandler(e.target.value)}
+ type={passwordVisible === false ? type : "text"}
+ placeholder={placeholder}
+ value={value}
+ required={isRequired}
+ className={`${
+ blurred
+ ? "text-bunker-800 focus:text-gray-400 active:text-gray-400 group-hover:text-gray-400"
+ : ""
+ } ${
+ error ? "focus:ring-red/50" : "focus:ring-primary/50"
+ } text-md peer relative w-full min-w-16 rounded-md bg-mineshaft-900 p-2 text-gray-400 outline-none duration-200 focus:ring-4`}
+ name={name}
+ spellCheck="false"
+ autoComplete={autoComplete}
+ id={id}
+ />
+ {label?.includes("Password") && (
+
{
+ setPasswordVisible(!passwordVisible);
+ }}
+ className="absolute mr-3 cursor-pointer self-end text-gray-400"
+ >
+ {passwordVisible ? (
+
+ ) : (
+
+ )}
+
+ )}
+ {blurred && (
+
+
+ {value
+ .split("")
+ .slice(0, 54)
+ .map(() => (
+
+ ))}
+
+ )}
+ {/* {error && (
+
+
+
+ )} */}
+
+ {error &&
{errorText}
}
+
+ );
+};
+
+export default memo(InputField);
diff --git a/frontend-v2/src/components/features/TtlFormLabel.tsx b/frontend-v2/src/components/features/TtlFormLabel.tsx
new file mode 100644
index 000000000..02f162200
--- /dev/null
+++ b/frontend-v2/src/components/features/TtlFormLabel.tsx
@@ -0,0 +1,35 @@
+import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { FormLabel, Tooltip } from "../v2";
+
+// To give users example of possible values of TTL
+export const TtlFormLabel = ({ label }: { label: string }) => (
+
+);
diff --git a/frontend-v2/src/components/features/index.tsx b/frontend-v2/src/components/features/index.tsx
new file mode 100644
index 000000000..f00724b65
--- /dev/null
+++ b/frontend-v2/src/components/features/index.tsx
@@ -0,0 +1 @@
+export { TtlFormLabel } from "./TtlFormLabel";
diff --git a/frontend-v2/src/components/integrations/NoEnvironmentsBanner.tsx b/frontend-v2/src/components/integrations/NoEnvironmentsBanner.tsx
new file mode 100644
index 000000000..3fe51cda8
--- /dev/null
+++ b/frontend-v2/src/components/integrations/NoEnvironmentsBanner.tsx
@@ -0,0 +1,29 @@
+import { useNavigate } from "@tanstack/react-router";
+
+import { Button } from "../v2";
+
+interface IProps {
+ projectId: string;
+}
+
+export const NoEnvironmentsBanner = ({ projectId }: IProps) => {
+ const navigate = useNavigate();
+
+ return (
+
+
+
+ No environments in your project was found
+
+
+ In order to use integrations, you need to create at least one environment in your project.
+
+
+
+ navigate({ to: `/project/${projectId}/settings#environments` })}>
+ Add environments
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/license/UpgradePlanModal/UpgradePlanModal.tsx b/frontend-v2/src/components/license/UpgradePlanModal/UpgradePlanModal.tsx
new file mode 100644
index 000000000..09af0139c
--- /dev/null
+++ b/frontend-v2/src/components/license/UpgradePlanModal/UpgradePlanModal.tsx
@@ -0,0 +1,71 @@
+import { useOrganization, useSubscription } from "@app/context";
+import { useGetOrgTrialUrl } from "@app/hooks/api";
+
+import { Button } from "../../v2/Button";
+import { Modal, ModalContent } from "../../v2/Modal";
+
+type Props = {
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ text: string;
+};
+
+export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
+ const { subscription } = useSubscription();
+ const { currentOrg } = useOrganization();
+ const { mutateAsync, isLoading } = useGetOrgTrialUrl();
+ const link =
+ subscription && subscription.slug !== null
+ ? `/org/${currentOrg?.id}/billing`
+ : "https://infisical.com/scheduledemo";
+
+ const handleUpgradeBtnClick = async () => {
+ try {
+ if (!subscription || !currentOrg) return;
+
+ if (!subscription.has_used_trial) {
+ // direct user to start pro trial
+
+ const url = await mutateAsync({
+ orgId: currentOrg.id,
+ success_url: window.location.href
+ });
+
+ window.location.href = url;
+ } else {
+ // direct user to upgrade their plan
+ window.location.href = link;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ return (
+
+
+ {text}
+
+ Upgrade and get access to this, as well as to other powerful enhancements.
+
+
+
+ {subscription && !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
+
+ onOpenChange && onOpenChange(false)}
+ >
+ Cancel
+
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/license/UpgradePlanModal/index.tsx b/frontend-v2/src/components/license/UpgradePlanModal/index.tsx
new file mode 100644
index 000000000..09a53d31c
--- /dev/null
+++ b/frontend-v2/src/components/license/UpgradePlanModal/index.tsx
@@ -0,0 +1 @@
+export { UpgradePlanModal } from "./UpgradePlanModal";
diff --git a/frontend-v2/src/components/mfa/TotpRegistration.tsx b/frontend-v2/src/components/mfa/TotpRegistration.tsx
new file mode 100644
index 000000000..d9d73748f
--- /dev/null
+++ b/frontend-v2/src/components/mfa/TotpRegistration.tsx
@@ -0,0 +1,80 @@
+import { useEffect, useState } from "react";
+import QRCode from "qrcode";
+import { twMerge } from "tailwind-merge";
+
+import { useGetUserTotpRegistration } from "@app/hooks/api";
+import { useVerifyUserTotpRegistration } from "@app/hooks/api/users/mutation";
+
+import { createNotification } from "../notifications";
+import { Button, ContentLoader, Input } from "../v2";
+
+type Props = {
+ onComplete?: () => Promise;
+ shouldCenterQr?: boolean;
+};
+
+const TotpRegistration = ({ onComplete, shouldCenterQr }: Props) => {
+ const { data: registration, isLoading } = useGetUserTotpRegistration();
+ const { mutateAsync: verifyUserTotp, isLoading: isVerifyLoading } =
+ useVerifyUserTotpRegistration();
+ const [qrCodeUrl, setQrCodeUrl] = useState("");
+ const [totp, setTotp] = useState("");
+
+ const handleTotpVerify = async (event: React.FormEvent) => {
+ event.preventDefault();
+ await verifyUserTotp({
+ totp
+ });
+
+ createNotification({
+ text: "Successfully configured mobile authenticator",
+ type: "success"
+ });
+
+ if (onComplete) {
+ onComplete();
+ }
+ };
+
+ useEffect(() => {
+ const generateQRCode = async () => {
+ if (registration?.otpUrl) {
+ const url = await QRCode.toDataURL(registration.otpUrl);
+ setQrCodeUrl(url);
+ }
+ };
+
+ generateQRCode();
+ }, [registration]);
+
+ if (isLoading) {
+ return ;
+ }
+
+ return (
+
+
+ 1. Download a two-step verification app (Duo, Google Authenticator, etc.) and scan the QR
+ code.
+
+
+
+
+
+
+ );
+};
+
+export default TotpRegistration;
diff --git a/frontend-v2/src/components/navigation/NavHeader.tsx b/frontend-v2/src/components/navigation/NavHeader.tsx
new file mode 100644
index 000000000..7c1341919
--- /dev/null
+++ b/frontend-v2/src/components/navigation/NavHeader.tsx
@@ -0,0 +1,220 @@
+import { ParsedUrlQuery } from "querystring";
+
+import { useState } from "react";
+import { Link, useLocation, useNavigate } from "@tanstack/react-router";
+import { faAngleRight, faCheck, faCopy, faLock } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
+
+import { useOrganization, useWorkspace } from "@app/context";
+import { useToggle } from "@app/hooks";
+
+import { createNotification } from "../notifications";
+import { IconButton, Select, SelectItem, Tooltip } from "../v2";
+
+type Props = {
+ pageName: string;
+ isProjectRelated?: boolean;
+ isOrganizationRelated?: boolean;
+ currentEnv?: string;
+ userAvailableEnvs?: any[];
+ onEnvChange?: (slug: string) => void;
+ secretPath?: string;
+ isFolderMode?: boolean;
+ isProtectedBranch?: boolean;
+ protectionPolicyName?: string;
+};
+
+// TODO: make links clickable and clean up
+
+/**
+ * This is the component at the top of almost every page.
+ * It shows how to navigate to a certain page.
+ * It future these links should also be clickable and hoverable
+ * @param {object} obj
+ * @param {string} obj.pageName - Name of the page
+ * @param {boolean} obj.isProjectRelated - whether or not this page is related to project (determine if it's 2 or 3 navigation steps)
+ * @param {boolean} obj.isOrganizationRelated - whether or not this page is related to organization (determine if it's 2 or 3 navigation steps)
+ * @param {string} obj.currentEnv - current environment inside a project
+ * @param {string} obj.userAvailableEnvs - environments that are available to a user in this project (used for the dropdown)
+ * @param {string} obj.onEnvChange - the action that happens when an env is changed
+ * @returns
+ */
+// TODO(akhilmhdh): simply this header and nav system later
+export default function NavHeader({
+ pageName,
+ isProjectRelated,
+ isOrganizationRelated,
+ currentEnv,
+ userAvailableEnvs = [],
+ onEnvChange,
+ isFolderMode,
+ secretPath = "/",
+ isProtectedBranch = false,
+ protectionPolicyName
+}: Props): JSX.Element {
+ const { currentWorkspace } = useWorkspace();
+ const { currentOrg } = useOrganization();
+
+ const [isCopied, { timedToggle: toggleIsCopied }] = useToggle(false);
+ const [isHoveringCopyButton, setIsHoveringCopyButton] = useState(false);
+
+ const navigate = useNavigate();
+
+ const secretPathSegments = secretPath.split("/").filter(Boolean);
+
+ return (
+
+
+ {currentOrg?.name?.charAt(0)}
+
+
+ {currentOrg?.name}
+
+ {isProjectRelated && (
+ <>
+
+
+ {currentWorkspace?.name}
+
+ >
+ )}
+ {isOrganizationRelated && (
+ <>
+
+
Organization Settings
+ >
+ )}
+
+ {pageName === "Secrets" ? (
+
+
{pageName}
+
+ ) : (
+
{pageName}
+ )}
+ {currentEnv && secretPath === "/" && (
+ <>
+
+
+
+ {
+ if (value && onEnvChange) onEnvChange(value);
+ }}
+ className="border-none bg-transparent pl-0 text-sm font-medium text-primary/80 hover:text-primary"
+ dropdownContainerClassName="text-bunker-200 bg-mineshaft-800 border border-mineshaft-600 drop-shadow-2xl"
+ >
+ {userAvailableEnvs?.map(({ name, slug }) => (
+
+ {name}
+
+ ))}
+
+
+
+ >
+ )}
+ {isFolderMode && Boolean(secretPathSegments.length) && (
+
+ )}
+ {isFolderMode &&
+ secretPathSegments?.map((folderName, index) => {
+ const query: ParsedUrlQuery & { secretPath: string } = {
+ ...navigate.query,
+ secretPath: `/${secretPathSegments.slice(0, index + 1).join("/")}`
+ };
+
+ return (
+
+
+ {index + 1 === secretPathSegments?.length ? (
+
+
+ {folderName}
+
+
+ setIsHoveringCopyButton(true)}
+ onMouseLeave={() => setIsHoveringCopyButton(false)}
+ onClick={() => {
+ if (isCopied) return;
+
+ navigator.clipboard.writeText(query.secretPath);
+
+ createNotification({
+ text: "Copied secret path to clipboard",
+ type: "info"
+ });
+
+ toggleIsCopied(2000);
+ }}
+ className="hover:bg-bunker-100/10"
+ >
+
+
+
+
+ ) : (
+
+
+ {folderName}
+
+
+ )}
+
+ );
+ })}
+ {isProtectedBranch && (
+
+
+
+ )}
+
+ );
+}
diff --git a/frontend-v2/src/components/navigation/RegionSelect.tsx b/frontend-v2/src/components/navigation/RegionSelect.tsx
new file mode 100644
index 000000000..a4a8b80fc
--- /dev/null
+++ b/frontend-v2/src/components/navigation/RegionSelect.tsx
@@ -0,0 +1,148 @@
+import { faCheck } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { Modal, ModalContent, ModalTrigger, Select, SelectItem } from "@app/components/v2";
+import { useLocation, useNavigate } from "@tanstack/react-router";
+
+enum Region {
+ US = "us",
+ EU = "eu"
+}
+
+const regions = [
+ {
+ value: Region.US,
+ label: "United States",
+ location: "Virginia, USA",
+ flag: (
+
+
+
+
+
+
+
+
+
+ )
+ },
+ {
+ value: Region.EU,
+ label: "Europe",
+ location: "Frankfurt, Germany",
+ flag: (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+ }
+];
+
+export const RegionSelect = () => {
+ const navigate = useNavigate();
+ const location = useLocation();
+
+ const handleRegionSelect = (value: Region) => {
+ navigate({ to: `https://${value}.infisical.com/${location.pathname}` });
+ };
+
+ const shouldDisplay =
+ window.location.origin.includes("https://app.infisical.com") ||
+ window.location.origin.includes("https://us.infisical.com") ||
+ window.location.origin.includes("https://eu.infisical.com") ||
+ window.location.origin.includes("http://localhost:8080");
+
+ // only display region select for cloud
+ if (!shouldDisplay) return null;
+
+ const [subdomain] = window.location.host.split(".");
+
+ // default to US if not eu
+ const currentRegion = subdomain === Region.EU ? regions[1] : regions[0];
+
+ return (
+
+
+ {regions.map(({ value, label, flag }) => (
+
+
+
+ ))}
+
+
+
+
+ Help me pick a data region
+
+
+
+ {regions.map(({ value, label, location: regionLocation, flag }) => (
+
+
+ {flag}
+ {value.toUpperCase()} Region
+
+
+
+ Fastest
+ option if you are based in {value === Region.US ? "the" : ""} {label}
+
+
+ Data
+ storage compliance for this region
+
+
+ Hosted
+ in {regionLocation}
+
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/notifications/Notifications.tsx b/frontend-v2/src/components/notifications/Notifications.tsx
new file mode 100644
index 000000000..fdc35b9af
--- /dev/null
+++ b/frontend-v2/src/components/notifications/Notifications.tsx
@@ -0,0 +1,79 @@
+import { ReactNode } from "react";
+import { Id, toast, ToastContainer, ToastOptions, TypeOptions } from "react-toastify";
+import { faCopy, IconDefinition } from "@fortawesome/free-solid-svg-icons";
+import { twMerge } from "tailwind-merge";
+
+import { CopyButton } from "../v2/CopyButton";
+
+export type TNotification = {
+ title?: string;
+ text: ReactNode;
+ children?: ReactNode;
+ callToAction?: ReactNode;
+ copyActions?: { icon?: IconDefinition; value: string; name: string; label?: string }[];
+};
+
+export const NotificationContent = ({
+ title,
+ text,
+ children,
+ callToAction,
+ copyActions
+}: TNotification) => {
+ return (
+
+ {title &&
{title}
}
+
{text}
+ {children &&
{children}
}
+ {(callToAction || copyActions) && (
+
+ {callToAction}
+
+ {copyActions && (
+
+ {copyActions.map((action) => (
+
+ {action.label && (
+ {action.label}
+ )}
+
+
+ ))}
+
+ )}
+
+ )}
+
+ );
+};
+
+export const createNotification = (
+ myProps: TNotification & { type?: TypeOptions },
+ toastProps: ToastOptions = {}
+): Id =>
+ toast( , {
+ position: "bottom-right",
+ ...toastProps,
+ theme: "dark",
+ type: myProps?.type || "info"
+ });
+
+export const NotificationContainer = () => (
+
+);
diff --git a/frontend-v2/src/components/notifications/index.tsx b/frontend-v2/src/components/notifications/index.tsx
new file mode 100644
index 000000000..e752e6d26
--- /dev/null
+++ b/frontend-v2/src/components/notifications/index.tsx
@@ -0,0 +1 @@
+export { createNotification, NotificationContainer } from "./Notifications";
diff --git a/frontend-v2/src/components/permissions/GlobPermissionInfo.tsx b/frontend-v2/src/components/permissions/GlobPermissionInfo.tsx
new file mode 100644
index 000000000..b65b0c307
--- /dev/null
+++ b/frontend-v2/src/components/permissions/GlobPermissionInfo.tsx
@@ -0,0 +1,33 @@
+import { useState } from "react";
+import picomatch from "picomatch";
+
+import { FormControl } from "../v2/FormControl";
+import { Input } from "../v2/Input";
+
+export const GlobPermissionInfo = () => {
+ const [pattern, setPattern] = useState("");
+ const [text, setText] = useState("");
+
+ return (
+
+
A glob pattern uses wildcards to match resources or paths.
+
+
+ setPattern(e.target.value)} />
+
+
+
+
+ setText(e.target.value)} />
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/permissions/OrgPermissionCan.tsx b/frontend-v2/src/components/permissions/OrgPermissionCan.tsx
new file mode 100644
index 000000000..006d43232
--- /dev/null
+++ b/frontend-v2/src/components/permissions/OrgPermissionCan.tsx
@@ -0,0 +1,49 @@
+import { FunctionComponent, ReactNode } from "react";
+import { BoundCanProps, Can } from "@casl/react";
+
+import { TOrgPermission, useOrgPermission } from "@app/context/OrgPermissionContext";
+
+import { Tooltip } from "../v2";
+
+type Props = {
+ label?: ReactNode;
+ // this prop is used when there exist already a tooltip as helper text for users
+ // so when permission is allowed same tooltip will be reused to show helpertext
+ renderTooltip?: boolean;
+ allowedLabel?: string;
+} & BoundCanProps;
+
+export const OrgPermissionCan: FunctionComponent = ({
+ label = "Access restricted",
+ children,
+ passThrough = true,
+ renderTooltip,
+ allowedLabel,
+ ...props
+}) => {
+ const { permission } = useOrgPermission();
+
+ return (
+
+ {(isAllowed, ability) => {
+ // akhilmhdh: This is set as type due to error in casl react type.
+ const finalChild =
+ typeof children === "function"
+ ? children(isAllowed, ability as TOrgPermission)
+ : children;
+
+ if (!isAllowed && passThrough) {
+ return {finalChild} ;
+ }
+
+ if (isAllowed && renderTooltip) {
+ return {finalChild} ;
+ }
+
+ if (!isAllowed) return null;
+
+ return finalChild;
+ }}
+
+ );
+};
diff --git a/frontend-v2/src/components/permissions/PermissionDeniedBanner.tsx b/frontend-v2/src/components/permissions/PermissionDeniedBanner.tsx
new file mode 100644
index 000000000..b3c7a4f53
--- /dev/null
+++ b/frontend-v2/src/components/permissions/PermissionDeniedBanner.tsx
@@ -0,0 +1,38 @@
+import { ReactNode } from "react";
+import { faLock } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
+
+type Props = {
+ containerClassName?: string;
+ className?: string;
+ children?: ReactNode;
+};
+
+export const PermissionDeniedBanner = ({ containerClassName, className, children }: Props) => {
+ return (
+
+
+
+
+
+
+
+
Access Restricted
+ {children || (
+
+ Your role has limited permissions, please contact your administrator to gain
+ access
+
+ )}
+
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/permissions/ProjectPermissionCan.tsx b/frontend-v2/src/components/permissions/ProjectPermissionCan.tsx
new file mode 100644
index 000000000..4fc5acde9
--- /dev/null
+++ b/frontend-v2/src/components/permissions/ProjectPermissionCan.tsx
@@ -0,0 +1,52 @@
+import { FunctionComponent, ReactNode } from "react";
+import { AbilityTuple, MongoAbility } from "@casl/ability";
+import { Can } from "@casl/react";
+
+import { ProjectPermissionSet, useProjectPermission } from "@app/context/ProjectPermissionContext";
+
+import { Tooltip } from "../v2/Tooltip";
+
+type Props = {
+ label?: ReactNode;
+ // this prop is used when there exist already a tooltip as helper text for users
+ // so when permission is allowed same tooltip will be reused to show helpertext
+ renderTooltip?: boolean;
+ allowedLabel?: string;
+ children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode);
+ passThrough?: boolean;
+ I: T[0];
+ a: T[1];
+ ability?: MongoAbility;
+};
+
+export const ProjectPermissionCan: FunctionComponent> = ({
+ label = "Access restricted",
+ children,
+ passThrough = true,
+ renderTooltip,
+ allowedLabel,
+ ...props
+}) => {
+ const { permission } = useProjectPermission();
+ return (
+
+ {(isAllowed, ability) => {
+ // akhilmhdh: This is set as type due to error in casl react type.
+ const finalChild =
+ typeof children === "function" ? children(isAllowed, ability as any) : children;
+
+ if (!isAllowed && passThrough) {
+ return {finalChild} ;
+ }
+
+ if (isAllowed && renderTooltip) {
+ return {finalChild} ;
+ }
+
+ if (!isAllowed) return null;
+
+ return finalChild;
+ }}
+
+ );
+};
diff --git a/frontend-v2/src/components/permissions/index.tsx b/frontend-v2/src/components/permissions/index.tsx
new file mode 100644
index 000000000..5103b0f73
--- /dev/null
+++ b/frontend-v2/src/components/permissions/index.tsx
@@ -0,0 +1,4 @@
+export { GlobPermissionInfo } from "./GlobPermissionInfo";
+export { OrgPermissionCan } from "./OrgPermissionCan";
+export { PermissionDeniedBanner } from "./PermissionDeniedBanner";
+export { ProjectPermissionCan } from "./ProjectPermissionCan";
diff --git a/frontend-v2/src/components/signup/CodeInputStep.tsx b/frontend-v2/src/components/signup/CodeInputStep.tsx
new file mode 100644
index 000000000..09958fafd
--- /dev/null
+++ b/frontend-v2/src/components/signup/CodeInputStep.tsx
@@ -0,0 +1,149 @@
+/* eslint-disable react/jsx-props-no-spreading */
+import { useState } from "react";
+import ReactCodeInput from "react-code-input";
+import { useTranslation } from "react-i18next";
+
+import { useSendVerificationEmail } from "@app/hooks/api";
+
+import Error from "../basic/Error";
+import { Button } from "../v2";
+
+// The style for the verification code input
+const props = {
+ inputStyle: {
+ fontFamily: "monospace",
+ margin: "4px",
+ MozAppearance: "textfield",
+ width: "55px",
+ borderRadius: "5px",
+ fontSize: "24px",
+ height: "55px",
+ paddingLeft: "7",
+ backgroundColor: "#0d1117",
+ color: "white",
+ border: "1px solid #2d2f33",
+ textAlign: "center",
+ outlineColor: "#8ca542",
+ borderColor: "#2d2f33"
+ }
+} as const;
+const propsPhone = {
+ inputStyle: {
+ fontFamily: "monospace",
+ margin: "4px",
+ MozAppearance: "textfield",
+ width: "40px",
+ borderRadius: "5px",
+ fontSize: "24px",
+ height: "40px",
+ paddingLeft: "7",
+ backgroundColor: "#0d1117",
+ color: "white",
+ border: "1px solid #2d2f33",
+ textAlign: "center",
+ outlineColor: "#8ca542",
+ borderColor: "#2d2f33"
+ }
+} as const;
+
+interface CodeInputStepProps {
+ email: string;
+ incrementStep: () => void;
+ setCode: (value: string) => void;
+ codeError: boolean;
+ isCodeInputCheckLoading: boolean;
+}
+
+/**
+ * This is the second step of sign up where users need to verify their email
+ * @param {object} obj
+ * @param {string} obj.email - user's email to which we just sent a verification email
+ * @param {function} obj.incrementStep - goes to the next step of signup
+ * @param {function} obj.setCode - state updating function that set the current value of the emai verification code
+ * @param {boolean} obj.codeError - whether the code was inputted wrong or now
+ * @returns
+ */
+export default function CodeInputStep({
+ email,
+ incrementStep,
+ setCode,
+ codeError,
+ isCodeInputCheckLoading
+}: CodeInputStepProps): JSX.Element {
+ const { mutateAsync } = useSendVerificationEmail();
+ const [isLoading, setIsLoading] = useState(false);
+ const [isResendingVerificationEmail, setIsResendingVerificationEmail] = useState(false);
+ const { t } = useTranslation();
+
+ const resendVerificationEmail = async () => {
+ setIsResendingVerificationEmail(true);
+ setIsLoading(true);
+ await mutateAsync({ email });
+ setTimeout(() => {
+ setIsLoading(false);
+ setIsResendingVerificationEmail(false);
+ }, 2000);
+ };
+
+ return (
+
+
{t("signup.step2-message")}
+
{email}
+
+
+
+
+
+
+ {codeError &&
}
+
+
+
+ {" "}
+ {String(t("signup.verify"))}{" "}
+
+
+
+
+
+
{t("signup.step2-resend-alert")}
+
+
+
+ {isResendingVerificationEmail
+ ? t("signup.step2-resend-progress")
+ : t("signup.step2-resend-submit")}
+
+
+
+
+
{t("signup.step2-spam-alert")}
+
+
+ );
+}
diff --git a/frontend-v2/src/components/signup/DonwloadBackupPDFStep.tsx b/frontend-v2/src/components/signup/DonwloadBackupPDFStep.tsx
new file mode 100644
index 000000000..b11ca8bd2
--- /dev/null
+++ b/frontend-v2/src/components/signup/DonwloadBackupPDFStep.tsx
@@ -0,0 +1,93 @@
+import { useTranslation } from "react-i18next";
+import { faWarning } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { useToggle } from "@app/hooks";
+import { generateUserBackupKey } from "@app/lib/crypto";
+
+import { createNotification } from "../notifications";
+import { generateBackupPDFAsync } from "../utilities/generateBackupPDF";
+import { Button } from "../v2";
+
+interface DownloadBackupPDFStepProps {
+ incrementStep: () => void;
+ email: string;
+ password: string;
+ name: string;
+}
+
+/**
+ * This is the step of the signup flow where the user downloads the backup pdf
+ * @param {object} obj
+ * @param {function} obj.incrementStep - function that moves the user on to the next stage of signup
+ * @param {string} obj.email - user's email
+ * @param {string} obj.password - user's password
+ * @param {string} obj.name - user's name
+ * @returns
+ */
+export default function DonwloadBackupPDFStep({
+ incrementStep,
+ email,
+ password,
+ name
+}: DownloadBackupPDFStepProps): JSX.Element {
+ const { t } = useTranslation();
+
+ const [isLoading, setIsLoading] = useToggle();
+
+ const handleBackupKeyGenerate = async () => {
+ try {
+ setIsLoading.on();
+ const generatedKey = await generateUserBackupKey(email, password);
+ await generateBackupPDFAsync({
+ generatedKey,
+ personalEmail: email,
+ personalName: name
+ });
+ incrementStep();
+ } catch (err) {
+ console.log(err);
+ createNotification({
+ type: "error",
+ text: "Failed to generate backup key"
+ });
+ } finally {
+ setIsLoading.off();
+ }
+ };
+
+ return (
+
+
+
+ {t("signup.step4-message")}
+
+
+
+
+ {t("signup.step4-description1")} {t("signup.step4-description3")}
+
+
+
+
+
+ );
+}
diff --git a/frontend-v2/src/components/signup/EnterEmailStep.tsx b/frontend-v2/src/components/signup/EnterEmailStep.tsx
new file mode 100644
index 000000000..689dad2f8
--- /dev/null
+++ b/frontend-v2/src/components/signup/EnterEmailStep.tsx
@@ -0,0 +1,118 @@
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { Link } from "@tanstack/react-router";
+import axios from "axios";
+
+import { createNotification } from "@app/components/notifications";
+import { useSendVerificationEmail } from "@app/hooks/api";
+
+import { Button, Input } from "../v2";
+
+interface DownloadBackupPDFStepProps {
+ incrementStep: () => void;
+ email: string;
+ setEmail: (value: string) => void;
+}
+
+/**
+ * This is the first step of the sign up process - users need to enter their email
+ * @param {object} obj
+ * @param {string} obj.email - email of a user signing up
+ * @param {function} obj.setEmail - funciton that manages the state of the email variable
+ * @param {function} obj.incrementStep - function to go to the next step of the signup flow
+ * @returns
+ */
+export default function EnterEmailStep({
+ email,
+ setEmail,
+ incrementStep
+}: DownloadBackupPDFStepProps): JSX.Element {
+ const { mutateAsync, isLoading } = useSendVerificationEmail();
+ const [emailError, setEmailError] = useState(false);
+ const { t } = useTranslation();
+
+ /**
+ * Verifies if the entered email "looks" correct
+ */
+ const emailCheck = async () => {
+ let emailCheckBool = false;
+ if (!email) {
+ setEmailError(true);
+ emailCheckBool = true;
+ } else if (!email.includes("@") || !email.includes(".") || !/[a-z]/.test(email)) {
+ setEmailError(true);
+ emailCheckBool = true;
+ } else {
+ setEmailError(false);
+ }
+
+ // If everything is correct, go to the next step
+ if (!emailCheckBool) {
+ try {
+ await mutateAsync({ email: email.toLowerCase() });
+ setEmail(email.toLowerCase());
+ incrementStep();
+ } catch (e) {
+ if (axios.isAxiosError(e)) {
+ const { message = "Something went wrong" } = e.response?.data as { message: string };
+ createNotification({
+ type: "error",
+ text: message
+ });
+ }
+ }
+ }
+ };
+
+ return (
+
+
+
+ {t("signup.step1-start")}
+
+
+
setEmail(e.target.value)}
+ value={email}
+ isRequired
+ autoComplete="username"
+ className="h-12"
+ />
+ {emailError && (
+
+ Please enter a valid email.
+
+ )}
+
+
+
+
+ {" "}
+ {String(t("signup.step1-submit"))}{" "}
+
+
+
+
+
+
+
+
+ {t("signup.already-have-account")}
+
+
+
+
+
+ );
+}
diff --git a/frontend-v2/src/components/signup/InitialSignupStep.tsx b/frontend-v2/src/components/signup/InitialSignupStep.tsx
new file mode 100644
index 000000000..c4b6ce95e
--- /dev/null
+++ b/frontend-v2/src/components/signup/InitialSignupStep.tsx
@@ -0,0 +1,105 @@
+import { useTranslation } from "react-i18next";
+import { Link } from "@tanstack/react-router";
+import { faGithub, faGitlab, faGoogle } from "@fortawesome/free-brands-svg-icons";
+import { faEnvelope } from "@fortawesome/free-regular-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+import { RegionSelect } from "@app/components/navigation/RegionSelect";
+import { useServerConfig } from "@app/context";
+import { LoginMethod } from "@app/hooks/api/admin/types";
+
+import { Button } from "../v2";
+
+export default function InitialSignupStep({
+ setIsSignupWithEmail
+}: {
+ setIsSignupWithEmail: (value: boolean) => void;
+}) {
+ const { t } = useTranslation();
+ const { config } = useServerConfig();
+
+ const shouldDisplaySignupMethod = (method: LoginMethod) =>
+ !config.enabledLoginMethods || config.enabledLoginMethods.includes(method);
+
+ return (
+
+
+ {t("signup.initial-title")}
+
+
+ {shouldDisplaySignupMethod(LoginMethod.GOOGLE) && (
+
+ {
+ window.open("/api/v1/sso/redirect/google");
+ window.close();
+ }}
+ leftIcon={ }
+ className="mx-0 h-12 w-full"
+ >
+ {t("signup.continue-with-google")}
+
+
+ )}
+ {shouldDisplaySignupMethod(LoginMethod.GITHUB) && (
+
+ {
+ window.open("/api/v1/sso/redirect/github");
+ window.close();
+ }}
+ leftIcon={ }
+ className="mx-0 h-12 w-full"
+ >
+ Continue with GitHub
+
+
+ )}
+ {shouldDisplaySignupMethod(LoginMethod.GITLAB) && (
+
+ {
+ window.open("/api/v1/sso/redirect/gitlab");
+ window.close();
+ }}
+ leftIcon={ }
+ className="mx-0 h-12 w-full"
+ >
+ Continue with GitLab
+
+
+ )}
+ {shouldDisplaySignupMethod(LoginMethod.EMAIL) && (
+
+ {
+ setIsSignupWithEmail(true);
+ }}
+ leftIcon={ }
+ className="mx-0 h-12 w-full"
+ >
+ Continue with Email
+
+
+ )}
+
+ {t("signup.create-policy")}
+
+
+
+
+ {t("signup.already-have-account")}
+
+
+
+
+ );
+}
diff --git a/frontend-v2/src/components/signup/TeamInviteStep.tsx b/frontend-v2/src/components/signup/TeamInviteStep.tsx
new file mode 100644
index 000000000..60276d217
--- /dev/null
+++ b/frontend-v2/src/components/signup/TeamInviteStep.tsx
@@ -0,0 +1,102 @@
+import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useRouter } from "next/router";
+
+import { useAddUsersToOrg } from "@app/hooks/api";
+import { useFetchServerStatus } from "@app/hooks/api/serverDetails";
+import { usePopUp } from "@app/hooks/usePopUp";
+
+import { Button, EmailServiceSetupModal } from "../v2";
+
+/**
+ * This is the last step of the signup flow. People can optionally invite their teammates here.
+ */
+export default function TeamInviteStep(): JSX.Element {
+ const { t } = useTranslation();
+ const router = useRouter();
+ const [emails, setEmails] = useState("");
+ const { data: serverDetails } = useFetchServerStatus();
+
+ const { mutateAsync } = useAddUsersToOrg();
+ const { handlePopUpToggle, popUp, handlePopUpOpen } = usePopUp(["setUpEmail"] as const);
+
+ // Redirect user to the getting started page
+ const redirectToHome = async () => {
+ router.push(`/org/${localStorage.getItem("orgData.id")}/overview`);
+ };
+
+ const inviteUsers = async ({ emails: inviteEmails }: { emails: string }) => {
+ inviteEmails
+ .split(",")
+ .map((email) => email.trim())
+ .map(async (email) => {
+ mutateAsync({
+ inviteeEmails: [email],
+ organizationId: String(localStorage.getItem("orgData.id")),
+ organizationRoleSlug: "member"
+ });
+ });
+
+ await redirectToHome();
+ };
+
+ return (
+
+
+ {t("signup.step5-invite-team")}
+
+
+ {t("signup.step5-subtitle")}
+
+
+
+
+ {
+ if (serverDetails?.emailConfigured) {
+ inviteUsers({ emails });
+ } else {
+ handlePopUpOpen("setUpEmail");
+ }
+ }}
+ size="sm"
+ // isFullWidth
+ className="h-10"
+ colorSchema="primary"
+ variant="solid"
+ >
+ {" "}
+ {t("signup.step5-send-invites") ?? ""}{" "}
+
+
+
handlePopUpToggle("setUpEmail", isOpen)}
+ />
+
+
+
+ {" "}
+ {t("signup.step5-skip") ?? "Skip"}{" "}
+
+
+
+ );
+}
diff --git a/frontend-v2/src/components/signup/UserInfoStep.tsx b/frontend-v2/src/components/signup/UserInfoStep.tsx
new file mode 100644
index 000000000..23d574c71
--- /dev/null
+++ b/frontend-v2/src/components/signup/UserInfoStep.tsx
@@ -0,0 +1,336 @@
+import crypto from "crypto";
+
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { faXmark } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import jsrp from "jsrp";
+import nacl from "tweetnacl";
+import { encodeBase64 } from "tweetnacl-util";
+
+import { completeAccountSignup, useSelectOrganization } from "@app/hooks/api/auth/queries";
+import { fetchOrganizations } from "@app/hooks/api/organization/queries";
+import ProjectService from "@app/services/ProjectService";
+
+import InputField from "../basic/InputField";
+import checkPassword from "../utilities/checks/password/checkPassword";
+import Aes256Gcm from "../utilities/cryptography/aes-256-gcm";
+import { deriveArgonKey } from "../utilities/cryptography/crypto";
+import { saveTokenToLocalStorage } from "../utilities/saveTokenToLocalStorage";
+import SecurityClient from "../utilities/SecurityClient";
+import { Button, Input } from "../v2";
+
+// eslint-disable-next-line new-cap
+const client = new jsrp.client();
+
+interface UserInfoStepProps {
+ incrementStep: () => void;
+ email: string;
+ password: string;
+ setPassword: (value: string) => void;
+ name: string;
+ setName: (value: string) => void;
+ organizationName: string;
+ setOrganizationName: (value: string) => void;
+ attributionSource: string;
+ setAttributionSource: (value: string) => void;
+ providerAuthToken?: string;
+}
+
+type Errors = {
+ tooShort?: string;
+ tooLong?: string;
+ noLetterChar?: string;
+ noNumOrSpecialChar?: string;
+ repeatedChar?: string;
+ escapeChar?: string;
+ lowEntropy?: string;
+ breached?: string;
+};
+
+/**
+ * This is the step of the sign up flow where people provife their name/surname and password
+ * @param {object} obj
+ * @param {string} obj.verificationToken - the token which we use to verify the legitness of a user
+ * @param {string} obj.incrementStep - a function to move to the next signup step
+ * @param {string} obj.email - email of a user who is signing up
+ * @param {string} obj.password - user's password
+ * @param {string} obj.setPassword - function managing the state of user's password
+ * @param {string} obj.firstName - user's first name
+ * @param {string} obj.setFirstName - function managing the state of user's first name
+ * @param {string} obj.lastName - user's lastName
+ * @param {string} obj.setLastName - function managing the state of user's last name
+ */
+export default function UserInfoStep({
+ incrementStep,
+ email,
+ password,
+ setPassword,
+ name,
+ setName,
+ organizationName,
+ setOrganizationName,
+ attributionSource,
+ setAttributionSource,
+ providerAuthToken
+}: UserInfoStepProps): JSX.Element {
+ const [nameError, setNameError] = useState(false);
+ const [organizationNameError, setOrganizationNameError] = useState(false);
+
+ const [errors, setErrors] = useState({});
+
+ const { mutateAsync: selectOrganization } = useSelectOrganization();
+ const [isLoading, setIsLoading] = useState(false);
+ const { t } = useTranslation();
+
+ // Verifies if the information that the users entered (name, workspace)
+ // is there, and if the password matches the criteria.
+ const signupErrorCheck = async () => {
+ setIsLoading(true);
+ let errorCheck = false;
+ if (!name) {
+ setNameError(true);
+ errorCheck = true;
+ } else {
+ setNameError(false);
+ }
+ if (!organizationName) {
+ setOrganizationNameError(true);
+ errorCheck = true;
+ } else {
+ setOrganizationNameError(false);
+ }
+
+ errorCheck = await checkPassword({
+ password,
+ setErrors
+ });
+
+ if (!errorCheck) {
+ // Generate a random pair of a public and a private key
+ const pair = nacl.box.keyPair();
+ const secretKeyUint8Array = pair.secretKey;
+ const publicKeyUint8Array = pair.publicKey;
+ const privateKey = encodeBase64(secretKeyUint8Array);
+ const publicKey = encodeBase64(publicKeyUint8Array);
+ localStorage.setItem("PRIVATE_KEY", privateKey);
+
+ client.init(
+ {
+ username: email,
+ password
+ },
+ async () => {
+ client.createVerifier(async (err: any, result: { salt: string; verifier: string }) => {
+ try {
+ // TODO: moduralize into KeyService
+ const derivedKey = await deriveArgonKey({
+ password,
+ salt: result.salt,
+ mem: 65536,
+ time: 3,
+ parallelism: 1,
+ hashLen: 32
+ });
+
+ if (!derivedKey) throw new Error("Failed to derive key from password");
+
+ const key = crypto.randomBytes(32);
+
+ // create encrypted private key by encrypting the private
+ // key with the symmetric key [key]
+ const {
+ ciphertext: encryptedPrivateKey,
+ iv: encryptedPrivateKeyIV,
+ tag: encryptedPrivateKeyTag
+ } = Aes256Gcm.encrypt({
+ text: privateKey,
+ secret: key
+ });
+
+ // create the protected key by encrypting the symmetric key
+ // [key] with the derived key
+ const {
+ ciphertext: protectedKey,
+ iv: protectedKeyIV,
+ tag: protectedKeyTag
+ } = Aes256Gcm.encrypt({
+ text: key.toString("hex"),
+ secret: Buffer.from(derivedKey.hash)
+ });
+
+ const response = await completeAccountSignup({
+ email,
+ password,
+ firstName: name.split(" ")[0],
+ lastName: name.split(" ").slice(1).join(" "),
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ publicKey,
+ encryptedPrivateKey,
+ encryptedPrivateKeyIV,
+ encryptedPrivateKeyTag,
+ providerAuthToken,
+ salt: result.salt,
+ verifier: result.verifier,
+ organizationName,
+ attributionSource
+ });
+
+ // unset signup JWT token and set JWT token
+ SecurityClient.setSignupToken("");
+ SecurityClient.setToken(response.token);
+ SecurityClient.setProviderAuthToken("");
+
+ if (response.organizationId) {
+ await selectOrganization({ organizationId: response.organizationId });
+ }
+
+ saveTokenToLocalStorage({
+ publicKey,
+ encryptedPrivateKey,
+ iv: encryptedPrivateKeyIV,
+ tag: encryptedPrivateKeyTag,
+ privateKey
+ });
+
+ const userOrgs = await fetchOrganizations();
+
+ const orgId = userOrgs[0]?.id;
+ const project = await ProjectService.initProject({
+ projectName: "Example Project"
+ });
+
+ localStorage.setItem("orgData.id", orgId);
+ localStorage.setItem("projectData.id", project.id);
+
+ incrementStep();
+ } catch (error) {
+ setIsLoading(false);
+ console.error(error);
+ }
+ });
+ }
+ );
+ } else {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+
+ {t("signup.step3-message")}
+
+
+
+
+ Your Name
+
+
setName(e.target.value)}
+ value={name}
+ isRequired
+ autoComplete="given-name"
+ className="h-12"
+ />
+ {nameError && (
+
+ Please, specify your name
+
+ )}
+
+
+
+ Organization Name
+
+
setOrganizationName(e.target.value)}
+ value={organizationName}
+ maxLength={64}
+ isRequired
+ className="h-12"
+ />
+ {organizationNameError && (
+
+ Please, specify your organization name
+
+ )}
+
+
+
+ Where did you hear about us? (optional)
+
+
setAttributionSource(e.target.value)}
+ value={attributionSource}
+ className="h-12"
+ />
+
+
+
{
+ setPassword(pass);
+ await checkPassword({
+ password: pass,
+ setErrors
+ });
+ }}
+ type="password"
+ value={password}
+ isRequired
+ error={Object.keys(errors).length > 0}
+ autoComplete="new-password"
+ id="new-password"
+ />
+ {Object.keys(errors).length > 0 && (
+
+
+ {t("section.password.validate-base")}
+
+ {Object.keys(errors).map((key) => {
+ if (errors[key as keyof Errors]) {
+ return (
+
+
+
+
+
{errors[key as keyof Errors]}
+
+ );
+ }
+
+ return null;
+ })}
+
+ )}
+
+
+
+
+ {" "}
+ {String(t("signup.signup"))}{" "}
+
+
+
+
+
+ );
+}
diff --git a/frontend-v2/src/components/tags/CreateTagModal/CreateTagModal.tsx b/frontend-v2/src/components/tags/CreateTagModal/CreateTagModal.tsx
new file mode 100644
index 000000000..b1773b2e3
--- /dev/null
+++ b/frontend-v2/src/components/tags/CreateTagModal/CreateTagModal.tsx
@@ -0,0 +1,269 @@
+import { useEffect, useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { faCheck } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import {
+ Button,
+ FormControl,
+ Input,
+ Modal,
+ ModalClose,
+ ModalContent,
+ Tooltip
+} from "@app/components/v2";
+import { useWorkspace } from "@app/context";
+import { useCreateWsTag } from "@app/hooks/api";
+import { slugSchema } from "@app/lib/schemas";
+
+export const secretTagsColors = [
+ {
+ id: 1,
+ hex: "#bec2c8",
+ rgba: "rgb(128,128,128, 0.8)",
+ name: "Grey"
+ },
+ {
+ id: 2,
+ hex: "#95a2b3",
+ rgba: "rgb(0,0,255, 0.8)",
+ name: "blue"
+ },
+ {
+ id: 3,
+ hex: "#5e6ad2",
+ rgba: "rgb(128,0,128, 0.8)",
+ name: "Purple"
+ },
+ {
+ id: 4,
+ hex: "#26b5ce",
+ rgba: "rgb(0,128,128, 0.8)",
+ name: "Teal"
+ },
+ {
+ id: 5,
+ hex: "#4cb782",
+ rgba: "rgb(0,128,0, 0.8)",
+ name: "Green"
+ },
+ {
+ id: 6,
+ hex: "#f2c94c",
+ rgba: "rgb(255,255,0, 0.8)",
+ name: "Yellow"
+ },
+ {
+ id: 7,
+ hex: "#f2994a",
+ rgba: "rgb(128,128,0, 0.8)",
+ name: "Orange"
+ },
+ {
+ id: 8,
+ hex: "#f7c8c1",
+ rgba: "rgb(128,0,0, 0.8)",
+ name: "Pink"
+ },
+ {
+ id: 9,
+ hex: "#eb5757",
+ rgba: "rgb(255,0,0, 0.8)",
+ name: "Red"
+ }
+];
+
+const isValidHexColor = (hexColor: string) => {
+ const hexColorPattern = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
+
+ return hexColorPattern.test(hexColor);
+};
+
+type Props = {
+ isOpen?: boolean;
+ onToggle: (isOpen: boolean) => void;
+};
+
+const createTagSchema = z.object({
+ slug: slugSchema({ min: 1, field: "Tag Slug" }),
+ color: z.string().trim()
+});
+
+type FormData = z.infer;
+type TagColor = {
+ id: number;
+ hex: string;
+ rgba: string;
+ name: string;
+};
+
+export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
+ const {
+ control,
+ reset,
+ watch,
+ setValue,
+ formState: { isSubmitting },
+ handleSubmit
+ } = useForm({
+ resolver: zodResolver(createTagSchema),
+ defaultValues: {
+ color: secretTagsColors[0].hex
+ }
+ });
+
+ const { currentWorkspace } = useWorkspace();
+ const workspaceId = currentWorkspace?.id || "";
+
+ const { mutateAsync: createWsTag } = useCreateWsTag();
+
+ const [showHexInput, setShowHexInput] = useState(false);
+ const selectedTagColor = watch("color", secretTagsColors[0].hex);
+
+ useEffect(() => {
+ if (!isOpen) reset();
+ }, [isOpen]);
+
+ const onFormSubmit = async ({ slug, color }: FormData) => {
+ try {
+ await createWsTag({
+ workspaceID: workspaceId,
+ tagColor: color,
+ tagSlug: slug
+ });
+ onToggle(false);
+ reset();
+ createNotification({
+ text: "Successfully created a tag",
+ type: "success"
+ });
+ } catch (error) {
+ console.error(error);
+ createNotification({
+ text: "Failed to create a tag",
+ type: "error"
+ });
+ }
+ };
+
+ return (
+
+
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/tags/CreateTagModal/index.tsx b/frontend-v2/src/components/tags/CreateTagModal/index.tsx
new file mode 100644
index 000000000..e9d283b3c
--- /dev/null
+++ b/frontend-v2/src/components/tags/CreateTagModal/index.tsx
@@ -0,0 +1 @@
+export { CreateTagModal } from "./CreateTagModal";
diff --git a/frontend-v2/src/components/utilities/SecurityClient.ts b/frontend-v2/src/components/utilities/SecurityClient.ts
new file mode 100644
index 000000000..f4011c2f0
--- /dev/null
+++ b/frontend-v2/src/components/utilities/SecurityClient.ts
@@ -0,0 +1,38 @@
+import { getAuthToken, setAuthToken, setMfaTempToken, setSignupTempToken } from "@app/reactQuery";
+
+export const PROVIDER_AUTH_TOKEN_KEY = "infisical__provider-auth-token";
+
+// depreciated: go for apiRequest module in config/api
+export default class SecurityClient {
+ static setProviderAuthToken(tokenStr: string) {
+ localStorage.setItem(PROVIDER_AUTH_TOKEN_KEY, tokenStr || "");
+ }
+
+ static getProviderAuthToken() {
+ return localStorage.getItem(PROVIDER_AUTH_TOKEN_KEY);
+ }
+
+ static setSignupToken(tokenStr: string) {
+ setSignupTempToken(tokenStr);
+ }
+
+ static setMfaToken(tokenStr: string) {
+ setMfaTempToken(tokenStr);
+ }
+
+ static setToken(tokenStr: string) {
+ setAuthToken(tokenStr);
+ }
+
+ static async fetchCall(resource: RequestInfo, options?: RequestInit | undefined) {
+ const req = new Request(resource, options);
+
+ const token = getAuthToken();
+
+ if (token) {
+ req.headers.set("Authorization", `Bearer ${token}`);
+ }
+
+ return fetch(req);
+ }
+}
diff --git a/frontend-v2/src/components/utilities/attemptChangePassword.ts b/frontend-v2/src/components/utilities/attemptChangePassword.ts
new file mode 100644
index 000000000..45281f2f2
--- /dev/null
+++ b/frontend-v2/src/components/utilities/attemptChangePassword.ts
@@ -0,0 +1,108 @@
+/* eslint-disable new-cap */
+import crypto from "crypto";
+
+import jsrp from "jsrp";
+
+import { changePassword, srp1 } from "@app/hooks/api/auth/queries";
+
+import Aes256Gcm from "./cryptography/aes-256-gcm";
+import { deriveArgonKey } from "./cryptography/crypto";
+import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
+
+const clientOldPassword = new jsrp.client();
+const clientNewPassword = new jsrp.client();
+
+type Params = {
+ email: string;
+ currentPassword: string;
+ newPassword: string;
+};
+
+const attemptChangePassword = ({ email, currentPassword, newPassword }: Params): Promise => {
+ return new Promise((resolve, reject) => {
+ clientOldPassword.init({ username: email, password: currentPassword }, async () => {
+ let serverPublicKey;
+ let salt;
+
+ try {
+ const clientPublicKey = clientOldPassword.getPublicKey();
+
+ const res = await srp1({ clientPublicKey });
+
+ serverPublicKey = res.serverPublicKey;
+ salt = res.salt;
+
+ clientOldPassword.setSalt(salt);
+ clientOldPassword.setServerPublicKey(serverPublicKey);
+
+ const clientProof = clientOldPassword.getProof();
+
+ clientNewPassword.init({ username: email, password: newPassword }, async () => {
+ clientNewPassword.createVerifier(async (err, result) => {
+ try {
+ const derivedKey = await deriveArgonKey({
+ password: newPassword,
+ salt: result.salt,
+ mem: 65536,
+ time: 3,
+ parallelism: 1,
+ hashLen: 32
+ });
+
+ if (!derivedKey) throw new Error("Failed to derive key from password");
+
+ const key = crypto.randomBytes(32);
+
+ const {
+ ciphertext: encryptedPrivateKey,
+ iv: encryptedPrivateKeyIV,
+ tag: encryptedPrivateKeyTag
+ } = Aes256Gcm.encrypt({
+ text: localStorage.getItem("PRIVATE_KEY") as string,
+ secret: key
+ });
+
+ const {
+ ciphertext: protectedKey,
+ iv: protectedKeyIV,
+ tag: protectedKeyTag
+ } = Aes256Gcm.encrypt({
+ text: key.toString("hex"),
+ secret: Buffer.from(derivedKey.hash)
+ });
+
+ await changePassword({
+ password: newPassword,
+ clientProof,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ encryptedPrivateKey,
+ encryptedPrivateKeyIV,
+ encryptedPrivateKeyTag,
+ salt: result.salt,
+ verifier: result.verifier
+ });
+
+ saveTokenToLocalStorage({
+ encryptedPrivateKey,
+ iv: encryptedPrivateKeyIV,
+ tag: encryptedPrivateKeyTag
+ });
+
+ resolve();
+ } catch (err2) {
+ console.error(err2);
+ reject(err2);
+ }
+ });
+ });
+ } catch (err) {
+ console.error(err);
+ reject(err);
+ }
+ });
+ });
+};
+
+export default attemptChangePassword;
diff --git a/frontend-v2/src/components/utilities/attemptCliLogin.ts b/frontend-v2/src/components/utilities/attemptCliLogin.ts
new file mode 100644
index 000000000..6eba08d61
--- /dev/null
+++ b/frontend-v2/src/components/utilities/attemptCliLogin.ts
@@ -0,0 +1,147 @@
+/* eslint-disable prefer-destructuring */
+import jsrp from "jsrp";
+
+import { login1, login2 } from "@app/hooks/api/auth/queries";
+import KeyService from "@app/services/KeyService";
+
+import Telemetry from "./telemetry/Telemetry";
+import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
+import SecurityClient from "./SecurityClient";
+
+// eslint-disable-next-line new-cap
+const client = new jsrp.client();
+
+export interface IsCliLoginSuccessful {
+ mfaEnabled: boolean;
+ loginResponse?: {
+ email: string;
+ privateKey: string;
+ JTWToken: string;
+ };
+ success: boolean;
+}
+
+/**
+ * Return whether or not login is successful for user with email [email]
+ * and password [password]
+ * @param {string} email - email of user to log in
+ * @param {string} password - password of user to log in
+ */
+const attemptLogin = async ({
+ email,
+ password,
+ providerAuthToken,
+ captchaToken
+}: {
+ email: string;
+ password: string;
+ providerAuthToken?: string;
+ captchaToken?: string;
+}): Promise => {
+ const telemetry = new Telemetry().getInstance();
+ return new Promise((resolve, reject) => {
+ client.init(
+ {
+ username: email,
+ password
+ },
+ async () => {
+ try {
+ const clientPublicKey = client.getPublicKey();
+ const { serverPublicKey, salt } = await login1({
+ email,
+ clientPublicKey,
+ providerAuthToken
+ });
+
+ client.setSalt(salt);
+ client.setServerPublicKey(serverPublicKey);
+ const clientProof = client.getProof(); // called M1
+
+ const {
+ mfaEnabled,
+ encryptionVersion,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ token,
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag
+ } = await login2({
+ email,
+ password,
+ clientProof,
+ providerAuthToken,
+ captchaToken
+ });
+ if (mfaEnabled) {
+ // case: MFA is enabled
+
+ // set temporary (MFA) JWT token
+ SecurityClient.setMfaToken(token);
+
+ resolve({
+ mfaEnabled,
+ success: true
+ });
+ } else if (
+ !mfaEnabled &&
+ encryptionVersion &&
+ encryptedPrivateKey &&
+ iv &&
+ tag &&
+ token
+ ) {
+ // case: MFA is not enabled
+
+ // unset provider auth token in case it was used
+ SecurityClient.setProviderAuthToken("");
+ // set JWT token
+ SecurityClient.setToken(token);
+
+ const privateKey = await KeyService.decryptPrivateKey({
+ encryptionVersion,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ password,
+ salt,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag
+ });
+
+ saveTokenToLocalStorage({
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ privateKey
+ });
+
+ if (email) {
+ telemetry.identify(email, email);
+ telemetry.capture("User Logged In");
+ }
+
+ resolve({
+ mfaEnabled: false,
+ loginResponse: {
+ email,
+ privateKey,
+ JTWToken: token
+ },
+ success: true
+ });
+ }
+ } catch (err) {
+ reject(err);
+ }
+ }
+ );
+ });
+};
+
+export default attemptLogin;
diff --git a/frontend-v2/src/components/utilities/attemptCliLoginMfa.ts b/frontend-v2/src/components/utilities/attemptCliLoginMfa.ts
new file mode 100644
index 000000000..b2fd19093
--- /dev/null
+++ b/frontend-v2/src/components/utilities/attemptCliLoginMfa.ts
@@ -0,0 +1,109 @@
+/* eslint-disable prefer-destructuring */
+import jsrp from "jsrp";
+
+import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
+import KeyService from "@app/services/KeyService";
+
+import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
+import SecurityClient from "./SecurityClient";
+
+// eslint-disable-next-line new-cap
+const client = new jsrp.client();
+
+interface IsMfaLoginSuccessful {
+ success: boolean;
+ loginResponse: {
+ privateKey: string;
+ JTWToken: string;
+ };
+}
+
+/**
+ * Return whether or not MFA-login is successful for user with email [email]
+ * and MFA token [mfaToken]
+ * @param {Object} obj
+ * @param {String} obj.email - email of user
+ * @param {String} obj.mfaToken - MFA code/token
+ */
+const attemptLoginMfa = async ({
+ email,
+ password,
+ providerAuthToken,
+ mfaToken
+}: {
+ email: string;
+ password: string;
+ providerAuthToken?: string;
+ mfaToken: string;
+}): Promise => {
+ return new Promise((resolve, reject) => {
+ client.init(
+ {
+ username: email,
+ password
+ },
+ async () => {
+ try {
+ const clientPublicKey = client.getPublicKey();
+ const { salt } = await login1({
+ email,
+ clientPublicKey,
+ providerAuthToken
+ });
+
+ const {
+ encryptionVersion,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ token,
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag
+ } = await verifyMfaToken({
+ email,
+ mfaCode: mfaToken
+ });
+
+ // unset temporary (MFA) JWT token and set JWT token
+ SecurityClient.setMfaToken("");
+ SecurityClient.setToken(token);
+ SecurityClient.setProviderAuthToken("");
+
+ const privateKey = await KeyService.decryptPrivateKey({
+ encryptionVersion,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ password,
+ salt,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag
+ });
+
+ saveTokenToLocalStorage({
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ privateKey
+ });
+
+ resolve({
+ success: true,
+ loginResponse: {
+ privateKey,
+ JTWToken: token
+ }
+ });
+ } catch (err) {
+ reject(err);
+ }
+ }
+ );
+ });
+};
+
+export default attemptLoginMfa;
diff --git a/frontend-v2/src/components/utilities/attemptLogin.ts b/frontend-v2/src/components/utilities/attemptLogin.ts
new file mode 100644
index 000000000..120ae62ec
--- /dev/null
+++ b/frontend-v2/src/components/utilities/attemptLogin.ts
@@ -0,0 +1,122 @@
+/* eslint-disable prefer-destructuring */
+import jsrp from "jsrp";
+
+import { login1, login2 } from "@app/hooks/api/auth/queries";
+import KeyService from "@app/services/KeyService";
+
+import Telemetry from "./telemetry/Telemetry";
+import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
+import SecurityClient from "./SecurityClient";
+
+interface IsLoginSuccessful {
+ mfaEnabled: boolean;
+ success: boolean;
+}
+
+/**
+ * Return whether or not login is successful for user with email [email]
+ * and password [password]
+ * @param {string} email - email of user to log in
+ * @param {string} password - password of user to log in
+ */
+const attemptLogin = async ({
+ email,
+ password,
+ providerAuthToken,
+ captchaToken
+}: {
+ email: string;
+ password: string;
+ providerAuthToken?: string;
+ captchaToken?: string;
+}): Promise => {
+ const telemetry = new Telemetry().getInstance();
+ // eslint-disable-next-line new-cap
+ const client = new jsrp.client();
+ await new Promise((resolve) => {
+ client.init({ username: email, password }, () => resolve(null));
+ });
+ const clientPublicKey = client.getPublicKey();
+
+ const { serverPublicKey, salt } = await login1({
+ email,
+ clientPublicKey,
+ providerAuthToken
+ });
+
+ client.setSalt(salt);
+ client.setServerPublicKey(serverPublicKey);
+ const clientProof = client.getProof(); // called M1
+
+ const {
+ mfaEnabled,
+ encryptionVersion,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ token,
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag
+ } = await login2({
+ captchaToken,
+ email,
+ password,
+ clientProof,
+ providerAuthToken
+ });
+
+ if (mfaEnabled) {
+ // case: MFA is enabled
+
+ // set temporary (MFA) JWT token
+ SecurityClient.setMfaToken(token);
+
+ return {
+ mfaEnabled,
+ success: true
+ };
+ }
+ if (!mfaEnabled && encryptionVersion && encryptedPrivateKey && iv && tag && token) {
+ // case: MFA is not enabled
+
+ // unset provider auth token in case it was used
+ SecurityClient.setProviderAuthToken("");
+ // set JWT token
+ SecurityClient.setToken(token);
+
+ const privateKey = await KeyService.decryptPrivateKey({
+ encryptionVersion,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ password,
+ salt,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag
+ });
+
+ saveTokenToLocalStorage({
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ privateKey
+ });
+
+ if (email) {
+ telemetry.identify(email, email);
+ telemetry.capture("User Logged In");
+ }
+
+ return {
+ mfaEnabled: false,
+ success: true
+ };
+ }
+ return { success: false, mfaEnabled: false };
+};
+
+export default attemptLogin;
diff --git a/frontend-v2/src/components/utilities/attemptLoginMfa.ts b/frontend-v2/src/components/utilities/attemptLoginMfa.ts
new file mode 100644
index 000000000..672cecea5
--- /dev/null
+++ b/frontend-v2/src/components/utilities/attemptLoginMfa.ts
@@ -0,0 +1,95 @@
+/* eslint-disable prefer-destructuring */
+import jsrp from "jsrp";
+
+import { login1, verifyMfaToken } from "@app/hooks/api/auth/queries";
+import KeyService from "@app/services/KeyService";
+
+import { saveTokenToLocalStorage } from "./saveTokenToLocalStorage";
+import SecurityClient from "./SecurityClient";
+
+// eslint-disable-next-line new-cap
+const client = new jsrp.client();
+
+/**
+ * Return whether or not MFA-login is successful for user with email [email]
+ * and MFA token [mfaToken]
+ * @param {Object} obj
+ * @param {String} obj.email - email of user
+ * @param {String} obj.mfaToken - MFA code/token
+ */
+const attemptLoginMfa = async ({
+ email,
+ password,
+ providerAuthToken,
+ mfaToken
+}: {
+ email: string;
+ password: string;
+ providerAuthToken?: string;
+ mfaToken: string;
+}): Promise => {
+ return new Promise((resolve, reject) => {
+ client.init(
+ {
+ username: email,
+ password
+ },
+ async () => {
+ try {
+ const clientPublicKey = client.getPublicKey();
+ const { salt } = await login1({
+ email,
+ clientPublicKey,
+ providerAuthToken
+ });
+
+ const {
+ encryptionVersion,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ token,
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag
+ } = await verifyMfaToken({
+ email,
+ mfaCode: mfaToken
+ });
+
+ // unset temporary (MFA) JWT token and set JWT token
+ SecurityClient.setMfaToken("");
+ SecurityClient.setToken(token);
+ SecurityClient.setProviderAuthToken("");
+
+ const privateKey = await KeyService.decryptPrivateKey({
+ encryptionVersion,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ password,
+ salt,
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag
+ });
+
+ saveTokenToLocalStorage({
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ privateKey
+ });
+
+ resolve(true);
+ } catch (err) {
+ reject(err);
+ }
+ }
+ );
+ });
+};
+
+export default attemptLoginMfa;
diff --git a/frontend-v2/src/components/utilities/checks/OnboardingCheck.ts b/frontend-v2/src/components/utilities/checks/OnboardingCheck.ts
new file mode 100644
index 000000000..b3abc9b85
--- /dev/null
+++ b/frontend-v2/src/components/utilities/checks/OnboardingCheck.ts
@@ -0,0 +1,53 @@
+import { fetchOrgUsers, fetchUserAction } from "@app/hooks/api/users/queries";
+
+interface OnboardingCheckProps {
+ orgId: string;
+ setTotalOnboardingActionsDone?: (value: number) => void;
+ setHasUserClickedSlack?: (value: boolean) => void;
+ setHasUserClickedIntro?: (value: boolean) => void;
+ setHasUserPushedSecrets?: (value: boolean) => void;
+ setUsersInOrg?: (value: boolean) => void;
+}
+
+/**
+ * This function checks which onboarding steps a user has already finished.
+ */
+const onboardingCheck = async ({
+ orgId,
+ setTotalOnboardingActionsDone,
+ setHasUserClickedSlack,
+ setHasUserClickedIntro,
+ setHasUserPushedSecrets,
+ setUsersInOrg
+}: OnboardingCheckProps) => {
+ let countActions = 0;
+ const userActionSlack = await fetchUserAction("slack_cta_clicked");
+
+ if (userActionSlack) {
+ countActions += 1;
+ }
+ if (setHasUserClickedSlack) setHasUserClickedSlack(!!userActionSlack);
+
+ const userActionSecrets = await fetchUserAction("first_time_secrets_pushed");
+
+ if (userActionSecrets) {
+ countActions += 1;
+ }
+ if (setHasUserPushedSecrets) setHasUserPushedSecrets(!!userActionSecrets);
+
+ const userActionIntro = await fetchUserAction("intro_cta_clicked");
+ if (userActionIntro) {
+ countActions += 1;
+ }
+ if (setHasUserClickedIntro) setHasUserClickedIntro(!!userActionIntro);
+
+ const orgUsers = await fetchOrgUsers(orgId || "");
+
+ if (orgUsers.length > 1) {
+ countActions += 1;
+ }
+ if (setUsersInOrg) setUsersInOrg(orgUsers.length > 1);
+ if (setTotalOnboardingActionsDone) setTotalOnboardingActionsDone(countActions);
+};
+
+export default onboardingCheck;
diff --git a/frontend-v2/src/components/utilities/checks/password/PasswordCheck.ts b/frontend-v2/src/components/utilities/checks/password/PasswordCheck.ts
new file mode 100644
index 000000000..e37abd475
--- /dev/null
+++ b/frontend-v2/src/components/utilities/checks/password/PasswordCheck.ts
@@ -0,0 +1,93 @@
+import { checkIsPasswordBreached } from "./checkIsPasswordBreached";
+import {
+ escapeCharRegex,
+ letterCharRegex,
+ lowEntropyRegexes,
+ numAndSpecialCharRegex,
+ repeatedCharRegex
+} from "./passwordRegexes";
+
+interface PasswordCheckProps {
+ password: string;
+ setPasswordErrorTooShort: (value: boolean) => void;
+ setPasswordErrorTooLong: (value: boolean) => void;
+ setPasswordErrorNoLetterChar: (value: boolean) => void;
+ setPasswordErrorNoNumOrSpecialChar: (value: boolean) => void;
+ setPasswordErrorRepeatedChar: (value: boolean) => void;
+ setPasswordErrorEscapeChar: (value: boolean) => void;
+ setPasswordErrorLowEntropy: (value: boolean) => void;
+ setPasswordErrorBreached: (value: boolean) => void;
+}
+
+const passwordCheck = async ({
+ password,
+ setPasswordErrorTooShort,
+ setPasswordErrorTooLong,
+ setPasswordErrorNoLetterChar,
+ setPasswordErrorNoNumOrSpecialChar,
+ setPasswordErrorRepeatedChar,
+ setPasswordErrorEscapeChar,
+ setPasswordErrorLowEntropy,
+ setPasswordErrorBreached
+}: PasswordCheckProps) => {
+ let errorCheck = false;
+ const tests = [
+ {
+ name: "tooShort",
+ validator: (pwd: string) => pwd.length >= 14,
+ setError: setPasswordErrorTooShort
+ },
+ {
+ name: "tooLong",
+ validator: (pwd: string) => pwd.length < 101,
+ setError: setPasswordErrorTooLong
+ },
+ {
+ name: "noLetterChar",
+ validator: (pwd: string) => letterCharRegex.test(pwd),
+ setError: setPasswordErrorNoLetterChar
+ },
+ {
+ name: "noNumOrSpecialChar",
+ validator: (pwd: string) => numAndSpecialCharRegex.test(pwd),
+ setError: setPasswordErrorNoNumOrSpecialChar
+ },
+ {
+ name: "repeatedChar",
+ validator: (pwd: string) => !repeatedCharRegex.test(pwd),
+ setError: setPasswordErrorRepeatedChar
+ },
+ {
+ name: "escapeChar",
+ validator: (pwd: string) => !escapeCharRegex.test(pwd),
+ setError: setPasswordErrorEscapeChar
+ },
+ {
+ name: "lowEntropy",
+ validator: (pwd: string) => !lowEntropyRegexes.some((regex) => regex.test(pwd)),
+ setError: setPasswordErrorLowEntropy
+ }
+ ];
+
+ const isBreached = await checkIsPasswordBreached(password);
+
+ if (isBreached) {
+ errorCheck = true;
+ setPasswordErrorBreached(true);
+ } else {
+ setPasswordErrorBreached(false);
+ }
+
+ tests.forEach((test) => {
+ if (!test.validator(password)) {
+ errorCheck = true;
+ test.setError(true);
+ } else {
+ test.setError(false);
+ }
+ });
+
+ return errorCheck;
+};
+
+export default passwordCheck;
diff --git a/frontend-v2/src/components/utilities/checks/password/checkIsPasswordBreached.ts b/frontend-v2/src/components/utilities/checks/password/checkIsPasswordBreached.ts
new file mode 100644
index 000000000..3e2500260
--- /dev/null
+++ b/frontend-v2/src/components/utilities/checks/password/checkIsPasswordBreached.ts
@@ -0,0 +1,113 @@
+import axios from "axios";
+
+// SHA-1 hash the password using the SubtleCrypto API
+async function hashPassword(passwordBytes: ArrayBuffer): Promise {
+ const buffer = await window.crypto.subtle.digest("SHA-1", passwordBytes);
+ return buffer;
+}
+
+// Convert the hashed password buffer to a hexadecimal string
+function bufferToHex(buffer: ArrayBuffer): string {
+ const byteArray = new Uint8Array(buffer);
+ const hexParts: string[] = [];
+ byteArray.forEach((byte) => {
+ const hex = byte.toString(16).padStart(2, "0");
+ hexParts.push(hex);
+ });
+ return hexParts.join("");
+}
+
+// see API details here: https://haveibeenpwned.com/API/v3#SearchingPwnedPasswordsByRange
+// in short, the pending password is hashed (SHA-1), the first 5 chars are sliced and compared against a ranged hash table
+// this hash table is formed from the 5 char hash prefix (ie. 00000-FFFFF) so 16^5 results
+// returns a hash table of 800-1000 results
+// padding has been added to prevent MitM attacker determining which hash table was called by the response size
+// the last 35 chars of the password hash are compared client-side against the table
+// if there is a match, that password has been involved in a password breach (ie. pwnd) and should NOT be accepted
+// the database consists of ~700 mln breached passwords and is continuously updated, including with law enforcement ingestion
+// https://www.troyhunt.com/open-source-pwned-passwords-with-fbi-feed-and-225m-new-nca-passwords-is-now-live/
+
+// The HIBP API follows NIST guidance (pg.14) https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-63b.pdf
+// "When processing requests to establish and change memorized secrets, verifiers SHALL compare
+// the prospective secrets against a list that contains values known to be commonly-used, expected,
+// or compromised. For example, the list MAY include, but is not limited to:
+// • Passwords obtained from previous breach corpuses.
+// • Dictionary words.
+// • Repetitive or sequential characters (e.g. ‘aaaaaa’, ‘1234abcd’).
+// • Context-specific words, such as the name of the service, the username, and derivatives
+// thereof."
+
+export const checkIsPasswordBreached = async (password: string): Promise => {
+ const HAVE_I_BEEN_PWNED_API_URL = "https://api.pwnedpasswords.com";
+ const maxRetryAttempts = 3;
+
+ let encodedPwd: Uint8Array | undefined;
+ let hashedPwdBuffer: ArrayBuffer | undefined;
+
+ try {
+ // Convert the password to a Uint8Array (UTF-8 encoded bytes)
+ const textEncoder = new TextEncoder();
+ encodedPwd = textEncoder.encode(password);
+
+ // Hash the password and convert it to a useful format for the HIBP API
+ hashedPwdBuffer = await hashPassword(encodedPwd!.buffer);
+ const hashedPwd = bufferToHex(hashedPwdBuffer).toUpperCase();
+ // ONLY send the first 5 hash chars (over HTTPS)
+ const hashedPwdToSend = hashedPwd.slice(0, 5);
+ const safeHashedPwdToSend = encodeURIComponent(hashedPwdToSend); // Ensure URL safety
+ const rangedHashTableUri = `${HAVE_I_BEEN_PWNED_API_URL}/range/${safeHashedPwdToSend}`;
+
+ let response;
+ let retryAttempt = 0;
+
+ /* eslint-disable no-await-in-loop */
+ while (retryAttempt < maxRetryAttempts) {
+ try {
+ response = await axios.get(rangedHashTableUri, {
+ headers: {
+ "Add-Padding": "true", // see https://www.troyhunt.com/enhancing-pwned-passwords-privacy-with-padding/
+ "Content-Type": "text/plain"
+ }
+ });
+
+ if (response.status === 200) {
+ // now we get back one of 16^5 hash prefix tables with random padding
+ const responseData = response.data.toUpperCase();
+ // check the last 35 hash chars to see if there's a match
+ const isBreachedPassword: boolean = responseData.includes(hashedPwd.slice(5, 40));
+ return isBreachedPassword;
+ }
+ retryAttempt += 1;
+ } catch (err) {
+ if (!axios.isAxiosError(err)) {
+ throw err;
+ }
+ retryAttempt += 1;
+ }
+ }
+
+ console.error(
+ `Received a non-200 response (${
+ response ? response.status : "unknown"
+ }) from the Pwnd Passwords API`
+ );
+ return false;
+ } catch (err: any) {
+ console.error("An unexpected error has occurred:", err.message);
+ return false;
+ } finally {
+ // Clear the UTF-8 encoded password from memory
+
+ if (encodedPwd) {
+ const zeroEncodedPwdBuffer = new Uint8Array(encodedPwd.length);
+ encodedPwd.set(zeroEncodedPwdBuffer);
+ }
+
+ // Clear the hashed password buffer from memory
+
+ if (hashedPwdBuffer) {
+ const zeroHashedPwdBuffer = new Uint8Array(hashedPwdBuffer);
+ zeroHashedPwdBuffer.fill(0);
+ }
+ }
+};
diff --git a/frontend-v2/src/components/utilities/checks/password/checkPassword.ts b/frontend-v2/src/components/utilities/checks/password/checkPassword.ts
new file mode 100644
index 000000000..6e8acaaba
--- /dev/null
+++ b/frontend-v2/src/components/utilities/checks/password/checkPassword.ts
@@ -0,0 +1,103 @@
+import { checkIsPasswordBreached } from "./checkIsPasswordBreached";
+import {
+ escapeCharRegex,
+ letterCharRegex,
+ lowEntropyRegexes,
+ numAndSpecialCharRegex,
+ repeatedCharRegex
+} from "./passwordRegexes";
+
+type Errors = {
+ tooShort?: string;
+ tooLong?: string;
+ noLetterChar?: string;
+ noNumOrSpecialChar?: string;
+ repeatedChar?: string;
+ escapeChar?: string;
+ lowEntropy?: string;
+ breached?: string;
+};
+
+interface CheckPasswordParams {
+ password: string;
+ setErrors: (value: Errors) => void;
+}
+
+/**
+ * Validate that the password [password]:
+ * - Contains at least 14 characters
+ * - Contains at most 100 characters
+ * - Contains at least 1 letter character (many languages supported) (case insensitive)
+ * - Contains at least 1 number (0-9) or special character (emojis included)
+ * - Does not contain 3 repeat, consecutive characters
+ * - Does not contain any escape characters/sequences
+ * - Does not contain PII and/or low entropy data (eg. email address, URL, phone number, DoB, SSN, driver's license, passport)
+ * - Is not in a database of breached passwords
+ *
+ * The function returns whether or not the password [password]
+ * passes the minimum requirements above. It sets errors on
+ * an erorr object via [setErrors].
+ *
+ * @param {Object} obj
+ * @param {String} obj.password - the password to check
+ * @param {Function} obj.setErrors - set state function to set error object
+ */
+
+const checkPassword = async ({ password, setErrors }: CheckPasswordParams): Promise => {
+ const errors: Errors = {};
+
+ const tests = [
+ {
+ name: "tooShort",
+ validator: (pwd: string) => pwd.length >= 14,
+ errorText: "at least 14 characters"
+ },
+ {
+ name: "tooLong",
+ validator: (pwd: string) => pwd.length < 101,
+ errorText: "at most 100 characters"
+ },
+ {
+ name: "noLetterChar",
+ validator: (pwd: string) => letterCharRegex.test(pwd),
+ errorText: "at least 1 letter character"
+ },
+ {
+ name: "noNumOrSpecialChar",
+ validator: (pwd: string) => numAndSpecialCharRegex.test(pwd),
+ errorText: "at least 1 number or special character"
+ },
+ {
+ name: "repeatedChar",
+ validator: (pwd: string) => !repeatedCharRegex.test(pwd),
+ errorText: "at most 3 repeated, consecutive characters"
+ },
+ {
+ name: "escapeChar",
+ validator: (pwd: string) => !escapeCharRegex.test(pwd),
+ errorText: "No escape characters allowed."
+ },
+ {
+ name: "lowEntropy",
+ validator: (pwd: string) => !lowEntropyRegexes.some((regex) => regex.test(pwd)),
+ errorText: "Password contains personal info."
+ }
+ ];
+
+ const isBreached = await checkIsPasswordBreached(password);
+
+ if (isBreached) {
+ errors.breached = "Password was found in a data breach.";
+ }
+
+ tests.forEach((test) => {
+ if (test.validator && !test.validator(password)) {
+ errors[test.name as keyof Errors] = test.errorText;
+ }
+ });
+
+ setErrors(errors);
+ return Object.keys(errors).length > 0;
+};
+
+export default checkPassword;
diff --git a/frontend-v2/src/components/utilities/checks/password/passwordRegexes.ts b/frontend-v2/src/components/utilities/checks/password/passwordRegexes.ts
new file mode 100644
index 000000000..7a70aa8f8
--- /dev/null
+++ b/frontend-v2/src/components/utilities/checks/password/passwordRegexes.ts
@@ -0,0 +1,37 @@
+// This regex covers letters (case insensitive) for the top 50 most spoken languages
+/* eslint-disable no-misleading-character-class */
+export const letterCharRegex =
+ /[A-Za-z\u00C0-\u00D6\u00D8-\u00DE\u00DF-\u00F6\u00F8-\u00FF\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FFF\u0600-\u06FF\u0400-\u04FF\u0500-\u052F\u2DE0-\u2DFF\uA640-\uA69F\u05B0-\u05FF\u0980-\u09FF\u1F00-\u1FFF\u0130\u015E\u011E\u00C7\u00FC\u00FB\u00EB\u00E7]/u;
+
+// This regex covers digits, special characters, symbols, and emojis.
+export const numAndSpecialCharRegex = /[\d!@#$%^&*(),.?":{}|<>]|[^\p{L}\p{N}\s]/u;
+
+// This regex covers 3 repeated consecutive chars (incl. spaces)
+export const repeatedCharRegex = /(.)\1\1\1|\s{4,}/;
+
+// This regex covers the escape sequences as a precaution
+export const escapeCharRegex = /[\n\t\r\\]/;
+
+// This regex covers some PII and/or low entropy data
+export const lowEntropyRegexes = [
+ // Email address
+ /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/,
+
+ // URL (incl. subdomains, paths, top-level domains & query params)
+ /^(?:(?:https?|ftp):\/\/)?(?:\w+\.)?[a-zA-Z0-9.-]+\.(?:com|org|net|edu)(?:\/\S*)?(?:\?\S*)?$/,
+
+ // Date in various formats
+ /(\b\d{1,4}[-/.]?\d{1,2}[-/.]?\d{1,4}\b)|(\b\d{1,4}[-/.]?\w{3}[-/.]?\d{1,4}\b)/,
+
+ // Phone numbers (generalized)
+ /(?:\+(?:[1-9]\d{0,2})\s?)?(?:\(\d{1,4}\)\s?)?(?:\d[-.\s]?){5,}\d/,
+
+ // Passport numbers (generalized)
+ /\b(?:[A-Z0-9]{6,9}|[A-Z0-9]{8,9}|[A-Z0-9]{9}|[A-Z0-9]{10,11})\b/,
+
+ // Driver's license numbers (generalized)
+ /\b(?:[A-Z0-9]{7,10}|[A-Z0-9]{10,11}|[A-Z0-9]{7,10})\b/,
+
+ // US social security number
+ /\b\d{3}[-\s]?\d{2}[-\s]?\d{4}\b/
+];
diff --git a/frontend-v2/src/components/utilities/checks/tempLocalStorage.ts b/frontend-v2/src/components/utilities/checks/tempLocalStorage.ts
new file mode 100644
index 000000000..f24d53aeb
--- /dev/null
+++ b/frontend-v2/src/components/utilities/checks/tempLocalStorage.ts
@@ -0,0 +1,11 @@
+// this is temporary util function. create error handling logic for localStorage and delete this.
+export const tempLocalStorage = (key: string) => {
+ const value = localStorage.getItem(key);
+
+ if (value === null || value === "") {
+ console.warn("No value found in localStorage for key");
+ return "";
+ }
+
+ return value;
+};
diff --git a/frontend-v2/src/components/utilities/config/index.ts b/frontend-v2/src/components/utilities/config/index.ts
new file mode 100644
index 000000000..9b3bf37f1
--- /dev/null
+++ b/frontend-v2/src/components/utilities/config/index.ts
@@ -0,0 +1,7 @@
+const ENV = process.env.NEXT_PUBLIC_ENV! || "development"; // investigate
+const POSTHOG_API_KEY = process.env.NEXT_PUBLIC_POSTHOG_API_KEY!;
+const POSTHOG_HOST = process.env.NEXT_PUBLIC_POSTHOG_HOST! || "https://app.posthog.com";
+const INTERCOMid = process.env.NEXT_PUBLIC_INTERCOMid!;
+const CAPTCHA_SITE_KEY = process.env.NEXT_PUBLIC_CAPTCHA_SITE_KEY!;
+
+export { CAPTCHA_SITE_KEY, ENV, INTERCOMid, POSTHOG_API_KEY, POSTHOG_HOST };
diff --git a/frontend-v2/src/components/utilities/cryptography/aes-256-gcm.ts b/frontend-v2/src/components/utilities/cryptography/aes-256-gcm.ts
new file mode 100644
index 000000000..b9bd72519
--- /dev/null
+++ b/frontend-v2/src/components/utilities/cryptography/aes-256-gcm.ts
@@ -0,0 +1,77 @@
+/**
+ * @fileoverview Provides easy encryption/decryption methods using AES 256 GCM.
+ */
+
+import crypto from "crypto";
+
+const ALGORITHM = "aes-256-gcm";
+const BLOCK_SIZE_BYTES = 16; // 128 bit
+
+interface EncryptProps {
+ text: string;
+ secret: string | Buffer;
+}
+
+interface DecryptProps {
+ ciphertext: string;
+ iv: string;
+ tag: string;
+ secret: string | Buffer;
+}
+
+interface EncryptOutputProps {
+ ciphertext: string;
+ iv: string;
+ tag: string;
+}
+
+/**
+ * Provides easy encryption/decryption methods using AES 256 GCM.
+ */
+class Aes256Gcm {
+ /**
+ * No need to run the constructor. The class only has static methods.
+ */
+
+ /**
+ * Encrypts text with AES 256 GCM.
+ * @param {object} obj
+ * @param {string} obj.text - Cleartext to encode.
+ * @param {string} obj.secret - Shared secret key, must be 32 bytes.
+ * @returns {object}
+ */
+ // { ciphertext: string; iv: string; tag: string; }
+ static encrypt({ text, secret }: EncryptProps): EncryptOutputProps {
+ const iv = crypto.randomBytes(BLOCK_SIZE_BYTES);
+ const cipher = crypto.createCipheriv(ALGORITHM, secret, iv);
+
+ let ciphertext = cipher.update(text, "utf8", "base64");
+ ciphertext += cipher.final("base64");
+ return {
+ ciphertext,
+ iv: iv.toString("base64"),
+ tag: cipher.getAuthTag().toString("base64")
+ };
+ }
+
+ /**
+ * Decrypts AES 256 CGM encrypted text.
+ * @param {object} obj
+ * @param {string} obj.ciphertext - Base64-encoded ciphertext.
+ * @param {string} obj.iv - The base64-encoded initialization vector.
+ * @param {string} obj.tag - The base64-encoded authentication tag generated by getAuthTag().
+ * @param {string} obj.secret - Shared secret key, must be 32 bytes.
+ * @returns {string}
+ */
+ static decrypt({ ciphertext, iv, tag, secret }: DecryptProps): string {
+ const decipher = crypto.createDecipheriv(ALGORITHM, secret, Buffer.from(iv, "base64"));
+ decipher.setAuthTag(Buffer.from(tag, "base64"));
+
+ let cleartext = decipher.update(ciphertext, "base64", "utf8");
+ cleartext += decipher.final("utf8");
+
+ return cleartext;
+ }
+}
+
+export default Aes256Gcm;
diff --git a/frontend-v2/src/components/utilities/cryptography/crypto.ts b/frontend-v2/src/components/utilities/cryptography/crypto.ts
new file mode 100644
index 000000000..c0e5d4c21
--- /dev/null
+++ b/frontend-v2/src/components/utilities/cryptography/crypto.ts
@@ -0,0 +1,235 @@
+import argon2 from "argon2-browser";
+
+import aes from "./aes-256-gcm";
+
+const nacl = require("tweetnacl");
+nacl.util = require("tweetnacl-util");
+
+/**
+ * Return new base64, NaCl, public-private key pair.
+ * @returns {Object} obj
+ * @returns {String} obj.publicKey - base64, NaCl, public key
+ * @returns {String} obj.privateKey - base64, NaCl, private key
+ */
+const generateKeyPair = () => {
+ const pair = nacl.box.keyPair();
+
+ return {
+ publicKey: nacl.util.encodeBase64(pair.publicKey),
+ privateKey: nacl.util.encodeBase64(pair.secretKey)
+ };
+};
+
+type EncryptAsymmetricProps = {
+ plaintext: string;
+ publicKey: string;
+ privateKey: string;
+};
+
+/**
+ * Verify that private key [privateKey] is the one that corresponds to
+ * the public key [publicKey]
+ * @param {Object}
+ * @param {String} - base64-encoded Nacl private key
+ * @param {String} - base64-encoded Nacl public key
+ */
+const verifyPrivateKey = ({ privateKey, publicKey }: { privateKey: string; publicKey: string }) => {
+ const derivedPublicKey = nacl.util.encodeBase64(
+ nacl.box.keyPair.fromSecretKey(nacl.util.decodeBase64(privateKey)).publicKey
+ );
+
+ if (derivedPublicKey !== publicKey) {
+ throw new Error("Failed to verify private key");
+ }
+};
+
+/**
+ * Derive a key from password [password] and salt [salt] using Argon2id
+ * @param {Object} obj
+ * @param {String} obj.password - password to derive key from
+ * @param {String} obj.salt - salt to derive key from
+ * @param {Number} obj.mem - used memory, in KiB
+ * @param {Number} obj.time - number of iterations
+ * @param {Number} obj.parallelism - desired parallelism
+ * @param {Number} obj.hashLen - desired hash length (i.e. byte-length of derived key)
+ * @returns
+ */
+const deriveArgonKey = async ({
+ password,
+ salt,
+ mem,
+ time,
+ parallelism,
+ hashLen
+}: {
+ password: string;
+ salt: string;
+ mem: number;
+ time: number;
+ parallelism: number;
+ hashLen: number;
+}) => {
+ let derivedKey;
+ try {
+ derivedKey = await argon2.hash({
+ pass: password,
+ salt,
+ type: argon2.ArgonType.Argon2id,
+ mem,
+ time,
+ parallelism,
+ hashLen
+ });
+ } catch (err) {
+ console.error(err);
+ }
+
+ return derivedKey;
+};
+
+/**
+ * Return assymmetrically encrypted [plaintext] using [publicKey] where
+ * [publicKey] likely belongs to the recipient.
+ * @param {Object} obj
+ * @param {String} obj.plaintext - plaintext to encrypt
+ * @param {String} obj.publicKey - public key of the recipient
+ * @param {String} obj.privateKey - private key of the sender (current user)
+ * @returns {Object} obj
+ * @returns {String} ciphertext - base64-encoded ciphertext
+ * @returns {String} nonce - base64-encoded nonce
+ */
+const encryptAssymmetric = ({
+ plaintext,
+ publicKey,
+ privateKey
+}: EncryptAsymmetricProps): {
+ ciphertext: string;
+ nonce: string;
+} => {
+ const nonce = nacl.randomBytes(24);
+ const ciphertext = nacl.box(
+ nacl.util.decodeUTF8(plaintext),
+ nonce,
+ nacl.util.decodeBase64(publicKey),
+ nacl.util.decodeBase64(privateKey)
+ );
+
+ return {
+ ciphertext: nacl.util.encodeBase64(ciphertext),
+ nonce: nacl.util.encodeBase64(nonce)
+ };
+};
+
+type DecryptAsymmetricProps = {
+ ciphertext: string;
+ nonce: string;
+ publicKey: string;
+ privateKey: string;
+};
+
+/**
+ * Return assymmetrically decrypted [ciphertext] using [privateKey] where
+ * [privateKey] likely belongs to the recipient.
+ * @param {Object} obj
+ * @param {String} obj.ciphertext - ciphertext to decrypt
+ * @param {String} obj.nonce - nonce
+ * @param {String} obj.publicKey - base64-encoded public key of the sender
+ * @param {String} obj.privateKey - base64-encoded private key of the receiver (current user)
+ */
+const decryptAssymmetric = ({
+ ciphertext,
+ nonce,
+ publicKey,
+ privateKey
+}: DecryptAsymmetricProps): string => {
+ const plaintext = nacl.box.open(
+ nacl.util.decodeBase64(ciphertext),
+ nacl.util.decodeBase64(nonce),
+ nacl.util.decodeBase64(publicKey),
+ nacl.util.decodeBase64(privateKey)
+ );
+
+ return nacl.util.encodeUTF8(plaintext);
+};
+
+type EncryptSymmetricProps = {
+ plaintext: string;
+ key: string;
+};
+
+type EncryptSymmetricReturn = {
+ ciphertext: string;
+ iv: string;
+ tag: string;
+};
+/**
+ * Return symmetrically encrypted [plaintext] using [key].
+ */
+const encryptSymmetric = ({ plaintext, key }: EncryptSymmetricProps): EncryptSymmetricReturn => {
+ let ciphertext;
+ let iv;
+ let tag;
+ try {
+ const obj = aes.encrypt({ text: plaintext, secret: key });
+ ciphertext = obj.ciphertext;
+ iv = obj.iv;
+ tag = obj.tag;
+ } catch (err) {
+ console.log("Failed to perform encryption");
+ console.log(err);
+ process.exit(1);
+ }
+
+ return {
+ ciphertext,
+ iv,
+ tag
+ };
+};
+
+type DecryptSymmetricProps = {
+ ciphertext: string;
+ iv: string;
+ tag: string;
+ key: string;
+};
+
+/**
+ * Return symmetrically decypted [ciphertext] using [iv], [tag],
+ * and [key].
+ * @param {Object} obj
+ * @param {String} obj.ciphertext - ciphertext to decrypt
+ * @param {String} obj.iv - iv
+ * @param {String} obj.tag - tag
+ * @param {String} obj.key - 32-byte hex key
+ *
+ */
+const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => {
+ if (!ciphertext) return "";
+ let plaintext;
+ try {
+ plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key });
+ } catch (err) {
+ console.log("Failed to decrypt with the following parameters", {
+ ciphertext,
+ iv,
+ tag,
+ key
+ });
+ console.log("Failed to perform decryption", err);
+
+ process.exit(1);
+ }
+
+ return plaintext;
+};
+
+export {
+ decryptAssymmetric,
+ decryptSymmetric,
+ deriveArgonKey,
+ encryptAssymmetric,
+ encryptSymmetric,
+ generateKeyPair,
+ verifyPrivateKey
+};
diff --git a/frontend-v2/src/components/utilities/cryptography/issueBackupKey.ts b/frontend-v2/src/components/utilities/cryptography/issueBackupKey.ts
new file mode 100644
index 000000000..e65c53748
--- /dev/null
+++ b/frontend-v2/src/components/utilities/cryptography/issueBackupKey.ts
@@ -0,0 +1,114 @@
+/* eslint-disable new-cap */
+import crypto from "crypto";
+
+import jsrp from "jsrp";
+
+import { issueBackupPrivateKey, srp1 } from "@app/hooks/api/auth/queries";
+
+import generateBackupPDF from "../generateBackupPDF";
+import Aes256Gcm from "./aes-256-gcm";
+
+const clientPassword = new jsrp.client();
+const clientKey = new jsrp.client();
+
+interface BackupKeyProps {
+ email: string;
+ password: string;
+ personalName: string;
+ setBackupKeyError: (value: boolean) => void;
+ setBackupKeyIssued: (value: boolean) => void;
+}
+
+/**
+ * This function issue a backup key for a user
+ * @param {obkect} obj
+ * @param {string} obj.email - email of a user issuing a backup key
+ * @param {string} obj.password - password of a user issuing a backup key
+ * @param {string} obj.personalName - name of a user issuing a backup key
+ * @param {function} obj.setBackupKeyError - state function that turns true if there is an erorr with a backup key
+ * @param {function} obj.setBackupKeyIssued - state function that turns true if a backup key was issued correctly
+ * @returns
+ */
+const issueBackupKey = async ({
+ email,
+ password,
+ personalName,
+ setBackupKeyError,
+ setBackupKeyIssued
+}: BackupKeyProps) => {
+ try {
+ setBackupKeyError(false);
+ setBackupKeyIssued(false);
+ clientPassword.init(
+ {
+ username: email,
+ password
+ },
+ async () => {
+ const clientPublicKey = clientPassword.getPublicKey();
+
+ let serverPublicKey;
+ let salt;
+ try {
+ const res = await srp1({
+ clientPublicKey
+ });
+ serverPublicKey = res.serverPublicKey;
+ salt = res.salt;
+ } catch (err) {
+ setBackupKeyError(true);
+ console.log("Wrong current password", err, 1);
+ }
+
+ clientPassword.setSalt(salt as string);
+ clientPassword.setServerPublicKey(serverPublicKey as string);
+ const clientProof = clientPassword.getProof(); // called M1
+
+ const generatedKey = crypto.randomBytes(16).toString("hex");
+
+ clientKey.init(
+ {
+ username: email,
+ password: generatedKey
+ },
+ async () => {
+ clientKey.createVerifier(
+ async (err: any, result: { salt: string; verifier: string }) => {
+ const { ciphertext, iv, tag } = Aes256Gcm.encrypt({
+ text: String(localStorage.getItem("PRIVATE_KEY")),
+ secret: generatedKey
+ });
+
+ try {
+ await issueBackupPrivateKey({
+ encryptedPrivateKey: ciphertext,
+ iv,
+ tag,
+ salt: result.salt,
+ verifier: result.verifier,
+ clientProof
+ });
+
+ generateBackupPDF({
+ personalName,
+ personalEmail: email,
+ generatedKey
+ });
+ setBackupKeyIssued(true);
+ } catch {
+ setBackupKeyError(true);
+ }
+ }
+ );
+ }
+ );
+ }
+ );
+ } catch (error) {
+ setBackupKeyError(true);
+ console.log("Failed to issue a backup key");
+ }
+ return true;
+};
+
+export default issueBackupKey;
diff --git a/frontend-v2/src/components/utilities/generateBackupPDF.ts b/frontend-v2/src/components/utilities/generateBackupPDF.ts
new file mode 100644
index 000000000..bf3b430c6
--- /dev/null
+++ b/frontend-v2/src/components/utilities/generateBackupPDF.ts
@@ -0,0 +1,105 @@
+import { jsPDF } from "jspdf";
+
+interface PDFProps {
+ personalName: string;
+ personalEmail: string;
+ generatedKey: string;
+}
+
+const imgData =
+ "data:image/png;base64,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";
+
+const today = new Date();
+const dd = String(today.getDate()).padStart(2, "0");
+const mm = String(today.getMonth() + 1).padStart(2, "0"); // January is 0!
+const yyyy = today.getFullYear();
+
+const todayFormatted = `${mm}/${dd}/${yyyy}`;
+
+function createPdfHeader(doc: jsPDF, personalName: string) {
+ doc.setFillColor(255, 255, 255);
+ doc.rect(0, 0, 600, 900, "F");
+ doc.setTextColor(23, 23, 23);
+ doc.addImage(imgData, "png", 30, 35, 535, 72, undefined, "FAST");
+ doc.addFont("helvetica", "normal", "");
+ doc.setFontSize(12);
+ doc.text(`Created for ${personalName} on ${todayFormatted}.`, 290, 130, { align: "center" });
+}
+
+function createPdfContent(doc: jsPDF, personalEmail: string, generatedKey: string) {
+ const { protocol, hostname, port } = window.location;
+ const portSuffix = port && port !== "80" ? `:${port}` : "";
+ const siteURL = `${protocol}//${hostname}${portSuffix}`;
+
+ doc.setFontSize(14);
+ doc.text(
+ "In case you get locked out of you Infisical account, you`ll need these account details",
+ 32,
+ 180
+ );
+ doc.text("to sign in —", 32, 200);
+ doc.setFont("helvetica", "bold");
+ doc.text("including your Secret Key, which we absolutely cannot access or", 110, 200);
+ doc.text("recover for you. ", 32, 220);
+ doc.setFont("helvetica", "normal");
+ doc.text("Recommendations:", 32, 250);
+ doc.text(
+ "1. We recommend to get your Emergency Kit off your computer and print a copy.",
+ 32,
+ 280
+ );
+ doc.text(
+ "2. Store it somewhere safe (such as with your birth certificate, your will, or on your",
+ 32,
+ 310
+ );
+ doc.text("personal cloud storage).", 32, 330);
+ doc.setFillColor(251, 255, 158);
+ doc.roundedRect(32, 350, 530, 190, 5, 5, "F");
+ doc.setDrawColor(228, 255, 0);
+ doc.setLineWidth(1);
+ doc.roundedRect(32, 350, 530, 190, 5, 5, "S");
+ doc.setTextColor(43, 43, 43);
+ doc.setFont("helvetica", "bold");
+ doc.setFontSize(15);
+ doc.text("Infisical Account Details", 290, 375, { align: "center" });
+ doc.setFontSize(12);
+ doc.text("SIGN-IN URL", 50, 420);
+ doc.text("EMAIL ADDRESS", 50, 465);
+ doc.text("SECRET KEY", 50, 510);
+ doc.setFont("helvetica", "normal");
+ doc.setFillColor(254, 255, 235);
+ doc.roundedRect(170, 398, 375, 35, 5, 5, "F");
+ doc.roundedRect(170, 443, 375, 35, 5, 5, "F");
+ doc.roundedRect(170, 488, 375, 35, 5, 5, "F");
+ doc.setTextColor(23, 23, 23);
+ doc.setFontSize(14);
+ doc.text(`${siteURL}/login`, 180, 420);
+ doc.text(personalEmail, 180, 465);
+ doc.text(generatedKey, 180, 510);
+ doc.text("Need help? Contact us at support@infisical.com", 32, 575);
+}
+
+/**
+ * This function generate a pdf with a secret key for a user.
+ */
+function generateBackupPDF({ personalName, personalEmail, generatedKey }: PDFProps) {
+ // eslint-disable-next-line new-cap
+ const doc = new jsPDF("p", "pt", "a4", true);
+ createPdfHeader(doc, personalName);
+ createPdfContent(doc, personalEmail, generatedKey);
+ doc.save("Infisical Emergency Kit.pdf");
+}
+
+/**
+ * This function generate a pdf with a secret key for a user.
+ */
+export function generateBackupPDFAsync({ personalName, personalEmail, generatedKey }: PDFProps) {
+ // eslint-disable-next-line new-cap
+ const doc = new jsPDF("p", "pt", "a4", true);
+ createPdfHeader(doc, personalName);
+ createPdfContent(doc, personalEmail, generatedKey);
+ return doc.save("Infisical Emergency Kit.pdf", { returnPromise: true });
+}
+
+export default generateBackupPDF;
diff --git a/frontend-v2/src/components/utilities/intercom/intercom.ts b/frontend-v2/src/components/utilities/intercom/intercom.ts
new file mode 100644
index 000000000..7228fceb7
--- /dev/null
+++ b/frontend-v2/src/components/utilities/intercom/intercom.ts
@@ -0,0 +1,63 @@
+/* eslint-disable prefer-template */
+/* eslint-disable prefer-rest-params */
+/* eslint-disable @typescript-eslint/no-unused-expressions */
+/* eslint-disable no-nested-ternary */
+/* eslint-disable no-unexpected-multiline */
+/* eslint-disable react-hooks/exhaustive-deps */
+/* eslint-disable vars-on-top */
+/* eslint-disable no-var */
+/* eslint-disable func-names */
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck
+
+import { INTERCOMid as APPid } from "@app/components/utilities/config";
+
+// Loads Intercom with the snippet
+// This must be run before boot, it initializes window.Intercom
+
+// prettier-ignore
+export const load = () => {
+ (function(){
+ var w=window;
+ var ic=w.Intercom;
+
+ if(typeof ic==="function"){
+ ic("reattach_activator");
+ ic("update",w.intercomSettings);
+ } else {
+ var d=document;
+ var i=function() {
+ i.c(arguments);
+ };
+ i.q=[];
+ i.c=function(args) {
+ i.q.push(args);
+ };
+ w.Intercom=i;
+ var l=function() {
+ var s=d.createElement("script");
+ s.type="text/javascript";
+ s.async=true;
+ s.src="https://widget.intercom.io/widget/" + APPid;
+ var x=d.getElementsByTagName("script")[0];
+ x.parentNode.insertBefore(s, x);
+ };
+ if (document.readyState==="complete") {
+ l();
+ } else if (w.attachEvent) {
+ w.attachEvent("onload",l);
+ } else {
+ w.addEventListener("load",l,false);
+ }
+ }
+ })();
+}
+
+// Initializes Intercom
+export const boot = (options = {}) => {
+ window && window.Intercom && window.Intercom("boot", { appid: APPid, ...options });
+};
+
+export const update = () => {
+ window && window.Intercom && window.Intercom("update");
+};
diff --git a/frontend-v2/src/components/utilities/intercom/intercomProvider.ts b/frontend-v2/src/components/utilities/intercom/intercomProvider.ts
new file mode 100644
index 000000000..ef586261d
--- /dev/null
+++ b/frontend-v2/src/components/utilities/intercom/intercomProvider.ts
@@ -0,0 +1,40 @@
+import { useEffect } from "react";
+import { useRouter } from "next/router";
+
+import { useUser } from "@app/context";
+
+import { boot as bootIntercom, load as loadIntercom, update as updateIntercom } from "./intercom";
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+export const IntercomProvider = ({ children }: { children: any }) => {
+ const { user } = useUser();
+ const router = useRouter();
+
+ if (typeof window !== "undefined") {
+ loadIntercom();
+ bootIntercom({
+ name: `${user?.firstName || ""} ${user?.lastName || ""}`,
+ email: user?.email || "",
+ created_at: Math.floor((new Date(user?.createdAt)?.getTime() || 0) / 1000)
+ });
+ }
+
+ useEffect(() => {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const handleRouteChange = (url: string) => {
+ if (typeof window !== "undefined") {
+ updateIntercom();
+ }
+ };
+
+ router.events.on("routeChangeStart", handleRouteChange);
+
+ // If the component is unmounted, unsubscribe
+ // from the event with the `off` method:
+ return () => {
+ router.events.off("routeChangeStart", handleRouteChange);
+ };
+ }, [router.events]);
+
+ return children;
+};
diff --git a/frontend-v2/src/components/utilities/isValidHexColor.ts b/frontend-v2/src/components/utilities/isValidHexColor.ts
new file mode 100644
index 000000000..f64ef0490
--- /dev/null
+++ b/frontend-v2/src/components/utilities/isValidHexColor.ts
@@ -0,0 +1,5 @@
+export const isValidHexColor = (hexColor: string) => {
+ const hexColorPattern = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
+
+ return hexColorPattern.test(hexColor);
+};
diff --git a/frontend-v2/src/components/utilities/parseSecrets.ts b/frontend-v2/src/components/utilities/parseSecrets.ts
new file mode 100644
index 000000000..d1df7cd68
--- /dev/null
+++ b/frontend-v2/src/components/utilities/parseSecrets.ts
@@ -0,0 +1,79 @@
+const LINE =
+ /(?:^|^)\s*(?:export\s+)?([\w.:-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
+
+/**
+ * Return text that is the buffer parsed
+ * @param {ArrayBuffer} src - source buffer
+ * @returns {String} text - text of buffer
+ */
+export function parseDotEnv(src: ArrayBuffer | string) {
+ const object: {
+ [key: string]: { value: string; comments: string[] };
+ } = {};
+
+ // Convert buffer to string
+ let lines = src.toString();
+
+ // Convert line breaks to same format
+ lines = lines.replace(/\r\n?/gm, "\n");
+
+ let comments: string[] = [];
+
+ lines
+ .split("\n")
+ .map((line) => {
+ // collect comments of each env variable
+ if (line.startsWith("#")) {
+ comments.push(line.replace("#", "").trim());
+ } else if (line) {
+ let match;
+ let item: [string, string, string[]] | [] = [];
+
+ // eslint-disable-next-line no-cond-assign
+ while ((match = LINE.exec(line)) !== null) {
+ const key = match[1];
+
+ // Default undefined or null to empty string
+ let value = match[2] || "";
+
+ // Remove whitespace
+ value = value.trim();
+
+ // Check if double quoted
+ const maybeQuote = value[0];
+
+ // Remove surrounding quotes
+ value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
+
+ // Expand newlines if double quoted
+ if (maybeQuote === '"') {
+ value = value.replace(/\\n/g, "\n");
+ value = value.replace(/\\r/g, "\r");
+ }
+ item = [key, value, comments];
+ }
+ comments = [];
+ return item;
+ }
+ return [];
+ })
+ .filter((line) => line.length > 1)
+ .forEach((line) => {
+ const [key, value, cmnts] = line;
+ object[key as string] = { value, comments: cmnts };
+ });
+
+ return object;
+}
+
+export const parseJson = (src: ArrayBuffer | string) => {
+ const file = src.toString();
+ const formatedData: Record = JSON.parse(file);
+ const env: Record = {};
+ Object.keys(formatedData).forEach((key) => {
+ if (typeof formatedData[key] === "string") {
+ env[key] = { value: formatedData[key], comments: [] };
+ }
+ });
+ return env;
+};
diff --git a/frontend-v2/src/components/utilities/randomId.ts b/frontend-v2/src/components/utilities/randomId.ts
new file mode 100644
index 000000000..9c9382c14
--- /dev/null
+++ b/frontend-v2/src/components/utilities/randomId.ts
@@ -0,0 +1,13 @@
+/**
+ * This function returns a random identifier.
+ * @returns
+ */
+const guidGenerator = () => {
+ const S4 = () => {
+ // eslint-disable-next-line no-bitwise
+ return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
+ };
+ return `${S4() + S4()}-${S4()}-${S4()}-${S4()}-${S4()}${S4()}${S4()}`;
+};
+
+export default guidGenerator;
diff --git a/frontend-v2/src/components/utilities/saveTokenToLocalStorage.ts b/frontend-v2/src/components/utilities/saveTokenToLocalStorage.ts
new file mode 100644
index 000000000..1539438f4
--- /dev/null
+++ b/frontend-v2/src/components/utilities/saveTokenToLocalStorage.ts
@@ -0,0 +1,67 @@
+interface Props {
+ protectedKey?: string;
+ protectedKeyIV?: string;
+ protectedKeyTag?: string;
+ publicKey?: string;
+ encryptedPrivateKey?: string;
+ iv?: string;
+ tag?: string;
+ privateKey?: string;
+}
+
+export const saveTokenToLocalStorage = ({
+ protectedKey,
+ protectedKeyIV,
+ protectedKeyTag,
+ publicKey,
+ encryptedPrivateKey,
+ iv,
+ tag,
+ privateKey
+}: Props) => {
+ try {
+ if (protectedKey) {
+ localStorage.removeItem("protectedKey");
+ localStorage.setItem("protectedKey", protectedKey);
+ }
+
+ if (protectedKeyIV) {
+ localStorage.removeItem("protectedKeyIV");
+ localStorage.setItem("protectedKeyIV", protectedKeyIV);
+ }
+
+ if (protectedKeyTag) {
+ localStorage.removeItem("protectedKeyTag");
+ localStorage.setItem("protectedKeyTag", protectedKeyTag);
+ }
+
+ if (publicKey) {
+ localStorage.removeItem("publicKey");
+ localStorage.setItem("publicKey", publicKey);
+ }
+
+ if (encryptedPrivateKey) {
+ localStorage.removeItem("encryptedPrivateKey");
+ localStorage.setItem("encryptedPrivateKey", encryptedPrivateKey);
+ }
+
+ if (iv) {
+ localStorage.removeItem("iv");
+ localStorage.setItem("iv", iv);
+ }
+
+ if (tag) {
+ localStorage.removeItem("tag");
+ localStorage.setItem("tag", tag);
+ }
+
+ if (privateKey) {
+ localStorage.removeItem("PRIVATE_KEY");
+ localStorage.setItem("PRIVATE_KEY", privateKey);
+ }
+ } catch (err) {
+ if (err instanceof Error) {
+ throw new Error(`Unable to send the tokens in local storage:${err.message}`);
+ }
+ }
+};
diff --git a/frontend-v2/src/components/utilities/secrets/checkOverrides.ts b/frontend-v2/src/components/utilities/secrets/checkOverrides.ts
new file mode 100644
index 000000000..7f311a252
--- /dev/null
+++ b/frontend-v2/src/components/utilities/secrets/checkOverrides.ts
@@ -0,0 +1,34 @@
+import { SecretDataProps } from "public/data/frequentInterfaces";
+
+import { SecretType } from "@app/hooks/api/types";
+
+/**
+ * This function downloads the secrets as a .env file
+ * @param {object} obj
+ * @param {SecretDataProps[]} obj.data - secrets that we want to check for overrides
+ * @returns
+ */
+const checkOverrides = async ({ data }: { data: SecretDataProps[] }) => {
+ let secrets: SecretDataProps[] = data!.map((secret) => Object.create(secret));
+ const overridenSecrets = data!.filter((secret) =>
+ secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
+ ? SecretType.Shared
+ : SecretType.Personal
+ );
+ if (overridenSecrets.length) {
+ overridenSecrets.forEach((secret) => {
+ const index = secrets!.findIndex(
+ (_secret) =>
+ _secret.key === secret.key &&
+ (secret.valueOverride === undefined || secret?.value !== secret?.valueOverride)
+ );
+ secrets![index].value = secret.value;
+ });
+ secrets = secrets!.filter(
+ (secret) => secret.valueOverride === undefined || secret?.value !== secret?.valueOverride
+ );
+ }
+ return secrets;
+};
+
+export default checkOverrides;
diff --git a/frontend-v2/src/components/utilities/secrets/downloadDotEnv.ts b/frontend-v2/src/components/utilities/secrets/downloadDotEnv.ts
new file mode 100644
index 000000000..ef71352b8
--- /dev/null
+++ b/frontend-v2/src/components/utilities/secrets/downloadDotEnv.ts
@@ -0,0 +1,37 @@
+import { SecretDataProps } from "public/data/frequentInterfaces";
+
+import checkOverrides from "./checkOverrides";
+
+/**
+ * This function downloads the secrets as a .env file
+ * @param {object} obj
+ * @param {SecretDataProps[]} obj.data - secrets that we want to download
+ * @param {string} obj.env - the environment which we're downloading (used for naming the file)
+ */
+const downloadDotEnv = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
+ if (!data) return;
+ const secrets = await checkOverrides({ data });
+
+ const file = secrets!
+ .map(
+ (item: SecretDataProps) =>
+ `${
+ item.comment
+ ? `${item.comment
+ .split("\n")
+ .map((comment) => "# ".concat(comment))
+ .join("\n")}\n`
+ : ""
+ }${[item.key, item.value].join("=")}`
+ )
+ .join("\n");
+
+ const blob = new Blob([file]);
+ const fileDownloadUrl = URL.createObjectURL(blob);
+ const alink = document.createElement("a");
+ alink.href = fileDownloadUrl;
+ alink.download = `${env}.env`;
+ alink.click();
+};
+
+export default downloadDotEnv;
diff --git a/frontend-v2/src/components/utilities/secrets/downloadYaml.ts b/frontend-v2/src/components/utilities/secrets/downloadYaml.ts
new file mode 100644
index 000000000..e8e6e0ab8
--- /dev/null
+++ b/frontend-v2/src/components/utilities/secrets/downloadYaml.ts
@@ -0,0 +1,43 @@
+// import YAML from 'yaml';
+// import { YAMLSeq } from 'yaml/types';
+
+import { SecretDataProps } from "public/data/frequentInterfaces";
+
+// import { envMapping } from "../../../public/data/frequentConstants";
+// import checkOverrides from './checkOverrides';
+
+/**
+ * This function downloads the secrets as a .yml file
+ * @param {object} obj
+ * @param {SecretDataProps[]} obj.data - secrets that we want to download
+ * @param {string} obj.env - used for naming the file
+ * @returns
+ */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+const downloadYaml = async ({ data, env }: { data: SecretDataProps[]; env: string }) => {
+ // if (!data) return;
+ // const doc = new YAML.Document();
+ // doc.contents = new YAMLSeq();
+ // const secrets = await checkOverrides({ data });
+ // secrets.forEach((secret) => {
+ // const pair = YAML.createNode({ [secret.key]: secret.value });
+ // pair.commentBefore = secret.comment
+ // .split('\n')
+ // .map((line) => (line ? ' '.concat(line) : ''))
+ // .join('\n');
+ // doc.add(pair);
+ // });
+ // const file = doc
+ // .toString()
+ // .split('\n')
+ // .map((line) => (line.startsWith('-') ? line.replace('- ', '') : line))
+ // .join('\n');
+ // const blob = new Blob([file]);
+ // const fileDownloadUrl = URL.createObjectURL(blob);
+ // const alink = document.createElement('a');
+ // alink.href = fileDownloadUrl;
+ // alink.download = envMapping[env] + '.yml';
+ // alink.click();
+};
+
+export default downloadYaml;
diff --git a/frontend-v2/src/components/utilities/secrets/encryptSecrets.ts b/frontend-v2/src/components/utilities/secrets/encryptSecrets.ts
new file mode 100644
index 000000000..121f3ab73
--- /dev/null
+++ b/frontend-v2/src/components/utilities/secrets/encryptSecrets.ts
@@ -0,0 +1,39 @@
+import { SecretDataProps } from "public/data/frequentInterfaces";
+/**
+ * Encypt secrets before pushing the to the DB
+ * @param {object} obj
+ * @param {object} obj.secretsToEncrypt - secrets that we want to encrypt
+ * @param {object} obj.workspaceId - the id of a project in which we are encrypting secrets
+ * @returns
+ */
+const encryptSecrets = async ({
+ secretsToEncrypt,
+ env
+}: {
+ secretsToEncrypt: SecretDataProps[];
+ workspaceId: string;
+ env: string;
+}) => {
+ let secrets;
+ try {
+ secrets = secretsToEncrypt.map((secret) => {
+ const result = {
+ id: secret.id,
+ createdAt: "",
+ environment: env,
+ secretKey: secret.key,
+ secretValue: secret.value,
+ secretComment: secret.comment,
+ tags: secret.tags
+ };
+
+ return result;
+ });
+ } catch (error) {
+ console.log("Error while encrypting secrets");
+ }
+
+ return secrets;
+};
+
+export default encryptSecrets;
diff --git a/frontend-v2/src/components/utilities/telemetry/Telemetry.ts b/frontend-v2/src/components/utilities/telemetry/Telemetry.ts
new file mode 100644
index 000000000..860314d16
--- /dev/null
+++ b/frontend-v2/src/components/utilities/telemetry/Telemetry.ts
@@ -0,0 +1,50 @@
+/* eslint-disable */
+import { PostHog } from "posthog-js";
+import { initPostHog } from "@app/components/analytics/posthog";
+import { ENV } from "@app/components/utilities/config";
+
+declare let TELEMETRY_CAPTURING_ENABLED: any;
+
+class Capturer {
+ api: PostHog;
+
+ constructor() {
+ this.api = initPostHog()!;
+ }
+
+ capture(item: string) {
+ if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) {
+ try {
+ this.api.capture(item);
+ } catch (error) {
+ console.error("PostHog", error);
+ }
+ }
+ }
+
+ identify(id: string, email?: string) {
+ if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED === true) {
+ try {
+ this.api.identify(id, {
+ email: email
+ });
+ } catch (error) {
+ console.error("PostHog", error);
+ }
+ }
+ }
+}
+
+export default class Telemetry {
+ static instance: Capturer;
+
+ constructor() {
+ if (!Telemetry.instance) {
+ Telemetry.instance = new Capturer();
+ }
+ }
+
+ getInstance() {
+ return Telemetry.instance;
+ }
+}
diff --git a/frontend-v2/src/components/v2/Accordion/Accordion.tsx b/frontend-v2/src/components/v2/Accordion/Accordion.tsx
new file mode 100644
index 000000000..c37649393
--- /dev/null
+++ b/frontend-v2/src/components/v2/Accordion/Accordion.tsx
@@ -0,0 +1,74 @@
+import { forwardRef } from "react";
+import { faChevronDown } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as AccordionPrimitive from "@radix-ui/react-accordion";
+import { twMerge } from "tailwind-merge";
+
+export const AccordionItem = forwardRef(
+ ({ children, className, ...props }, forwardedRef) => (
+
+ {children}
+
+ )
+);
+AccordionItem.displayName = "AccordionItem";
+
+export const AccordionTrigger = forwardRef<
+ HTMLButtonElement,
+ AccordionPrimitive.AccordionTriggerProps
+>(({ children, className, ...props }, forwardedRef) => (
+
+
+ {children}
+
+
+
+));
+
+AccordionTrigger.displayName = "AccordionTrigger";
+
+export const AccordionContent = forwardRef<
+ HTMLDivElement,
+ AccordionPrimitive.AccordionContentProps
+>(({ children, className, ...props }, forwardedRef) => (
+
+ {children}
+
+));
+
+AccordionContent.displayName = "AccordionContent";
+
+// ref: https://www.radix-ui.com/primitives/docs/components/accordion#root
+export const Accordion = ({
+ children,
+ ...props
+}: AccordionPrimitive.AccordionSingleProps | AccordionPrimitive.AccordionMultipleProps) => (
+
+ {children}
+
+);
diff --git a/frontend-v2/src/components/v2/Accordion/index.tsx b/frontend-v2/src/components/v2/Accordion/index.tsx
new file mode 100644
index 000000000..fd2e2ddd3
--- /dev/null
+++ b/frontend-v2/src/components/v2/Accordion/index.tsx
@@ -0,0 +1 @@
+export { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "./Accordion";
diff --git a/frontend-v2/src/components/v2/Alert/Alert.tsx b/frontend-v2/src/components/v2/Alert/Alert.tsx
new file mode 100644
index 000000000..9b68f83bb
--- /dev/null
+++ b/frontend-v2/src/components/v2/Alert/Alert.tsx
@@ -0,0 +1,88 @@
+import { forwardRef } from "react";
+import {
+ faExclamationCircle,
+ faExclamationTriangle,
+ faInfoCircle
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { type VariantProps, cva } from "cva";
+import { twMerge } from "tailwind-merge";
+
+const alertVariants = cva(
+ "w-full bg-mineshaft-800 rounded-lg border border-bunker-400 px-4 py-3 text-sm flex items-center gap-x-4",
+ {
+ variants: {
+ variant: {
+ default: "",
+ danger: "text-red border-red",
+ warning: "text-yellow border-yellow"
+ }
+ },
+ defaultVariants: {
+ variant: "default"
+ }
+ }
+);
+
+type AlertProps = {
+ title?: string;
+ hideTitle?: boolean;
+ icon?: React.ReactNode;
+};
+
+const variantTitleMap = {
+ default: "Info",
+ danger: "Danger",
+ warning: "Warning"
+};
+
+const variantIconMap = {
+ default: faInfoCircle,
+ danger: faExclamationCircle,
+ warning: faExclamationTriangle
+};
+
+const Alert = forwardRef<
+ HTMLDivElement,
+ React.HTMLAttributes & VariantProps & AlertProps
+>(({ className, variant, title, icon, hideTitle = false, children, ...props }, ref) => {
+ const defaultTitle = title ?? variantTitleMap[variant ?? "default"];
+ return (
+
+
+ {typeof icon !== "undefined" ? (
+ <>{icon} >
+ ) : (
+
+ )}
+
+
+ {hideTitle ? null : (
+
+ {defaultTitle}
+
+ )}
+ {children}
+
+
+ );
+});
+Alert.displayName = "Alert";
+
+const AlertDescription = forwardRef<
+ HTMLParagraphElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+));
+AlertDescription.displayName = "AlertDescription";
+
+export { Alert, AlertDescription };
diff --git a/frontend-v2/src/components/v2/Alert/index.tsx b/frontend-v2/src/components/v2/Alert/index.tsx
new file mode 100644
index 000000000..7851a0943
--- /dev/null
+++ b/frontend-v2/src/components/v2/Alert/index.tsx
@@ -0,0 +1 @@
+export { Alert, AlertDescription } from "./Alert";
diff --git a/frontend-v2/src/components/v2/Badge/Badge.tsx b/frontend-v2/src/components/v2/Badge/Badge.tsx
new file mode 100644
index 000000000..321c03296
--- /dev/null
+++ b/frontend-v2/src/components/v2/Badge/Badge.tsx
@@ -0,0 +1,32 @@
+import { cva, VariantProps } from "cva";
+import { twMerge } from "tailwind-merge";
+
+interface IProps {
+ children: React.ReactNode;
+ className?: string;
+}
+
+const badgeVariants = cva(
+ [
+ "inline-block cursor-default rounded-md bg-yellow/20 px-1.5 pb-[0.03rem] pt-[0.04rem] text-xs text-yellow opacity-80 hover:opacity-100"
+ ],
+ {
+ variants: {
+ variant: {
+ primary: "bg-yellow/20 text-yellow",
+ danger: "bg-red/20 text-red",
+ success: "bg-green/20 text-green"
+ }
+ }
+ }
+);
+
+export type BadgeProps = VariantProps & IProps;
+
+export const Badge = ({ children, className, variant }: BadgeProps) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/Badge/index.tsx b/frontend-v2/src/components/v2/Badge/index.tsx
new file mode 100644
index 000000000..5c7042709
--- /dev/null
+++ b/frontend-v2/src/components/v2/Badge/index.tsx
@@ -0,0 +1 @@
+export { Badge } from "./Badge";
diff --git a/frontend-v2/src/components/v2/Button/Button.tsx b/frontend-v2/src/components/v2/Button/Button.tsx
new file mode 100644
index 000000000..7707805e9
--- /dev/null
+++ b/frontend-v2/src/components/v2/Button/Button.tsx
@@ -0,0 +1,234 @@
+import { ButtonHTMLAttributes, forwardRef, ReactNode } from "react";
+import { cva, VariantProps } from "cva";
+import { twMerge } from "tailwind-merge";
+
+type Props = {
+ children: ReactNode;
+ isDisabled?: boolean;
+ leftIcon?: ReactNode;
+ rightIcon?: ReactNode;
+ // loading state
+ isLoading?: boolean;
+};
+
+// refactor(akhilmhdh): both color and size variants are together need to split it
+// colorSchema should handle all color class names
+// variant should handle how the button padding and other types should be set
+const buttonVariants = cva(
+ [
+ "button",
+ "transition-all",
+ "font-inter font-medium",
+ "cursor-pointer",
+ "inline-flex items-center justify-center",
+ "relative",
+ "whitespace-nowrap"
+ ],
+ {
+ variants: {
+ colorSchema: {
+ primary: ["bg-primary", "text-black", "border-primary bg-opacity-90 hover:bg-opacity-100"],
+ secondary: ["bg-mineshaft", "text-gray-300", "border-mineshaft hover:bg-opacity-80"],
+ danger: ["!bg-red", "!text-white", "!border-red hover:!bg-opacity-90"],
+ gray: ["bg-bunker-500", "text-bunker-200"]
+ },
+ variant: {
+ solid: "",
+ outline: ["bg-transparent", "border-2", "border-solid"],
+ plain: "",
+ selected: "",
+ outline_bg: "",
+ // a constant color not in use on hover or click goes colorSchema color
+ star: "text-bunker-200 bg-mineshaft-700 border-mineshaft-600",
+ link: "text-primary !p-0 bg-transparent outline-none border-none"
+ },
+ isDisabled: {
+ true: "bg-mineshaft-700 border border-mineshaft-600 text-white opacity-50 cursor-not-allowed",
+ false: "border"
+ },
+ isFullWidth: {
+ true: "w-full",
+ false: ""
+ },
+ isRounded: {
+ true: "rounded-md",
+ false: ""
+ },
+ size: {
+ xs: ["text-xs", "py-1", "px-2"],
+ sm: ["text-sm", "py-2", "px-4"],
+ md: ["text-md", "py-2", "px-5"],
+ lg: ["text-lg", "py-2", "px-6"]
+ }
+ },
+ compoundVariants: [
+ {
+ colorSchema: "primary",
+ variant: "star",
+ className:
+ "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary hover:text-black hover:border-primary-400 duration-100"
+ },
+ {
+ colorSchema: "primary",
+ variant: "solid",
+ className: "text-black bg-primary-500 bg-opacity-90 hover:bg-primary-500 hover:text-black"
+ },
+ {
+ colorSchema: "primary",
+ variant: "selected",
+ className: "bg-primary/10 border border-primary/50 text-bunker-200"
+ },
+ {
+ colorSchema: "primary",
+ variant: "outline_bg",
+ className:
+ "bg-mineshaft-600 border border-mineshaft-500 hover:bg-primary/[0.1] hover:border-primary/40 text-bunker-200"
+ },
+ {
+ colorSchema: "secondary",
+ variant: "star",
+ className:
+ "bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white"
+ },
+ {
+ colorSchema: "danger",
+ variant: "star",
+ className: "hover:bg-red hover:text-white"
+ },
+ {
+ colorSchema: "primary",
+ variant: "outline",
+ className: "text-primary hover:bg-primary hover:text-black"
+ },
+ {
+ colorSchema: "secondary",
+ variant: "outline",
+ className: "border-mineshaft-700 hover:border-mineshaft-500"
+ },
+ {
+ colorSchema: "danger",
+ variant: "outline",
+ className: "text-red hover:bg-red hover:text-black"
+ },
+ {
+ colorSchema: "danger",
+ variant: "outline_bg",
+ className:
+ "bg-mineshaft-600 border border-red-500 hover:bg-red/[0.1] hover:border-red/40 text-red-500"
+ },
+ {
+ colorSchema: "primary",
+ variant: "plain",
+ className: "text-primary"
+ },
+ {
+ colorSchema: "gray",
+ variant: "plain",
+ className: "bg-transparent text-bunker-200"
+ },
+ {
+ colorSchema: "secondary",
+ variant: "plain",
+ className: "text-mineshaft-300 hover:text-mineshaft-200 border-none"
+ },
+ {
+ colorSchema: "danger",
+ variant: "plain",
+ className: "text-red"
+ },
+ {
+ colorSchema: ["danger", "primary", "secondary"],
+ variant: ["plain"],
+ className: "bg-transparent py-1 px-1"
+ }
+ ]
+ }
+);
+
+export type ButtonProps = ButtonHTMLAttributes &
+ VariantProps &
+ Props;
+
+export const Button = forwardRef(
+ (
+ {
+ children,
+ isDisabled = false,
+ className = "",
+ size = "sm",
+ variant = "solid",
+ isFullWidth,
+ isRounded = true,
+ leftIcon,
+ rightIcon,
+ isLoading,
+ colorSchema = "primary",
+ ...props
+ },
+ ref
+ ): JSX.Element => {
+ const loadingToggleClass = isLoading ? "opacity-0" : "opacity-100";
+
+ return (
+
+ {isLoading && (
+
+ )}
+ {leftIcon && (
+
+ {leftIcon}
+
+ )}
+
+ {children}
+
+ {rightIcon && (
+
+ {rightIcon}
+
+ )}
+
+ );
+ }
+);
+
+Button.displayName = "Button";
diff --git a/frontend-v2/src/components/v2/Button/index.tsx b/frontend-v2/src/components/v2/Button/index.tsx
new file mode 100644
index 000000000..b3cb34cc8
--- /dev/null
+++ b/frontend-v2/src/components/v2/Button/index.tsx
@@ -0,0 +1,2 @@
+export type { ButtonProps } from "./Button";
+export { Button } from "./Button";
diff --git a/frontend-v2/src/components/v2/Card/Card.tsx b/frontend-v2/src/components/v2/Card/Card.tsx
new file mode 100644
index 000000000..a0a1f8338
--- /dev/null
+++ b/frontend-v2/src/components/v2/Card/Card.tsx
@@ -0,0 +1,77 @@
+import { CSSProperties, forwardRef, ReactNode } from "react";
+import { twMerge } from "tailwind-merge";
+
+export type CardTitleProps = {
+ children: ReactNode;
+ subTitle?: ReactNode;
+ className?: string;
+};
+
+export const CardTitle = ({ children, className, subTitle }: CardTitleProps) => (
+
+ {children}
+ {subTitle &&
{subTitle}
}
+
+);
+
+export type CardFooterProps = {
+ children: ReactNode;
+ className?: string;
+};
+
+export const CardFooter = ({ children, className }: CardFooterProps) => (
+ {children}
+);
+
+export type CardBodyProps = {
+ children: ReactNode;
+ className?: string;
+ style?: CSSProperties;
+};
+
+export const CardBody = ({ children, className, style }: CardBodyProps) => (
+
+ {children}
+
+);
+
+export type CardProps = {
+ children: ReactNode;
+ className?: string;
+ isFullHeight?: boolean;
+ isRounded?: boolean;
+ isPlain?: boolean;
+ isHoverable?: boolean;
+ style?: CSSProperties;
+};
+
+export const Card = forwardRef(
+ (
+ { children, isFullHeight, isRounded, isHoverable, isPlain, className, style },
+ ref
+ ): JSX.Element => {
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+Card.displayName = "Card";
diff --git a/frontend-v2/src/components/v2/Card/index.tsx b/frontend-v2/src/components/v2/Card/index.tsx
new file mode 100644
index 000000000..48b09705d
--- /dev/null
+++ b/frontend-v2/src/components/v2/Card/index.tsx
@@ -0,0 +1,2 @@
+export type { CardBodyProps, CardFooterProps, CardProps, CardTitleProps } from "./Card";
+export { Card, CardBody, CardFooter, CardTitle } from "./Card";
diff --git a/frontend-v2/src/components/v2/Checkbox/Checkbox.tsx b/frontend-v2/src/components/v2/Checkbox/Checkbox.tsx
new file mode 100644
index 000000000..6a1086996
--- /dev/null
+++ b/frontend-v2/src/components/v2/Checkbox/Checkbox.tsx
@@ -0,0 +1,68 @@
+import { ReactNode } from "react";
+import { faCheck, faMinus } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
+import { twMerge } from "tailwind-merge";
+
+export type CheckboxProps = Omit<
+ CheckboxPrimitive.CheckboxProps,
+ "checked" | "disabled" | "required"
+> & {
+ children?: ReactNode;
+ id: string;
+ isDisabled?: boolean;
+ isChecked?: boolean;
+ isRequired?: boolean;
+ checkIndicatorBg?: string | undefined;
+ isError?: boolean;
+ isIndeterminate?: boolean;
+ containerClassName?: string;
+};
+
+export const Checkbox = ({
+ children,
+ className,
+ id,
+ isChecked,
+ isDisabled,
+ isRequired,
+ checkIndicatorBg,
+ isError,
+ isIndeterminate,
+ containerClassName,
+ ...props
+}: CheckboxProps): JSX.Element => {
+ return (
+
+
+
+ {isIndeterminate ? (
+
+ ) : (
+
+ )}
+
+
+
+ {children}
+ {isRequired && * }
+
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/Checkbox/index.tsx b/frontend-v2/src/components/v2/Checkbox/index.tsx
new file mode 100644
index 000000000..374e8e394
--- /dev/null
+++ b/frontend-v2/src/components/v2/Checkbox/index.tsx
@@ -0,0 +1,2 @@
+export type { CheckboxProps } from "./Checkbox";
+export { Checkbox } from "./Checkbox";
diff --git a/frontend-v2/src/components/v2/ContentLoader/ContentLoader.tsx b/frontend-v2/src/components/v2/ContentLoader/ContentLoader.tsx
new file mode 100644
index 000000000..dd34e9941
--- /dev/null
+++ b/frontend-v2/src/components/v2/ContentLoader/ContentLoader.tsx
@@ -0,0 +1,58 @@
+// this will show a loading animation with text below
+// if you pass array it will say it one by one giving user clear instruction on what's happening
+
+import { useEffect, useState } from "react";
+import { AnimatePresence, motion } from "framer-motion";
+import { twMerge } from "tailwind-merge";
+
+type Props = {
+ text?: string | string[];
+ frequency?: number;
+ className?: string;
+};
+
+export const ContentLoader = ({ text, frequency = 2000, className }: Props) => {
+ const [pos, setPos] = useState(0);
+ const isTextArray = Array.isArray(text);
+ useEffect(() => {
+ let interval: NodeJS.Timeout;
+ if (isTextArray) {
+ interval = setInterval(() => {
+ setPos((state) => (state + 1) % text.length);
+ }, frequency);
+ }
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+
+ {text && isTextArray && (
+
+
+ {text[pos]}
+
+
+ )}
+ {text && !isTextArray &&
{text}
}
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/ContentLoader/index.tsx b/frontend-v2/src/components/v2/ContentLoader/index.tsx
new file mode 100644
index 000000000..78b373523
--- /dev/null
+++ b/frontend-v2/src/components/v2/ContentLoader/index.tsx
@@ -0,0 +1 @@
+export { ContentLoader } from "./ContentLoader";
diff --git a/frontend-v2/src/components/v2/CopyButton/CopyButton.tsx b/frontend-v2/src/components/v2/CopyButton/CopyButton.tsx
new file mode 100644
index 000000000..ff1161ca9
--- /dev/null
+++ b/frontend-v2/src/components/v2/CopyButton/CopyButton.tsx
@@ -0,0 +1,55 @@
+import { faCheck, faCopy, IconDefinition } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
+
+import { useTimedReset } from "@app/hooks";
+
+import { IconButton } from "../IconButton";
+import { Tooltip } from "../Tooltip";
+
+export type CopyButtonProps = {
+ value: string;
+ size?: "xs" | "sm" | "md" | "lg";
+ variant?: "solid" | "outline" | "plain" | "star" | "outline_bg";
+ color?: string;
+ name?: string;
+ icon?: IconDefinition;
+};
+
+export const CopyButton = ({
+ value,
+ size = "sm",
+ variant = "solid",
+ color,
+ name,
+ icon = faCopy
+}: CopyButtonProps) => {
+ const [copyText, isCopying, setCopyText] = useTimedReset({
+ initialState: name ? `Copy ${name}` : "Copy to clipboard"
+ });
+
+ async function handleCopyText() {
+ setCopyText("Copied");
+ navigator.clipboard.writeText(value);
+ }
+
+ return (
+
+
+ {
+ handleCopyText();
+ }}
+ >
+
+
+
+
+ );
+};
+
+CopyButton.displayName = "CopyButton";
diff --git a/frontend-v2/src/components/v2/CopyButton/index.tsx b/frontend-v2/src/components/v2/CopyButton/index.tsx
new file mode 100644
index 000000000..9a7bc5991
--- /dev/null
+++ b/frontend-v2/src/components/v2/CopyButton/index.tsx
@@ -0,0 +1,2 @@
+export type { CopyButtonProps } from "./CopyButton";
+export { CopyButton } from "./CopyButton";
diff --git a/frontend-v2/src/components/v2/CreatableSelect/CreatableSelect.tsx b/frontend-v2/src/components/v2/CreatableSelect/CreatableSelect.tsx
new file mode 100644
index 000000000..b662ab121
--- /dev/null
+++ b/frontend-v2/src/components/v2/CreatableSelect/CreatableSelect.tsx
@@ -0,0 +1,68 @@
+import { GroupBase } from "react-select";
+import ReactSelectCreatable, { CreatableProps } from "react-select/creatable";
+import { twMerge } from "tailwind-merge";
+
+import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
+
+export const CreatableSelect = ({
+ isMulti,
+ closeMenuOnSelect,
+ ...props
+}: CreatableProps>) => {
+ return (
+ ({
+ ...base,
+ "input:focus": {
+ boxShadow: "none"
+ }
+ }),
+ multiValueLabel: (base) => ({
+ ...base,
+ whiteSpace: "normal",
+ overflow: "visible"
+ }),
+ control: (base) => ({
+ ...base,
+ transition: "none"
+ })
+ }}
+ components={{ DropdownIndicator, ClearIndicator, MultiValueRemove, Option }}
+ classNames={{
+ container: () => "w-full font-inter",
+ control: ({ isFocused }) =>
+ twMerge(
+ isFocused ? "border-primary-400/50" : "border-mineshaft-600 hover:border-gray-400",
+ "border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 hover:cursor-pointer"
+ ),
+ placeholder: () => "text-mineshaft-400 text-sm pl-1 py-0.5",
+ input: () => "pl-1 py-0.5",
+ valueContainer: () => `p-1 max-h-[14rem] ${isMulti ? "!overflow-y-scroll" : ""} gap-1`,
+ singleValue: () => "leading-7 ml-1",
+ multiValue: () => "bg-mineshaft-600 rounded items-center py-0.5 px-2 gap-1.5",
+ multiValueLabel: () => "leading-6 text-sm",
+ multiValueRemove: () => "hover:text-red text-bunker-400",
+ indicatorsContainer: () => "p-1 gap-1",
+ clearIndicator: () => "p-1 hover:text-red text-bunker-400",
+ indicatorSeparator: () => "bg-bunker-400",
+ dropdownIndicator: () => "text-bunker-200 p-1",
+ menu: () =>
+ "mt-2 border text-sm text-mineshaft-200 bg-mineshaft-900 border-mineshaft-600 rounded-md",
+ groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
+ option: ({ isFocused, isSelected }) =>
+ twMerge(
+ isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
+ isSelected && "text-mineshaft-200",
+ "hover:cursor-pointer text-xs px-3 py-2"
+ ),
+ noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
+ }}
+ {...props}
+ />
+ );
+};
diff --git a/frontend-v2/src/components/v2/CreatableSelect/index.tsx b/frontend-v2/src/components/v2/CreatableSelect/index.tsx
new file mode 100644
index 000000000..7a8676c3b
--- /dev/null
+++ b/frontend-v2/src/components/v2/CreatableSelect/index.tsx
@@ -0,0 +1 @@
+export * from "./CreatableSelect";
diff --git a/frontend-v2/src/components/v2/DatePicker/DatePicker.tsx b/frontend-v2/src/components/v2/DatePicker/DatePicker.tsx
new file mode 100644
index 000000000..528e25ca8
--- /dev/null
+++ b/frontend-v2/src/components/v2/DatePicker/DatePicker.tsx
@@ -0,0 +1,84 @@
+import { ChangeEventHandler, useState } from "react";
+import { DayPicker, DayPickerProps } from "react-day-picker";
+import { faCalendar } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { PopoverContentProps, PopoverProps } from "@radix-ui/react-popover";
+import { format, setHours, setMinutes } from "date-fns";
+
+import { Button } from "../Button";
+import { Input } from "../Input";
+import { Popover, PopoverContent, PopoverTrigger } from "../Popoverv2";
+
+export type DatePickerProps = Omit & {
+ value?: Date;
+ onChange: (date?: Date) => void;
+ popUpProps: PopoverProps;
+ popUpContentProps: PopoverContentProps;
+ dateFormat?: "PPP" | "PP" | "P"; // extend as needed
+};
+
+// Doc: https://react-day-picker.js.org/
+export const DatePicker = ({
+ value,
+ onChange,
+ popUpProps,
+ popUpContentProps,
+ dateFormat = "PPP",
+ ...props
+}: DatePickerProps) => {
+ const [timeValue, setTimeValue] = useState(value ? format(value, "HH:mm") : "00:00");
+
+ const handleTimeChange: ChangeEventHandler = (e) => {
+ const time = e.target.value;
+ if (time) {
+ setTimeValue(time);
+ if (value) {
+ const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10));
+ const newSelectedDate = setHours(setMinutes(value, minutes), hours);
+ onChange(newSelectedDate);
+ }
+ }
+ };
+
+ const handleDaySelect = (date: Date | undefined) => {
+ if (!timeValue || !date) {
+ onChange(date);
+ return;
+ }
+
+ const [hours, minutes] = timeValue.split(":").map((str) => parseInt(str, 10));
+ const newDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), hours, minutes);
+ onChange(newDate);
+ };
+
+ return (
+
+
+ }>
+ {value ? format(value, dateFormat) : "Pick a date and time"}
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/DatePicker/index.tsx b/frontend-v2/src/components/v2/DatePicker/index.tsx
new file mode 100644
index 000000000..86fa9e298
--- /dev/null
+++ b/frontend-v2/src/components/v2/DatePicker/index.tsx
@@ -0,0 +1,2 @@
+export type { DatePickerProps } from "./DatePicker";
+export { DatePicker } from "./DatePicker";
diff --git a/frontend-v2/src/components/v2/DeleteActionModal/DeleteActionModal.tsx b/frontend-v2/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
new file mode 100644
index 000000000..299b23bb5
--- /dev/null
+++ b/frontend-v2/src/components/v2/DeleteActionModal/DeleteActionModal.tsx
@@ -0,0 +1,107 @@
+import { ReactNode, useEffect, useState } from "react";
+
+import { useToggle } from "@app/hooks";
+
+import { Button } from "../Button";
+import { FormControl } from "../FormControl";
+import { Input } from "../Input";
+import { Modal, ModalClose, ModalContent } from "../Modal";
+
+type Props = {
+ isOpen?: boolean;
+ onClose?: () => void;
+ onChange?: (isOpen: boolean) => void;
+ deleteKey: string;
+ title: string;
+ subTitle?: string;
+ onDeleteApproved: () => Promise;
+ buttonText?: string;
+ children?: ReactNode;
+};
+
+export const DeleteActionModal = ({
+ isOpen,
+ onClose,
+ onChange,
+ deleteKey,
+ onDeleteApproved,
+ title,
+ subTitle = "This action is irreversible.",
+ buttonText = "Delete",
+ children
+}: Props): JSX.Element => {
+ const [inputData, setInputData] = useState("");
+ const [isLoading, setIsLoading] = useToggle();
+
+ useEffect(() => {
+ setInputData("");
+ }, [isOpen]);
+
+ const onDelete = async () => {
+ setIsLoading.on();
+ try {
+ await onDeleteApproved();
+ } catch {
+ setIsLoading.off();
+ } finally {
+ setIsLoading.off();
+ }
+ };
+
+ return (
+ {
+ setInputData("");
+ if (onChange) onChange(isOpenState);
+ }}
+ >
+
+
+ {buttonText}
+
+
+
+ Cancel
+
+ {" "}
+
+ }
+ onClose={onClose}
+ >
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/DeleteActionModal/index.tsx b/frontend-v2/src/components/v2/DeleteActionModal/index.tsx
new file mode 100644
index 000000000..92db0d89d
--- /dev/null
+++ b/frontend-v2/src/components/v2/DeleteActionModal/index.tsx
@@ -0,0 +1 @@
+export { DeleteActionModal } from "./DeleteActionModal";
diff --git a/frontend-v2/src/components/v2/Divider/Divider.tsx b/frontend-v2/src/components/v2/Divider/Divider.tsx
new file mode 100644
index 000000000..39b0f84c5
--- /dev/null
+++ b/frontend-v2/src/components/v2/Divider/Divider.tsx
@@ -0,0 +1,13 @@
+import { twMerge } from "tailwind-merge";
+
+interface IProps {
+ className?: string;
+}
+
+export const Divider = ({ className }: IProps): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/Divider/index.tsx b/frontend-v2/src/components/v2/Divider/index.tsx
new file mode 100644
index 000000000..ac407aa37
--- /dev/null
+++ b/frontend-v2/src/components/v2/Divider/index.tsx
@@ -0,0 +1 @@
+export { Divider } from "./Divider";
diff --git a/frontend-v2/src/components/v2/Drawer/Drawer.tsx b/frontend-v2/src/components/v2/Drawer/Drawer.tsx
new file mode 100644
index 000000000..027946a2e
--- /dev/null
+++ b/frontend-v2/src/components/v2/Drawer/Drawer.tsx
@@ -0,0 +1,84 @@
+import { forwardRef, ReactNode } from "react";
+import { faTimes } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { cva, VariantProps } from "cva";
+import { twMerge } from "tailwind-merge";
+
+import { Card, CardBody, CardFooter, CardTitle } from "../Card";
+import { IconButton } from "../IconButton";
+
+export type DrawerContentProps = DialogPrimitive.DialogContentProps & {
+ title?: ReactNode;
+ subTitle?: ReactNode;
+ footerContent?: ReactNode;
+ onClose?: () => void;
+} & VariantProps;
+
+const drawerContentVariation = cva(
+ "fixed ease-in-out duration-300 z-[90] border border-mineshaft-600 drop-shadow-2xl",
+ {
+ variants: {
+ direction: {
+ right: [
+ "right-0 top-0",
+ "h-full w-96",
+ "data-[state=open]:animate-drawerRightIn data-[state=closed]:animate-drawerRightOut"
+ ]
+ }
+ }
+ }
+);
+
+export const DrawerContent = forwardRef(
+ (
+ { children, title, subTitle, className, footerContent, direction = "right", onClose, ...props },
+ forwardedRef
+ ) => (
+
+
+
+
+ {title && (
+
+ {title}
+
+ )}
+
+ {children}
+
+ {footerContent && {footerContent} }{" "}
+
+
+
+
+
+
+
+
+ )
+);
+
+DrawerContent.displayName = "ModalContent";
+
+export type DrawerProps = Omit & { isOpen?: boolean };
+export const Drawer = ({ isOpen, ...props }: DrawerProps) => (
+
+);
+
+export const DrawerTrigger = DialogPrimitive.Trigger;
+export type DrawerTriggerProps = DialogPrimitive.DialogTriggerProps;
+
+export const DrawerClose = DialogPrimitive.Close;
+export type DrawerCloseProps = DialogPrimitive.DialogCloseProps;
diff --git a/frontend-v2/src/components/v2/Drawer/index.tsx b/frontend-v2/src/components/v2/Drawer/index.tsx
new file mode 100644
index 000000000..4f1a34981
--- /dev/null
+++ b/frontend-v2/src/components/v2/Drawer/index.tsx
@@ -0,0 +1 @@
+export { Drawer, DrawerClose, DrawerContent, DrawerTrigger } from "./Drawer";
diff --git a/frontend-v2/src/components/v2/Dropdown/Dropdown.tsx b/frontend-v2/src/components/v2/Dropdown/Dropdown.tsx
new file mode 100644
index 000000000..b02684154
--- /dev/null
+++ b/frontend-v2/src/components/v2/Dropdown/Dropdown.tsx
@@ -0,0 +1,170 @@
+import { ComponentPropsWithRef, ElementType, forwardRef, ReactNode, Ref } from "react";
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+import { twMerge } from "tailwind-merge";
+
+// Main menu or parent container
+export type DropdownMenuProps = DropdownMenuPrimitive.DropdownMenuProps;
+export const DropdownMenu = DropdownMenuPrimitive.Root;
+
+export type DropdownSubMenuProps = DropdownMenuPrimitive.DropdownMenuSubProps;
+export const DropdownSubMenu = DropdownMenuPrimitive.Sub;
+
+// trigger
+export type DropdownMenuTriggerProps = DropdownMenuPrimitive.DropdownMenuTriggerProps;
+export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+
+// item container
+export type DropdownMenuContentProps = DropdownMenuPrimitive.MenuContentProps;
+export const DropdownMenuContent = forwardRef(
+ ({ children, className, ...props }, forwardedRef) => {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+);
+
+DropdownMenuContent.displayName = "DropdownMenuContent";
+
+// item container
+export type DropdownSubMenuContentProps = DropdownMenuPrimitive.MenuSubContentProps;
+export const DropdownSubMenuContent = forwardRef(
+ ({ children, className, ...props }, forwardedRef) => {
+ return (
+
+
+ {children}
+
+
+ );
+ }
+);
+
+DropdownSubMenuContent.displayName = "DropdownMenuContent";
+
+// item label component
+export type DropdownLabelProps = DropdownMenuPrimitive.MenuLabelProps;
+export const DropdownMenuLabel = ({ className, ...props }: DropdownLabelProps) => (
+
+);
+
+// dropdown items
+export type DropdownMenuItemProps =
+ DropdownMenuPrimitive.MenuContentProps & {
+ icon?: ReactNode;
+ as?: T;
+ inputRef?: Ref;
+ iconPos?: "left" | "right";
+ };
+
+export const DropdownMenuItem = ({
+ children,
+ inputRef,
+ className,
+ icon,
+ as: Item = "button",
+ iconPos = "left",
+ isDisabled = false,
+ ...props
+}: DropdownMenuItemProps & ComponentPropsWithRef & { isDisabled?: boolean }) => (
+
+ -
+ {icon && iconPos === "left" &&
{icon} }
+ {children}
+ {icon && iconPos === "right" && {icon} }
+
+
+);
+
+// trigger
+export type DropdownSubMenuTriggerProps =
+ DropdownMenuPrimitive.DropdownMenuSubTriggerProps & {
+ icon?: ReactNode;
+ as?: T;
+ inputRef?: Ref;
+ iconPos?: "left" | "right";
+ };
+
+export const DropdownSubMenuTrigger = ({
+ children,
+ inputRef,
+ className,
+ icon,
+ as: Item = "button",
+ iconPos = "left",
+ ...props
+}: DropdownMenuItemProps & ComponentPropsWithRef) => (
+
+ -
+ {icon && iconPos === "left" &&
{icon} }
+ {children}
+ {icon && iconPos === "right" && {icon} }
+
+
+);
+
+// grouping items into 1
+export type DropdownMenuGroupProps = DropdownMenuPrimitive.DropdownMenuGroupProps;
+
+export const DropdownMenuGroup = forwardRef(
+ ({ ...props }, ref) => (
+
+ )
+);
+
+DropdownMenuGroup.displayName = "DropdownMenuGroup";
+
+// Divider
+export const DropdownMenuSeparator = forwardRef<
+ HTMLDivElement,
+ DropdownMenuPrimitive.DropdownMenuSeparatorProps
+>(({ className, ...props }, ref) => (
+
+));
+
+DropdownMenuSeparator.displayName = "DropdownMenuSeperator";
+
+DropdownMenuSeparator.displayName = "DropdownMenuSeperator";
diff --git a/frontend-v2/src/components/v2/Dropdown/index.tsx b/frontend-v2/src/components/v2/Dropdown/index.tsx
new file mode 100644
index 000000000..bde01449e
--- /dev/null
+++ b/frontend-v2/src/components/v2/Dropdown/index.tsx
@@ -0,0 +1,23 @@
+export type {
+ DropdownLabelProps,
+ DropdownMenuContentProps,
+ DropdownMenuGroupProps,
+ DropdownMenuItemProps,
+ DropdownMenuProps,
+ DropdownMenuTriggerProps,
+ DropdownSubMenuContentProps,
+ DropdownSubMenuProps,
+ DropdownSubMenuTriggerProps
+} from "./Dropdown";
+export {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuLabel,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+ DropdownSubMenu,
+ DropdownSubMenuContent,
+ DropdownSubMenuTrigger
+} from "./Dropdown";
diff --git a/frontend-v2/src/components/v2/EmailServiceSetupModal/EmailServiceSetupModal.tsx b/frontend-v2/src/components/v2/EmailServiceSetupModal/EmailServiceSetupModal.tsx
new file mode 100644
index 000000000..a81321cd5
--- /dev/null
+++ b/frontend-v2/src/components/v2/EmailServiceSetupModal/EmailServiceSetupModal.tsx
@@ -0,0 +1,22 @@
+import { Button } from "../Button";
+import { Modal, ModalContent } from "../Modal";
+
+type Props = {
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+};
+
+export const EmailServiceSetupModal = ({ isOpen, onOpenChange }: Props): JSX.Element => (
+
+
+
+ The administrators of this Infisical instance have not yet set up an email service provider
+ required to perform this action
+
+
+
+ Learn more
+
+
+
+);
diff --git a/frontend-v2/src/components/v2/EmailServiceSetupModal/index.tsx b/frontend-v2/src/components/v2/EmailServiceSetupModal/index.tsx
new file mode 100644
index 000000000..c9989623f
--- /dev/null
+++ b/frontend-v2/src/components/v2/EmailServiceSetupModal/index.tsx
@@ -0,0 +1 @@
+export { EmailServiceSetupModal } from "./EmailServiceSetupModal";
diff --git a/frontend-v2/src/components/v2/EmptyState/EmptyState.tsx b/frontend-v2/src/components/v2/EmptyState/EmptyState.tsx
new file mode 100644
index 000000000..9816a3fe2
--- /dev/null
+++ b/frontend-v2/src/components/v2/EmptyState/EmptyState.tsx
@@ -0,0 +1,34 @@
+import { ReactNode } from "react";
+import { SizeProp } from "@fortawesome/fontawesome-svg-core";
+import { faCubesStacked, IconDefinition } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
+
+type Props = {
+ title: ReactNode;
+ className?: string;
+ children?: ReactNode;
+ icon?: IconDefinition;
+ iconSize?: SizeProp;
+};
+
+export const EmptyState = ({
+ title,
+ className,
+ children,
+ icon = faCubesStacked,
+ iconSize = "2x"
+}: Props) => (
+
+);
diff --git a/frontend-v2/src/components/v2/EmptyState/index.tsx b/frontend-v2/src/components/v2/EmptyState/index.tsx
new file mode 100644
index 000000000..aac2d4d67
--- /dev/null
+++ b/frontend-v2/src/components/v2/EmptyState/index.tsx
@@ -0,0 +1 @@
+export { EmptyState } from "./EmptyState";
diff --git a/frontend-v2/src/components/v2/FilterableSelect/FilterableSelect.tsx b/frontend-v2/src/components/v2/FilterableSelect/FilterableSelect.tsx
new file mode 100644
index 000000000..dd8ba330d
--- /dev/null
+++ b/frontend-v2/src/components/v2/FilterableSelect/FilterableSelect.tsx
@@ -0,0 +1,81 @@
+import Select, { Props } from "react-select";
+import { twMerge } from "tailwind-merge";
+
+import { ClearIndicator, DropdownIndicator, MultiValueRemove, Option } from "../Select/components";
+
+export const FilterableSelect = ({
+ isMulti,
+ closeMenuOnSelect,
+ tabSelectsValue = false,
+ ...props
+}: Props) => (
+ ({
+ ...base,
+ "input:focus": {
+ boxShadow: "none"
+ }
+ }),
+ multiValueLabel: (base) => ({
+ ...base,
+ whiteSpace: "normal",
+ overflow: "visible"
+ }),
+ control: (base) => ({
+ ...base,
+ transition: "none"
+ })
+ }}
+ tabSelectsValue={tabSelectsValue}
+ components={{
+ DropdownIndicator,
+ ClearIndicator,
+ MultiValueRemove,
+ Option,
+ ...props.components
+ }}
+ classNames={{
+ container: ({ isDisabled }) =>
+ twMerge("w-full text-sm font-inter", isDisabled && "!pointer-events-auto opacity-50"),
+ control: ({ isFocused, isDisabled }) =>
+ twMerge(
+ isFocused ? "border-primary-400/50" : "border-mineshaft-600 ",
+ `border w-full p-0.5 rounded-md text-mineshaft-200 font-inter bg-mineshaft-900 ${
+ isDisabled ? "!cursor-not-allowed" : "hover:border-gray-400 hover:cursor-pointer"
+ } `
+ ),
+ placeholder: () =>
+ `${isMulti ? "py-[0.22rem]" : "leading-7"} text-mineshaft-400 text-sm pl-1`,
+ input: () => "pl-1",
+ valueContainer: () =>
+ `px-1 max-h-[8.2rem] ${
+ isMulti ? "!overflow-y-auto thin-scrollbar py-1" : "py-[0.1rem]"
+ } gap-1`,
+ singleValue: () => "leading-7 ml-1",
+ multiValue: () => "bg-mineshaft-600 text-sm rounded items-center py-0.5 px-2 gap-1.5",
+ multiValueLabel: () => "leading-6 text-sm",
+ multiValueRemove: () => "hover:text-red text-bunker-400",
+ indicatorsContainer: () => "p-1 gap-1",
+ clearIndicator: () => "p-1 hover:text-red text-bunker-400",
+ indicatorSeparator: () => "bg-bunker-400",
+ dropdownIndicator: () => "text-bunker-200 p-1",
+ menuList: () => "flex flex-col gap-1",
+ menu: () =>
+ "my-2 p-2 border text-sm text-mineshaft-200 thin-scrollbar bg-mineshaft-900 border-mineshaft-600 rounded-md",
+ groupHeading: () => "ml-3 mt-2 mb-1 text-mineshaft-400 text-sm",
+ option: ({ isFocused, isSelected }) =>
+ twMerge(
+ isFocused && "bg-mineshaft-700 active:bg-mineshaft-600",
+ isSelected && "text-mineshaft-200",
+ "hover:cursor-pointer rounded text-xs px-3 py-2"
+ ),
+ noOptionsMessage: () => "text-mineshaft-400 p-2 rounded-md"
+ }}
+ {...props}
+ />
+);
diff --git a/frontend-v2/src/components/v2/FilterableSelect/index.tsx b/frontend-v2/src/components/v2/FilterableSelect/index.tsx
new file mode 100644
index 000000000..03113e4fd
--- /dev/null
+++ b/frontend-v2/src/components/v2/FilterableSelect/index.tsx
@@ -0,0 +1 @@
+export * from "./FilterableSelect";
diff --git a/frontend-v2/src/components/v2/FontAwesomeSymbol/FontAwesomeSymbol.tsx b/frontend-v2/src/components/v2/FontAwesomeSymbol/FontAwesomeSymbol.tsx
new file mode 100644
index 000000000..69639ae5f
--- /dev/null
+++ b/frontend-v2/src/components/v2/FontAwesomeSymbol/FontAwesomeSymbol.tsx
@@ -0,0 +1,19 @@
+import { forwardRef, HTMLAttributes } from "react";
+
+type Props = {
+ symbolName: string;
+} & HTMLAttributes;
+
+export const FontAwesomeSymbol = forwardRef(
+ ({ symbolName, ...props }, ref) => {
+ return (
+
+
+
+
+
+ );
+ }
+);
+
+FontAwesomeSymbol.displayName = "FontAwesomeSymbol";
diff --git a/frontend-v2/src/components/v2/FontAwesomeSymbol/index.tsx b/frontend-v2/src/components/v2/FontAwesomeSymbol/index.tsx
new file mode 100644
index 000000000..baaa34e7e
--- /dev/null
+++ b/frontend-v2/src/components/v2/FontAwesomeSymbol/index.tsx
@@ -0,0 +1 @@
+export { FontAwesomeSymbol } from "./FontAwesomeSymbol";
diff --git a/frontend-v2/src/components/v2/FormControl/FormControl.tsx b/frontend-v2/src/components/v2/FormControl/FormControl.tsx
new file mode 100644
index 000000000..8651422a1
--- /dev/null
+++ b/frontend-v2/src/components/v2/FormControl/FormControl.tsx
@@ -0,0 +1,123 @@
+import { cloneElement, ReactElement, ReactNode } from "react";
+import { faExclamationTriangle, faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as Label from "@radix-ui/react-label";
+import { twMerge } from "tailwind-merge";
+
+import { Tooltip } from "../Tooltip";
+
+export type FormLabelProps = {
+ id?: string;
+ isRequired?: boolean;
+ isOptional?: boolean;
+ label?: ReactNode;
+ icon?: ReactNode;
+ className?: string;
+ tooltipClassName?: string;
+ tooltipText?: ReactNode;
+};
+
+export const FormLabel = ({
+ id,
+ label,
+ isRequired,
+ icon,
+ className,
+ isOptional,
+ tooltipClassName,
+ tooltipText
+}: FormLabelProps) => (
+
+ {label}
+ {isRequired && * }
+ {isOptional && - Optional }
+ {icon && !tooltipText && (
+
+ {icon}
+
+ )}
+ {tooltipText && (
+
+
+
+ )}
+
+);
+
+export type FormHelperTextProps = {
+ isError?: boolean;
+ text?: ReactNode;
+};
+
+export const FormHelperText = ({ isError, text }: FormHelperTextProps) => (
+
+ {isError && (
+
+
+
+ )}
+ {text}
+
+);
+
+export type FormControlProps = {
+ id?: string;
+ isRequired?: boolean;
+ isOptional?: boolean;
+ isError?: boolean;
+ label?: ReactNode;
+ helperText?: ReactNode;
+ errorText?: ReactNode;
+ children: JSX.Element;
+ className?: string;
+ icon?: ReactNode;
+ tooltipText?: ReactElement | string;
+ tooltipClassName?: string;
+};
+
+export const FormControl = ({
+ children,
+ isRequired,
+ isOptional,
+ label,
+ helperText,
+ errorText,
+ id,
+ isError,
+ icon,
+ className,
+ tooltipText,
+ tooltipClassName
+}: FormControlProps): JSX.Element => {
+ return (
+
+ {typeof label === "string" ? (
+
+ ) : (
+ label
+ )}
+ {cloneElement(children, { isRequired, "data-required": isRequired, isError })}
+ {!isError && helperText && }
+ {isError && errorText && }
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/FormControl/index.tsx b/frontend-v2/src/components/v2/FormControl/index.tsx
new file mode 100644
index 000000000..b48b788d4
--- /dev/null
+++ b/frontend-v2/src/components/v2/FormControl/index.tsx
@@ -0,0 +1,2 @@
+export type { FormControlProps, FormHelperTextProps, FormLabelProps } from "./FormControl";
+export { FormControl, FormHelperText, FormLabel } from "./FormControl";
diff --git a/frontend-v2/src/components/v2/HoverCard/HoverCard.tsx b/frontend-v2/src/components/v2/HoverCard/HoverCard.tsx
new file mode 100644
index 000000000..32393a238
--- /dev/null
+++ b/frontend-v2/src/components/v2/HoverCard/HoverCard.tsx
@@ -0,0 +1,36 @@
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as HoverCard from "@radix-ui/react-hover-card";
+
+type Props = {
+ text: string;
+ icon: IconProp;
+ color: string;
+};
+
+export type HoverCardProps = Props;
+
+export const HoverObject = ({ text, icon, color }: Props): JSX.Element => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+HoverObject.displayName = "HoverCard";
diff --git a/frontend-v2/src/components/v2/HoverCard/index.tsx b/frontend-v2/src/components/v2/HoverCard/index.tsx
new file mode 100644
index 000000000..13b7ee520
--- /dev/null
+++ b/frontend-v2/src/components/v2/HoverCard/index.tsx
@@ -0,0 +1,2 @@
+export type { HoverCardProps } from "./HoverCard";
+export { HoverObject } from "./HoverCard";
diff --git a/frontend-v2/src/components/v2/HoverCardv2/HoverCardv2.tsx b/frontend-v2/src/components/v2/HoverCardv2/HoverCardv2.tsx
new file mode 100644
index 000000000..816287349
--- /dev/null
+++ b/frontend-v2/src/components/v2/HoverCardv2/HoverCardv2.tsx
@@ -0,0 +1,26 @@
+import { ReactNode } from "react";
+import * as HoverCardPrimitive from "@radix-ui/react-hover-card";
+import { twMerge } from "tailwind-merge";
+
+export const HoverCardTrigger = HoverCardPrimitive.Trigger;
+
+export const HoverCard = HoverCardPrimitive.Root;
+
+export type HoverCardContentProps = {
+ children?: ReactNode;
+} & HoverCardPrimitive.HoverCardContentProps;
+
+export const HoverCardContent = ({ children, className, ...props }: HoverCardContentProps) => (
+
+
+ {children}
+
+
+
+);
diff --git a/frontend-v2/src/components/v2/HoverCardv2/index.tsx b/frontend-v2/src/components/v2/HoverCardv2/index.tsx
new file mode 100644
index 000000000..30419fca6
--- /dev/null
+++ b/frontend-v2/src/components/v2/HoverCardv2/index.tsx
@@ -0,0 +1 @@
+export { HoverCard, HoverCardContent, HoverCardTrigger } from "./HoverCardv2";
diff --git a/frontend-v2/src/components/v2/IconButton/IconButton.tsx b/frontend-v2/src/components/v2/IconButton/IconButton.tsx
new file mode 100644
index 000000000..2f6a2837a
--- /dev/null
+++ b/frontend-v2/src/components/v2/IconButton/IconButton.tsx
@@ -0,0 +1,149 @@
+import { ButtonHTMLAttributes, forwardRef, ReactNode } from "react";
+import { cva, VariantProps } from "cva";
+import { twMerge } from "tailwind-merge";
+
+type Props = {
+ children: ReactNode;
+ // This is kept as required because by accessibility convention and eslint
+ // when button doesn't have text an aria-label needs to be passed
+ ariaLabel: string;
+ isDisabled?: boolean;
+};
+
+const iconButtonVariants = cva(
+ [
+ "button",
+ "transition-all",
+ "font-inter font-medium user-select-none",
+ "cursor-pointer",
+ "inline-flex items-center justify-center",
+ "relative"
+ ],
+ {
+ variants: {
+ colorSchema: {
+ primary: ["bg-primary", "text-black", "border-primary hover:opacity-80"],
+ secondary: ["bg-mineshaft", "text-gray-300", "border-mineshaft hover:bg-bunker-400"],
+ danger: ["bg-[#973939]", "text-white", "border-red"]
+ },
+ variant: {
+ solid: "",
+ outline: ["bg-transparent", "border-2", "border-solid"],
+ plain: "",
+ star: "text-bunker-200 bg-mineshaft-500",
+ outline_bg: ""
+ },
+ isDisabled: {
+ true: "bg-opacity-70 cursor-not-allowed",
+ false: ""
+ },
+ isRounded: {
+ true: "rounded-md",
+ false: ""
+ },
+ size: {
+ xs: ["text-xs", "rounded-sm", "py-1.5", "px-2"],
+ sm: ["text-sm", "py-3", "px-3"],
+ md: ["text-md", "py-4", "px-4"],
+ lg: ["text-lg", "py-6", "px-6"]
+ }
+ },
+ compoundVariants: [
+ {
+ colorSchema: "primary",
+ variant: "star",
+ className: "hover:bg-primary hover:text-black"
+ },
+ {
+ colorSchema: "primary",
+ variant: "outline_bg",
+ className:
+ "bg-mineshaft-700 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200 hover:text-bunker-100 duration-100"
+ },
+ {
+ colorSchema: "danger",
+ variant: "star",
+ className: "hover:bg-red hover:text-white"
+ },
+ {
+ colorSchema: "primary",
+ variant: "outline",
+ className: "text-primary hover:bg-primary hover:text-black"
+ },
+ {
+ colorSchema: "secondary",
+ variant: "outline",
+ className: "hover:bg-mineshaft"
+ },
+ {
+ colorSchema: "danger",
+ variant: "outline",
+ className: "text-red hover:bg-red hover:text-black"
+ },
+ {
+ colorSchema: "primary",
+ variant: "plain",
+ className: "hover:text-primary"
+ },
+ {
+ colorSchema: "secondary",
+ variant: "plain",
+ className: "text-mineshaft"
+ },
+ {
+ colorSchema: "danger",
+ variant: "plain",
+ className: "hover:text-red"
+ },
+ {
+ colorSchema: ["danger", "primary", "secondary"],
+ variant: ["plain"],
+ className: "bg-transparent py-1 px-1 text-bunker-300"
+ }
+ ]
+ }
+);
+
+export type IconButtonProps = Omit, "aria-label"> &
+ VariantProps &
+ Props;
+
+export const IconButton = forwardRef(
+ (
+ {
+ children,
+ ariaLabel,
+ isDisabled = false,
+ className,
+ size = "sm",
+ variant = "solid",
+ isRounded = true,
+ colorSchema = "primary",
+ ...props
+ },
+ ref
+ ): JSX.Element => (
+
+ {children}
+
+ )
+);
+
+IconButton.displayName = "IconButton";
diff --git a/frontend-v2/src/components/v2/IconButton/index.tsx b/frontend-v2/src/components/v2/IconButton/index.tsx
new file mode 100644
index 000000000..a12a262b3
--- /dev/null
+++ b/frontend-v2/src/components/v2/IconButton/index.tsx
@@ -0,0 +1,2 @@
+export type { IconButtonProps } from "./IconButton";
+export { IconButton } from "./IconButton";
diff --git a/frontend-v2/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx b/frontend-v2/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx
new file mode 100644
index 000000000..45b45b01a
--- /dev/null
+++ b/frontend-v2/src/components/v2/InfisicalSecretInput/InfisicalSecretInput.tsx
@@ -0,0 +1,354 @@
+import { forwardRef, TextareaHTMLAttributes, useCallback, useMemo, useRef, useState } from "react";
+import { faCircle, faFolder, faKey } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as Popover from "@radix-ui/react-popover";
+
+import { useWorkspace } from "@app/context";
+import { useDebounce, useToggle } from "@app/hooks";
+import { useGetProjectFolders, useGetProjectSecrets } from "@app/hooks/api";
+
+import { SecretInput } from "../SecretInput";
+
+const getIndexOfUnclosedRefToTheLeft = (value: string, pos: number) => {
+ // take substring up to pos in order to consider edits for closed references
+ for (let i = pos; i >= 1; i -= 1) {
+ if (value[i] === "}") return -1;
+ if (value[i - 1] === "$" && value[i] === "{") {
+ return i;
+ }
+ }
+ return -1;
+};
+
+const getIndexOfUnclosedRefToTheRight = (value: string, pos: number) => {
+ // use it with above to identify an open ${
+ for (let i = pos; i < value.length; i += 1) {
+ if (value[i] === "}") return i - 1;
+ }
+ return -1;
+};
+
+const getClosingSymbol = (isSelectedSecret: boolean, isClosed: boolean) => {
+ if (!isClosed) {
+ return isSelectedSecret ? "}" : ".";
+ }
+ if (!isSelectedSecret) return ".";
+ return "";
+};
+
+const mod = (n: number, m: number) => ((n % m) + m) % m;
+
+export enum ReferenceType {
+ ENVIRONMENT = "environment",
+ FOLDER = "folder",
+ SECRET = "secret"
+}
+
+type Props = Omit, "onChange" | "value"> & {
+ value?: string;
+ onChange: (val: string) => void;
+ isImport?: boolean;
+ isVisible?: boolean;
+ isReadOnly?: boolean;
+ isDisabled?: boolean;
+ secretPath?: string;
+ environment?: string;
+ containerClassName?: string;
+};
+
+type ReferenceItem = {
+ label: string;
+ type: ReferenceType;
+ slug: string;
+};
+
+export const InfisicalSecretInput = forwardRef(
+ (
+ {
+ value = "",
+ onChange,
+ containerClassName,
+ secretPath: propSecretPath,
+ environment: propEnvironment,
+ ...props
+ },
+ ref
+ ) => {
+ const { currentWorkspace } = useWorkspace();
+ const workspaceId = currentWorkspace?.id || "";
+
+ const [debouncedValue] = useDebounce(value, 500);
+
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
+
+ const inputRef = useRef(null);
+ const popoverContentRef = useRef(null);
+ const [isFocused, setIsFocused] = useToggle(false);
+ const currentCursorPosition = inputRef.current?.selectionStart || 0;
+
+ const suggestionSource = useMemo(() => {
+ const left = getIndexOfUnclosedRefToTheLeft(debouncedValue, currentCursorPosition - 1);
+ if (left === -1) return { left, value: "", predicate: "", isDeep: false };
+
+ const suggestionSourceValue = debouncedValue.slice(left + 1, currentCursorPosition);
+ let suggestionSourceEnv: string | undefined = propEnvironment;
+ let suggestionSourceSecretPath: string | undefined = propSecretPath || "/";
+
+ // means its like ..<...more folder>.secret
+ const isDeep = suggestionSourceValue.includes(".");
+ let predicate = suggestionSourceValue;
+ if (isDeep) {
+ const [envSlug, ...folderPaths] = suggestionSourceValue.split(".");
+ const isValidEnvSlug = currentWorkspace?.environments.find((e) => e.slug === envSlug);
+ suggestionSourceEnv = isValidEnvSlug ? envSlug : undefined;
+ suggestionSourceSecretPath = `/${folderPaths.slice(0, -1)?.join("/")}`;
+ predicate = folderPaths[folderPaths.length - 1];
+ }
+
+ return {
+ left: left + 1,
+ // the full value inside a ${}
+ value: suggestionSourceValue,
+ // the final part after staging.dev..
+ predicate,
+ isOpen: left !== -1,
+ isDeep,
+ environment: suggestionSourceEnv,
+ secretPath: suggestionSourceSecretPath
+ };
+ }, [debouncedValue]);
+
+ const isPopupOpen = Boolean(suggestionSource.isOpen) && isFocused;
+ const { data: secrets } = useGetProjectSecrets({
+ environment: suggestionSource.environment || "",
+ secretPath: suggestionSource.secretPath || "",
+ workspaceId,
+ options: {
+ enabled: isPopupOpen
+ }
+ });
+ const { data: folders } = useGetProjectFolders({
+ environment: suggestionSource.environment || "",
+ path: suggestionSource.secretPath || "",
+ projectId: workspaceId,
+ options: {
+ enabled: isPopupOpen
+ }
+ });
+
+ const suggestions = useMemo(() => {
+ if (!isPopupOpen) return [];
+ // reset highlight whenever recomputation happens
+ setHighlightedIndex(-1);
+ const suggestionsArr: ReferenceItem[] = [];
+ const predicate = suggestionSource.predicate.toLowerCase();
+
+ if (!suggestionSource.isDeep) {
+ // At first level only environments and secrets
+ (currentWorkspace?.environments || []).forEach(({ name, slug }) => {
+ if (name.toLowerCase().startsWith(predicate))
+ suggestionsArr.push({
+ label: name,
+ slug,
+ type: ReferenceType.ENVIRONMENT
+ });
+ });
+ } else {
+ // one deeper levels its based on an environment folders and secrets
+ (folders || []).forEach(({ name }) => {
+ if (name.toLowerCase().startsWith(predicate))
+ suggestionsArr.push({
+ label: name,
+ slug: name,
+ type: ReferenceType.FOLDER
+ });
+ });
+ }
+ (secrets || []).forEach(({ key }) => {
+ if (key.toLowerCase().startsWith(predicate))
+ suggestionsArr.push({
+ label: key,
+ slug: key,
+ type: ReferenceType.SECRET
+ });
+ });
+ return suggestionsArr;
+ }, [secrets, folders, currentWorkspace?.environments, isPopupOpen, suggestionSource.value]);
+
+ const handleSuggestionSelect = (selectIndex?: number) => {
+ const selectedSuggestion =
+ suggestions[typeof selectIndex !== "undefined" ? selectIndex : highlightedIndex];
+ if (!selectedSuggestion) {
+ return;
+ }
+
+ const rightBracketIndex = getIndexOfUnclosedRefToTheRight(value, suggestionSource.left);
+ const isEnclosed = rightBracketIndex !== -1;
+ // ${}
+ const lhsValue = value.slice(0, suggestionSource.left);
+ const rhsValue = value.slice(
+ rightBracketIndex !== -1 ? rightBracketIndex + 1 : currentCursorPosition
+ );
+ // mid will be computed value inside the interpolation
+ const mid = suggestionSource.isDeep
+ ? `${suggestionSource.value.slice(0, -suggestionSource.predicate.length || undefined)}${
+ selectedSuggestion.slug
+ }`
+ : selectedSuggestion.slug;
+ // whether we should append . or closing bracket on selecting suggestion
+ const closingSymbol = getClosingSymbol(
+ selectedSuggestion.type === ReferenceType.SECRET,
+ isEnclosed
+ );
+
+ const newValue = `${lhsValue}${mid}${closingSymbol}${rhsValue}`;
+ onChange?.(newValue);
+ // this delay is for cursor adjustment
+ // cannot do this without a delay because what happens in onChange gets propogated after the cursor change
+ // Thus the cursor goes last to avoid that we put a slight delay on cursor change to make it happen later
+ const delay = setTimeout(() => {
+ clearTimeout(delay);
+ if (inputRef.current)
+ inputRef.current.selectionEnd =
+ lhsValue.length +
+ mid.length +
+ closingSymbol.length +
+ (isEnclosed && selectedSuggestion.type === ReferenceType.SECRET ? 1 : 0); // if secret is selected the cursor should move after the closing bracket -> }
+ }, 10);
+ setHighlightedIndex(-1); // reset highlight
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ // key operation should trigger only when popup is open
+ if (isPopupOpen) {
+ if (e.key === "ArrowDown" || (e.key === "Tab" && !e.shiftKey)) {
+ setHighlightedIndex((prevIndex) => {
+ const pos = mod(prevIndex + 1, suggestions.length);
+ popoverContentRef.current?.children?.[pos]?.scrollIntoView({
+ block: "nearest",
+ behavior: "smooth"
+ });
+ return pos;
+ });
+ } else if (e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey)) {
+ setHighlightedIndex((prevIndex) => {
+ const pos = mod(prevIndex - 1, suggestions.length);
+ popoverContentRef.current?.children?.[pos]?.scrollIntoView({
+ block: "nearest",
+ behavior: "smooth"
+ });
+ return pos;
+ });
+ } else if (e.key === "Enter" && highlightedIndex >= 0) {
+ e.preventDefault();
+ handleSuggestionSelect();
+ }
+ if (["ArrowDown", "ArrowUp", "Tab"].includes(e.key)) {
+ e.preventDefault();
+ }
+ }
+ };
+
+ const handlePopUpOpen = () => {
+ setHighlightedIndex(-1);
+ };
+
+ // to handle multiple ref for single component
+ const handleRef = useCallback((el: HTMLTextAreaElement) => {
+ // @ts-expect-error this is for multiple ref single component
+ inputRef.current = el;
+ if (ref) {
+ if (typeof ref === "function") {
+ ref(el);
+ } else {
+ // eslint-disable-next-line
+ ref.current = el;
+ }
+ }
+ }, []);
+
+ return (
+
+
+ setIsFocused.on()}
+ onBlur={(evt) => {
+ // should not on blur when its mouse down selecting a item from suggestion
+ if (!(evt.relatedTarget?.getAttribute("aria-label") === "suggestion-item"))
+ setIsFocused.off();
+ }}
+ onChange={(e) => onChange?.(e.target.value)}
+ containerClassName={containerClassName}
+ />
+
+ e.preventDefault()}
+ className="relative top-2 z-[100] max-h-64 overflow-auto rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
+ style={{
+ width: "var(--radix-popover-trigger-width)"
+ }}
+ >
+
+ {suggestions.map((item, i) => {
+ let entryIcon;
+ if (item.type === ReferenceType.SECRET) {
+ entryIcon = faKey;
+ } else if (item.type === ReferenceType.ENVIRONMENT) {
+ entryIcon = faCircle;
+ } else {
+ entryIcon = faFolder;
+ }
+
+ return (
+
{
+ if (e.key === "Enter") handleSuggestionSelect(i);
+ }}
+ aria-label="suggestion-item"
+ onClick={(e) => {
+ inputRef.current?.focus();
+ e.preventDefault();
+ e.stopPropagation();
+ handleSuggestionSelect(i);
+ }}
+ onMouseEnter={() => setHighlightedIndex(i)}
+ style={{ pointerEvents: "auto" }}
+ className="flex items-center justify-between border-mineshaft-600 text-left"
+ key={`secret-reference-secret-${i + 1}`}
+ >
+
+
+ );
+ })}
+
+
+
+ );
+ }
+);
+
+InfisicalSecretInput.displayName = "InfisicalSecretInput";
diff --git a/frontend-v2/src/components/v2/InfisicalSecretInput/index.tsx b/frontend-v2/src/components/v2/InfisicalSecretInput/index.tsx
new file mode 100644
index 000000000..bf5be9452
--- /dev/null
+++ b/frontend-v2/src/components/v2/InfisicalSecretInput/index.tsx
@@ -0,0 +1 @@
+export { InfisicalSecretInput } from "./InfisicalSecretInput";
diff --git a/frontend-v2/src/components/v2/Input/Input.tsx b/frontend-v2/src/components/v2/Input/Input.tsx
new file mode 100644
index 000000000..d325719e6
--- /dev/null
+++ b/frontend-v2/src/components/v2/Input/Input.tsx
@@ -0,0 +1,129 @@
+import { ChangeEvent, forwardRef, InputHTMLAttributes, ReactNode } from "react";
+import { cva, VariantProps } from "cva";
+import { twMerge } from "tailwind-merge";
+
+type Props = {
+ placeholder?: string;
+ isFullWidth?: boolean;
+ isRequired?: boolean;
+ leftIcon?: ReactNode;
+ rightIcon?: ReactNode;
+ isDisabled?: boolean;
+ isReadOnly?: boolean;
+ autoCapitalization?: boolean;
+ containerClassName?: string;
+};
+
+const inputVariants = cva(
+ "input w-full py-[0.375rem] text-gray-400 placeholder:text-sm placeholder-gray-500 placeholder-opacity-50 outline-none focus:ring-2 hover:ring-bunker-400/60 duration-100",
+ {
+ variants: {
+ size: {
+ xs: ["text-xs"],
+ sm: ["text-sm"],
+ md: ["text-md"],
+ lg: ["text-lg"]
+ },
+ isRounded: {
+ true: ["rounded-md"],
+ false: ""
+ },
+ variant: {
+ filled: ["bg-mineshaft-900", "text-gray-400"],
+ outline: ["bg-transparent"],
+ plain: "bg-transparent outline-none"
+ },
+ isError: {
+ true: "focus:ring-red/50 placeholder-red-300",
+ false: "focus:ring-primary-400/50 focus:ring-1"
+ }
+ },
+ compoundVariants: []
+ }
+);
+
+const inputParentContainerVariants = cva("inline-flex font-inter items-center border relative", {
+ variants: {
+ isRounded: {
+ true: ["rounded-md"],
+ false: ""
+ },
+ isError: {
+ true: "border-red",
+ false: "border-mineshaft-500"
+ },
+ isFullWidth: {
+ true: "w-full",
+ false: ""
+ },
+ variant: {
+ filled: ["bg-bunker-800", "text-gray-400"],
+ outline: ["bg-transparent"],
+ plain: "border-none"
+ }
+ }
+});
+
+export type InputProps = Omit, "size"> &
+ VariantProps &
+ Props;
+
+export const Input = forwardRef(
+ (
+ {
+ className,
+ containerClassName,
+ isRounded = true,
+ isFullWidth = true,
+ isDisabled,
+ isError = false,
+ isRequired,
+ leftIcon,
+ rightIcon,
+ variant = "filled",
+ size = "md",
+ isReadOnly,
+ autoCapitalization,
+ ...props
+ },
+ ref
+ ): JSX.Element => {
+ const handleInput = (event: ChangeEvent) => {
+ if (autoCapitalization) {
+ // eslint-disable-next-line no-param-reassign
+ event.target.value = event.target.value.toUpperCase();
+ }
+ };
+
+ return (
+
+ {leftIcon && {leftIcon} }
+
+ {rightIcon && {rightIcon} }
+
+ );
+ }
+);
+
+Input.displayName = "Input";
diff --git a/frontend-v2/src/components/v2/Input/index.tsx b/frontend-v2/src/components/v2/Input/index.tsx
new file mode 100644
index 000000000..ef5c3cc4e
--- /dev/null
+++ b/frontend-v2/src/components/v2/Input/index.tsx
@@ -0,0 +1,2 @@
+export type { InputProps } from "./Input";
+export { Input } from "./Input";
diff --git a/frontend-v2/src/components/v2/LeaveProjectModal/LeaveProjectModal.tsx b/frontend-v2/src/components/v2/LeaveProjectModal/LeaveProjectModal.tsx
new file mode 100644
index 000000000..563c33bef
--- /dev/null
+++ b/frontend-v2/src/components/v2/LeaveProjectModal/LeaveProjectModal.tsx
@@ -0,0 +1,104 @@
+import { useEffect, useState } from "react";
+
+import { useToggle } from "@app/hooks";
+
+import { Button } from "../Button";
+import { FormControl } from "../FormControl";
+import { Input } from "../Input";
+import { Modal, ModalClose, ModalContent } from "../Modal";
+
+type Props = {
+ deleteKey: string;
+ title: string;
+ onLeaveApproved: () => Promise;
+ onClose?: () => void;
+ onChange?: (isOpen: boolean) => void;
+ isOpen?: boolean;
+ subTitle?: string;
+ buttonText?: string;
+};
+
+export const LeaveProjectModal = ({
+ isOpen,
+ onClose,
+ onChange,
+ deleteKey,
+ onLeaveApproved,
+ title,
+ subTitle,
+ buttonText = "Leave Project"
+}: Props): JSX.Element => {
+ const [inputData, setInputData] = useState("");
+ const [isLoading, setIsLoading] = useToggle();
+
+ useEffect(() => {
+ setInputData("");
+ }, [isOpen]);
+
+ const onDelete = async () => {
+ setIsLoading.on();
+ try {
+ await onLeaveApproved();
+ } catch {
+ setIsLoading.off();
+ } finally {
+ setIsLoading.off();
+ }
+ };
+
+ return (
+ {
+ setInputData("");
+ if (onChange) onChange(isOpenState);
+ }}
+ >
+
+
+ {buttonText}
+
+
+
+ Cancel
+
+ {" "}
+
+ }
+ onClose={onClose}
+ >
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/LeaveProjectModal/index.tsx b/frontend-v2/src/components/v2/LeaveProjectModal/index.tsx
new file mode 100644
index 000000000..5d33aa108
--- /dev/null
+++ b/frontend-v2/src/components/v2/LeaveProjectModal/index.tsx
@@ -0,0 +1 @@
+export { LeaveProjectModal } from "./LeaveProjectModal";
diff --git a/frontend-v2/src/components/v2/Menu/Menu.tsx b/frontend-v2/src/components/v2/Menu/Menu.tsx
new file mode 100644
index 000000000..0555ef826
--- /dev/null
+++ b/frontend-v2/src/components/v2/Menu/Menu.tsx
@@ -0,0 +1,150 @@
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-nocheck
+/* eslint-disable import/no-extraneous-dependencies */
+/* eslint-disable global-require */
+import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
+import { motion } from "framer-motion";
+import Lottie from "lottie-react";
+import { twMerge } from "tailwind-merge";
+
+export type MenuProps = {
+ children: ReactNode;
+ className?: string;
+};
+
+export const Menu = ({ children, className }: MenuProps): JSX.Element => {
+ return ;
+};
+
+export type MenuItemProps = {
+ // Kudos to https://itnext.io/react-polymorphic-components-with-typescript-f7ce72ea7af2
+ as?: T;
+ children: ReactNode;
+ icon?: string;
+ description?: ReactNode;
+ isDisabled?: boolean;
+ isSelected?: boolean;
+ className?: string;
+ inputRef?: Ref;
+};
+
+export const MenuItem = ({
+ children,
+ icon,
+ className,
+ isDisabled,
+ isSelected,
+ as: Item = "button",
+ description,
+ // wrapping in forward ref with generic component causes the loss of ts definitions on props
+ inputRef,
+ ...props
+}: MenuItemProps & ComponentPropsWithRef): JSX.Element => {
+ const iconRef = useRef();
+
+ return (
+ iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}>
+
+
+ -
+
+ {/* {icon && {icon} } */}
+ {icon && (
+
+ )}
+ {children}
+
+ {description && {description} }
+
+
+
+ );
+};
+
+export const SubMenuItem = ({
+ children,
+ icon,
+ className,
+ isDisabled,
+ isSelected,
+ as: Item = "button",
+ description,
+ // wrapping in forward ref with generic component causes the loss of ts definitions on props
+ inputRef,
+ ...props
+}: MenuItemProps & ComponentPropsWithRef): JSX.Element => {
+ const iconRef = useRef();
+
+ return (
+ iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}>
+
+
+ -
+
+ {children}
+
+ {description && {description} }
+
+
+
+ );
+};
+
+MenuItem.displayName = "MenuItem";
+
+export type MenuGroupProps = {
+ children: ReactNode;
+ title: ReactNode;
+};
+
+export const MenuGroup = ({ children, title }: MenuGroupProps): JSX.Element => (
+ <>
+ {title}
+ {children}
+ >
+);
diff --git a/frontend-v2/src/components/v2/Menu/index.tsx b/frontend-v2/src/components/v2/Menu/index.tsx
new file mode 100644
index 000000000..97e9fb7ad
--- /dev/null
+++ b/frontend-v2/src/components/v2/Menu/index.tsx
@@ -0,0 +1,2 @@
+export type { MenuGroupProps, MenuItemProps, MenuProps } from "./Menu";
+export { Menu, MenuGroup, MenuItem } from "./Menu";
diff --git a/frontend-v2/src/components/v2/Modal/Modal.tsx b/frontend-v2/src/components/v2/Modal/Modal.tsx
new file mode 100644
index 000000000..a741d6d02
--- /dev/null
+++ b/frontend-v2/src/components/v2/Modal/Modal.tsx
@@ -0,0 +1,82 @@
+import { forwardRef, ReactNode } from "react";
+import { faTimes } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as DialogPrimitive from "@radix-ui/react-dialog";
+import { twMerge } from "tailwind-merge";
+
+import { Card, CardBody, CardFooter, CardTitle } from "../Card";
+import { IconButton } from "../IconButton";
+
+export type ModalContentProps = DialogPrimitive.DialogContentProps & {
+ title?: ReactNode;
+ subTitle?: ReactNode;
+ footerContent?: ReactNode;
+ bodyClassName?: string;
+ onClose?: () => void;
+ overlayClassName?: string;
+};
+
+export const ModalContent = forwardRef(
+ (
+ {
+ children,
+ title,
+ subTitle,
+ className,
+ overlayClassName,
+ footerContent,
+ bodyClassName,
+ onClose,
+ ...props
+ },
+ forwardedRef
+ ) => (
+
+
+
+
+ {title && {title} }
+
+ {children}
+
+ {footerContent && {footerContent} }
+
+
+
+
+
+
+
+
+ )
+);
+
+ModalContent.displayName = "ModalContent";
+
+export type ModalProps = Omit & { isOpen?: boolean };
+export const Modal = ({ isOpen, ...props }: ModalProps) => (
+
+);
+
+export const ModalTrigger = DialogPrimitive.Trigger;
+export type ModalTriggerProps = DialogPrimitive.DialogTriggerProps;
+
+export const ModalClose = DialogPrimitive.Close;
+export type ModalCloseProps = DialogPrimitive.DialogCloseProps;
diff --git a/frontend-v2/src/components/v2/Modal/index.tsx b/frontend-v2/src/components/v2/Modal/index.tsx
new file mode 100644
index 000000000..bb03f6be8
--- /dev/null
+++ b/frontend-v2/src/components/v2/Modal/index.tsx
@@ -0,0 +1,2 @@
+export type { ModalCloseProps, ModalContentProps, ModalProps, ModalTriggerProps } from "./Modal";
+export { Modal, ModalClose, ModalContent, ModalTrigger } from "./Modal";
diff --git a/frontend-v2/src/components/v2/NoticeBanner/NoticeBanner.tsx b/frontend-v2/src/components/v2/NoticeBanner/NoticeBanner.tsx
new file mode 100644
index 000000000..cf32427a3
--- /dev/null
+++ b/frontend-v2/src/components/v2/NoticeBanner/NoticeBanner.tsx
@@ -0,0 +1,26 @@
+import { ReactNode } from "react";
+import { faWarning, IconDefinition } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
+
+type Props = {
+ icon?: IconDefinition;
+ title: string;
+ children: ReactNode;
+ className?: string;
+};
+
+export const NoticeBanner = ({ icon = faWarning, title, children, className }: Props) => (
+
+);
diff --git a/frontend-v2/src/components/v2/NoticeBanner/index.tsx b/frontend-v2/src/components/v2/NoticeBanner/index.tsx
new file mode 100644
index 000000000..0006c75a5
--- /dev/null
+++ b/frontend-v2/src/components/v2/NoticeBanner/index.tsx
@@ -0,0 +1 @@
+export { NoticeBanner } from "./NoticeBanner";
diff --git a/frontend-v2/src/components/v2/Pagination/Pagination.tsx b/frontend-v2/src/components/v2/Pagination/Pagination.tsx
new file mode 100644
index 000000000..2d0e8b1a9
--- /dev/null
+++ b/frontend-v2/src/components/v2/Pagination/Pagination.tsx
@@ -0,0 +1,121 @@
+import { ReactElement } from "react";
+import {
+ faCaretDown,
+ faCheck,
+ faChevronLeft,
+ faChevronRight
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger
+} from "../Dropdown";
+import { IconButton } from "../IconButton";
+
+export type PaginationProps = {
+ count: number;
+ page: number;
+ perPage?: number;
+ onChangePage: (pageNumber: number) => void;
+ onChangePerPage: (newRows: number) => void;
+ className?: string;
+ perPageList?: number[];
+ startAdornment?: ReactElement;
+};
+
+export const Pagination = ({
+ count,
+ page = 1,
+ perPage = 20,
+ onChangePage,
+ onChangePerPage,
+ perPageList = [10, 20, 50, 100],
+ className,
+ startAdornment
+}: PaginationProps) => {
+ const prevPageNumber = Math.max(1, page - 1);
+ const canGoPrev = page > 1;
+
+ const upperLimit = Math.ceil(count / perPage);
+ const nextPageNumber = Math.min(upperLimit, page + 1);
+ const canGoNext = page + 1 <= upperLimit;
+ const canGoFirst = page > 1;
+ const canGoLast = page < upperLimit;
+
+ return (
+
+ {startAdornment}
+
+
+ {(page - 1) * perPage + 1} - {Math.min((page - 1) * perPage + perPage, count)} of {count}
+
+
+
+
+
+
+
+
+ {perPageList.map((perPageOption) => (
+ }
+ iconPos="right"
+ onClick={() => onChangePerPage(perPageOption)}
+ >
+ {perPageOption} rows per page
+
+ ))}
+
+
+
+
+ onChangePage(1)}
+ isDisabled={!canGoFirst}
+ >
+
+
+
+ onChangePage(prevPageNumber)}
+ isDisabled={!canGoPrev}
+ >
+
+
+ onChangePage(nextPageNumber)}
+ isDisabled={!canGoNext}
+ >
+
+
+ onChangePage(upperLimit)}
+ isDisabled={!canGoLast}
+ >
+
+
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/Pagination/index.tsx b/frontend-v2/src/components/v2/Pagination/index.tsx
new file mode 100644
index 000000000..583a0b550
--- /dev/null
+++ b/frontend-v2/src/components/v2/Pagination/index.tsx
@@ -0,0 +1,2 @@
+export type { PaginationProps } from "./Pagination";
+export { Pagination } from "./Pagination";
diff --git a/frontend-v2/src/components/v2/Popover/Popover.tsx b/frontend-v2/src/components/v2/Popover/Popover.tsx
new file mode 100644
index 000000000..04c5d688b
--- /dev/null
+++ b/frontend-v2/src/components/v2/Popover/Popover.tsx
@@ -0,0 +1,52 @@
+import { faXmark } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as Popover from "@radix-ui/react-popover";
+
+type Props = {
+ children: any;
+ text: string;
+ onChangeHandler: (value: string, id: string) => void;
+ id: string;
+};
+
+export type PopoverProps = Props;
+
+export const PopoverObject = ({ children, text, onChangeHandler, id }: Props) => (
+
+
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+);
+
+PopoverObject.displayName = "Popover";
diff --git a/frontend-v2/src/components/v2/Popover/index.tsx b/frontend-v2/src/components/v2/Popover/index.tsx
new file mode 100644
index 000000000..fa1d76998
--- /dev/null
+++ b/frontend-v2/src/components/v2/Popover/index.tsx
@@ -0,0 +1,2 @@
+export type { PopoverProps } from "./Popover";
+export { PopoverObject } from "./Popover";
diff --git a/frontend-v2/src/components/v2/Popoverv2/Popoverv2.tsx b/frontend-v2/src/components/v2/Popoverv2/Popoverv2.tsx
new file mode 100644
index 000000000..d78ede201
--- /dev/null
+++ b/frontend-v2/src/components/v2/Popoverv2/Popoverv2.tsx
@@ -0,0 +1,56 @@
+import { ReactNode } from "react";
+import { faTimes } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as PopoverPrimitive from "@radix-ui/react-popover";
+import { twMerge } from "tailwind-merge";
+
+import { IconButton } from "../IconButton";
+
+export const PopoverTrigger = PopoverPrimitive.Trigger;
+
+export const Popover = PopoverPrimitive.Root;
+
+export type PopoverContentProps = {
+ children?: ReactNode;
+ arrowClassName?: string;
+ hideCloseBtn?: boolean;
+} & PopoverPrimitive.PopoverContentProps;
+
+export const PopoverContent = ({
+ children,
+ className,
+ hideCloseBtn,
+ arrowClassName,
+ ...props
+}: PopoverContentProps) => (
+
+
+ {children}
+ {!hideCloseBtn && (
+
+
+
+
+
+ )}
+
+
+
+);
diff --git a/frontend-v2/src/components/v2/Popoverv2/index.tsx b/frontend-v2/src/components/v2/Popoverv2/index.tsx
new file mode 100644
index 000000000..ffad09cdc
--- /dev/null
+++ b/frontend-v2/src/components/v2/Popoverv2/index.tsx
@@ -0,0 +1 @@
+export { Popover, PopoverContent, PopoverTrigger } from "./Popoverv2";
diff --git a/frontend-v2/src/components/v2/RadioGroup/RadioGroup.tsx b/frontend-v2/src/components/v2/RadioGroup/RadioGroup.tsx
new file mode 100644
index 000000000..d03770455
--- /dev/null
+++ b/frontend-v2/src/components/v2/RadioGroup/RadioGroup.tsx
@@ -0,0 +1,40 @@
+/* eslint-disable jsx-a11y/label-has-associated-control */
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
+import { twMerge } from "tailwind-merge";
+
+export type RadioGroupProps = RadioGroupPrimitive.RadioGroupProps;
+
+// Note this component is not customizable (Heroku integration and potentially other pages depend on it)
+export const RadioGroup = ({ className, children, ...props }: RadioGroupProps) => (
+
+
+
+
+
+
+ App
+
+
+
+
+
+
+
+ Pipeline
+
+
+
+);
diff --git a/frontend-v2/src/components/v2/RadioGroup/index.tsx b/frontend-v2/src/components/v2/RadioGroup/index.tsx
new file mode 100644
index 000000000..bf62b2e6d
--- /dev/null
+++ b/frontend-v2/src/components/v2/RadioGroup/index.tsx
@@ -0,0 +1,2 @@
+export type { RadioGroupProps } from "./RadioGroup";
+export { RadioGroup } from "./RadioGroup";
diff --git a/frontend-v2/src/components/v2/SecretInput/SecretInput.tsx b/frontend-v2/src/components/v2/SecretInput/SecretInput.tsx
new file mode 100644
index 000000000..6818b0c37
--- /dev/null
+++ b/frontend-v2/src/components/v2/SecretInput/SecretInput.tsx
@@ -0,0 +1,113 @@
+/* eslint-disable react/no-danger */
+import { forwardRef, TextareaHTMLAttributes } from "react";
+import { twMerge } from "tailwind-merge";
+
+import { useToggle } from "@app/hooks";
+
+const REGEX = /(\${([a-zA-Z0-9-_.]+)})/g;
+const replaceContentWithDot = (str: string) => {
+ let finalStr = "";
+ for (let i = 0; i < str.length; i += 1) {
+ const char = str.at(i);
+ finalStr += char === "\n" ? "\n" : "*";
+ }
+ return finalStr;
+};
+
+const syntaxHighlight = (content?: string | null, isVisible?: boolean, isImport?: boolean) => {
+ if (isImport && !content) return "IMPORTED";
+ if (content === "") return "EMPTY";
+ if (!content) return "EMPTY";
+ if (!isVisible) return replaceContentWithDot(content);
+
+ let skipNext = false;
+ const formattedContent = content.split(REGEX).flatMap((el, i) => {
+ const isInterpolationSyntax = el.startsWith("${") && el.endsWith("}");
+ if (isInterpolationSyntax) {
+ skipNext = true;
+ return (
+
+ ${{el.slice(2, -1)}
+ }
+
+ );
+ }
+ if (skipNext) {
+ skipNext = false;
+ return [];
+ }
+ return el;
+ });
+
+ // akhilmhdh: Dont remove this br. I am still clueless how this works but weirdly enough
+ // when break is added a line break works properly
+ return formattedContent.concat( );
+};
+
+type Props = TextareaHTMLAttributes & {
+ value?: string | null;
+ isVisible?: boolean;
+ isImport?: boolean;
+ isReadOnly?: boolean;
+ isDisabled?: boolean;
+ containerClassName?: string;
+};
+
+const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
+
+export const SecretInput = forwardRef(
+ (
+ {
+ value,
+ isVisible,
+ isImport,
+ containerClassName,
+ onBlur,
+ isDisabled,
+ isReadOnly,
+ onFocus,
+ ...props
+ },
+ ref
+ ) => {
+ const [isSecretFocused, setIsSecretFocused] = useToggle();
+
+ return (
+
+
+
+
+
+ {syntaxHighlight(value, isVisible || isSecretFocused, isImport)}
+
+
+
+
+
+ );
+ }
+);
+
+SecretInput.displayName = "SecretInput";
diff --git a/frontend-v2/src/components/v2/SecretInput/index.tsx b/frontend-v2/src/components/v2/SecretInput/index.tsx
new file mode 100644
index 000000000..1c322fac4
--- /dev/null
+++ b/frontend-v2/src/components/v2/SecretInput/index.tsx
@@ -0,0 +1 @@
+export { SecretInput } from "./SecretInput";
diff --git a/frontend-v2/src/components/v2/SecretPathInput/SecretPathInput.tsx b/frontend-v2/src/components/v2/SecretPathInput/SecretPathInput.tsx
new file mode 100644
index 000000000..d8f4c7546
--- /dev/null
+++ b/frontend-v2/src/components/v2/SecretPathInput/SecretPathInput.tsx
@@ -0,0 +1,176 @@
+import { InputHTMLAttributes, useEffect, useState } from "react";
+import { faFolder } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as Popover from "@radix-ui/react-popover";
+import { twMerge } from "tailwind-merge";
+
+import { useWorkspace } from "@app/context";
+import { useDebounce } from "@app/hooks";
+import { useGetFoldersByEnv } from "@app/hooks/api";
+
+import { Input } from "../Input";
+
+type Props = Omit, "size" | "onChange"> & {
+ value?: string | null;
+ isImport?: boolean;
+ isVisible?: boolean;
+ isReadOnly?: boolean;
+ isDisabled?: boolean;
+ environment?: string;
+ containerClassName?: string;
+ onChange?: (arg: string) => void;
+};
+
+export const SecretPathInput = ({
+ containerClassName,
+ onChange,
+ environment,
+ value: propValue,
+ ...props
+}: Props) => {
+ const [inputValue, setInputValue] = useState(propValue ?? "");
+ const [secretPath, setSecretPath] = useState("/");
+ const [suggestions, setSuggestions] = useState([]);
+ const [isInputFocused, setIsInputFocus] = useState(false);
+ const [highlightedIndex, setHighlightedIndex] = useState(-1);
+ const [debouncedInputValue] = useDebounce(inputValue, 200);
+
+ const { currentWorkspace } = useWorkspace();
+ const workspaceId = currentWorkspace?.id || "";
+ const { folderNames: folders } = useGetFoldersByEnv({
+ path: secretPath,
+ environments: [environment || currentWorkspace?.environments?.[0].slug],
+ projectId: workspaceId
+ });
+
+ useEffect(() => {
+ setInputValue(propValue ?? "/");
+ }, [propValue]);
+
+ useEffect(() => {
+ // update secret path if input is valid
+ if (
+ (debouncedInputValue.length > 0 &&
+ debouncedInputValue[debouncedInputValue.length - 1] === "/") ||
+ debouncedInputValue.length === 0
+ ) {
+ setSecretPath(debouncedInputValue);
+ }
+ }, [debouncedInputValue]);
+
+ useEffect(() => {
+ // filter suggestions based on matching
+ const searchFragment = debouncedInputValue.split("/").pop() || "";
+ const filteredSuggestions = folders
+ .filter((suggestionEntry) =>
+ suggestionEntry.toUpperCase().startsWith(searchFragment.toUpperCase())
+ )
+ .sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
+
+ setSuggestions(filteredSuggestions);
+ }, [debouncedInputValue, folders]);
+
+ const handleSuggestionSelect = (selectedIndex: number) => {
+ if (!suggestions[selectedIndex]) {
+ return;
+ }
+
+ const validPaths = inputValue.split("/");
+ validPaths.pop();
+
+ // removed trailing slash
+ const newValue = `${validPaths.join("/")}/${suggestions[selectedIndex]}`;
+ onChange?.(newValue);
+ setInputValue(newValue);
+ setSecretPath(newValue);
+ setHighlightedIndex(-1);
+ setSuggestions([]);
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ const mod = (n: number, m: number) => ((n % m) + m) % m;
+ if (e.key === "ArrowDown") {
+ setHighlightedIndex((prevIndex) => mod(prevIndex + 1, suggestions.length));
+ } else if (e.key === "ArrowUp") {
+ setHighlightedIndex((prevIndex) => mod(prevIndex - 1, suggestions.length));
+ } else if (e.key === "Enter" && highlightedIndex >= 0) {
+ handleSuggestionSelect(highlightedIndex);
+ }
+ if (["ArrowDown", "ArrowUp", "Enter"].includes(e.key)) {
+ e.preventDefault();
+ }
+ };
+
+ const handleInputChange = (e: any) => {
+ // propagate event to react-hook-form onChange
+ if (onChange) {
+ onChange(e.target.value);
+ }
+
+ setInputValue(e.target.value);
+ };
+
+ return (
+ 0 && isInputFocused}
+ onOpenChange={() => {
+ setHighlightedIndex(-1);
+ }}
+ >
+
+ setIsInputFocus(true)}
+ onBlur={() => setIsInputFocus(false)}
+ value={inputValue}
+ onChange={handleInputChange}
+ className={containerClassName}
+ />
+
+ e.preventDefault()}
+ className={twMerge(
+ "relative top-2 z-[100] overflow-hidden rounded-md border border-mineshaft-600 bg-mineshaft-900 font-inter text-bunker-100 shadow-md"
+ )}
+ style={{
+ width: "var(--radix-popover-trigger-width)",
+ maxHeight: "var(--radix-select-content-available-height)"
+ }}
+ >
+
+ {suggestions.map((suggestion, i) => (
+
{
+ e.preventDefault();
+ setHighlightedIndex(i);
+ handleSuggestionSelect(i);
+ }}
+ style={{ pointerEvents: "auto" }}
+ className="flex items-center justify-between border-mineshaft-600 text-left"
+ key={`secret-reference-secret-${i + 1}`}
+ >
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/SecretPathInput/index.tsx b/frontend-v2/src/components/v2/SecretPathInput/index.tsx
new file mode 100644
index 000000000..5d8595891
--- /dev/null
+++ b/frontend-v2/src/components/v2/SecretPathInput/index.tsx
@@ -0,0 +1 @@
+export { SecretPathInput } from "./SecretPathInput";
diff --git a/frontend-v2/src/components/v2/Select/Select.tsx b/frontend-v2/src/components/v2/Select/Select.tsx
new file mode 100644
index 000000000..cfc70450b
--- /dev/null
+++ b/frontend-v2/src/components/v2/Select/Select.tsx
@@ -0,0 +1,186 @@
+import { forwardRef, ReactNode } from "react";
+import { IconProp } from "@fortawesome/fontawesome-svg-core";
+import { faCaretDown, faCaretUp, faCheck } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import * as SelectPrimitive from "@radix-ui/react-select";
+import { twMerge } from "tailwind-merge";
+
+import { Spinner } from "../Spinner";
+
+type Props = {
+ children: ReactNode;
+ placeholder?: string;
+ className?: string;
+ dropdownContainerClassName?: string;
+ containerClassName?: string;
+ isLoading?: boolean;
+ position?: "item-aligned" | "popper";
+ isDisabled?: boolean;
+ icon?: IconProp;
+ isMulti?: boolean;
+};
+
+export type SelectProps = Omit & Props;
+
+export const Select = forwardRef(
+ (
+ {
+ children,
+ placeholder,
+ className,
+ isLoading,
+ isDisabled,
+ dropdownContainerClassName,
+ position,
+ containerClassName,
+ ...props
+ },
+ ref
+ ): JSX.Element => {
+ return (
+
+
{
+ if (!props.onValueChange) return;
+
+ const newValue = value === "EMPTY-VALUE" ? "" : value;
+ props.onValueChange(newValue);
+ }}
+ disabled={isDisabled}
+ >
+
+
+ {props.icon && }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {isLoading ? (
+
+
+ Loading...
+
+ ) : (
+ children
+ )}
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+);
+
+Select.displayName = "Select";
+
+export type SelectItemProps = Omit & {
+ isDisabled?: boolean;
+ isSelected?: boolean;
+ customIcon?: IconProp;
+};
+
+export const SelectItem = forwardRef(
+ ({ children, className, isSelected, isDisabled, ...props }, forwardedRef) => {
+ return (
+
+
+
+
+ {children}
+
+ );
+ }
+);
+
+SelectItem.displayName = "SelectItem";
+
+export type SelectClearProps = Omit & {
+ onClear: () => void;
+ selectValue: string;
+};
+
+export const SelectClear = forwardRef(
+ (
+ { children, className, isSelected, isDisabled, onClear, selectValue, ...props },
+ forwardedRef
+ ) => {
+ return (
+ onClear()}
+ onClick={() => onClear()}
+ className={twMerge(
+ `relative mb-0.5 flex
+ cursor-pointer select-none items-center rounded-md py-2 pl-10 pr-4 text-sm
+ outline-none transition-all hover:bg-mineshaft-500 data-[highlighted]:bg-mineshaft-700/80`,
+ isSelected && "bg-primary",
+ isDisabled &&
+ "cursor-not-allowed text-gray-600 hover:bg-transparent hover:text-mineshaft-600",
+ className
+ )}
+ ref={forwardedRef}
+ >
+
+
+
+ {children}
+
+ );
+ }
+);
+SelectClear.displayName = "SelectClear";
diff --git a/frontend-v2/src/components/v2/Select/components/index.tsx b/frontend-v2/src/components/v2/Select/components/index.tsx
new file mode 100644
index 000000000..8d0e57d87
--- /dev/null
+++ b/frontend-v2/src/components/v2/Select/components/index.tsx
@@ -0,0 +1,47 @@
+import {
+ ClearIndicatorProps,
+ components,
+ DropdownIndicatorProps,
+ MultiValueRemoveProps,
+ OptionProps
+} from "react-select";
+import { faCheckCircle, faCircleXmark } from "@fortawesome/free-regular-svg-icons";
+import { faChevronDown, faXmark } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+
+export const DropdownIndicator = (props: DropdownIndicatorProps) => {
+ return (
+
+
+
+ );
+};
+
+export const ClearIndicator = (props: ClearIndicatorProps) => {
+ return (
+
+
+
+ );
+};
+
+export const MultiValueRemove = (props: MultiValueRemoveProps) => {
+ return (
+
+
+
+ );
+};
+
+export const Option = ({ isSelected, children, ...props }: OptionProps) => {
+ return (
+
+
+
{children}
+ {isSelected && (
+
+ )}
+
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/Select/index.tsx b/frontend-v2/src/components/v2/Select/index.tsx
new file mode 100644
index 000000000..3765851d5
--- /dev/null
+++ b/frontend-v2/src/components/v2/Select/index.tsx
@@ -0,0 +1,2 @@
+export type { SelectItemProps, SelectProps } from "./Select";
+export { Select, SelectClear, SelectItem } from "./Select";
diff --git a/frontend-v2/src/components/v2/Skeleton/Skeleton.tsx b/frontend-v2/src/components/v2/Skeleton/Skeleton.tsx
new file mode 100644
index 000000000..477456bb8
--- /dev/null
+++ b/frontend-v2/src/components/v2/Skeleton/Skeleton.tsx
@@ -0,0 +1,12 @@
+import { twMerge } from "tailwind-merge";
+
+export type Props = {
+ className?: string;
+};
+
+// To show something is coming up
+// Can be used with cards
+// Tables etc
+export const Skeleton = ({ className }: Props) => (
+
+);
diff --git a/frontend-v2/src/components/v2/Skeleton/index.tsx b/frontend-v2/src/components/v2/Skeleton/index.tsx
new file mode 100644
index 000000000..0ea770122
--- /dev/null
+++ b/frontend-v2/src/components/v2/Skeleton/index.tsx
@@ -0,0 +1 @@
+export { Skeleton } from "./Skeleton";
diff --git a/frontend-v2/src/components/v2/Spinner/Spinner.tsx b/frontend-v2/src/components/v2/Spinner/Spinner.tsx
new file mode 100644
index 000000000..d8a344e3e
--- /dev/null
+++ b/frontend-v2/src/components/v2/Spinner/Spinner.tsx
@@ -0,0 +1,42 @@
+import { twMerge } from "tailwind-merge";
+
+type SizeOptions = "xs" | "sm" | "md" | "lg";
+
+type Props = {
+ className?: string;
+ size?: SizeOptions;
+};
+
+const sizeChart: Record = {
+ xs: "w-4 h-4",
+ sm: "w-6 h-6",
+ md: "w-8 h-8",
+ lg: "w-12 h-12"
+};
+
+export const Spinner = ({ className, size = "md" }: Props): JSX.Element => {
+ return (
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/Spinner/index.tsx b/frontend-v2/src/components/v2/Spinner/index.tsx
new file mode 100644
index 000000000..47cb73c76
--- /dev/null
+++ b/frontend-v2/src/components/v2/Spinner/index.tsx
@@ -0,0 +1 @@
+export { Spinner } from "./Spinner";
diff --git a/frontend-v2/src/components/v2/Stepper/Stepper.tsx b/frontend-v2/src/components/v2/Stepper/Stepper.tsx
new file mode 100644
index 000000000..89a8e30ab
--- /dev/null
+++ b/frontend-v2/src/components/v2/Stepper/Stepper.tsx
@@ -0,0 +1,78 @@
+import { Children, cloneElement, ReactElement, ReactNode } from "react";
+import { faCheck } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { twMerge } from "tailwind-merge";
+
+export type StepperProps = {
+ activeStep: number;
+ children: ReactNode;
+ direction: "vertical" | "horizontal";
+ className?: string;
+};
+
+export const Stepper = ({ activeStep, children, direction, className }: StepperProps) => {
+ return (
+
+ {Children.map(children as ReactNode, (child: ReactNode, index) => {
+ const isCompleted = activeStep > index;
+ const isActive = index === activeStep;
+ const isNotLast = index + 1 !== (children as Array
).length;
+ return (
+
+
+
+ {isCompleted ? : index + 1}
+
+ {cloneElement(child as ReactElement, {
+ direction,
+ activeStep,
+ isCompleted,
+ isActive
+ })}
+
+ {isNotLast && (
+
+ )}
+
+ );
+ })}
+
+ );
+};
+
+export type StepProps = {
+ title: string;
+ description?: ReactNode;
+ // isActive?: boolean;
+ // isCompleted?: boolean;
+ // activeStep?: number;
+ // direction?: "vertical" | "horizontal";
+};
+
+export const Step = ({ title, description }: StepProps) => {
+ return (
+
+
{title}
+ {description &&
{description}
}
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/Stepper/index.tsx b/frontend-v2/src/components/v2/Stepper/index.tsx
new file mode 100644
index 000000000..a3d7aa3f8
--- /dev/null
+++ b/frontend-v2/src/components/v2/Stepper/index.tsx
@@ -0,0 +1,2 @@
+export type { StepperProps, StepProps } from "./Stepper";
+export { Step, Stepper } from "./Stepper";
diff --git a/frontend-v2/src/components/v2/Switch/Switch.tsx b/frontend-v2/src/components/v2/Switch/Switch.tsx
new file mode 100644
index 000000000..ce955354a
--- /dev/null
+++ b/frontend-v2/src/components/v2/Switch/Switch.tsx
@@ -0,0 +1,44 @@
+import { ReactNode } from "react";
+import * as SwitchPrimitive from "@radix-ui/react-switch";
+import { twMerge } from "tailwind-merge";
+
+export type SwitchProps = Omit & {
+ children?: ReactNode;
+ id: string;
+ isChecked?: boolean;
+ isRequired?: boolean;
+ isDisabled?: boolean;
+ containerClassName?: string;
+};
+
+export const Switch = ({
+ children,
+ id,
+ className,
+ isChecked,
+ isDisabled,
+ isRequired,
+ containerClassName,
+ ...props
+}: SwitchProps): JSX.Element => (
+
+
+ {children}
+ {isRequired && * }
+
+
+
+
+
+);
diff --git a/frontend-v2/src/components/v2/Switch/index.tsx b/frontend-v2/src/components/v2/Switch/index.tsx
new file mode 100644
index 000000000..6ac4908d2
--- /dev/null
+++ b/frontend-v2/src/components/v2/Switch/index.tsx
@@ -0,0 +1,2 @@
+export type { SwitchProps } from "./Switch";
+export { Switch } from "./Switch";
diff --git a/frontend-v2/src/components/v2/Table/Table.tsx b/frontend-v2/src/components/v2/Table/Table.tsx
new file mode 100644
index 000000000..065efab1e
--- /dev/null
+++ b/frontend-v2/src/components/v2/Table/Table.tsx
@@ -0,0 +1,159 @@
+import { DetailedHTMLProps, HTMLAttributes, ReactNode, TdHTMLAttributes } from "react";
+import { twMerge } from "tailwind-merge";
+
+import { Skeleton } from "../Skeleton";
+
+export type TableContainerProps = {
+ children: ReactNode;
+ isRounded?: boolean;
+ className?: string;
+} & DetailedHTMLProps, HTMLDivElement>;
+
+export const TableContainer = ({
+ children,
+ className,
+ isRounded = true,
+ ...props
+}: TableContainerProps): JSX.Element => (
+
+ {children}
+
+);
+
+// main parent table
+export type TableProps = {
+ className?: string;
+ children: ReactNode;
+};
+
+export const Table = ({ children, className }: TableProps): JSX.Element => (
+
+);
+
+// table head
+export type THeadProps = {
+ children: ReactNode;
+ className?: string;
+};
+
+export const THead = ({ children, className }: THeadProps): JSX.Element => (
+
+ {children}
+
+);
+
+export type TFootProps = {
+ children: ReactNode;
+ className?: string;
+};
+
+export const TFoot = ({ children, className }: TFootProps): JSX.Element => (
+
+ {children}
+
+);
+
+// table rows
+export type TrProps = {
+ children: ReactNode;
+ className?: string;
+ isHoverable?: boolean;
+ isSelectable?: boolean;
+} & HTMLAttributes;
+
+export const Tr = ({
+ children,
+ className,
+ isHoverable,
+ isSelectable,
+ ...props
+}: TrProps): JSX.Element => (
+
+ {children}
+
+);
+
+// table head columns
+export type ThProps = {
+ children?: ReactNode;
+ className?: string;
+};
+
+export const Th = ({ children, className }: ThProps): JSX.Element => (
+
+ {children}
+
+);
+
+// table body
+export type TBodyProps = {
+ children: ReactNode;
+ className?: string;
+};
+
+export const TBody = ({ children, className }: TBodyProps): JSX.Element => (
+ {children}
+);
+
+// table body columns
+export type TdProps = {
+ children?: ReactNode;
+ className?: string;
+} & TdHTMLAttributes;
+
+export const Td = ({ children, className, ...props }: TdProps): JSX.Element => (
+
+ {children}
+
+);
+
+export type TBodyLoader = {
+ rows?: number;
+ columns: number;
+ className?: string;
+ // unique key for mapping
+ innerKey: string;
+};
+
+export const TableSkeleton = ({
+ rows = 3,
+ columns,
+ innerKey,
+ className
+}: TBodyLoader): JSX.Element => (
+ <>
+ {Array.apply(0, Array(rows)).map((_x, i) => (
+
+ {Array.apply(0, Array(columns)).map((_y, j) => (
+
+
+
+ ))}
+
+ ))}
+ >
+);
diff --git a/frontend-v2/src/components/v2/Table/index.tsx b/frontend-v2/src/components/v2/Table/index.tsx
new file mode 100644
index 000000000..ef3987cc0
--- /dev/null
+++ b/frontend-v2/src/components/v2/Table/index.tsx
@@ -0,0 +1,11 @@
+export type {
+ TableContainerProps,
+ TableProps,
+ TBodyProps,
+ TdProps,
+ TFootProps,
+ THeadProps,
+ ThProps,
+ TrProps
+} from "./Table";
+export { Table, TableContainer, TableSkeleton, TBody, Td, TFoot, Th, THead, Tr } from "./Table";
diff --git a/frontend-v2/src/components/v2/Tabs/Tabs.tsx b/frontend-v2/src/components/v2/Tabs/Tabs.tsx
new file mode 100644
index 000000000..ca5c22dcb
--- /dev/null
+++ b/frontend-v2/src/components/v2/Tabs/Tabs.tsx
@@ -0,0 +1,46 @@
+import * as TabsPrimitive from "@radix-ui/react-tabs";
+import { twMerge } from "tailwind-merge";
+
+export type TabsProps = TabsPrimitive.TabsProps;
+
+export const Tabs = ({ className, children, ...props }: TabsProps) => (
+
+ {children}
+
+);
+
+export type TabListProps = TabsPrimitive.TabsListProps;
+
+export const TabList = ({ className, children, ...props }: TabListProps) => (
+
+ {children}
+
+);
+
+export type TabProps = TabsPrimitive.TabsTriggerProps;
+
+export const Tab = ({ className, children, ...props }: TabProps) => (
+
+ {children}
+
+);
+
+export type TabPanelProps = TabsPrimitive.TabsContentProps;
+
+export const TabPanel = ({ className, children, ...props }: TabPanelProps) => (
+
+ {children}
+
+);
diff --git a/frontend-v2/src/components/v2/Tabs/index.tsx b/frontend-v2/src/components/v2/Tabs/index.tsx
new file mode 100644
index 000000000..0a17c0dca
--- /dev/null
+++ b/frontend-v2/src/components/v2/Tabs/index.tsx
@@ -0,0 +1,2 @@
+export type { TabListProps, TabPanelProps, TabProps, TabsProps } from "./Tabs";
+export { Tab, TabList, TabPanel, Tabs } from "./Tabs";
diff --git a/frontend-v2/src/components/v2/Tag/Tag.tsx b/frontend-v2/src/components/v2/Tag/Tag.tsx
new file mode 100644
index 000000000..7210e388b
--- /dev/null
+++ b/frontend-v2/src/components/v2/Tag/Tag.tsx
@@ -0,0 +1,39 @@
+import { ReactNode } from "react";
+import { faClose } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { cva, VariantProps } from "cva";
+import { twMerge } from "tailwind-merge";
+
+type Props = {
+ children: ReactNode;
+ className?: string;
+ onClose?: () => void;
+} & VariantProps;
+
+const tagVariants = cva(
+ "inline-flex items-center whitespace-nowrap text-sm rounded mr-1.5 text-bunker-200 text-gray-400 ",
+ {
+ variants: {
+ colorSchema: {
+ gray: "bg-mineshaft-500",
+ red: "bg-red/80 text-bunker-100",
+ green: "bg-primary-800 text-white"
+ },
+ size: {
+ xs: "text-xs px-1 py-0.5",
+ sm: "px-2 py-0.5"
+ }
+ }
+ }
+);
+
+export const Tag = ({ children, className, colorSchema = "gray", size = "sm", onClose }: Props) => (
+
+ {children}
+ {onClose && (
+
+
+
+ )}
+
+);
diff --git a/frontend-v2/src/components/v2/Tag/index.tsx b/frontend-v2/src/components/v2/Tag/index.tsx
new file mode 100644
index 000000000..736cac7be
--- /dev/null
+++ b/frontend-v2/src/components/v2/Tag/index.tsx
@@ -0,0 +1 @@
+export { Tag } from "./Tag";
diff --git a/frontend-v2/src/components/v2/TextArea/TextArea.tsx b/frontend-v2/src/components/v2/TextArea/TextArea.tsx
new file mode 100644
index 000000000..a1e71edd2
--- /dev/null
+++ b/frontend-v2/src/components/v2/TextArea/TextArea.tsx
@@ -0,0 +1,77 @@
+import { forwardRef, TextareaHTMLAttributes } from "react";
+import { cva, VariantProps } from "cva";
+import { twMerge } from "tailwind-merge";
+
+type Props = {
+ isDisabled?: boolean;
+ placeholder?: string;
+ isFullWidth?: boolean;
+ isRequired?: boolean;
+ reSize?: "none" | "both" | "vertical" | "horizontal";
+};
+
+const textAreaVariants = cva(
+ "textarea w-full p-2 focus:ring-2 ring-primary-800 outline-none border text-gray-400 font-inter placeholder-gray-500 placeholder-opacity-50",
+ {
+ variants: {
+ size: {
+ xs: ["text-xs"],
+ sm: ["text-sm"],
+ md: ["text-md"],
+ lg: ["text-lg"]
+ },
+ isRounded: {
+ true: ["rounded-md"],
+ false: ""
+ },
+ variant: {
+ filled: ["bg-mineshaft-900", "text-gray-400"],
+ outline: ["bg-transparent"],
+ plain: "bg-transparent outline-none"
+ },
+ isError: {
+ true: "focus:ring-red/50 placeholder-red-300 border-red",
+ false: "focus:ring-primary-400/50 focus:ring-1 border-mineshaft-500"
+ }
+ },
+ compoundVariants: [
+ {
+ variant: "plain",
+ isError: [true, false],
+ className: "border-none"
+ }
+ ]
+ }
+);
+
+export type TextAreaProps = Omit, "size" | "disabled"> &
+ VariantProps &
+ Props;
+
+export const TextArea = forwardRef(
+ (
+ {
+ className,
+ isRounded = true,
+ isDisabled = false,
+ isError = false,
+ isRequired,
+ variant = "filled",
+ size = "md",
+ reSize = "both",
+ ...props
+ },
+ ref
+ ): JSX.Element => (
+
+ )
+);
+
+TextArea.displayName = "TextArea";
diff --git a/frontend-v2/src/components/v2/TextArea/index.tsx b/frontend-v2/src/components/v2/TextArea/index.tsx
new file mode 100644
index 000000000..6c44fcbaa
--- /dev/null
+++ b/frontend-v2/src/components/v2/TextArea/index.tsx
@@ -0,0 +1,2 @@
+export type { TextAreaProps } from "./TextArea";
+export { TextArea } from "./TextArea";
diff --git a/frontend-v2/src/components/v2/Tooltip/Tooltip.tsx b/frontend-v2/src/components/v2/Tooltip/Tooltip.tsx
new file mode 100644
index 000000000..abfebc5b4
--- /dev/null
+++ b/frontend-v2/src/components/v2/Tooltip/Tooltip.tsx
@@ -0,0 +1,70 @@
+import { ReactNode } from "react";
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+import { twMerge } from "tailwind-merge";
+
+export type TooltipProps = Omit & {
+ children: ReactNode;
+ content?: ReactNode;
+ isOpen?: boolean;
+ className?: string;
+ asChild?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ defaultOpen?: boolean;
+ position?: "top" | "bottom" | "left" | "right";
+ isDisabled?: boolean;
+ center?: boolean;
+ size?: "sm" | "md";
+};
+
+export const Tooltip = ({
+ children,
+ content,
+ isOpen,
+ onOpenChange,
+ defaultOpen,
+ className,
+ center,
+ asChild = true,
+ isDisabled,
+ position = "top",
+ size = "md",
+ ...props
+}: TooltipProps) =>
+ // just render children if tooltip content is empty
+ content ? (
+
+ {children}
+
+ {content}
+
+
+
+ ) : (
+ // eslint-disable-next-line react/jsx-no-useless-fragment
+ <>{children}>
+ );
+
+export const TooltipProvider = TooltipPrimitive.Provider;
diff --git a/frontend-v2/src/components/v2/Tooltip/index.tsx b/frontend-v2/src/components/v2/Tooltip/index.tsx
new file mode 100644
index 000000000..348059dad
--- /dev/null
+++ b/frontend-v2/src/components/v2/Tooltip/index.tsx
@@ -0,0 +1 @@
+export { Tooltip, TooltipProvider } from "./Tooltip";
diff --git a/frontend-v2/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx b/frontend-v2/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx
new file mode 100644
index 000000000..19621a25d
--- /dev/null
+++ b/frontend-v2/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx
@@ -0,0 +1,71 @@
+import { useOrganization, useSubscription } from "@app/context";
+import { useGetOrgTrialUrl } from "@app/hooks/api";
+
+import { Button } from "../Button";
+import { Modal, ModalContent } from "../Modal";
+
+type Props = {
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
+ text: string;
+};
+
+export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
+ const { subscription } = useSubscription();
+ const { currentOrg } = useOrganization();
+ const { mutateAsync, isLoading } = useGetOrgTrialUrl();
+ const link =
+ subscription && subscription.slug !== null
+ ? `/org/${currentOrg?.id}/billing`
+ : "https://infisical.com/scheduledemo";
+
+ const handleUpgradeBtnClick = async () => {
+ try {
+ if (!subscription || !currentOrg) return;
+
+ if (!subscription.has_used_trial) {
+ // direct user to start pro trial
+
+ const url = await mutateAsync({
+ orgId: currentOrg.id,
+ success_url: window.location.href
+ });
+
+ window.location.href = url;
+ } else {
+ // direct user to upgrade their plan
+ window.location.href = link;
+ }
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ return (
+
+
+ {text}
+
+ Upgrade and get access to this, as well as to other powerful enhancements.
+
+
+
+ {subscription && !subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
+
+ onOpenChange && onOpenChange(false)}
+ >
+ Cancel
+
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/UpgradePlanModal/index.tsx b/frontend-v2/src/components/v2/UpgradePlanModal/index.tsx
new file mode 100644
index 000000000..09a53d31c
--- /dev/null
+++ b/frontend-v2/src/components/v2/UpgradePlanModal/index.tsx
@@ -0,0 +1 @@
+export { UpgradePlanModal } from "./UpgradePlanModal";
diff --git a/frontend-v2/src/components/v2/index.tsx b/frontend-v2/src/components/v2/index.tsx
new file mode 100644
index 000000000..f35d006e9
--- /dev/null
+++ b/frontend-v2/src/components/v2/index.tsx
@@ -0,0 +1,37 @@
+/* eslint-disable react-refresh/only-export-components */
+export * from "./Accordion";
+export * from "./Alert";
+export * from "./Badge";
+export * from "./Button";
+export * from "./Card";
+export * from "./Checkbox";
+export * from "./ContentLoader";
+export * from "./DatePicker";
+export * from "./DeleteActionModal";
+export * from "./Drawer";
+export * from "./Dropdown";
+export * from "./EmailServiceSetupModal";
+export * from "./EmptyState";
+export * from "./FilterableSelect";
+export * from "./FontAwesomeSymbol";
+export * from "./FormControl";
+export * from "./HoverCardv2";
+export * from "./IconButton";
+export * from "./Input";
+export * from "./Menu";
+export * from "./Modal";
+export * from "./NoticeBanner";
+export * from "./Pagination";
+export * from "./Popoverv2";
+export * from "./SecretInput";
+export * from "./Select";
+export * from "./Skeleton";
+export * from "./Spinner";
+export * from "./Stepper";
+export * from "./Switch";
+export * from "./Table";
+export * from "./Tabs";
+export * from "./Tag";
+export * from "./TextArea";
+export * from "./Tooltip";
+export * from "./UpgradePlanModal";
diff --git a/frontend-v2/src/components/v2/projects/NewProjectModal.tsx b/frontend-v2/src/components/v2/projects/NewProjectModal.tsx
new file mode 100644
index 000000000..1662b07ff
--- /dev/null
+++ b/frontend-v2/src/components/v2/projects/NewProjectModal.tsx
@@ -0,0 +1,330 @@
+import { FC, useEffect } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { useRouter } from "next/router";
+import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { zodResolver } from "@hookform/resolvers/zod";
+import z from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import { OrgPermissionCan } from "@app/components/permissions";
+import {
+ Accordion,
+ AccordionContent,
+ AccordionItem,
+ AccordionTrigger,
+ Button,
+ Checkbox,
+ FormControl,
+ Input,
+ Modal,
+ ModalClose,
+ ModalContent,
+ Select,
+ SelectItem,
+ TextArea
+} from "@app/components/v2";
+import {
+ OrgPermissionActions,
+ OrgPermissionSubjects,
+ useOrganization,
+ useOrgPermission,
+ useSubscription,
+ useUser
+} from "@app/context";
+import {
+ fetchOrgUsers,
+ useAddUserToWsNonE2EE,
+ useCreateWorkspace,
+ useGetExternalKmsList,
+ useGetUserWorkspaces
+} from "@app/hooks/api";
+import { INTERNAL_KMS_KEY_ID } from "@app/hooks/api/kms/types";
+import { InfisicalProjectTemplate, useListProjectTemplates } from "@app/hooks/api/projectTemplates";
+
+const formSchema = z.object({
+ name: z.string().trim().min(1, "Required").max(64, "Too long, maximum length is 64 characters"),
+ description: z
+ .string()
+ .trim()
+ .max(256, "Description too long, max length is 256 characters")
+ .optional(),
+ addMembers: z.boolean(),
+ kmsKeyId: z.string(),
+ template: z.string()
+});
+
+type TAddProjectFormData = z.infer;
+
+interface NewProjectModalProps {
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+}
+
+type NewProjectFormProps = Pick;
+
+const NewProjectForm = ({ onOpenChange }: NewProjectFormProps) => {
+ const router = useRouter();
+ const { currentOrg } = useOrganization();
+ const { permission } = useOrgPermission();
+ const { user } = useUser();
+ const createWs = useCreateWorkspace();
+ const { refetch: refetchWorkspaces } = useGetUserWorkspaces();
+ const addUsersToProject = useAddUserToWsNonE2EE();
+ const { subscription } = useSubscription();
+
+ const canReadProjectTemplates = permission.can(
+ OrgPermissionActions.Read,
+ OrgPermissionSubjects.ProjectTemplates
+ );
+
+ const { data: projectTemplates = [] } = useListProjectTemplates({
+ enabled: Boolean(canReadProjectTemplates && subscription?.projectTemplates)
+ });
+
+ const { data: externalKmsList } = useGetExternalKmsList(currentOrg?.id!, {
+ enabled: permission.can(OrgPermissionActions.Read, OrgPermissionSubjects.Kms)
+ });
+
+ const {
+ control,
+ handleSubmit,
+ reset,
+ formState: { isSubmitting, errors }
+ } = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ kmsKeyId: INTERNAL_KMS_KEY_ID,
+ template: InfisicalProjectTemplate.Default
+ }
+ });
+
+ useEffect(() => {
+ if (Object.keys(errors).length > 0) {
+ console.log("Current form errors:", errors);
+ }
+ }, [errors]);
+
+ const onCreateProject = async ({
+ name,
+ description,
+ addMembers,
+ kmsKeyId,
+ template
+ }: TAddProjectFormData) => {
+ // type check
+ if (!currentOrg) return;
+ if (!user) return;
+ try {
+ const {
+ data: {
+ project: { id: newProjectId }
+ }
+ } = await createWs.mutateAsync({
+ projectName: name,
+ projectDescription: description,
+ kmsKeyId: kmsKeyId !== INTERNAL_KMS_KEY_ID ? kmsKeyId : undefined,
+ template
+ });
+
+ if (addMembers) {
+ const orgUsers = await fetchOrgUsers(currentOrg.id);
+ await addUsersToProject.mutateAsync({
+ usernames: orgUsers
+ .filter(
+ (member) => member.user.username !== user.username && member.status === "accepted"
+ )
+ .map((member) => member.user.username),
+ projectId: newProjectId,
+ orgId: currentOrg.id
+ });
+ }
+
+ await refetchWorkspaces();
+
+ createNotification({ text: "Project created", type: "success" });
+ reset();
+ onOpenChange(false);
+ router.push(`/project/${newProjectId}/secrets/overview`);
+ } catch (err) {
+ console.error(err);
+ createNotification({ text: "Failed to create project", type: "error" });
+ }
+ };
+ const onSubmit = handleSubmit((data) => {
+ return onCreateProject(data);
+ });
+ return (
+
+ );
+};
+
+export const NewProjectModal: FC = ({ isOpen, onOpenChange }) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/frontend-v2/src/components/v2/projects/index.tsx b/frontend-v2/src/components/v2/projects/index.tsx
new file mode 100644
index 000000000..1fb78225d
--- /dev/null
+++ b/frontend-v2/src/components/v2/projects/index.tsx
@@ -0,0 +1 @@
+export { NewProjectModal } from "./NewProjectModal";
diff --git a/frontend-v2/src/config/request.ts b/frontend-v2/src/config/request.ts
new file mode 100644
index 000000000..9a0629619
--- /dev/null
+++ b/frontend-v2/src/config/request.ts
@@ -0,0 +1,36 @@
+import axios from "axios";
+
+import SecurityClient from "@app/components/utilities/SecurityClient";
+import { getAuthToken, getMfaTempToken, getSignupTempToken } from "@app/hooks/api/reactQuery";
+
+export const apiRequest = axios.create({
+ baseURL: "/",
+ headers: {
+ "Content-Type": "application/json"
+ }
+});
+
+apiRequest.interceptors.request.use((config) => {
+ const signupTempToken = getSignupTempToken();
+ const mfaTempToken = getMfaTempToken();
+ const token = getAuthToken();
+ const providerAuthToken = SecurityClient.getProviderAuthToken();
+
+ if (config.headers) {
+ if (signupTempToken) {
+ // eslint-disable-next-line no-param-reassign
+ config.headers.Authorization = `Bearer ${signupTempToken}`;
+ } else if (mfaTempToken) {
+ // eslint-disable-next-line no-param-reassign
+ config.headers.Authorization = `Bearer ${mfaTempToken}`;
+ } else if (token) {
+ // eslint-disable-next-line no-param-reassign
+ config.headers.Authorization = `Bearer ${token}`;
+ } else if (providerAuthToken) {
+ // eslint-disable-next-line no-param-reassign
+ config.headers.Authorization = `Bearer ${providerAuthToken}`;
+ }
+ }
+
+ return config;
+});
diff --git a/frontend-v2/src/const.ts b/frontend-v2/src/const.ts
new file mode 100644
index 000000000..d2623431d
--- /dev/null
+++ b/frontend-v2/src/const.ts
@@ -0,0 +1,128 @@
+export const publicPaths = [
+ "/",
+ // `/integrations`,
+ "/signupinvite",
+ "/pricing",
+ "/signup",
+ "/signup/sso",
+ "/login",
+ "/login/ldap",
+ "/blog",
+ "/docs",
+ "/changelog",
+ "/security",
+ "/scheduledemo",
+ "/blog/[slug]",
+ "/faq",
+ "/privacy",
+ "/terms",
+ "/subprocessors",
+ "/verify-email",
+ "/password-reset",
+ "/saml-sso",
+ "/login/provider/success", // TODO: change
+ "/login/provider/error", // TODO: change
+ "/login/sso",
+ "/admin/signup",
+ "/shared/secret/[id]",
+ "/share-secret"
+];
+
+export const languageMap = {
+ en: "English",
+ ko: "한êµì–´",
+ fr: "Français"
+};
+
+interface Mapping {
+ [key: string]: string;
+}
+
+const plansDev: Mapping = {
+ starter: "prod_Nt6kPvYsVBuzVH",
+ team: "prod_NEpD2WMXUS2eDn",
+ professional: "prod_Mb4CetZ2jE7jdl",
+ enterprise: "licence_key_required"
+};
+
+const plansProd: Mapping = {
+ starter: "prod_Mb8oR5XNwyFTul",
+ team: "prod_NEp7fAB3UJWK6A",
+ professional: "prod_Mb8pUIpA0OUi5N",
+ enterprise: "licence_key_required"
+};
+
+export const plans = plansProd || plansDev;
+
+export const leaveConfirmDefaultMessage =
+ "Your changes will be lost if you leave the page. Are you sure you want to continue?";
+
+export enum SessionStorageKeys {
+ CLI_TERMINAL_TOKEN = "CLI_TERMINAL_TOKEN"
+}
+
+export const secretTagsColors = [
+ {
+ id: 1,
+ hex: "#bec2c8",
+ rgba: "rgb(128,128,128, 0.8)",
+ name: "Grey",
+ selected: true
+ },
+ {
+ id: 2,
+ hex: "#95a2b3",
+ rgba: "rgb(0,0,255, 0.8)",
+ name: "blue",
+ selected: false
+ },
+ {
+ id: 3,
+ hex: "#5e6ad2",
+ rgba: "rgb(128,0,128, 0.8)",
+ name: "Purple",
+ selected: false
+ },
+ {
+ id: 4,
+ hex: "#26b5ce",
+ rgba: "rgb(0,128,128, 0.8)",
+ name: "Teal",
+ selected: false
+ },
+ {
+ id: 5,
+ hex: "#4cb782",
+ rgba: "rgb(0,128,0, 0.8)",
+ name: "Green",
+ selected: false
+ },
+ {
+ id: 6,
+ hex: "#f2c94c",
+ rgba: "rgb(255,255,0, 0.8)",
+ name: "Yellow",
+ selected: false
+ },
+ {
+ id: 7,
+ hex: "#f2994a",
+ rgba: "rgb(128,128,0, 0.8)",
+ name: "Orange",
+ selected: false
+ },
+ {
+ id: 8,
+ hex: "#f7c8c1",
+ rgba: "rgb(128,0,0, 0.8)",
+ name: "Pink",
+ selected: false
+ },
+ {
+ id: 9,
+ hex: "#eb5757",
+ rgba: "rgb(255,0,0, 0.8)",
+ name: "Red",
+ selected: false
+ }
+];
diff --git a/frontend-v2/src/hooks/api/accessApproval/index.tsx b/frontend-v2/src/hooks/api/accessApproval/index.tsx
new file mode 100644
index 000000000..ec9789d05
--- /dev/null
+++ b/frontend-v2/src/hooks/api/accessApproval/index.tsx
@@ -0,0 +1,13 @@
+export {
+ useCreateAccessApprovalPolicy,
+ useCreateAccessRequest,
+ useDeleteAccessApprovalPolicy,
+ useReviewAccessRequest,
+ useUpdateAccessApprovalPolicy
+} from "./mutation";
+export {
+ useGetAccessApprovalPolicies,
+ useGetAccessApprovalRequests,
+ useGetAccessPolicyApprovalCount,
+ useGetAccessRequestsCount
+} from "./queries";
diff --git a/frontend-v2/src/hooks/api/accessApproval/mutation.tsx b/frontend-v2/src/hooks/api/accessApproval/mutation.tsx
new file mode 100644
index 000000000..9c3199a99
--- /dev/null
+++ b/frontend-v2/src/hooks/api/accessApproval/mutation.tsx
@@ -0,0 +1,133 @@
+import { packRules } from "@casl/ability/extra";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { accessApprovalKeys } from "./queries";
+import {
+ TAccessApproval,
+ TCreateAccessPolicyDTO,
+ TCreateAccessRequestDTO,
+ TDeleteSecretPolicyDTO,
+ TUpdateAccessPolicyDTO
+} from "./types";
+
+export const useCreateAccessApprovalPolicy = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TCreateAccessPolicyDTO>({
+ mutationFn: async ({
+ environment,
+ projectSlug,
+ approvals,
+ approvers,
+ name,
+ secretPath,
+ enforcementLevel
+ }) => {
+ const { data } = await apiRequest.post("/api/v1/access-approvals/policies", {
+ environment,
+ projectSlug,
+ approvals,
+ approvers,
+ secretPath,
+ name,
+ enforcementLevel
+ });
+ return data;
+ },
+ onSuccess: (_, { projectSlug }) => {
+ queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
+ }
+ });
+};
+
+export const useUpdateAccessApprovalPolicy = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TUpdateAccessPolicyDTO>({
+ mutationFn: async ({ id, approvers, approvals, name, secretPath, enforcementLevel }) => {
+ const { data } = await apiRequest.patch(`/api/v1/access-approvals/policies/${id}`, {
+ approvals,
+ approvers,
+ secretPath,
+ name,
+ enforcementLevel
+ });
+ return data;
+ },
+ onSuccess: (_, { projectSlug }) => {
+ queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
+ }
+ });
+};
+
+export const useDeleteAccessApprovalPolicy = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
+ mutationFn: async ({ id }) => {
+ const { data } = await apiRequest.delete(`/api/v1/access-approvals/policies/${id}`);
+ return data;
+ },
+ onSuccess: (_, { projectSlug }) => {
+ queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalPolicies(projectSlug));
+ }
+ });
+};
+
+export const useCreateAccessRequest = () => {
+ const queryClient = useQueryClient();
+ return useMutation<{}, {}, TCreateAccessRequestDTO>({
+ mutationFn: async ({ projectSlug, ...request }) => {
+ const { data } = await apiRequest.post(
+ "/api/v1/access-approvals/requests",
+ {
+ ...request,
+ permissions: request.permissions ? packRules(request.permissions) : undefined
+ },
+ {
+ params: {
+ projectSlug
+ }
+ }
+ );
+
+ return data;
+ },
+ onSuccess: (_, { projectSlug }) => {
+ queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug));
+ }
+ });
+};
+
+export const useReviewAccessRequest = () => {
+ const queryClient = useQueryClient();
+ return useMutation<
+ {},
+ {},
+ {
+ requestId: string;
+ status: "approved" | "rejected";
+ projectSlug: string;
+ envSlug?: string;
+ requestedBy?: string;
+ }
+ >({
+ mutationFn: async ({ requestId, status }) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/access-approvals/requests/${requestId}/review`,
+ {
+ status
+ }
+ );
+ return data;
+ },
+ onSuccess: (_, { projectSlug, envSlug, requestedBy }) => {
+ queryClient.invalidateQueries(
+ accessApprovalKeys.getAccessApprovalRequests(projectSlug, envSlug, requestedBy)
+ );
+ queryClient.invalidateQueries(accessApprovalKeys.getAccessApprovalRequestCount(projectSlug));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/accessApproval/queries.tsx b/frontend-v2/src/hooks/api/accessApproval/queries.tsx
new file mode 100644
index 000000000..599962e43
--- /dev/null
+++ b/frontend-v2/src/hooks/api/accessApproval/queries.tsx
@@ -0,0 +1,159 @@
+import { PackRule, unpackRules } from "@casl/ability/extra";
+import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { TProjectPermission } from "../roles/types";
+import {
+ TAccessApprovalPolicy,
+ TAccessApprovalRequest,
+ TAccessRequestCount,
+ TGetAccessApprovalRequestsDTO,
+ TGetAccessPolicyApprovalCountDTO
+} from "./types";
+
+export const accessApprovalKeys = {
+ getAccessApprovalPolicies: (projectSlug: string) =>
+ [{ projectSlug }, "access-approval-policies"] as const,
+ getAccessApprovalPolicyOfABoard: (workspaceId: string, environment: string) =>
+ [{ workspaceId, environment }, "access-approval-policy"] as const,
+
+ getAccessApprovalRequests: (projectSlug: string, envSlug?: string, requestedBy?: string) =>
+ [{ projectSlug, envSlug, requestedBy }, "access-approvals-requests"] as const,
+ getAccessApprovalRequestCount: (projectSlug: string) =>
+ [{ projectSlug }, "access-approval-request-count"] as const
+};
+
+export const fetchPolicyApprovalCount = async ({
+ projectSlug,
+ envSlug
+}: TGetAccessPolicyApprovalCountDTO) => {
+ const { data } = await apiRequest.get<{ count: number }>(
+ "/api/v1/access-approvals/policies/count",
+ {
+ params: { projectSlug, envSlug }
+ }
+ );
+ return data.count;
+};
+
+export const useGetAccessPolicyApprovalCount = ({
+ projectSlug,
+ envSlug,
+ options = {}
+}: TGetAccessPolicyApprovalCountDTO & {
+ options?: UseQueryOptions<
+ number,
+ unknown,
+ number,
+ ReturnType
+ >;
+}) =>
+ useQuery({
+ queryFn: () => fetchPolicyApprovalCount({ projectSlug, envSlug }),
+ ...options,
+ enabled: Boolean(projectSlug) && (options?.enabled ?? true)
+ });
+
+const fetchApprovalPolicies = async ({ projectSlug }: TGetAccessApprovalRequestsDTO) => {
+ const { data } = await apiRequest.get<{ approvals: TAccessApprovalPolicy[] }>(
+ "/api/v1/access-approvals/policies",
+ { params: { projectSlug } }
+ );
+ return data.approvals;
+};
+
+const fetchApprovalRequests = async ({
+ projectSlug,
+ envSlug,
+ authorProjectMembershipId
+}: TGetAccessApprovalRequestsDTO) => {
+ const { data } = await apiRequest.get<{ requests: TAccessApprovalRequest[] }>(
+ "/api/v1/access-approvals/requests",
+ { params: { projectSlug, envSlug, authorProjectMembershipId } }
+ );
+
+ return data.requests.map((request) => ({
+ ...request,
+
+ privilege: request.privilege
+ ? {
+ ...request.privilege,
+ permissions: unpackRules(
+ request.privilege.permissions as unknown as PackRule[]
+ )
+ }
+ : null,
+ permissions: unpackRules(request.permissions as unknown as PackRule[])
+ }));
+};
+
+const fetchAccessRequestsCount = async (projectSlug: string) => {
+ const { data } = await apiRequest.get(
+ "/api/v1/access-approvals/requests/count",
+ { params: { projectSlug } }
+ );
+ return data;
+};
+
+export const useGetAccessRequestsCount = ({
+ projectSlug,
+ options = {}
+}: TGetAccessApprovalRequestsDTO & {
+ options?: UseQueryOptions<
+ TAccessRequestCount,
+ unknown,
+ { pendingCount: number; finalizedCount: number },
+ ReturnType
+ >;
+}) =>
+ useQuery({
+ queryKey: accessApprovalKeys.getAccessApprovalRequestCount(projectSlug),
+ queryFn: () => fetchAccessRequestsCount(projectSlug),
+ ...options,
+ enabled: Boolean(projectSlug) && (options?.enabled ?? true)
+ });
+
+export const useGetAccessApprovalPolicies = ({
+ projectSlug,
+ envSlug,
+ authorProjectMembershipId,
+ options = {}
+}: TGetAccessApprovalRequestsDTO & {
+ options?: UseQueryOptions<
+ TAccessApprovalPolicy[],
+ unknown,
+ TAccessApprovalPolicy[],
+ ReturnType
+ >;
+}) =>
+ useQuery({
+ queryKey: accessApprovalKeys.getAccessApprovalPolicies(projectSlug),
+ queryFn: () => fetchApprovalPolicies({ projectSlug, envSlug, authorProjectMembershipId }),
+ ...options,
+ enabled: Boolean(projectSlug) && (options?.enabled ?? true)
+ });
+
+export const useGetAccessApprovalRequests = ({
+ projectSlug,
+ envSlug,
+ authorProjectMembershipId,
+ options = {}
+}: TGetAccessApprovalRequestsDTO & {
+ options?: UseQueryOptions<
+ TAccessApprovalRequest[],
+ unknown,
+ TAccessApprovalRequest[],
+ ReturnType
+ >;
+}) =>
+ useQuery({
+ queryKey: accessApprovalKeys.getAccessApprovalRequests(
+ projectSlug,
+ envSlug,
+ authorProjectMembershipId
+ ),
+ queryFn: () => fetchApprovalRequests({ projectSlug, envSlug, authorProjectMembershipId }),
+ ...options,
+ enabled: Boolean(projectSlug) && (options?.enabled ?? true)
+ });
diff --git a/frontend-v2/src/hooks/api/accessApproval/types.ts b/frontend-v2/src/hooks/api/accessApproval/types.ts
new file mode 100644
index 000000000..bd6173d91
--- /dev/null
+++ b/frontend-v2/src/hooks/api/accessApproval/types.ts
@@ -0,0 +1,165 @@
+import { EnforcementLevel, PolicyType } from "../policies/enums";
+import { TProjectPermission } from "../roles/types";
+import { WorkspaceEnv } from "../workspace/types";
+
+export type TAccessApprovalPolicy = {
+ id: string;
+ name: string;
+ approvals: number;
+ secretPath: string;
+ envId: string;
+ workspace: string;
+ environment: WorkspaceEnv;
+ projectId: string;
+ policyType: PolicyType;
+ approversRequired: boolean;
+ enforcementLevel: EnforcementLevel;
+ updatedAt: Date;
+ approvers?: Approver[];
+};
+
+export enum ApproverType {
+ User = "user",
+ Group = "group"
+}
+
+export type Approver = {
+ id: string;
+ type: ApproverType;
+};
+
+export type TAccessApprovalRequest = {
+ id: string;
+ policyId: string;
+ privilegeId: string | null;
+ requestedByUserId: string;
+ requestedByUser: {
+ email: string;
+ firstName?: string;
+ lastName?: string;
+ userId: string;
+ username: string;
+ };
+ createdAt: Date;
+ updatedAt: Date;
+ isTemporary: boolean;
+ temporaryRange: string | null | undefined;
+
+ permissions: TProjectPermission[] | null;
+
+ // Computed
+ environmentName: string;
+ isApproved: boolean;
+
+ privilege: {
+ membershipId: string;
+ isTemporary: boolean;
+ temporaryMode?: string | null;
+ temporaryRange?: string | null;
+ temporaryAccessStartTime?: Date | null;
+ temporaryAccessEndTime?: Date | null;
+ permissions: TProjectPermission[];
+ isApproved: boolean;
+ } | null;
+
+ policy: {
+ id: string;
+ name: string;
+ approvals: number;
+ approvers: string[];
+ secretPath?: string | null;
+ envId: string;
+ enforcementLevel: EnforcementLevel;
+ deletedAt: Date | null;
+ };
+
+ reviewers: {
+ member: string;
+ status: string;
+ }[];
+};
+
+export type TAccessApproval = {
+ id: string;
+ policyId: string;
+ privilegeId: string;
+ requestedBy: string;
+};
+
+export type TAccessRequestCount = {
+ pendingCount: number;
+ finalizedCount: number;
+};
+
+export type TProjectUserPrivilege = {
+ projectMembershipId: string;
+ slug: string;
+ id: string;
+ createdAt: Date;
+ updatedAt: Date;
+ permissions?: TProjectPermission[];
+} & (
+ | {
+ isTemporary: true;
+ temporaryMode: string;
+ temporaryRange: string;
+ temporaryAccessStartTime: string;
+ temporaryAccessEndTime?: string;
+ }
+ | {
+ isTemporary: false;
+ temporaryMode?: null;
+ temporaryRange?: null;
+ temporaryAccessStartTime?: null;
+ temporaryAccessEndTime?: null;
+ }
+);
+
+export type TCreateAccessRequestDTO = {
+ projectSlug: string;
+} & Omit;
+
+export type TGetAccessApprovalRequestsDTO = {
+ projectSlug: string;
+ envSlug?: string;
+ authorProjectMembershipId?: string;
+};
+
+export type TGetAccessPolicyApprovalCountDTO = {
+ projectSlug: string;
+ envSlug: string;
+};
+
+export type TGetSecretApprovalPolicyOfBoardDTO = {
+ workspaceId: string;
+ environment: string;
+ secretPath: string;
+};
+
+export type TCreateAccessPolicyDTO = {
+ projectSlug: string;
+ name?: string;
+ environment: string;
+ approvers?: Approver[];
+ approvals?: number;
+ secretPath?: string;
+ enforcementLevel?: EnforcementLevel;
+};
+
+export type TUpdateAccessPolicyDTO = {
+ id: string;
+ name?: string;
+ approvers?: Approver[];
+ secretPath?: string;
+ environment?: string;
+ approvals?: number;
+ enforcementLevel?: EnforcementLevel;
+ // for invalidating list
+ projectSlug: string;
+};
+
+export type TDeleteSecretPolicyDTO = {
+ id: string;
+ // for invalidating list
+ projectSlug: string;
+};
diff --git a/frontend-v2/src/hooks/api/admin/index.ts b/frontend-v2/src/hooks/api/admin/index.ts
new file mode 100644
index 000000000..5405878c7
--- /dev/null
+++ b/frontend-v2/src/hooks/api/admin/index.ts
@@ -0,0 +1,13 @@
+export {
+ useAdminDeleteUser,
+ useCreateAdminUser,
+ useUpdateAdminSlackConfig,
+ useUpdateServerConfig,
+ useUpdateServerEncryptionStrategy
+} from "./mutation";
+export {
+ useAdminGetUsers,
+ useGetAdminSlackConfig,
+ useGetServerConfig,
+ useGetServerRootKmsEncryptionDetails
+} from "./queries";
diff --git a/frontend-v2/src/hooks/api/admin/mutation.ts b/frontend-v2/src/hooks/api/admin/mutation.ts
new file mode 100644
index 000000000..6cd13050e
--- /dev/null
+++ b/frontend-v2/src/hooks/api/admin/mutation.ts
@@ -0,0 +1,100 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { organizationKeys } from "../organization/queries";
+import { User } from "../users/types";
+import { adminQueryKeys, adminStandaloneKeys } from "./queries";
+import {
+ AdminSlackConfig,
+ RootKeyEncryptionStrategy,
+ TCreateAdminUserDTO,
+ TServerConfig,
+ TUpdateAdminSlackConfigDTO
+} from "./types";
+
+export const useCreateAdminUser = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<
+ { user: User; token: string; organization: { id: string } },
+ {},
+ TCreateAdminUserDTO
+ >({
+ mutationFn: async (opt) => {
+ const { data } = await apiRequest.post("/api/v1/admin/signup", opt);
+ return data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries(adminQueryKeys.serverConfig());
+ }
+ });
+};
+
+export const useUpdateServerConfig = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<
+ TServerConfig,
+ {},
+ Partial
+ >({
+ mutationFn: async (opt) => {
+ const { data } = await apiRequest.patch<{ config: TServerConfig }>(
+ "/api/v1/admin/config",
+ opt
+ );
+ return data.config;
+ },
+ onSuccess: (data) => {
+ queryClient.setQueryData(adminQueryKeys.serverConfig(), data);
+ queryClient.invalidateQueries(adminQueryKeys.serverConfig());
+ queryClient.invalidateQueries(organizationKeys.getUserOrganizations);
+ }
+ });
+};
+
+export const useAdminDeleteUser = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (userId: string) => {
+ await apiRequest.delete(`/api/v1/admin/user-management/users/${userId}`);
+
+ return {};
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: [adminStandaloneKeys.getUsers]
+ });
+ }
+ });
+};
+
+export const useUpdateAdminSlackConfig = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.put(
+ "/api/v1/admin/integrations/slack/config",
+ dto
+ );
+
+ return data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries(adminQueryKeys.getAdminSlackConfig());
+ }
+ });
+};
+
+export const useUpdateServerEncryptionStrategy = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (strategy: RootKeyEncryptionStrategy) => {
+ await apiRequest.patch("/api/v1/admin/encryption-strategies", { strategy });
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries(adminQueryKeys.getServerEncryptionStrategies());
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/admin/queries.ts b/frontend-v2/src/hooks/api/admin/queries.ts
new file mode 100644
index 000000000..a1d32bce3
--- /dev/null
+++ b/frontend-v2/src/hooks/api/admin/queries.ts
@@ -0,0 +1,94 @@
+import { useInfiniteQuery, useQuery, UseQueryOptions } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { User } from "../types";
+import {
+ AdminGetUsersFilters,
+ AdminSlackConfig,
+ TGetServerRootKmsEncryptionDetails,
+ TServerConfig
+} from "./types";
+
+export const adminStandaloneKeys = {
+ getUsers: "get-users"
+};
+
+export const adminQueryKeys = {
+ serverConfig: () => ["server-config"] as const,
+ getUsers: (filters: AdminGetUsersFilters) => [adminStandaloneKeys.getUsers, { filters }] as const,
+ getAdminSlackConfig: () => ["admin-slack-config"] as const,
+ getServerEncryptionStrategies: () => ["server-encryption-strategies"] as const
+};
+
+const fetchServerConfig = async () => {
+ const { data } = await apiRequest.get<{ config: TServerConfig }>("/api/v1/admin/config");
+ return data.config;
+};
+
+export const useGetServerConfig = ({
+ options = {}
+}: {
+ options?: Omit<
+ UseQueryOptions<
+ TServerConfig,
+ unknown,
+ TServerConfig,
+ ReturnType
+ >,
+ "queryKey" | "queryFn"
+ >;
+} = {}) =>
+ useQuery({
+ queryKey: adminQueryKeys.serverConfig(),
+ queryFn: fetchServerConfig,
+ ...options,
+ enabled: options?.enabled ?? true
+ });
+
+export const useAdminGetUsers = (filters: AdminGetUsersFilters) => {
+ return useInfiniteQuery({
+ queryKey: adminQueryKeys.getUsers(filters),
+ queryFn: async ({ pageParam }) => {
+ const { data } = await apiRequest.get<{ users: User[] }>(
+ "/api/v1/admin/user-management/users",
+ {
+ params: {
+ ...filters,
+ offset: pageParam
+ }
+ }
+ );
+
+ return data.users;
+ },
+ getNextPageParam: (lastPage, pages) =>
+ lastPage.length !== 0 ? pages.length * filters.limit : undefined
+ });
+};
+
+export const useGetAdminSlackConfig = () => {
+ return useQuery({
+ queryKey: adminQueryKeys.getAdminSlackConfig(),
+ queryFn: async () => {
+ const { data } = await apiRequest.get(
+ "/api/v1/admin/integrations/slack/config"
+ );
+
+ return data;
+ }
+ });
+};
+
+export const useGetServerRootKmsEncryptionDetails = () => {
+ return useQuery({
+ queryKey: adminQueryKeys.getServerEncryptionStrategies(),
+ queryFn: async () => {
+ const { data } = await apiRequest.get(
+ "/api/v1/admin/encryption-strategies"
+ );
+
+ return data;
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/admin/types.ts b/frontend-v2/src/hooks/api/admin/types.ts
new file mode 100644
index 000000000..60fa3ab98
--- /dev/null
+++ b/frontend-v2/src/hooks/api/admin/types.ts
@@ -0,0 +1,68 @@
+export enum LoginMethod {
+ EMAIL = "email",
+ GOOGLE = "google",
+ GITHUB = "github",
+ GITLAB = "gitlab",
+ SAML = "saml",
+ LDAP = "ldap",
+ OIDC = "oidc"
+}
+
+export type TServerConfig = {
+ initialized: boolean;
+ allowSignUp: boolean;
+ allowedSignUpDomain?: string | null;
+ isMigrationModeOn?: boolean;
+ trustSamlEmails: boolean;
+ trustLdapEmails: boolean;
+ trustOidcEmails: boolean;
+ isSecretScanningDisabled: boolean;
+ defaultAuthOrgSlug: string | null;
+ defaultAuthOrgId: string | null;
+ defaultAuthOrgAuthMethod?: string | null;
+ defaultAuthOrgAuthEnforced?: boolean | null;
+ enabledLoginMethods: LoginMethod[];
+};
+
+export type TCreateAdminUserDTO = {
+ email: string;
+ password: string;
+ firstName: string;
+ lastName?: string;
+ protectedKey: string;
+ protectedKeyTag: string;
+ protectedKeyIV: string;
+ encryptedPrivateKey: string;
+ encryptedPrivateKeyIV: string;
+ encryptedPrivateKeyTag: string;
+ publicKey: string;
+ verifier: string;
+ salt: string;
+};
+
+export type TUpdateAdminSlackConfigDTO = {
+ clientId: string;
+ clientSecret: string;
+};
+
+export type AdminGetUsersFilters = {
+ limit: number;
+ searchTerm: string;
+};
+
+export type AdminSlackConfig = {
+ clientId: string;
+ clientSecret: string;
+};
+
+export type TGetServerRootKmsEncryptionDetails = {
+ strategies: {
+ strategy: RootKeyEncryptionStrategy;
+ enabled: boolean;
+ }[];
+};
+
+export enum RootKeyEncryptionStrategy {
+ Software = "SOFTWARE",
+ HSM = "HSM"
+}
diff --git a/frontend-v2/src/hooks/api/apiKeys/index.ts b/frontend-v2/src/hooks/api/apiKeys/index.ts
new file mode 100644
index 000000000..58c3717ab
--- /dev/null
+++ b/frontend-v2/src/hooks/api/apiKeys/index.ts
@@ -0,0 +1 @@
+export { useCreateAPIKeyV2, useDeleteAPIKeyV2, useUpdateAPIKeyV2 } from "./queries";
diff --git a/frontend-v2/src/hooks/api/apiKeys/queries.tsx b/frontend-v2/src/hooks/api/apiKeys/queries.tsx
new file mode 100644
index 000000000..c1a2e4355
--- /dev/null
+++ b/frontend-v2/src/hooks/api/apiKeys/queries.tsx
@@ -0,0 +1,60 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { userKeys } from "../users";
+import {
+ APIKeyDataV2,
+ CreateAPIKeyDataV2DTO,
+ CreateServiceTokenDataV3Res,
+ DeleteAPIKeyDataV2DTO,
+ UpdateAPIKeyDataV2DTO
+} from "./types";
+
+export const useCreateAPIKeyV2 = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ name }) => {
+ const { data } = await apiRequest.post("/api/v3/api-key", {
+ name
+ });
+
+ return data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries(userKeys.myAPIKeysV2);
+ }
+ });
+};
+
+export const useUpdateAPIKeyV2 = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ apiKeyDataId, name }) => {
+ const {
+ data: { apiKeyData }
+ } = await apiRequest.patch(`/api/v3/api-key/${apiKeyDataId}`, {
+ name
+ });
+ return apiKeyData;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries(userKeys.myAPIKeysV2);
+ }
+ });
+};
+
+export const useDeleteAPIKeyV2 = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ apiKeyDataId }) => {
+ const {
+ data: { apiKeyData }
+ } = await apiRequest.delete(`/api/v3/api-key/${apiKeyDataId}`);
+ return apiKeyData;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries(userKeys.myAPIKeysV2);
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/apiKeys/types.ts b/frontend-v2/src/hooks/api/apiKeys/types.ts
new file mode 100644
index 000000000..b05bd5981
--- /dev/null
+++ b/frontend-v2/src/hooks/api/apiKeys/types.ts
@@ -0,0 +1,27 @@
+export type APIKeyDataV2 = {
+ id: string;
+ name: string;
+ user: string;
+ lastUsed?: string;
+ usageCount: number;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type CreateAPIKeyDataV2DTO = {
+ name: string;
+};
+
+export type CreateServiceTokenDataV3Res = {
+ apiKeyData: APIKeyDataV2;
+ apiKey: string;
+};
+
+export type UpdateAPIKeyDataV2DTO = {
+ apiKeyDataId: string;
+ name: string;
+};
+
+export type DeleteAPIKeyDataV2DTO = {
+ apiKeyDataId: string;
+};
diff --git a/frontend-v2/src/hooks/api/auditLogStreams/index.tsx b/frontend-v2/src/hooks/api/auditLogStreams/index.tsx
new file mode 100644
index 000000000..72b1fba1a
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auditLogStreams/index.tsx
@@ -0,0 +1,6 @@
+export {
+ useCreateAuditLogStream,
+ useDeleteAuditLogStream,
+ useUpdateAuditLogStream
+} from "./mutations";
+export { useGetAuditLogStreamDetails, useGetAuditLogStreams } from "./queries";
diff --git a/frontend-v2/src/hooks/api/auditLogStreams/mutations.tsx b/frontend-v2/src/hooks/api/auditLogStreams/mutations.tsx
new file mode 100644
index 000000000..2d99f57c4
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auditLogStreams/mutations.tsx
@@ -0,0 +1,61 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { auditLogStreamKeys } from "./queries";
+import {
+ TAuditLogStream,
+ TCreateAuditLogStreamDTO,
+ TDeleteAuditLogStreamDTO,
+ TUpdateAuditLogStreamDTO
+} from "./types";
+
+export const useCreateAuditLogStream = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{ auditLogStream: TAuditLogStream }, {}, TCreateAuditLogStreamDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.post<{ auditLogStream: TAuditLogStream }>(
+ "/api/v1/audit-log-streams",
+ dto
+ );
+ return data;
+ },
+ onSuccess: (_, { orgId }) => {
+ queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
+ }
+ });
+};
+
+export const useUpdateAuditLogStream = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{ auditLogStream: TAuditLogStream }, {}, TUpdateAuditLogStreamDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.patch<{ auditLogStream: TAuditLogStream }>(
+ `/api/v1/audit-log-streams/${dto.id}`,
+ dto
+ );
+ return data;
+ },
+ onSuccess: (_, { orgId }) => {
+ queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
+ }
+ });
+};
+
+export const useDeleteAuditLogStream = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{ auditLogStream: TAuditLogStream }, {}, TDeleteAuditLogStreamDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.delete<{ auditLogStream: TAuditLogStream }>(
+ `/api/v1/audit-log-streams/${dto.id}`
+ );
+ return data;
+ },
+ onSuccess: (_, { orgId }) => {
+ queryClient.invalidateQueries(auditLogStreamKeys.list(orgId));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/auditLogStreams/queries.tsx b/frontend-v2/src/hooks/api/auditLogStreams/queries.tsx
new file mode 100644
index 000000000..f86ca0dce
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auditLogStreams/queries.tsx
@@ -0,0 +1,40 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { TAuditLogStream } from "./types";
+
+export const auditLogStreamKeys = {
+ list: (orgId: string) => ["audit-log-stream", { orgId }],
+ getById: (id: string) => ["audit-log-stream-details", { id }]
+};
+
+const fetchAuditLogStreams = async () => {
+ const { data } = await apiRequest.get<{ auditLogStreams: TAuditLogStream[] }>(
+ "/api/v1/audit-log-streams"
+ );
+
+ return data.auditLogStreams;
+};
+
+export const useGetAuditLogStreams = (orgId: string) =>
+ useQuery({
+ queryKey: auditLogStreamKeys.list(orgId),
+ queryFn: () => fetchAuditLogStreams(),
+ enabled: Boolean(orgId)
+ });
+
+const fetchAuditLogStreamDetails = async (id: string) => {
+ const { data } = await apiRequest.get<{ auditLogStream: TAuditLogStream }>(
+ `/api/v1/audit-log-streams/${id}`
+ );
+
+ return data.auditLogStream;
+};
+
+export const useGetAuditLogStreamDetails = (id: string) =>
+ useQuery({
+ queryKey: auditLogStreamKeys.getById(id),
+ queryFn: () => fetchAuditLogStreamDetails(id),
+ enabled: Boolean(id)
+ });
diff --git a/frontend-v2/src/hooks/api/auditLogStreams/types.ts b/frontend-v2/src/hooks/api/auditLogStreams/types.ts
new file mode 100644
index 000000000..8e21a3209
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auditLogStreams/types.ts
@@ -0,0 +1,28 @@
+export type LogStreamHeaders = {
+ key: string;
+ value: string;
+};
+
+export type TAuditLogStream = {
+ id: string;
+ url: string;
+ headers?: LogStreamHeaders[];
+};
+
+export type TCreateAuditLogStreamDTO = {
+ url: string;
+ headers?: LogStreamHeaders[];
+ orgId: string;
+};
+
+export type TUpdateAuditLogStreamDTO = {
+ id: string;
+ url?: string;
+ headers?: LogStreamHeaders[];
+ orgId: string;
+};
+
+export type TDeleteAuditLogStreamDTO = {
+ id: string;
+ orgId: string;
+};
diff --git a/frontend-v2/src/hooks/api/auditLogs/constants.tsx b/frontend-v2/src/hooks/api/auditLogs/constants.tsx
new file mode 100644
index 000000000..a75767108
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auditLogs/constants.tsx
@@ -0,0 +1,95 @@
+import { EventType, UserAgentType } from "./enums";
+
+export const eventToNameMap: { [K in EventType]: string } = {
+ [EventType.GET_SECRETS]: "List secrets",
+ [EventType.GET_SECRET]: "Read secret",
+ [EventType.CREATE_SECRET]: "Create secret",
+ [EventType.UPDATE_SECRET]: "Update secret",
+ [EventType.DELETE_SECRET]: "Delete secret",
+ [EventType.GET_WORKSPACE_KEY]: "Read project key",
+ [EventType.AUTHORIZE_INTEGRATION]: "Authorize integration",
+ [EventType.UPDATE_INTEGRATION_AUTH]: "Update integration auth",
+ [EventType.UNAUTHORIZE_INTEGRATION]: "Unauthorize integration",
+ [EventType.CREATE_INTEGRATION]: "Create integration",
+ [EventType.DELETE_INTEGRATION]: "Delete integration",
+ [EventType.ADD_TRUSTED_IP]: "Add trusted IP",
+ [EventType.UPDATE_TRUSTED_IP]: "Update trusted IP",
+ [EventType.DELETE_TRUSTED_IP]: "Delete trusted IP",
+ [EventType.CREATE_SERVICE_TOKEN]: "Create service token",
+ [EventType.DELETE_SERVICE_TOKEN]: "Delete service token",
+ [EventType.CREATE_IDENTITY]: "Create identity",
+ [EventType.UPDATE_IDENTITY]: "Update identity",
+ [EventType.DELETE_IDENTITY]: "Delete identity",
+ [EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH]: "Login via universal auth",
+ [EventType.ADD_IDENTITY_UNIVERSAL_AUTH]: "Add universal auth",
+ [EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH]: "Update universal auth",
+ [EventType.GET_IDENTITY_UNIVERSAL_AUTH]: "Get universal auth",
+ [EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Create universal auth client secret",
+ [EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET]: "Revoke universal auth client secret",
+ [EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS]: "Get universal auth client secrets",
+ [EventType.CREATE_ENVIRONMENT]: "Create environment",
+ [EventType.UPDATE_ENVIRONMENT]: "Update environment",
+ [EventType.DELETE_ENVIRONMENT]: "Delete environment",
+ [EventType.ADD_WORKSPACE_MEMBER]: "Add member",
+ [EventType.REMOVE_WORKSPACE_MEMBER]: "Remove member",
+ [EventType.CREATE_FOLDER]: "Create folder",
+ [EventType.UPDATE_FOLDER]: "Update folder",
+ [EventType.DELETE_FOLDER]: "Delete folder",
+ [EventType.CREATE_WEBHOOK]: "Create webhook",
+ [EventType.UPDATE_WEBHOOK_STATUS]: "Update webhook status",
+ [EventType.DELETE_WEBHOOK]: "Delete webhook",
+ [EventType.GET_SECRET_IMPORTS]: "List secret imports",
+ [EventType.CREATE_SECRET_IMPORT]: "Create secret import",
+ [EventType.UPDATE_SECRET_IMPORT]: "Update secret import",
+ [EventType.DELETE_SECRET_IMPORT]: "Delete secret import",
+ [EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS]: "Update denied permissions",
+ [EventType.UPDATE_USER_WORKSPACE_ROLE]: "Update user role",
+ [EventType.CREATE_CA]: "Create CA",
+ [EventType.GET_CA]: "Get CA",
+ [EventType.UPDATE_CA]: "Update CA",
+ [EventType.DELETE_CA]: "Delete CA",
+ [EventType.GET_CA_CSR]: "Get CA CSR",
+ [EventType.GET_CA_CERT]: "Get CA certificate",
+ [EventType.SIGN_INTERMEDIATE]: "Sign intermediate",
+ [EventType.IMPORT_CA_CERT]: "Import CA certificate",
+ [EventType.GET_CA_CRL]: "Get CA CRL",
+ [EventType.ISSUE_CERT]: "Issue certificate",
+ [EventType.GET_CERT]: "Get certificate",
+ [EventType.DELETE_CERT]: "Delete certificate",
+ [EventType.REVOKE_CERT]: "Revoke certificate",
+ [EventType.GET_CERT_BODY]: "Get certificate body",
+ [EventType.CREATE_PKI_ALERT]: "Create PKI alert",
+ [EventType.GET_PKI_ALERT]: "Get PKI alert",
+ [EventType.UPDATE_PKI_ALERT]: "Update PKI alert",
+ [EventType.DELETE_PKI_ALERT]: "Delete PKI alert",
+ [EventType.CREATE_PKI_COLLECTION]: "Create PKI collection",
+ [EventType.GET_PKI_COLLECTION]: "Get PKI collection",
+ [EventType.UPDATE_PKI_COLLECTION]: "Update PKI collection",
+ [EventType.DELETE_PKI_COLLECTION]: "Delete PKI collection",
+ [EventType.GET_PKI_COLLECTION_ITEMS]: "Get PKI collection items",
+ [EventType.ADD_PKI_COLLECTION_ITEM]: "Add PKI collection item",
+ [EventType.DELETE_PKI_COLLECTION_ITEM]: "Delete PKI collection item",
+ [EventType.ORG_ADMIN_ACCESS_PROJECT]: "Org admin accessed project",
+ [EventType.CREATE_CERTIFICATE_TEMPLATE]: "Create certificate template",
+ [EventType.UPDATE_CERTIFICATE_TEMPLATE]: "Update certificate template",
+ [EventType.DELETE_CERTIFICATE_TEMPLATE]: "Delete certificate template",
+ [EventType.GET_CERTIFICATE_TEMPLATE]: "Get certificate template",
+ [EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG]: "Get certificate template EST configuration",
+ [EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
+ "Create certificate template EST configuration",
+ [EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG]:
+ "Update certificate template EST configuration",
+ [EventType.UPDATE_PROJECT_SLACK_CONFIG]: "Update project slack configuration",
+ [EventType.GET_PROJECT_SLACK_CONFIG]: "Get project slack configuration",
+ [EventType.INTEGRATION_SYNCED]: "Integration sync"
+};
+
+export const userAgentTTypeoNameMap: { [K in UserAgentType]: string } = {
+ [UserAgentType.WEB]: "Web",
+ [UserAgentType.CLI]: "CLI",
+ [UserAgentType.K8_OPERATOR]: "K8s operator",
+ [UserAgentType.TERRAFORM]: "Terraform",
+ [UserAgentType.NODE_SDK]: "InfisicalNodeSDK",
+ [UserAgentType.PYTHON_SDK]: "InfisicalPythonSDK",
+ [UserAgentType.OTHER]: "Other"
+};
diff --git a/frontend-v2/src/hooks/api/auditLogs/enums.tsx b/frontend-v2/src/hooks/api/auditLogs/enums.tsx
new file mode 100644
index 000000000..0b0c44d7b
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auditLogs/enums.tsx
@@ -0,0 +1,98 @@
+export enum ActorType {
+ PLATFORM = "platform",
+ USER = "user",
+ SERVICE = "service",
+ IDENTITY = "identity"
+}
+
+export enum UserAgentType {
+ WEB = "web",
+ CLI = "cli",
+ K8_OPERATOR = "k8-operator",
+ TERRAFORM = "terraform",
+ NODE_SDK = "node-sdk",
+ PYTHON_SDK = "python-sdk",
+ OTHER = "other"
+}
+
+export enum EventType {
+ GET_SECRETS = "get-secrets",
+ GET_SECRET = "get-secret",
+ CREATE_SECRET = "create-secret",
+ UPDATE_SECRET = "update-secret",
+ DELETE_SECRET = "delete-secret",
+ GET_WORKSPACE_KEY = "get-workspace-key",
+ AUTHORIZE_INTEGRATION = "authorize-integration",
+ UPDATE_INTEGRATION_AUTH = "update-integration-auth",
+ UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
+ CREATE_INTEGRATION = "create-integration",
+ DELETE_INTEGRATION = "delete-integration",
+ ADD_TRUSTED_IP = "add-trusted-ip",
+ UPDATE_TRUSTED_IP = "update-trusted-ip",
+ DELETE_TRUSTED_IP = "delete-trusted-ip",
+ CREATE_SERVICE_TOKEN = "create-service-token", // v2
+ DELETE_SERVICE_TOKEN = "delete-service-token", // v2
+ CREATE_IDENTITY = "create-identity",
+ UPDATE_IDENTITY = "update-identity",
+ DELETE_IDENTITY = "delete-identity",
+ LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
+ ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
+ UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
+ GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
+ CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
+ REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
+ GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
+ CREATE_ENVIRONMENT = "create-environment",
+ UPDATE_ENVIRONMENT = "update-environment",
+ DELETE_ENVIRONMENT = "delete-environment",
+ ADD_WORKSPACE_MEMBER = "add-workspace-member",
+ REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
+ CREATE_FOLDER = "create-folder",
+ UPDATE_FOLDER = "update-folder",
+ DELETE_FOLDER = "delete-folder",
+ CREATE_WEBHOOK = "create-webhook",
+ UPDATE_WEBHOOK_STATUS = "update-webhook-status",
+ DELETE_WEBHOOK = "delete-webhook",
+ GET_SECRET_IMPORTS = "get-secret-imports",
+ CREATE_SECRET_IMPORT = "create-secret-import",
+ UPDATE_SECRET_IMPORT = "update-secret-import",
+ DELETE_SECRET_IMPORT = "delete-secret-import",
+ UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
+ UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
+ CREATE_CA = "create-certificate-authority",
+ GET_CA = "get-certificate-authority",
+ UPDATE_CA = "update-certificate-authority",
+ DELETE_CA = "delete-certificate-authority",
+ GET_CA_CSR = "get-certificate-authority-csr",
+ GET_CA_CERT = "get-certificate-authority-cert",
+ SIGN_INTERMEDIATE = "sign-intermediate",
+ IMPORT_CA_CERT = "import-certificate-authority-cert",
+ GET_CA_CRL = "get-certificate-authority-crl",
+ ISSUE_CERT = "issue-cert",
+ GET_CERT = "get-cert",
+ DELETE_CERT = "delete-cert",
+ REVOKE_CERT = "revoke-cert",
+ GET_CERT_BODY = "get-cert-body",
+ CREATE_PKI_ALERT = "create-pki-alert",
+ GET_PKI_ALERT = "get-pki-alert",
+ UPDATE_PKI_ALERT = "update-pki-alert",
+ DELETE_PKI_ALERT = "delete-pki-alert",
+ CREATE_PKI_COLLECTION = "create-pki-collection",
+ GET_PKI_COLLECTION = "get-pki-collection",
+ UPDATE_PKI_COLLECTION = "update-pki-collection",
+ DELETE_PKI_COLLECTION = "delete-pki-collection",
+ GET_PKI_COLLECTION_ITEMS = "get-pki-collection-items",
+ ADD_PKI_COLLECTION_ITEM = "add-pki-collection-item",
+ DELETE_PKI_COLLECTION_ITEM = "delete-pki-collection-item",
+ ORG_ADMIN_ACCESS_PROJECT = "org-admin-accessed-project",
+ CREATE_CERTIFICATE_TEMPLATE = "create-certificate-template",
+ UPDATE_CERTIFICATE_TEMPLATE = "update-certificate-template",
+ DELETE_CERTIFICATE_TEMPLATE = "delete-certificate-template",
+ GET_CERTIFICATE_TEMPLATE = "get-certificate-template",
+ CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "create-certificate-template-est-config",
+ UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG = "update-certificate-template-est-config",
+ GET_CERTIFICATE_TEMPLATE_EST_CONFIG = "get-certificate-template-est-config",
+ UPDATE_PROJECT_SLACK_CONFIG = "update-project-slack-config",
+ GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
+ INTEGRATION_SYNCED = "integration-synced"
+}
diff --git a/frontend-v2/src/hooks/api/auditLogs/index.tsx b/frontend-v2/src/hooks/api/auditLogs/index.tsx
new file mode 100644
index 000000000..b69c25120
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auditLogs/index.tsx
@@ -0,0 +1 @@
+export * from "./queries";
diff --git a/frontend-v2/src/hooks/api/auditLogs/queries.tsx b/frontend-v2/src/hooks/api/auditLogs/queries.tsx
new file mode 100644
index 000000000..5ec69b0e7
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auditLogs/queries.tsx
@@ -0,0 +1,81 @@
+import { useInfiniteQuery, UseInfiniteQueryOptions, useQuery } from "@tanstack/react-query";
+import { AxiosError } from "axios";
+
+import { createNotification } from "@app/components/notifications";
+import { apiRequest } from "@app/config/request";
+
+import { Actor, AuditLog, TGetAuditLogsFilter } from "./types";
+
+export const auditLogKeys = {
+ getAuditLogs: (workspaceId: string | null, filters: TGetAuditLogsFilter) =>
+ [{ workspaceId, filters }, "audit-logs"] as const,
+ getAuditLogActorFilterOpts: (workspaceId: string) =>
+ [{ workspaceId }, "audit-log-actor-filters"] as const
+};
+
+export const useGetAuditLogs = (
+ filters: TGetAuditLogsFilter,
+ projectId: string | null,
+ options: Omit<
+ UseInfiniteQueryOptions<
+ AuditLog[],
+ unknown,
+ AuditLog[],
+ AuditLog[],
+ ReturnType
+ >,
+ "queryFn" | "queryKey" | "getNextPageParam"
+ > = {}
+) => {
+ return useInfiniteQuery({
+ queryKey: auditLogKeys.getAuditLogs(projectId, filters),
+ queryFn: async ({ pageParam }) => {
+ try {
+ const { data } = await apiRequest.get<{ auditLogs: AuditLog[] }>(
+ "/api/v1/organization/audit-logs",
+ {
+ params: {
+ ...filters,
+ offset: pageParam,
+ startDate: filters?.startDate?.toISOString(),
+ endDate: filters?.endDate?.toISOString(),
+ ...(filters.eventMetadata && Object.keys(filters.eventMetadata).length
+ ? {
+ eventMetadata: Object.entries(filters.eventMetadata)
+ .map(([key, value]) => `${key}=${value}`)
+ .join(",")
+ }
+ : {}),
+ ...(filters.eventType?.length ? { eventType: filters.eventType.join(",") } : {}),
+ ...(projectId ? { projectId } : {})
+ }
+ }
+ );
+ return data.auditLogs;
+ } catch (error) {
+ if (error instanceof AxiosError) {
+ createNotification({
+ type: "error",
+ text: error.response?.data.message
+ });
+ }
+ return [];
+ }
+ },
+ getNextPageParam: (lastPage, pages) =>
+ lastPage.length !== 0 ? pages.length * filters.limit : undefined,
+ ...options
+ });
+};
+
+export const useGetAuditLogActorFilterOpts = (workspaceId: string) => {
+ return useQuery({
+ queryKey: auditLogKeys.getAuditLogActorFilterOpts(workspaceId),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<{ actors: Actor[] }>(
+ `/api/v1/workspace/${workspaceId}/audit-logs/filters/actors`
+ );
+ return data.actors;
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/auditLogs/types.tsx b/frontend-v2/src/hooks/api/auditLogs/types.tsx
new file mode 100644
index 000000000..5221b5033
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auditLogs/types.tsx
@@ -0,0 +1,891 @@
+import { CaStatus } from "../ca";
+import { IdentityTrustedIp } from "../identities/types";
+import { PkiItemType } from "../pkiCollections/constants";
+import { ActorType, EventType, UserAgentType } from "./enums";
+
+export type TGetAuditLogsFilter = {
+ eventType?: EventType[];
+ userAgentType?: UserAgentType;
+ eventMetadata?: Record;
+ actorType?: ActorType;
+ projectId?: string;
+ actor?: string; // user ID format
+ startDate?: Date;
+ endDate?: Date;
+ limit: number;
+};
+
+interface UserActorMetadata {
+ userId: string;
+ email: string;
+}
+
+interface ServiceActorMetadata {
+ serviceId: string;
+ name: string;
+}
+
+interface IdentityActorMetadata {
+ identityId: string;
+ name: string;
+}
+
+interface UserActor {
+ type: ActorType.USER;
+ metadata: UserActorMetadata;
+}
+
+export interface ServiceActor {
+ type: ActorType.SERVICE;
+ metadata: ServiceActorMetadata;
+}
+
+export interface IdentityActor {
+ type: ActorType.IDENTITY;
+ metadata: IdentityActorMetadata;
+}
+
+export interface PlatformActorMetadata {}
+export interface PlatformActor {
+ type: ActorType.PLATFORM;
+ metadata: PlatformActorMetadata;
+}
+
+export type Actor = UserActor | ServiceActor | IdentityActor | PlatformActor;
+
+interface GetSecretsEvent {
+ type: EventType.GET_SECRETS;
+ metadata: {
+ environment: string;
+ secretPath: string;
+ numberOfSecrets: number;
+ };
+}
+
+interface GetSecretEvent {
+ type: EventType.GET_SECRET;
+ metadata: {
+ environment: string;
+ secretPath: string;
+ secretId: string;
+ secretKey: string;
+ secretVersion: number;
+ };
+}
+
+interface CreateSecretEvent {
+ type: EventType.CREATE_SECRET;
+ metadata: {
+ environment: string;
+ secretPath: string;
+ secretId: string;
+ secretKey: string;
+ secretVersion: number;
+ };
+}
+
+interface UpdateSecretEvent {
+ type: EventType.UPDATE_SECRET;
+ metadata: {
+ environment: string;
+ secretPath: string;
+ secretId: string;
+ secretKey: string;
+ secretVersion: number;
+ };
+}
+
+interface DeleteSecretEvent {
+ type: EventType.DELETE_SECRET;
+ metadata: {
+ environment: string;
+ secretPath: string;
+ secretId: string;
+ secretKey: string;
+ secretVersion: number;
+ };
+}
+
+interface GetWorkspaceKeyEvent {
+ type: EventType.GET_WORKSPACE_KEY;
+ metadata: {
+ keyId: string;
+ };
+}
+
+interface AuthorizeIntegrationEvent {
+ type: EventType.AUTHORIZE_INTEGRATION;
+ metadata: {
+ integration: string;
+ };
+}
+
+interface UnauthorizeIntegrationEvent {
+ type: EventType.UNAUTHORIZE_INTEGRATION;
+ metadata: {
+ integration: string;
+ };
+}
+
+interface CreateIntegrationEvent {
+ type: EventType.CREATE_INTEGRATION;
+ metadata: {
+ integrationId: string;
+ integration: string;
+ environment: string;
+ secretPath: string;
+ url?: string;
+ app?: string;
+ appId?: string;
+ targetEnvironment?: string;
+ targetEnvironmentId?: string;
+ targetService?: string;
+ targetServiceId?: string;
+ path?: string;
+ region?: string;
+ };
+}
+
+interface DeleteIntegrationEvent {
+ type: EventType.DELETE_INTEGRATION;
+ metadata: {
+ integrationId: string;
+ integration: string;
+ environment: string;
+ secretPath: string;
+ url?: string;
+ app?: string;
+ appId?: string;
+ targetEnvironment?: string;
+ targetEnvironmentId?: string;
+ targetService?: string;
+ targetServiceId?: string;
+ path?: string;
+ region?: string;
+ };
+}
+
+interface AddTrustedIPEvent {
+ type: EventType.ADD_TRUSTED_IP;
+ metadata: {
+ trustedIpId: string;
+ ipAddress: string;
+ prefix?: number;
+ };
+}
+
+interface UpdateTrustedIPEvent {
+ type: EventType.UPDATE_TRUSTED_IP;
+ metadata: {
+ trustedIpId: string;
+ ipAddress: string;
+ prefix?: number;
+ };
+}
+
+interface DeleteTrustedIPEvent {
+ type: EventType.DELETE_TRUSTED_IP;
+ metadata: {
+ trustedIpId: string;
+ ipAddress: string;
+ prefix?: number;
+ };
+}
+
+interface CreateServiceTokenEvent {
+ type: EventType.CREATE_SERVICE_TOKEN;
+ metadata: {
+ name: string;
+ scopes: Array<{
+ environment: string;
+ secretPath: string;
+ }>;
+ };
+}
+
+interface DeleteServiceTokenEvent {
+ type: EventType.DELETE_SERVICE_TOKEN;
+ metadata: {
+ name: string;
+ scopes: Array<{
+ environment: string;
+ secretPath: string;
+ }>;
+ };
+}
+
+interface CreateIdentityEvent {
+ // note: currently not logging org-role
+ type: EventType.CREATE_IDENTITY;
+ metadata: {
+ identityId: string;
+ name: string;
+ };
+}
+
+interface UpdateIdentityEvent {
+ type: EventType.UPDATE_IDENTITY;
+ metadata: {
+ identityId: string;
+ name?: string;
+ };
+}
+
+interface DeleteIdentityEvent {
+ type: EventType.DELETE_IDENTITY;
+ metadata: {
+ identityId: string;
+ };
+}
+
+interface LoginIdentityUniversalAuthEvent {
+ type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH;
+ metadata: {
+ identityId: string;
+ identityUniversalAuthId: string;
+ clientSecretId: string;
+ identityAccessTokenId: string;
+ };
+}
+
+interface AddIdentityUniversalAuthEvent {
+ type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
+ metadata: {
+ identityId: string;
+ clientSecretTrustedIps: Array;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: Array;
+ };
+}
+
+interface UpdateIdentityUniversalAuthEvent {
+ type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH;
+ metadata: {
+ identityId: string;
+ clientSecretTrustedIps?: Array;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: Array;
+ };
+}
+
+interface GetIdentityUniversalAuthEvent {
+ type: EventType.GET_IDENTITY_UNIVERSAL_AUTH;
+ metadata: {
+ identityId: string;
+ };
+}
+
+interface CreateIdentityUniversalAuthClientSecretEvent {
+ type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
+ metadata: {
+ identityId: string;
+ clientSecretId: string;
+ };
+}
+
+interface GetIdentityUniversalAuthClientSecretsEvent {
+ type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS;
+ metadata: {
+ identityId: string;
+ };
+}
+
+interface RevokeIdentityUniversalAuthClientSecretEvent {
+ type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET;
+ metadata: {
+ identityId: string;
+ clientSecretId: string;
+ };
+}
+
+interface CreateEnvironmentEvent {
+ type: EventType.CREATE_ENVIRONMENT;
+ metadata: {
+ name: string;
+ slug: string;
+ };
+}
+
+interface UpdateEnvironmentEvent {
+ type: EventType.UPDATE_ENVIRONMENT;
+ metadata: {
+ oldName: string;
+ newName: string;
+ oldSlug: string;
+ newSlug: string;
+ };
+}
+
+interface DeleteEnvironmentEvent {
+ type: EventType.DELETE_ENVIRONMENT;
+ metadata: {
+ name: string;
+ slug: string;
+ };
+}
+
+interface AddWorkspaceMemberEvent {
+ type: EventType.ADD_WORKSPACE_MEMBER;
+ metadata: {
+ userId: string;
+ email: string;
+ };
+}
+
+interface RemoveWorkspaceMemberEvent {
+ type: EventType.REMOVE_WORKSPACE_MEMBER;
+ metadata: {
+ userId: string;
+ email: string;
+ };
+}
+
+interface CreateFolderEvent {
+ type: EventType.CREATE_FOLDER;
+ metadata: {
+ environment: string;
+ folderId: string;
+ folderName: string;
+ folderPath: string;
+ };
+}
+
+interface UpdateFolderEvent {
+ type: EventType.UPDATE_FOLDER;
+ metadata: {
+ environment: string;
+ folderId: string;
+ oldFolderName: string;
+ newFolderName: string;
+ folderPath: string;
+ };
+}
+
+interface DeleteFolderEvent {
+ type: EventType.DELETE_FOLDER;
+ metadata: {
+ environment: string;
+ folderId: string;
+ folderName: string;
+ folderPath: string;
+ };
+}
+
+interface CreateWebhookEvent {
+ type: EventType.CREATE_WEBHOOK;
+ metadata: {
+ webhookId: string;
+ environment: string;
+ secretPath: string;
+ isDisabled: boolean;
+ };
+}
+
+interface UpdateWebhookStatusEvent {
+ type: EventType.UPDATE_WEBHOOK_STATUS;
+ metadata: {
+ webhookId: string;
+ environment: string;
+ secretPath: string;
+ isDisabled: boolean;
+ };
+}
+
+interface DeleteWebhookEvent {
+ type: EventType.DELETE_WEBHOOK;
+ metadata: {
+ webhookId: string;
+ environment: string;
+ secretPath: string;
+ isDisabled: boolean;
+ };
+}
+
+interface GetSecretImportsEvent {
+ type: EventType.GET_SECRET_IMPORTS;
+ metadata: {
+ environment: string;
+ secretImportId: string;
+ folderId: string;
+ numberOfImports: number;
+ };
+}
+
+interface CreateSecretImportEvent {
+ type: EventType.CREATE_SECRET_IMPORT;
+ metadata: {
+ secretImportId: string;
+ folderId: string;
+ importFromEnvironment: string;
+ importFromSecretPath: string;
+ importToEnvironment: string;
+ importToSecretPath: string;
+ };
+}
+
+interface UpdateSecretImportEvent {
+ type: EventType.UPDATE_SECRET_IMPORT;
+ metadata: {
+ secretImportId: string;
+ folderId: string;
+ importToEnvironment: string;
+ importToSecretPath: string;
+ orderBefore: {
+ environment: string;
+ secretPath: string;
+ }[];
+ orderAfter: {
+ environment: string;
+ secretPath: string;
+ }[];
+ };
+}
+
+interface DeleteSecretImportEvent {
+ type: EventType.DELETE_SECRET_IMPORT;
+ metadata: {
+ secretImportId: string;
+ folderId: string;
+ importFromEnvironment: string;
+ importFromSecretPath: string;
+ importToEnvironment: string;
+ importToSecretPath: string;
+ };
+}
+
+interface UpdateUserRole {
+ type: EventType.UPDATE_USER_WORKSPACE_ROLE;
+ metadata: {
+ userId: string;
+ email: string;
+ oldRole: string;
+ newRole: string;
+ };
+}
+
+interface UpdateUserDeniedPermissions {
+ type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
+ metadata: {
+ userId: string;
+ email: string;
+ deniedPermissions: {
+ environmentSlug: string;
+ ability: string;
+ }[];
+ };
+}
+
+interface CreateCa {
+ type: EventType.CREATE_CA;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface GetCa {
+ type: EventType.GET_CA;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface UpdateCa {
+ type: EventType.UPDATE_CA;
+ metadata: {
+ caId: string;
+ dn: string;
+ status: CaStatus;
+ };
+}
+
+interface DeleteCa {
+ type: EventType.DELETE_CA;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface GetCaCsr {
+ type: EventType.GET_CA_CSR;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface GetCaCert {
+ type: EventType.GET_CA_CERT;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface SignIntermediate {
+ type: EventType.SIGN_INTERMEDIATE;
+ metadata: {
+ caId: string;
+ dn: string;
+ serialNumber: string;
+ };
+}
+
+interface ImportCaCert {
+ type: EventType.IMPORT_CA_CERT;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface GetCaCrl {
+ type: EventType.GET_CA_CRL;
+ metadata: {
+ caId: string;
+ dn: string;
+ };
+}
+
+interface IssueCert {
+ type: EventType.ISSUE_CERT;
+ metadata: {
+ caId: string;
+ dn: string;
+ serialNumber: string;
+ };
+}
+
+interface GetCert {
+ type: EventType.GET_CERT;
+ metadata: {
+ certId: string;
+ cn: string;
+ serialNumber: string;
+ };
+}
+
+interface DeleteCert {
+ type: EventType.DELETE_CERT;
+ metadata: {
+ certId: string;
+ cn: string;
+ serialNumber: string;
+ };
+}
+
+interface RevokeCert {
+ type: EventType.REVOKE_CERT;
+ metadata: {
+ certId: string;
+ cn: string;
+ serialNumber: string;
+ };
+}
+
+interface GetCertBody {
+ type: EventType.GET_CERT_BODY;
+ metadata: {
+ certId: string;
+ cn: string;
+ serialNumber: string;
+ };
+}
+
+interface CreatePkiAlert {
+ type: EventType.CREATE_PKI_ALERT;
+ metadata: {
+ pkiAlertId: string;
+ pkiCollectionId: string;
+ name: string;
+ alertBeforeDays: number;
+ recipientEmails: string;
+ };
+}
+interface GetPkiAlert {
+ type: EventType.GET_PKI_ALERT;
+ metadata: {
+ pkiAlertId: string;
+ };
+}
+
+interface UpdatePkiAlert {
+ type: EventType.UPDATE_PKI_ALERT;
+ metadata: {
+ pkiAlertId: string;
+ pkiCollectionId?: string;
+ name?: string;
+ alertBeforeDays?: number;
+ recipientEmails?: string;
+ };
+}
+interface DeletePkiAlert {
+ type: EventType.DELETE_PKI_ALERT;
+ metadata: {
+ pkiAlertId: string;
+ };
+}
+
+interface CreatePkiCollection {
+ type: EventType.CREATE_PKI_COLLECTION;
+ metadata: {
+ pkiCollectionId: string;
+ name: string;
+ };
+}
+
+interface GetPkiCollection {
+ type: EventType.GET_PKI_COLLECTION;
+ metadata: {
+ pkiCollectionId: string;
+ };
+}
+
+interface UpdatePkiCollection {
+ type: EventType.UPDATE_PKI_COLLECTION;
+ metadata: {
+ pkiCollectionId: string;
+ name?: string;
+ };
+}
+
+interface DeletePkiCollection {
+ type: EventType.DELETE_PKI_COLLECTION;
+ metadata: {
+ pkiCollectionId: string;
+ };
+}
+
+interface GetPkiCollectionItems {
+ type: EventType.GET_PKI_COLLECTION_ITEMS;
+ metadata: {
+ pkiCollectionId: string;
+ };
+}
+
+interface AddPkiCollectionItem {
+ type: EventType.ADD_PKI_COLLECTION_ITEM;
+ metadata: {
+ pkiCollectionItemId: string;
+ pkiCollectionId: string;
+ type: PkiItemType;
+ itemId: string;
+ };
+}
+
+interface DeletePkiCollectionItem {
+ type: EventType.DELETE_PKI_COLLECTION_ITEM;
+ metadata: {
+ pkiCollectionItemId: string;
+ pkiCollectionId: string;
+ };
+}
+
+interface OrgAdminAccessProjectEvent {
+ type: EventType.ORG_ADMIN_ACCESS_PROJECT;
+ metadata: {
+ userId: string;
+ username: string;
+ email: string;
+ projectId: string;
+ }; // no metadata yet
+}
+
+interface CreateCertificateTemplate {
+ type: EventType.CREATE_CERTIFICATE_TEMPLATE;
+ metadata: {
+ certificateTemplateId: string;
+ caId: string;
+ pkiCollectionId?: string;
+ name: string;
+ commonName: string;
+ subjectAlternativeName: string;
+ ttl: string;
+ };
+}
+
+interface GetCertificateTemplate {
+ type: EventType.GET_CERTIFICATE_TEMPLATE;
+ metadata: {
+ certificateTemplateId: string;
+ };
+}
+
+interface UpdateCertificateTemplate {
+ type: EventType.UPDATE_CERTIFICATE_TEMPLATE;
+ metadata: {
+ certificateTemplateId: string;
+ caId: string;
+ pkiCollectionId?: string;
+ name: string;
+ commonName: string;
+ subjectAlternativeName: string;
+ ttl: string;
+ };
+}
+
+interface DeleteCertificateTemplate {
+ type: EventType.DELETE_CERTIFICATE_TEMPLATE;
+ metadata: {
+ certificateTemplateId: string;
+ };
+}
+
+interface CreateCertificateTemplateEstConfig {
+ type: EventType.CREATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
+ metadata: {
+ certificateTemplateId: string;
+ isEnabled: boolean;
+ };
+}
+
+interface UpdateCertificateTemplateEstConfig {
+ type: EventType.UPDATE_CERTIFICATE_TEMPLATE_EST_CONFIG;
+ metadata: {
+ certificateTemplateId: string;
+ isEnabled: boolean;
+ };
+}
+
+interface GetCertificateTemplateEstConfig {
+ type: EventType.GET_CERTIFICATE_TEMPLATE_EST_CONFIG;
+ metadata: {
+ certificateTemplateId: string;
+ };
+}
+
+interface UpdateProjectSlackConfig {
+ type: EventType.UPDATE_PROJECT_SLACK_CONFIG;
+ metadata: {
+ id: string;
+ slackIntegrationId: string;
+ isAccessRequestNotificationEnabled: boolean;
+ accessRequestChannels: string;
+ isSecretRequestNotificationEnabled: boolean;
+ secretRequestChannels: string;
+ };
+}
+
+interface GetProjectSlackConfig {
+ type: EventType.GET_PROJECT_SLACK_CONFIG;
+ metadata: {
+ id: string;
+ };
+}
+
+export enum IntegrationSyncedEventTrigger {
+ MANUAL = "manual",
+ AUTO = "auto"
+}
+
+interface IntegrationSyncedEvent {
+ type: EventType.INTEGRATION_SYNCED;
+ metadata: {
+ integrationId: string;
+ lastSyncJobId: string;
+ lastUsed: Date;
+ syncMessage: string;
+ isSynced: boolean;
+ };
+}
+
+export type Event =
+ | GetSecretsEvent
+ | GetSecretEvent
+ | CreateSecretEvent
+ | UpdateSecretEvent
+ | DeleteSecretEvent
+ | GetWorkspaceKeyEvent
+ | AuthorizeIntegrationEvent
+ | UnauthorizeIntegrationEvent
+ | CreateIntegrationEvent
+ | DeleteIntegrationEvent
+ | AddTrustedIPEvent
+ | UpdateTrustedIPEvent
+ | DeleteTrustedIPEvent
+ | CreateServiceTokenEvent
+ | DeleteServiceTokenEvent
+ | CreateIdentityEvent
+ | UpdateIdentityEvent
+ | DeleteIdentityEvent
+ | LoginIdentityUniversalAuthEvent
+ | AddIdentityUniversalAuthEvent
+ | UpdateIdentityUniversalAuthEvent
+ | GetIdentityUniversalAuthEvent
+ | CreateIdentityUniversalAuthClientSecretEvent
+ | GetIdentityUniversalAuthClientSecretsEvent
+ | RevokeIdentityUniversalAuthClientSecretEvent
+ | CreateEnvironmentEvent
+ | UpdateEnvironmentEvent
+ | DeleteEnvironmentEvent
+ | AddWorkspaceMemberEvent
+ | RemoveWorkspaceMemberEvent
+ | CreateFolderEvent
+ | UpdateFolderEvent
+ | DeleteFolderEvent
+ | CreateWebhookEvent
+ | UpdateWebhookStatusEvent
+ | DeleteWebhookEvent
+ | GetSecretImportsEvent
+ | CreateSecretImportEvent
+ | UpdateSecretImportEvent
+ | DeleteSecretImportEvent
+ | UpdateUserRole
+ | UpdateUserDeniedPermissions
+ | CreateCa
+ | GetCa
+ | UpdateCa
+ | DeleteCa
+ | GetCaCsr
+ | GetCaCert
+ | SignIntermediate
+ | ImportCaCert
+ | GetCaCrl
+ | IssueCert
+ | GetCert
+ | DeleteCert
+ | RevokeCert
+ | GetCertBody
+ | CreatePkiAlert
+ | GetPkiAlert
+ | UpdatePkiAlert
+ | DeletePkiAlert
+ | CreatePkiCollection
+ | GetPkiCollection
+ | UpdatePkiCollection
+ | DeletePkiCollection
+ | GetPkiCollectionItems
+ | AddPkiCollectionItem
+ | DeletePkiCollectionItem
+ | OrgAdminAccessProjectEvent
+ | CreateCertificateTemplate
+ | UpdateCertificateTemplate
+ | GetCertificateTemplate
+ | DeleteCertificateTemplate
+ | UpdateCertificateTemplateEstConfig
+ | CreateCertificateTemplateEstConfig
+ | GetCertificateTemplateEstConfig
+ | UpdateProjectSlackConfig
+ | GetProjectSlackConfig
+ | IntegrationSyncedEvent;
+
+export type AuditLog = {
+ id: string;
+ actor: Actor;
+ organization: string;
+ workspace: string;
+ ipAddress: string;
+ event: Event;
+ userAgent: string;
+ userAgentType: UserAgentType;
+ createdAt: string;
+ updatedAt: string;
+ projectName?: string;
+ projectId?: string;
+};
diff --git a/frontend-v2/src/hooks/api/auth/index.tsx b/frontend-v2/src/hooks/api/auth/index.tsx
new file mode 100644
index 000000000..66688cbdc
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auth/index.tsx
@@ -0,0 +1,12 @@
+export {
+ useGetAuthToken,
+ useOauthTokenExchange,
+ useResetPassword,
+ useSelectOrganization,
+ useSendMfaToken,
+ useSendPasswordResetEmail,
+ useSendVerificationEmail,
+ useVerifyMfaToken,
+ useVerifyPasswordResetCode,
+ useVerifySignupEmailVerificationCode
+} from "./queries";
diff --git a/frontend-v2/src/hooks/api/auth/queries.tsx b/frontend-v2/src/hooks/api/auth/queries.tsx
new file mode 100644
index 000000000..ad93a5745
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auth/queries.tsx
@@ -0,0 +1,322 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+import SecurityClient from "@app/components/utilities/SecurityClient";
+import { apiRequest } from "@app/config/request";
+import { setAuthToken } from "@app/hooks/api/reactQuery";
+
+import { organizationKeys } from "../organization/queries";
+import { workspaceKeys } from "../workspace";
+import {
+ ChangePasswordDTO,
+ CompleteAccountDTO,
+ CompleteAccountSignupDTO,
+ GetAuthTokenAPI,
+ GetBackupEncryptedPrivateKeyDTO,
+ IssueBackupPrivateKeyDTO,
+ Login1DTO,
+ Login1Res,
+ Login2DTO,
+ Login2Res,
+ LoginLDAPDTO,
+ LoginLDAPRes,
+ MfaMethod,
+ ResetPasswordDTO,
+ SendMfaTokenDTO,
+ SRP1DTO,
+ SRPR1Res,
+ TOauthTokenExchangeDTO,
+ UserAgentType,
+ VerifyMfaTokenDTO,
+ VerifyMfaTokenRes,
+ VerifySignupInviteDTO
+} from "./types";
+
+const authKeys = {
+ getAuthToken: ["token"] as const
+};
+
+export const login1 = async (loginDetails: Login1DTO) => {
+ const { data } = await apiRequest.post("/api/v3/auth/login1", loginDetails);
+ return data;
+};
+
+export const login2 = async (loginDetails: Login2DTO) => {
+ const { data } = await apiRequest.post("/api/v3/auth/login2", loginDetails);
+ return data;
+};
+
+export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
+ const { data } = await apiRequest.post("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
+ return data;
+};
+
+export const useLogin1 = () => {
+ return useMutation({
+ mutationFn: async (details: {
+ email: string;
+ clientPublicKey: string;
+ providerAuthToken?: string;
+ }) => {
+ return login1(details);
+ }
+ });
+};
+
+export const selectOrganization = async (data: {
+ organizationId: string;
+ userAgent?: UserAgentType;
+}) => {
+ const { data: res } = await apiRequest.post<{
+ token: string;
+ isMfaEnabled: boolean;
+ mfaMethod?: MfaMethod;
+ }>("/api/v3/auth/select-organization", data);
+ return res;
+};
+
+export const useSelectOrganization = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (details: { organizationId: string; userAgent?: UserAgentType }) => {
+ 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.
+ if (!details.userAgent && !data.isMfaEnabled) {
+ SecurityClient.setToken(data.token);
+ SecurityClient.setProviderAuthToken("");
+ }
+
+ return data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries([
+ organizationKeys.getUserOrganizations,
+ workspaceKeys.getAllUserWorkspace
+ ]);
+ }
+ });
+};
+
+export const useLogin2 = () => {
+ return useMutation({
+ mutationFn: async (details: {
+ email: string;
+ clientProof: string;
+ password: string;
+ providerAuthToken?: string;
+ }) => {
+ return login2(details);
+ }
+ });
+};
+
+export const oauthTokenExchange = async (details: TOauthTokenExchangeDTO) => {
+ const { data } = await apiRequest.post("/api/v1/sso/token-exchange", details);
+ return data;
+};
+
+export const useOauthTokenExchange = () => {
+ // note: use after srp1
+ return useMutation({
+ mutationFn: async (details: TOauthTokenExchangeDTO) => {
+ return oauthTokenExchange(details);
+ }
+ });
+};
+
+export const srp1 = async (details: SRP1DTO) => {
+ const { data } = await apiRequest.post("/api/v1/password/srp1", details);
+ return data;
+};
+
+export const completeAccountSignup = async (details: CompleteAccountSignupDTO) => {
+ const { data } = await apiRequest.post("/api/v3/signup/complete-account/signup", details);
+ return data;
+};
+
+export const completeAccountSignupInvite = async (details: CompleteAccountDTO) => {
+ const { data } = await apiRequest.post("/api/v3/signup/complete-account/invite", details);
+ return data;
+};
+
+export const useCompleteAccountSignup = () => {
+ return useMutation({
+ mutationFn: async (details: CompleteAccountSignupDTO) => {
+ return completeAccountSignup(details);
+ }
+ });
+};
+
+export const useSendMfaToken = () => {
+ return useMutation({
+ mutationFn: async ({ email }) => {
+ const { data } = await apiRequest.post("/api/v2/auth/mfa/send", { email });
+ return data;
+ }
+ });
+};
+
+export const verifyMfaToken = async ({
+ email,
+ mfaCode,
+ mfaMethod
+}: {
+ email: string;
+ mfaCode: string;
+ mfaMethod?: string;
+}) => {
+ const { data } = await apiRequest.post("/api/v2/auth/mfa/verify", {
+ email,
+ mfaToken: mfaCode,
+ mfaMethod
+ });
+
+ return data;
+};
+
+export const useVerifyMfaToken = () => {
+ return useMutation({
+ mutationFn: async ({ email, mfaCode, mfaMethod }) => {
+ return verifyMfaToken({
+ email,
+ mfaCode,
+ mfaMethod
+ });
+ }
+ });
+};
+
+export const verifySignupInvite = async (details: VerifySignupInviteDTO) => {
+ const { data } = await apiRequest.post("/api/v1/invite-org/verify", details);
+ return data;
+};
+
+export const useSendVerificationEmail = () => {
+ return useMutation({
+ mutationFn: async ({ email }: { email: string }) => {
+ const { data } = await apiRequest.post("/api/v3/signup/email/signup", {
+ email
+ });
+
+ return data;
+ }
+ });
+};
+
+export const useVerifySignupEmailVerificationCode = () => {
+ return useMutation({
+ mutationFn: async ({ email, code }: { email: string; code: string }) => {
+ const { data } = await apiRequest.post("/api/v3/signup/email/verify", {
+ email,
+ code
+ });
+
+ return data;
+ }
+ });
+};
+
+export const useSendPasswordResetEmail = () => {
+ return useMutation({
+ mutationFn: async ({ email }: { email: string }) => {
+ const { data } = await apiRequest.post("/api/v1/password/email/password-reset", {
+ email
+ });
+
+ return data;
+ }
+ });
+};
+
+export const useVerifyPasswordResetCode = () => {
+ return useMutation({
+ mutationFn: async ({ email, code }: { email: string; code: string }) => {
+ const { data } = await apiRequest.post("/api/v1/password/email/password-reset-verify", {
+ email,
+ code
+ });
+
+ return data;
+ }
+ });
+};
+
+export const issueBackupPrivateKey = async (details: IssueBackupPrivateKeyDTO) => {
+ const { data } = await apiRequest.post("/api/v1/password/backup-private-key", details);
+ return data;
+};
+
+export const getBackupEncryptedPrivateKey = async ({
+ verificationToken
+}: GetBackupEncryptedPrivateKeyDTO) => {
+ const { data } = await apiRequest.get("/api/v1/password/backup-private-key", {
+ headers: {
+ Authorization: `Bearer ${verificationToken}`
+ }
+ });
+
+ return data.backupPrivateKey;
+};
+
+export const useResetPassword = () => {
+ return useMutation({
+ mutationFn: async (details: ResetPasswordDTO) => {
+ const { data } = await apiRequest.post(
+ "/api/v1/password/password-reset",
+ {
+ protectedKey: details.protectedKey,
+ protectedKeyIV: details.protectedKeyIV,
+ protectedKeyTag: details.protectedKeyTag,
+ encryptedPrivateKey: details.encryptedPrivateKey,
+ encryptedPrivateKeyIV: details.encryptedPrivateKeyIV,
+ encryptedPrivateKeyTag: details.encryptedPrivateKeyTag,
+ salt: details.salt,
+ verifier: details.verifier
+ },
+ {
+ headers: {
+ Authorization: `Bearer ${details.verificationToken}`
+ }
+ }
+ );
+
+ return data;
+ }
+ });
+};
+
+export const changePassword = async (details: ChangePasswordDTO) => {
+ const { data } = await apiRequest.post("/api/v1/password/change-password", details);
+ return data;
+};
+
+export const useChangePassword = () => {
+ // note: use after srp1
+ return useMutation({
+ mutationFn: async (details: ChangePasswordDTO) => {
+ return changePassword(details);
+ }
+ });
+};
+
+// Refresh token is set as cookie when logged in
+// Using that we fetch the auth bearer token needed for auth calls
+const fetchAuthToken = async () => {
+ const { data } = await apiRequest.post("/api/v1/auth/token", undefined, {
+ withCredentials: true
+ });
+
+ return data;
+};
+
+export const useGetAuthToken = () =>
+ useQuery(authKeys.getAuthToken, fetchAuthToken, {
+ onSuccess: (data) => setAuthToken(data.token),
+ retry: 0
+ });
+
+export const checkUserTotpMfa = async () => {
+ const { data } = await apiRequest.get<{ isVerified: boolean }>("/api/v2/auth/mfa/check/totp");
+
+ return data.isVerified;
+};
diff --git a/frontend-v2/src/hooks/api/auth/types.ts b/frontend-v2/src/hooks/api/auth/types.ts
new file mode 100644
index 000000000..fd0604a88
--- /dev/null
+++ b/frontend-v2/src/hooks/api/auth/types.ts
@@ -0,0 +1,157 @@
+export type GetAuthTokenAPI = {
+ token: string;
+};
+
+export type SendMfaTokenDTO = {
+ email: string;
+};
+
+export type VerifyMfaTokenDTO = {
+ email: string;
+ mfaCode: string;
+ mfaMethod: MfaMethod;
+};
+
+export type VerifyMfaTokenRes = {
+ encryptionVersion: number;
+ protectedKey?: string;
+ protectedKeyIV?: string;
+ protectedKeyTag?: string;
+ token: string;
+ publicKey: string;
+ encryptedPrivateKey: string;
+ iv: string;
+ tag: string;
+};
+
+export type TOauthTokenExchangeDTO = {
+ providerAuthToken: string;
+ email: string;
+};
+
+export type Login1DTO = {
+ email: string;
+ clientPublicKey: string;
+ providerAuthToken?: string;
+};
+
+export type Login2DTO = {
+ captchaToken?: string;
+ email: string;
+ clientProof: string;
+ providerAuthToken?: string;
+ password: string;
+};
+
+export type Login1Res = {
+ serverPublicKey: string;
+ salt: string;
+};
+
+export type Login2Res = {
+ mfaEnabled: boolean;
+ token: string;
+ encryptionVersion?: number;
+ protectedKey?: string;
+ protectedKeyIV?: string;
+ protectedKeyTag?: string;
+ publicKey?: string;
+ encryptedPrivateKey?: string;
+ iv?: string;
+ tag?: string;
+};
+
+export type LoginLDAPDTO = {
+ organizationSlug: string;
+ username: string;
+ password: string;
+};
+
+export type LoginLDAPRes = {
+ nextUrl: string;
+};
+
+export type SRP1DTO = {
+ clientPublicKey: string;
+};
+
+export type SRPR1Res = {
+ serverPublicKey: string;
+ salt: string;
+};
+
+export type CompleteAccountDTO = {
+ email: string;
+ firstName: string;
+ lastName: string;
+ protectedKey: string;
+ protectedKeyIV: string;
+ protectedKeyTag: string;
+ publicKey: string;
+ encryptedPrivateKey: string;
+ encryptedPrivateKeyIV: string;
+ encryptedPrivateKeyTag: string;
+ salt: string;
+ verifier: string;
+ password: string;
+ tokenMetadata?: string;
+};
+
+export type CompleteAccountSignupDTO = CompleteAccountDTO & {
+ providerAuthToken?: string;
+ attributionSource?: string;
+ organizationName: string;
+};
+
+export type VerifySignupInviteDTO = {
+ email: string;
+ code: string;
+ organizationId: string;
+};
+
+export type ChangePasswordDTO = {
+ password: string;
+ clientProof: string;
+ protectedKey: string;
+ protectedKeyIV: string;
+ protectedKeyTag: string;
+ encryptedPrivateKey: string;
+ encryptedPrivateKeyIV: string;
+ encryptedPrivateKeyTag: string;
+ salt: string;
+ verifier: string;
+};
+
+export type ResetPasswordDTO = {
+ protectedKey: string;
+ protectedKeyIV: string;
+ protectedKeyTag: string;
+ encryptedPrivateKey: string;
+ encryptedPrivateKeyIV: string;
+ encryptedPrivateKeyTag: string;
+ salt: string;
+ verifier: string;
+ verificationToken: string;
+};
+
+export type IssueBackupPrivateKeyDTO = {
+ encryptedPrivateKey: string;
+ iv: string;
+ tag: string;
+ salt: string;
+ verifier: string;
+ clientProof: string;
+};
+
+export type GetBackupEncryptedPrivateKeyDTO = {
+ verificationToken: string;
+};
+
+export enum UserAgentType {
+ CLI = "cli"
+}
+
+export enum MfaMethod {
+ EMAIL = "email",
+ TOTP = "totp"
+}
diff --git a/frontend-v2/src/hooks/api/bots/index.tsx b/frontend-v2/src/hooks/api/bots/index.tsx
new file mode 100644
index 000000000..17a5fe768
--- /dev/null
+++ b/frontend-v2/src/hooks/api/bots/index.tsx
@@ -0,0 +1 @@
+export { useGetWorkspaceBot, useUpdateBotActiveStatus } from "./queries";
diff --git a/frontend-v2/src/hooks/api/bots/queries.tsx b/frontend-v2/src/hooks/api/bots/queries.tsx
new file mode 100644
index 000000000..23049892c
--- /dev/null
+++ b/frontend-v2/src/hooks/api/bots/queries.tsx
@@ -0,0 +1,37 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { TBot, TSetBotActiveStatusDto } from "./types";
+
+const queryKeys = {
+ getBot: (workspaceId: string) => [{ workspaceId }, "bot"] as const
+};
+
+export const useGetWorkspaceBot = (workspaceId: string) =>
+ useQuery({
+ queryKey: queryKeys.getBot(workspaceId),
+ queryFn: async () => {
+ const {
+ data: { bot }
+ } = await apiRequest.get<{ bot: TBot }>(`/api/v1/bot/${workspaceId}`);
+ return bot;
+ },
+ enabled: Boolean(workspaceId)
+ });
+
+export const useUpdateBotActiveStatus = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TSetBotActiveStatusDto>({
+ mutationFn: ({ botId, isActive, botKey }) => {
+ return apiRequest.patch(`/api/v1/bot/${botId}/active`, {
+ isActive,
+ botKey
+ });
+ },
+ onSuccess: (_, { workspaceId }) => {
+ queryClient.invalidateQueries(queryKeys.getBot(workspaceId));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/bots/types.ts b/frontend-v2/src/hooks/api/bots/types.ts
new file mode 100644
index 000000000..abc7f4313
--- /dev/null
+++ b/frontend-v2/src/hooks/api/bots/types.ts
@@ -0,0 +1,20 @@
+export type TBot = {
+ id: string;
+ name: string;
+ workspace: string;
+ isActive: boolean;
+ publicKey: string;
+ createdAt: string;
+ updatedAt: string;
+ __v: number;
+};
+
+export type TSetBotActiveStatusDto = {
+ workspaceId: string;
+ botId: string;
+ isActive: boolean;
+ botKey?: {
+ encryptedKey: string;
+ nonce: string;
+ };
+};
diff --git a/frontend-v2/src/hooks/api/ca/constants.tsx b/frontend-v2/src/hooks/api/ca/constants.tsx
new file mode 100644
index 000000000..9bb7b89d5
--- /dev/null
+++ b/frontend-v2/src/hooks/api/ca/constants.tsx
@@ -0,0 +1,23 @@
+import { CaStatus, CaType } from "./enums";
+
+export const caTypeToNameMap: { [K in CaType]: string } = {
+ [CaType.ROOT]: "Root",
+ [CaType.INTERMEDIATE]: "Intermediate"
+};
+
+export const caStatusToNameMap: { [K in CaStatus]: string } = {
+ [CaStatus.ACTIVE]: "Active",
+ [CaStatus.DISABLED]: "Disabled",
+ [CaStatus.PENDING_CERTIFICATE]: "Pending Certificate"
+};
+
+export const getCaStatusBadgeVariant = (status: CaStatus) => {
+ switch (status) {
+ case CaStatus.ACTIVE:
+ return "success";
+ case CaStatus.DISABLED:
+ return "danger";
+ default:
+ return "primary";
+ }
+};
diff --git a/frontend-v2/src/hooks/api/ca/enums.tsx b/frontend-v2/src/hooks/api/ca/enums.tsx
new file mode 100644
index 000000000..35d86c452
--- /dev/null
+++ b/frontend-v2/src/hooks/api/ca/enums.tsx
@@ -0,0 +1,14 @@
+export enum CaType {
+ ROOT = "root",
+ INTERMEDIATE = "intermediate"
+}
+
+export enum CaStatus {
+ ACTIVE = "active",
+ DISABLED = "disabled",
+ PENDING_CERTIFICATE = "pending-certificate"
+}
+
+export enum CaRenewalType {
+ EXISTING = "existing"
+}
diff --git a/frontend-v2/src/hooks/api/ca/index.tsx b/frontend-v2/src/hooks/api/ca/index.tsx
new file mode 100644
index 000000000..4993a8fdc
--- /dev/null
+++ b/frontend-v2/src/hooks/api/ca/index.tsx
@@ -0,0 +1,11 @@
+export { CaRenewalType, CaStatus, CaType } from "./enums";
+export {
+ useCreateCa,
+ useCreateCertificate,
+ useDeleteCa,
+ useImportCaCertificate,
+ useRenewCa,
+ useSignIntermediate,
+ useUpdateCa
+} from "./mutations";
+export { useGetCaById, useGetCaCert, useGetCaCerts, useGetCaCertTemplates,useGetCaCrls, useGetCaCsr } from "./queries";
diff --git a/frontend-v2/src/hooks/api/ca/mutations.tsx b/frontend-v2/src/hooks/api/ca/mutations.tsx
new file mode 100644
index 000000000..0bbec617b
--- /dev/null
+++ b/frontend-v2/src/hooks/api/ca/mutations.tsx
@@ -0,0 +1,135 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { workspaceKeys } from "../workspace";
+import { caKeys } from "./queries";
+import {
+ TCertificateAuthority,
+ TCreateCaDTO,
+ TCreateCertificateDTO,
+ TCreateCertificateResponse,
+ TDeleteCaDTO,
+ TImportCaCertificateDTO,
+ TImportCaCertificateResponse,
+ TRenewCaDTO,
+ TRenewCaResponse,
+ TSignIntermediateDTO,
+ TSignIntermediateResponse,
+ TUpdateCaDTO
+} from "./types";
+
+export const useCreateCa = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (body) => {
+ const {
+ data: { ca }
+ } = await apiRequest.post<{ ca: TCertificateAuthority }>("/api/v1/pki/ca/", body);
+ return ca;
+ },
+ onSuccess: (_, { projectSlug }) => {
+ queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
+ }
+ });
+};
+
+export const useUpdateCa = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ caId, projectSlug, ...body }) => {
+ const {
+ data: { ca }
+ } = await apiRequest.patch<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`, body);
+ return ca;
+ },
+ onSuccess: ({ id }, { projectSlug }) => {
+ queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
+ queryClient.invalidateQueries(caKeys.getCaById(id));
+ }
+ });
+};
+
+export const useDeleteCa = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ caId }) => {
+ const {
+ data: { ca }
+ } = await apiRequest.delete<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`);
+ return ca;
+ },
+ onSuccess: (_, { projectSlug }) => {
+ queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
+ }
+ });
+};
+
+export const useSignIntermediate = () => {
+ // TODO: consider renaming
+ return useMutation({
+ mutationFn: async (body) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/pki/ca/${body.caId}/sign-intermediate`,
+ body
+ );
+ return data;
+ }
+ });
+};
+
+export const useImportCaCertificate = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ caId, ...body }) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/pki/ca/${caId}/import-certificate`,
+ body
+ );
+ return data;
+ },
+ onSuccess: (_, { caId, projectSlug }) => {
+ queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
+ queryClient.invalidateQueries(caKeys.getCaCerts(caId));
+ queryClient.invalidateQueries(caKeys.getCaCert(caId));
+ }
+ });
+};
+
+// consider rename to issue certificate
+export const useCreateCertificate = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (body) => {
+ const { data } = await apiRequest.post(
+ "/api/v1/pki/certificates/issue-certificate",
+ body
+ );
+ return data;
+ },
+ onSuccess: (_, { projectSlug }) => {
+ queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug));
+ }
+ });
+};
+
+export const useRenewCa = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (body) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/pki/ca/${body.caId}/renew`,
+ body
+ );
+ return data;
+ },
+ onSuccess: (_, { caId, projectSlug }) => {
+ queryClient.invalidateQueries(workspaceKeys.getWorkspaceCas({ projectSlug }));
+ queryClient.invalidateQueries(caKeys.getCaById(caId));
+ queryClient.invalidateQueries(caKeys.getCaCert(caId));
+ queryClient.invalidateQueries(caKeys.getCaCerts(caId));
+ queryClient.invalidateQueries(caKeys.getCaCsr(caId));
+ queryClient.invalidateQueries(caKeys.getCaCrl(caId));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/ca/queries.tsx b/frontend-v2/src/hooks/api/ca/queries.tsx
new file mode 100644
index 000000000..a1e633776
--- /dev/null
+++ b/frontend-v2/src/hooks/api/ca/queries.tsx
@@ -0,0 +1,107 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { TCertificateTemplate } from "../certificateTemplates/types";
+import { TCertificateAuthority } from "./types";
+
+export const caKeys = {
+ getCaById: (caId: string) => [{ caId }, "ca"],
+ getCaCerts: (caId: string) => [{ caId }, "ca-cert"],
+ getCaCrls: (caId: string) => [{ caId }, "ca-crls"],
+ getCaCert: (caId: string) => [{ caId }, "ca-cert"],
+ getCaCsr: (caId: string) => [{ caId }, "ca-csr"],
+ getCaCrl: (caId: string) => [{ caId }, "ca-crl"],
+ getCaCertTemplates: (caId: string) => [{ caId }, "ca-cert-templates"],
+ getCaEstConfig: (caId: string) => [{ caId }, "ca-est-config"]
+};
+
+export const useGetCaById = (caId: string) => {
+ return useQuery({
+ queryKey: caKeys.getCaById(caId),
+ queryFn: async () => {
+ const {
+ data: { ca }
+ } = await apiRequest.get<{ ca: TCertificateAuthority }>(`/api/v1/pki/ca/${caId}`);
+ return ca;
+ },
+ enabled: Boolean(caId)
+ });
+};
+
+export const useGetCaCerts = (caId: string) => {
+ return useQuery({
+ queryKey: caKeys.getCaCerts(caId),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<
+ {
+ certificate: string;
+ certificateChain: string;
+ serialNumber: string;
+ version: number;
+ }[]
+ >(`/api/v1/pki/ca/${caId}/ca-certificates`); // TODO: consider updating endpoint structure
+ return data;
+ },
+ enabled: Boolean(caId)
+ });
+};
+
+export const useGetCaCert = (caId: string) => {
+ return useQuery({
+ queryKey: caKeys.getCaCert(caId),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<{
+ certificate: string;
+ certificateChain: string;
+ serialNumber: string;
+ }>(`/api/v1/pki/ca/${caId}/certificate`); // TODO: consider updating endpoint structure
+ return data;
+ },
+ enabled: Boolean(caId)
+ });
+};
+
+export const useGetCaCsr = (caId: string) => {
+ return useQuery({
+ queryKey: caKeys.getCaCsr(caId),
+ queryFn: async () => {
+ const {
+ data: { csr }
+ } = await apiRequest.get<{
+ csr: string;
+ }>(`/api/v1/pki/ca/${caId}/csr`);
+ return csr;
+ },
+ enabled: Boolean(caId)
+ });
+};
+
+export const useGetCaCrls = (caId: string) => {
+ return useQuery({
+ queryKey: caKeys.getCaCrls(caId),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<
+ {
+ id: string;
+ crl: string;
+ }[]
+ >(`/api/v1/pki/ca/${caId}/crls`);
+ return data;
+ },
+ enabled: Boolean(caId)
+ });
+};
+
+export const useGetCaCertTemplates = (caId: string) => {
+ return useQuery({
+ queryKey: caKeys.getCaCertTemplates(caId),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<{
+ certificateTemplates: TCertificateTemplate[];
+ }>(`/api/v1/pki/ca/${caId}/certificate-templates`);
+ return data;
+ },
+ enabled: Boolean(caId)
+ });
+};
\ No newline at end of file
diff --git a/frontend-v2/src/hooks/api/ca/types.ts b/frontend-v2/src/hooks/api/ca/types.ts
new file mode 100644
index 000000000..25e5112e0
--- /dev/null
+++ b/frontend-v2/src/hooks/api/ca/types.ts
@@ -0,0 +1,117 @@
+import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "../certificates/enums";
+import { CaRenewalType, CaStatus, CaType } from "./enums";
+
+export type TCertificateAuthority = {
+ id: string;
+ parentCaId?: string;
+ projectId: string;
+ type: CaType;
+ status: CaStatus;
+ friendlyName: string;
+ organization: string;
+ ou: string;
+ country: string;
+ province: string;
+ locality: string;
+ commonName: string;
+ dn: string;
+ maxPathLength?: number;
+ notAfter?: string;
+ notBefore?: string;
+ keyAlgorithm: CertKeyAlgorithm;
+ requireTemplateForIssuance: boolean;
+ activeCaCertId?: string;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type TCreateCaDTO = {
+ projectSlug: string;
+ type: string;
+ friendlyName?: string;
+ organization: string;
+ ou: string;
+ country: string;
+ province: string;
+ locality: string;
+ commonName: string;
+ notAfter?: string;
+ maxPathLength: number;
+ keyAlgorithm: CertKeyAlgorithm;
+ requireTemplateForIssuance: boolean;
+};
+
+export type TUpdateCaDTO = {
+ projectSlug: string;
+ caId: string;
+ status?: CaStatus;
+ requireTemplateForIssuance?: boolean;
+};
+
+export type TDeleteCaDTO = {
+ projectSlug: string;
+ caId: string;
+};
+
+export type TSignIntermediateDTO = {
+ caId: string;
+ csr: string;
+ maxPathLength: number;
+ notBefore?: string;
+ notAfter?: string;
+};
+
+export type TSignIntermediateResponse = {
+ certificate: string;
+ certificateChain: string;
+ issuingCaCertificate: string;
+ serialNumber: string;
+};
+
+export type TImportCaCertificateDTO = {
+ caId: string;
+ projectSlug: string;
+ certificate: string;
+ certificateChain: string;
+};
+
+export type TImportCaCertificateResponse = {
+ message: string;
+ caId: string;
+};
+
+export type TCreateCertificateDTO = {
+ projectSlug: string;
+ caId?: string;
+ certificateTemplateId?: string;
+ pkiCollectionId?: string;
+ friendlyName?: string;
+ commonName: string;
+ altNames: string; // sans
+ ttl: string; // string compatible with ms
+ notBefore?: string;
+ notAfter?: string;
+ keyUsages: CertKeyUsage[];
+ extendedKeyUsages: CertExtendedKeyUsage[];
+};
+
+export type TCreateCertificateResponse = {
+ certificate: string;
+ issuingCertificate: string;
+ certificateChain: string;
+ privateKey: string;
+ serialNumber: string;
+};
+
+export type TRenewCaDTO = {
+ projectSlug: string;
+ caId: string;
+ type: CaRenewalType;
+ notAfter: string;
+};
+
+export type TRenewCaResponse = {
+ certificate: string;
+ certificateChain: string;
+ serialNumber: string;
+};
diff --git a/frontend-v2/src/hooks/api/certificateTemplates/index.tsx b/frontend-v2/src/hooks/api/certificateTemplates/index.tsx
new file mode 100644
index 000000000..61dfb35a2
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificateTemplates/index.tsx
@@ -0,0 +1,8 @@
+export {
+ useCreateCertTemplate,
+ useCreateEstConfig,
+ useDeleteCertTemplate,
+ useUpdateCertTemplate,
+ useUpdateEstConfig
+} from "./mutations";
+export { useGetCertTemplate, useGetEstConfig } from "./queries";
diff --git a/frontend-v2/src/hooks/api/certificateTemplates/mutations.tsx b/frontend-v2/src/hooks/api/certificateTemplates/mutations.tsx
new file mode 100644
index 000000000..8545cf70c
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificateTemplates/mutations.tsx
@@ -0,0 +1,100 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { caKeys } from "../ca/queries";
+import { workspaceKeys } from "../workspace";
+import { certTemplateKeys } from "./queries";
+import {
+ TCertificateTemplate,
+ TCreateCertificateTemplateDTO,
+ TCreateEstConfigDTO,
+ TDeleteCertificateTemplateDTO,
+ TUpdateCertificateTemplateDTO,
+ TUpdateEstConfigDTO
+} from "./types";
+
+export const useCreateCertTemplate = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (data) => {
+ const { data: certificateTemplate } = await apiRequest.post(
+ "/api/v1/pki/certificate-templates",
+ data
+ );
+ return certificateTemplate;
+ },
+ onSuccess: ({ caId }, { projectId }) => {
+ queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
+ queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
+ }
+ });
+};
+
+export const useUpdateCertTemplate = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (data) => {
+ const { data: certificateTemplate } = await apiRequest.patch(
+ `/api/v1/pki/certificate-templates/${data.id}`,
+ data
+ );
+
+ return certificateTemplate;
+ },
+ onSuccess: ({ caId }, { projectId, id }) => {
+ queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
+ queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
+ queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
+ }
+ });
+};
+
+export const useDeleteCertTemplate = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (data) => {
+ const { data: certificateTemplate } = await apiRequest.delete(
+ `/api/v1/pki/certificate-templates/${data.id}`
+ );
+ return certificateTemplate;
+ },
+ onSuccess: ({ caId }, { projectId, id }) => {
+ queryClient.invalidateQueries(workspaceKeys.getWorkspaceCertificateTemplates(projectId));
+ queryClient.invalidateQueries(certTemplateKeys.getCertTemplateById(id));
+ queryClient.invalidateQueries(caKeys.getCaCertTemplates(caId));
+ }
+ });
+};
+
+export const useCreateEstConfig = () => {
+ const queryClient = useQueryClient();
+ return useMutation<{}, {}, TCreateEstConfigDTO>({
+ mutationFn: async (body) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/pki/certificate-templates/${body.certificateTemplateId}/est-config`,
+ body
+ );
+ return data;
+ },
+ onSuccess: (_, { certificateTemplateId }) => {
+ queryClient.invalidateQueries(certTemplateKeys.getEstConfig(certificateTemplateId));
+ }
+ });
+};
+
+export const useUpdateEstConfig = () => {
+ const queryClient = useQueryClient();
+ return useMutation<{}, {}, TUpdateEstConfigDTO>({
+ mutationFn: async (body) => {
+ const { data } = await apiRequest.patch(
+ `/api/v1/pki/certificate-templates/${body.certificateTemplateId}/est-config`,
+ body
+ );
+ return data;
+ },
+ onSuccess: (_, { certificateTemplateId }) => {
+ queryClient.invalidateQueries(certTemplateKeys.getEstConfig(certificateTemplateId));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/certificateTemplates/queries.tsx b/frontend-v2/src/hooks/api/certificateTemplates/queries.tsx
new file mode 100644
index 000000000..7ee5bbd30
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificateTemplates/queries.tsx
@@ -0,0 +1,37 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { TCertificateTemplate, TEstConfig } from "./types";
+
+export const certTemplateKeys = {
+ getCertTemplateById: (id: string) => [{ id }, "cert-template"],
+ getEstConfig: (id: string) => [{ id }, "cert-template-est-config"]
+};
+
+export const useGetCertTemplate = (id: string) => {
+ return useQuery({
+ queryKey: certTemplateKeys.getCertTemplateById(id),
+ queryFn: async () => {
+ const { data: certificateTemplate } = await apiRequest.get(
+ `/api/v1/pki/certificate-templates/${id}`
+ );
+ return certificateTemplate;
+ },
+ enabled: Boolean(id)
+ });
+};
+
+export const useGetEstConfig = (certificateTemplateId: string) => {
+ return useQuery({
+ queryKey: certTemplateKeys.getEstConfig(certificateTemplateId),
+ queryFn: async () => {
+ const { data: estConfig } = await apiRequest.get(
+ `/api/v1/pki/certificate-templates/${certificateTemplateId}/est-config`
+ );
+
+ return estConfig;
+ },
+ enabled: Boolean(certificateTemplateId)
+ });
+};
diff --git a/frontend-v2/src/hooks/api/certificateTemplates/types.ts b/frontend-v2/src/hooks/api/certificateTemplates/types.ts
new file mode 100644
index 000000000..14367a280
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificateTemplates/types.ts
@@ -0,0 +1,69 @@
+import { CertExtendedKeyUsage, CertKeyUsage } from "../certificates/enums";
+
+export type TCertificateTemplate = {
+ id: string;
+ caId: string;
+ caName: string;
+ projectId: string;
+ pkiCollectionId?: string;
+ name: string;
+ commonName: string;
+ subjectAlternativeName: string;
+ ttl: string;
+ keyUsages: CertKeyUsage[];
+ extendedKeyUsages: CertExtendedKeyUsage[];
+};
+
+export type TCreateCertificateTemplateDTO = {
+ caId: string;
+ pkiCollectionId?: string;
+ name: string;
+ commonName: string;
+ subjectAlternativeName: string;
+ ttl: string;
+ projectId: string;
+ keyUsages: CertKeyUsage[];
+ extendedKeyUsages: CertExtendedKeyUsage[];
+};
+
+export type TUpdateCertificateTemplateDTO = {
+ id: string;
+ caId?: string;
+ pkiCollectionId?: string;
+ name?: string;
+ commonName?: string;
+ subjectAlternativeName?: string;
+ ttl?: string;
+ projectId: string;
+ keyUsages?: CertKeyUsage[];
+ extendedKeyUsages?: CertExtendedKeyUsage[];
+};
+
+export type TDeleteCertificateTemplateDTO = {
+ id: string;
+ projectId: string;
+};
+
+export type TCreateEstConfigDTO = {
+ certificateTemplateId: string;
+ caChain?: string;
+ passphrase: string;
+ isEnabled: boolean;
+ disableBootstrapCertValidation: boolean;
+};
+
+export type TUpdateEstConfigDTO = {
+ certificateTemplateId: string;
+ caChain?: string;
+ passphrase?: string;
+ isEnabled?: boolean;
+ disableBootstrapCertValidation?: boolean;
+};
+
+export type TEstConfig = {
+ id: string;
+ certificateTemplateId: string;
+ caChain: string;
+ isEnabled: boolean;
+ disableBootstrapCertValidation: boolean;
+};
diff --git a/frontend-v2/src/hooks/api/certificates/constants.tsx b/frontend-v2/src/hooks/api/certificates/constants.tsx
new file mode 100644
index 000000000..0384ea6cd
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificates/constants.tsx
@@ -0,0 +1,98 @@
+import {
+ CertExtendedKeyUsage,
+ CertKeyAlgorithm,
+ CertKeyUsage,
+ CertStatus,
+ CrlReason
+} from "./enums";
+
+export const certStatusToNameMap: { [K in CertStatus]: string } = {
+ [CertStatus.ACTIVE]: "Active",
+ [CertStatus.REVOKED]: "Revoked"
+};
+
+export const getCertStatusBadgeVariant = (status: CertStatus) => {
+ switch (status) {
+ case CertStatus.ACTIVE:
+ return "success";
+ case CertStatus.REVOKED:
+ return "danger";
+ default:
+ return "primary";
+ }
+};
+
+export const certKeyAlgorithmToNameMap: { [K in CertKeyAlgorithm]: string } = {
+ [CertKeyAlgorithm.RSA_2048]: "RSA 2048",
+ [CertKeyAlgorithm.RSA_4096]: "RSA 4096",
+ [CertKeyAlgorithm.ECDSA_P256]: "ECDSA P256",
+ [CertKeyAlgorithm.ECDSA_P384]: "ECDSA P384"
+};
+
+export const certKeyAlgorithms = [
+ { label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_2048], value: CertKeyAlgorithm.RSA_2048 },
+ { label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.RSA_4096], value: CertKeyAlgorithm.RSA_4096 },
+ {
+ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.ECDSA_P256],
+ value: CertKeyAlgorithm.ECDSA_P256
+ },
+ {
+ label: certKeyAlgorithmToNameMap[CertKeyAlgorithm.ECDSA_P384],
+ value: CertKeyAlgorithm.ECDSA_P384
+ }
+];
+
+export const crlReasonToNameMap: { [K in CrlReason]: string } = {
+ [CrlReason.UNSPECIFIED]: "Unspecified",
+ [CrlReason.KEY_COMPROMISE]: "Key Compromise",
+ [CrlReason.CA_COMPROMISE]: "CA Compromise",
+ [CrlReason.AFFILIATION_CHANGED]: "Affiliation Changed",
+ [CrlReason.SUPERSEDED]: "Superseded",
+ [CrlReason.CESSATION_OF_OPERATION]: "Cessation of Operation",
+ [CrlReason.CERTIFICATE_HOLD]: "Certificate Hold",
+ // [CrlReason.REMOVE_FROM_CRL]: "Remove from CRL",
+ [CrlReason.PRIVILEGE_WITHDRAWN]: "Privilege Withdrawn",
+ [CrlReason.A_A_COMPROMISE]: "A/A Compromise"
+};
+
+export const crlReasons = [
+ { label: crlReasonToNameMap[CrlReason.UNSPECIFIED], value: CrlReason.UNSPECIFIED },
+ { label: crlReasonToNameMap[CrlReason.KEY_COMPROMISE], value: CrlReason.KEY_COMPROMISE },
+ { label: crlReasonToNameMap[CrlReason.CA_COMPROMISE], value: CrlReason.CA_COMPROMISE },
+ {
+ label: crlReasonToNameMap[CrlReason.AFFILIATION_CHANGED],
+ value: CrlReason.AFFILIATION_CHANGED
+ },
+ { label: crlReasonToNameMap[CrlReason.SUPERSEDED], value: CrlReason.SUPERSEDED },
+ {
+ label: crlReasonToNameMap[CrlReason.CESSATION_OF_OPERATION],
+ value: CrlReason.CESSATION_OF_OPERATION
+ },
+ { label: crlReasonToNameMap[CrlReason.CERTIFICATE_HOLD], value: CrlReason.CERTIFICATE_HOLD },
+ {
+ label: crlReasonToNameMap[CrlReason.PRIVILEGE_WITHDRAWN],
+ value: CrlReason.PRIVILEGE_WITHDRAWN
+ },
+ { label: crlReasonToNameMap[CrlReason.A_A_COMPROMISE], value: CrlReason.A_A_COMPROMISE }
+];
+
+export const KEY_USAGES_OPTIONS = [
+ { value: CertKeyUsage.DIGITAL_SIGNATURE, label: "Digital Signature" },
+ { value: CertKeyUsage.KEY_ENCIPHERMENT, label: "Key Encipherment" },
+ { value: CertKeyUsage.NON_REPUDIATION, label: "Non Repudiation" },
+ { value: CertKeyUsage.DATA_ENCIPHERMENT, label: "Data Encipherment" },
+ { value: CertKeyUsage.KEY_AGREEMENT, label: "Key Agreement" },
+ { value: CertKeyUsage.KEY_CERT_SIGN, label: "Certificate Sign" },
+ { value: CertKeyUsage.CRL_SIGN, label: "CRL Sign" },
+ { value: CertKeyUsage.ENCIPHER_ONLY, label: "Encipher Only" },
+ { value: CertKeyUsage.DECIPHER_ONLY, label: "Decipher Only" }
+] as const;
+
+export const EXTENDED_KEY_USAGES_OPTIONS = [
+ { value: CertExtendedKeyUsage.CLIENT_AUTH, label: "Client Auth" },
+ { value: CertExtendedKeyUsage.SERVER_AUTH, label: "Server Auth" },
+ { value: CertExtendedKeyUsage.EMAIL_PROTECTION, label: "Email Protection" },
+ { value: CertExtendedKeyUsage.OCSP_SIGNING, label: "OCSP Signing" },
+ { value: CertExtendedKeyUsage.CODE_SIGNING, label: "Code Signing" },
+ { value: CertExtendedKeyUsage.TIMESTAMPING, label: "Timestamping" }
+] as const;
diff --git a/frontend-v2/src/hooks/api/certificates/enums.tsx b/frontend-v2/src/hooks/api/certificates/enums.tsx
new file mode 100644
index 000000000..566da7506
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificates/enums.tsx
@@ -0,0 +1,45 @@
+export enum CertStatus {
+ ACTIVE = "active",
+ REVOKED = "revoked"
+}
+
+export enum CertKeyAlgorithm {
+ RSA_2048 = "RSA_2048",
+ RSA_4096 = "RSA_4096",
+ ECDSA_P256 = "EC_prime256v1",
+ ECDSA_P384 = "EC_secp384r1"
+}
+
+export enum CrlReason {
+ UNSPECIFIED = "UNSPECIFIED",
+ KEY_COMPROMISE = "KEY_COMPROMISE",
+ CA_COMPROMISE = "CA_COMPROMISE",
+ AFFILIATION_CHANGED = "AFFILIATION_CHANGED",
+ SUPERSEDED = "SUPERSEDED",
+ CESSATION_OF_OPERATION = "CESSATION_OF_OPERATION",
+ CERTIFICATE_HOLD = "CERTIFICATE_HOLD",
+ // REMOVE_FROM_CRL = "REMOVE_FROM_CRL",
+ PRIVILEGE_WITHDRAWN = "PRIVILEGE_WITHDRAWN",
+ A_A_COMPROMISE = "A_A_COMPROMISE"
+}
+
+export enum CertKeyUsage {
+ DIGITAL_SIGNATURE = "digitalSignature",
+ KEY_ENCIPHERMENT = "keyEncipherment",
+ NON_REPUDIATION = "nonRepudiation",
+ DATA_ENCIPHERMENT = "dataEncipherment",
+ KEY_AGREEMENT = "keyAgreement",
+ KEY_CERT_SIGN = "keyCertSign",
+ CRL_SIGN = "cRLSign",
+ ENCIPHER_ONLY = "encipherOnly",
+ DECIPHER_ONLY = "decipherOnly"
+}
+
+export enum CertExtendedKeyUsage {
+ CLIENT_AUTH = "clientAuth",
+ SERVER_AUTH = "serverAuth",
+ CODE_SIGNING = "codeSigning",
+ EMAIL_PROTECTION = "emailProtection",
+ TIMESTAMPING = "timeStamping",
+ OCSP_SIGNING = "ocspSigning"
+}
diff --git a/frontend-v2/src/hooks/api/certificates/index.tsx b/frontend-v2/src/hooks/api/certificates/index.tsx
new file mode 100644
index 000000000..dd922fd6a
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificates/index.tsx
@@ -0,0 +1,2 @@
+export { useDeleteCert, useRevokeCert } from "./mutations";
+export { useGetCert, useGetCertBody } from "./queries";
diff --git a/frontend-v2/src/hooks/api/certificates/mutations.tsx b/frontend-v2/src/hooks/api/certificates/mutations.tsx
new file mode 100644
index 000000000..d73a0cd15
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificates/mutations.tsx
@@ -0,0 +1,43 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { workspaceKeys } from "../workspace";
+import { TCertificate, TDeleteCertDTO, TRevokeCertDTO } from "./types";
+
+export const useDeleteCert = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ serialNumber }) => {
+ const {
+ data: { certificate }
+ } = await apiRequest.delete<{ certificate: TCertificate }>(
+ `/api/v1/pki/certificates/${serialNumber}`
+ );
+ return certificate;
+ },
+ onSuccess: (_, { projectSlug }) => {
+ queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug));
+ }
+ });
+};
+
+export const useRevokeCert = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ serialNumber, revocationReason }) => {
+ const {
+ data: { certificate }
+ } = await apiRequest.post<{ certificate: TCertificate }>(
+ `/api/v1/pki/certificates/${serialNumber}/revoke`,
+ {
+ revocationReason
+ }
+ );
+ return certificate;
+ },
+ onSuccess: (_, { projectSlug }) => {
+ queryClient.invalidateQueries(workspaceKeys.forWorkspaceCertificates(projectSlug));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/certificates/queries.tsx b/frontend-v2/src/hooks/api/certificates/queries.tsx
new file mode 100644
index 000000000..50c751c06
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificates/queries.tsx
@@ -0,0 +1,40 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { TCertificate } from "./types";
+
+export const certKeys = {
+ getCertById: (serialNumber: string) => [{ serialNumber }, "cert"],
+ getCertBody: (serialNumber: string) => [{ serialNumber }, "certBody"]
+};
+
+export const useGetCert = (serialNumber: string) => {
+ return useQuery({
+ queryKey: certKeys.getCertById(serialNumber),
+ queryFn: async () => {
+ const {
+ data: { certificate }
+ } = await apiRequest.get<{ certificate: TCertificate }>(
+ `/api/v1/pki/certificates/${serialNumber}`
+ );
+ return certificate;
+ },
+ enabled: Boolean(serialNumber)
+ });
+};
+
+export const useGetCertBody = (serialNumber: string) => {
+ return useQuery({
+ queryKey: certKeys.getCertBody(serialNumber),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<{
+ certificate: string;
+ certificateChain: string;
+ serialNumber: string;
+ }>(`/api/v1/pki/certificates/${serialNumber}/certificate`);
+ return data;
+ },
+ enabled: Boolean(serialNumber)
+ });
+};
diff --git a/frontend-v2/src/hooks/api/certificates/types.ts b/frontend-v2/src/hooks/api/certificates/types.ts
new file mode 100644
index 000000000..a9bcf5fbc
--- /dev/null
+++ b/frontend-v2/src/hooks/api/certificates/types.ts
@@ -0,0 +1,27 @@
+import { CertExtendedKeyUsage, CertKeyUsage, CertStatus } from "./enums";
+
+export type TCertificate = {
+ id: string;
+ caId: string;
+ certificateTemplateId?: string;
+ status: CertStatus;
+ friendlyName: string;
+ commonName: string;
+ altNames: string;
+ serialNumber: string;
+ notBefore: string;
+ notAfter: string;
+ keyUsages: CertKeyUsage[];
+ extendedKeyUsages: CertExtendedKeyUsage[];
+};
+
+export type TDeleteCertDTO = {
+ projectSlug: string;
+ serialNumber: string;
+};
+
+export type TRevokeCertDTO = {
+ projectSlug: string;
+ serialNumber: string;
+ revocationReason: string;
+};
diff --git a/frontend-v2/src/hooks/api/cmeks/index.ts b/frontend-v2/src/hooks/api/cmeks/index.ts
new file mode 100644
index 000000000..177955438
--- /dev/null
+++ b/frontend-v2/src/hooks/api/cmeks/index.ts
@@ -0,0 +1,3 @@
+export * from "./mutations";
+export * from "./queries";
+export * from "./types";
diff --git a/frontend-v2/src/hooks/api/cmeks/mutations.tsx b/frontend-v2/src/hooks/api/cmeks/mutations.tsx
new file mode 100644
index 000000000..f3a1e6eae
--- /dev/null
+++ b/frontend-v2/src/hooks/api/cmeks/mutations.tsx
@@ -0,0 +1,90 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { encodeBase64 } from "tweetnacl-util";
+
+import { apiRequest } from "@app/config/request";
+import { cmekKeys } from "@app/hooks/api/cmeks/queries";
+import {
+ TCmekDecrypt,
+ TCmekDecryptResponse,
+ TCmekEncrypt,
+ TCmekEncryptResponse,
+ TCreateCmek,
+ TDeleteCmek,
+ TUpdateCmek
+} from "@app/hooks/api/cmeks/types";
+
+export const useCreateCmek = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (payload: TCreateCmek) => {
+ const { data } = await apiRequest.post("/api/v1/kms/keys", payload);
+
+ return data;
+ },
+ onSuccess: (_, { projectId }) => {
+ queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
+ }
+ });
+};
+
+export const useUpdateCmek = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ keyId, name, description, isDisabled }: TUpdateCmek) => {
+ const { data } = await apiRequest.patch(`/api/v1/kms/keys/${keyId}`, {
+ name,
+ description,
+ isDisabled
+ });
+
+ return data;
+ },
+ onSuccess: (_, { projectId }) => {
+ queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
+ }
+ });
+};
+
+export const useDeleteCmek = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ keyId }: TDeleteCmek) => {
+ const { data } = await apiRequest.delete(`/api/v1/kms/keys/${keyId}`);
+
+ return data;
+ },
+ onSuccess: (_, { projectId }) => {
+ queryClient.invalidateQueries(cmekKeys.getCmeksByProjectId({ projectId }));
+ }
+ });
+};
+
+export const useCmekEncrypt = () => {
+ return useMutation({
+ mutationFn: async ({ keyId, plaintext, isBase64Encoded }: TCmekEncrypt) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/kms/keys/${keyId}/encrypt`,
+ {
+ plaintext: isBase64Encoded ? plaintext : encodeBase64(Buffer.from(plaintext))
+ }
+ );
+
+ return data;
+ }
+ });
+};
+
+export const useCmekDecrypt = () => {
+ return useMutation({
+ mutationFn: async ({ keyId, ciphertext }: TCmekDecrypt) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/kms/keys/${keyId}/decrypt`,
+ {
+ ciphertext
+ }
+ );
+
+ return data;
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/cmeks/queries.tsx b/frontend-v2/src/hooks/api/cmeks/queries.tsx
new file mode 100644
index 000000000..5c2dac895
--- /dev/null
+++ b/frontend-v2/src/hooks/api/cmeks/queries.tsx
@@ -0,0 +1,53 @@
+import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+import { CmekOrderBy, TListProjectCmeksDTO, TProjectCmeksList } from "@app/hooks/api/cmeks/types";
+import { OrderByDirection } from "@app/hooks/api/generic/types";
+
+export const cmekKeys = {
+ all: ["cmek"] as const,
+ lists: () => [...cmekKeys.all, "list"] as const,
+ getCmeksByProjectId: ({ projectId, ...filters }: TListProjectCmeksDTO) =>
+ [...cmekKeys.lists(), projectId, filters] as const
+};
+
+export const useGetCmeksByProjectId = (
+ {
+ projectId,
+ offset = 0,
+ limit = 100,
+ orderBy = CmekOrderBy.Name,
+ orderDirection = OrderByDirection.ASC,
+ search = ""
+ }: TListProjectCmeksDTO,
+ options?: Omit<
+ UseQueryOptions<
+ TProjectCmeksList,
+ unknown,
+ TProjectCmeksList,
+ ReturnType
+ >,
+ "queryKey" | "queryFn"
+ >
+) => {
+ return useQuery({
+ queryKey: cmekKeys.getCmeksByProjectId({
+ projectId,
+ offset,
+ limit,
+ orderBy,
+ orderDirection,
+ search
+ }),
+ queryFn: async () => {
+ const { data } = await apiRequest.get("/api/v1/kms/keys", {
+ params: { projectId, offset, limit, search, orderBy, orderDirection }
+ });
+
+ return data;
+ },
+ enabled: Boolean(projectId) && (options?.enabled ?? true),
+ keepPreviousData: true,
+ ...options
+ });
+};
diff --git a/frontend-v2/src/hooks/api/cmeks/types.ts b/frontend-v2/src/hooks/api/cmeks/types.ts
new file mode 100644
index 000000000..c557c1fc2
--- /dev/null
+++ b/frontend-v2/src/hooks/api/cmeks/types.ts
@@ -0,0 +1,58 @@
+import { OrderByDirection } from "@app/hooks/api/generic/types";
+
+export type TCmek = {
+ id: string;
+ name: string;
+ description?: string;
+ encryptionAlgorithm: EncryptionAlgorithm;
+ projectId: string;
+ isDisabled: boolean;
+ isReserved: boolean;
+ orgId: string;
+ version: number;
+ createdAt: string;
+ updatedAt: string;
+};
+
+type ProjectRef = { projectId: string };
+type KeyRef = { keyId: string };
+
+export type TCreateCmek = Pick & ProjectRef;
+export type TUpdateCmek = KeyRef &
+ Partial> &
+ ProjectRef;
+export type TDeleteCmek = KeyRef & ProjectRef;
+
+export type TCmekEncrypt = KeyRef & { plaintext: string; isBase64Encoded?: boolean };
+export type TCmekDecrypt = KeyRef & { ciphertext: string };
+
+export type TProjectCmeksList = {
+ keys: TCmek[];
+ totalCount: number;
+};
+
+export type TListProjectCmeksDTO = {
+ projectId: string;
+ offset?: number;
+ limit?: number;
+ orderBy?: CmekOrderBy;
+ orderDirection?: OrderByDirection;
+ search?: string;
+};
+
+export type TCmekEncryptResponse = {
+ ciphertext: string;
+};
+
+export type TCmekDecryptResponse = {
+ plaintext: string;
+};
+
+export enum CmekOrderBy {
+ Name = "name"
+}
+
+export enum EncryptionAlgorithm {
+ AES_GCM_256 = "aes-256-gcm",
+ AES_GCM_128 = "aes-128-gcm"
+}
diff --git a/frontend-v2/src/hooks/api/dashboard/index.ts b/frontend-v2/src/hooks/api/dashboard/index.ts
new file mode 100644
index 000000000..83206bdf8
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dashboard/index.ts
@@ -0,0 +1,5 @@
+export {
+ useGetProjectSecretsDetails,
+ useGetProjectSecretsOverview,
+ useGetProjectSecretsQuickSearch
+} from "./queries";
diff --git a/frontend-v2/src/hooks/api/dashboard/queries.tsx b/frontend-v2/src/hooks/api/dashboard/queries.tsx
new file mode 100644
index 000000000..3aa599716
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dashboard/queries.tsx
@@ -0,0 +1,418 @@
+import { useCallback } from "react";
+import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+import axios from "axios";
+
+import { createNotification } from "@app/components/notifications";
+import { apiRequest } from "@app/config/request";
+import {
+ DashboardProjectSecretsByKeys,
+ DashboardProjectSecretsDetails,
+ DashboardProjectSecretsDetailsResponse,
+ DashboardProjectSecretsOverview,
+ DashboardProjectSecretsOverviewResponse,
+ DashboardSecretsOrderBy,
+ TDashboardProjectSecretsQuickSearch,
+ TDashboardProjectSecretsQuickSearchResponse,
+ TGetDashboardProjectSecretsByKeys,
+ TGetDashboardProjectSecretsDetailsDTO,
+ TGetDashboardProjectSecretsOverviewDTO,
+ TGetDashboardProjectSecretsQuickSearchDTO
+} from "@app/hooks/api/dashboard/types";
+import { OrderByDirection } from "@app/hooks/api/generic/types";
+import { mergePersonalSecrets } from "@app/hooks/api/secrets/queries";
+import { groupBy, unique } from "@app/lib/fn/array";
+
+export const dashboardKeys = {
+ all: () => ["dashboard"] as const,
+ getDashboardSecrets: ({
+ projectId,
+ secretPath
+ }: Pick) =>
+ [...dashboardKeys.all(), { projectId, secretPath }] as const,
+ getProjectSecretsOverview: ({
+ projectId,
+ secretPath,
+ ...params
+ }: TGetDashboardProjectSecretsOverviewDTO) =>
+ [
+ ...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
+ "secrets-overview",
+ params
+ ] as const,
+ getProjectSecretsDetails: ({
+ projectId,
+ secretPath,
+ environment,
+ ...params
+ }: TGetDashboardProjectSecretsDetailsDTO) =>
+ [
+ ...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
+ "secrets-details",
+ environment,
+ params
+ ] as const,
+ getProjectSecretsQuickSearch: ({
+ projectId,
+ secretPath,
+ ...params
+ }: TGetDashboardProjectSecretsQuickSearchDTO) =>
+ [
+ ...dashboardKeys.getDashboardSecrets({ projectId, secretPath }),
+ "quick-search",
+ params
+ ] as const
+};
+
+export const fetchProjectSecretsOverview = async ({
+ environments,
+ ...params
+}: TGetDashboardProjectSecretsOverviewDTO) => {
+ const { data } = await apiRequest.get(
+ "/api/v1/dashboard/secrets-overview",
+ {
+ params: {
+ ...params,
+ environments: encodeURIComponent(environments.join(","))
+ }
+ }
+ );
+
+ return data;
+};
+
+export const fetchProjectSecretsDetails = async ({
+ tags,
+ ...params
+}: TGetDashboardProjectSecretsDetailsDTO) => {
+ const { data } = await apiRequest.get(
+ "/api/v1/dashboard/secrets-details",
+ {
+ params: {
+ ...params,
+ tags: encodeURIComponent(
+ Object.entries(tags)
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ .filter(([_, enabled]) => enabled)
+ .map(([tag]) => tag)
+ .join(",")
+ )
+ }
+ }
+ );
+
+ return data;
+};
+
+export const fetchDashboardProjectSecretsByKeys = async ({
+ keys,
+ ...params
+}: TGetDashboardProjectSecretsByKeys) => {
+ const { data } = await apiRequest.get(
+ "/api/v1/dashboard/secrets-by-keys",
+ {
+ params: {
+ ...params,
+ keys: encodeURIComponent(keys.join(","))
+ }
+ }
+ );
+
+ return data;
+};
+
+export const useGetProjectSecretsOverview = (
+ {
+ projectId,
+ secretPath,
+ offset = 0,
+ limit = 100,
+ orderBy = DashboardSecretsOrderBy.Name,
+ orderDirection = OrderByDirection.ASC,
+ search = "",
+ includeSecrets,
+ includeFolders,
+ includeDynamicSecrets,
+ environments
+ }: TGetDashboardProjectSecretsOverviewDTO,
+ options?: Omit<
+ UseQueryOptions<
+ DashboardProjectSecretsOverviewResponse,
+ unknown,
+ DashboardProjectSecretsOverview,
+ ReturnType
+ >,
+ "queryKey" | "queryFn"
+ >
+) => {
+ return useQuery({
+ ...options,
+ // wait for all values to be available
+ enabled: Boolean(projectId) && (options?.enabled ?? true) && Boolean(environments.length),
+ queryKey: dashboardKeys.getProjectSecretsOverview({
+ secretPath,
+ search,
+ limit,
+ orderBy,
+ orderDirection,
+ offset,
+ projectId,
+ includeSecrets,
+ includeFolders,
+ includeDynamicSecrets,
+ environments
+ }),
+ queryFn: () =>
+ fetchProjectSecretsOverview({
+ secretPath,
+ search,
+ limit,
+ orderBy,
+ orderDirection,
+ offset,
+ projectId,
+ includeSecrets,
+ includeFolders,
+ includeDynamicSecrets,
+ environments
+ }),
+ onError: (error) => {
+ if (axios.isAxiosError(error)) {
+ const { message, requestId } = error.response?.data as {
+ message: string;
+ requestId: string;
+ };
+ createNotification({
+ title: "Error fetching secret details",
+ type: "error",
+ text: message,
+ copyActions: [
+ {
+ value: requestId,
+ name: "Request ID",
+ label: `Request ID: ${requestId}`
+ }
+ ]
+ });
+ }
+ },
+ select: useCallback((data: Awaited>) => {
+ const { secrets, ...select } = data;
+ const uniqueSecrets = secrets ? unique(secrets, (i) => i.secretKey) : [];
+
+ const uniqueFolders = select.folders ? unique(select.folders, (i) => i.name) : [];
+
+ const uniqueDynamicSecrets = select.dynamicSecrets
+ ? unique(select.dynamicSecrets, (i) => i.name)
+ : [];
+
+ return {
+ ...select,
+ secrets: secrets ? mergePersonalSecrets(secrets) : undefined,
+ totalUniqueSecretsInPage: uniqueSecrets.length,
+ totalUniqueDynamicSecretsInPage: uniqueDynamicSecrets.length,
+ totalUniqueFoldersInPage: uniqueFolders.length
+ };
+ }, []),
+ keepPreviousData: true
+ });
+};
+
+export const useGetProjectSecretsDetails = (
+ {
+ projectId,
+ secretPath,
+ environment,
+ offset = 0,
+ limit = 100,
+ orderBy = DashboardSecretsOrderBy.Name,
+ orderDirection = OrderByDirection.ASC,
+ search = "",
+ includeSecrets,
+ includeFolders,
+ includeImports,
+ includeDynamicSecrets,
+ tags
+ }: TGetDashboardProjectSecretsDetailsDTO,
+ options?: Omit<
+ UseQueryOptions<
+ DashboardProjectSecretsDetailsResponse,
+ unknown,
+ DashboardProjectSecretsDetails,
+ ReturnType
+ >,
+ "queryKey" | "queryFn"
+ >
+) => {
+ return useQuery({
+ ...options,
+ // wait for all values to be available
+ enabled: Boolean(projectId) && (options?.enabled ?? true),
+ queryKey: dashboardKeys.getProjectSecretsDetails({
+ secretPath,
+ search,
+ limit,
+ orderBy,
+ orderDirection,
+ offset,
+ projectId,
+ environment,
+ includeSecrets,
+ includeFolders,
+ includeImports,
+ includeDynamicSecrets,
+ tags
+ }),
+ queryFn: () =>
+ fetchProjectSecretsDetails({
+ secretPath,
+ search,
+ limit,
+ orderBy,
+ orderDirection,
+ offset,
+ projectId,
+ environment,
+ includeSecrets,
+ includeFolders,
+ includeImports,
+ includeDynamicSecrets,
+ tags
+ }),
+ onError: (error) => {
+ if (axios.isAxiosError(error)) {
+ const { message, requestId } = error.response?.data as {
+ message: string;
+ requestId: string;
+ };
+ createNotification({
+ title: "Error fetching secret details",
+ type: "error",
+ text: message,
+ copyActions: [
+ {
+ value: requestId,
+ name: "Request ID",
+ label: `Request ID: ${requestId}`
+ }
+ ]
+ });
+ }
+ },
+ select: useCallback(
+ (data: Awaited>) => ({
+ ...data,
+ secrets: data.secrets ? mergePersonalSecrets(data.secrets) : undefined
+ }),
+ []
+ ),
+ keepPreviousData: true
+ });
+};
+
+export const fetchProjectSecretsQuickSearch = async ({
+ environments,
+ tags,
+ ...params
+}: TGetDashboardProjectSecretsQuickSearchDTO) => {
+ const { data } = await apiRequest.get(
+ "/api/v1/dashboard/secrets-deep-search",
+ {
+ params: {
+ ...params,
+ environments: encodeURIComponent(environments.join(",")),
+ tags: encodeURIComponent(
+ Object.entries(tags)
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ .filter(([_, enabled]) => enabled)
+ .map(([tag]) => tag)
+ .join(",")
+ )
+ }
+ }
+ );
+
+ return data;
+};
+
+export const useGetProjectSecretsQuickSearch = (
+ {
+ projectId,
+ secretPath,
+ search = "",
+ environments,
+ tags
+ }: TGetDashboardProjectSecretsQuickSearchDTO,
+ options?: Omit<
+ UseQueryOptions<
+ TDashboardProjectSecretsQuickSearchResponse,
+ unknown,
+ TDashboardProjectSecretsQuickSearch,
+ ReturnType
+ >,
+ "queryKey" | "queryFn"
+ >
+) => {
+ return useQuery({
+ ...options,
+ enabled:
+ Boolean(search?.trim() || Object.values(tags).length) &&
+ (options?.enabled ?? true) &&
+ Boolean(environments.length),
+ queryKey: dashboardKeys.getProjectSecretsQuickSearch({
+ secretPath,
+ search,
+ projectId,
+ environments,
+ tags
+ }),
+ queryFn: () =>
+ fetchProjectSecretsQuickSearch({
+ secretPath,
+ search,
+ projectId,
+ environments,
+ tags
+ }),
+ onError: (error) => {
+ if (axios.isAxiosError(error)) {
+ const { message, requestId } = error.response?.data as {
+ message: string;
+ requestId: string;
+ };
+ createNotification({
+ title: "Error fetching secrets deep search",
+ type: "error",
+ text: message,
+ copyActions: [
+ {
+ value: requestId,
+ name: "Request ID",
+ label: `Request ID: ${requestId}`
+ }
+ ]
+ });
+ }
+ },
+ select: useCallback((data: Awaited>) => {
+ const { secrets, folders, dynamicSecrets } = data;
+
+ const groupedFolders = groupBy(folders, (folder) => folder.path);
+ const groupedSecrets = groupBy(
+ mergePersonalSecrets(secrets),
+ (secret) => `${secret.path === "/" ? "" : secret.path}/${secret.key}`
+ );
+ const groupedDynamicSecrets = groupBy(
+ dynamicSecrets,
+ (dynamicSecret) =>
+ `${dynamicSecret.path === "/" ? "" : dynamicSecret.path}/${dynamicSecret.name}`
+ );
+
+ return {
+ folders: groupedFolders,
+ secrets: groupedSecrets,
+ dynamicSecrets: groupedDynamicSecrets
+ };
+ }, []),
+ keepPreviousData: true
+ });
+};
diff --git a/frontend-v2/src/hooks/api/dashboard/types.ts b/frontend-v2/src/hooks/api/dashboard/types.ts
new file mode 100644
index 000000000..444614b82
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dashboard/types.ts
@@ -0,0 +1,102 @@
+import { TDynamicSecret } from "@app/hooks/api/dynamicSecret/types";
+import { OrderByDirection } from "@app/hooks/api/generic/types";
+import { TSecretFolder } from "@app/hooks/api/secretFolders/types";
+import { TSecretImport } from "@app/hooks/api/secretImports/types";
+import { SecretV3Raw, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
+
+export type DashboardProjectSecretsOverviewResponse = {
+ folders?: (TSecretFolder & { environment: string })[];
+ dynamicSecrets?: (TDynamicSecret & { environment: string })[];
+ secrets?: SecretV3Raw[];
+ totalSecretCount?: number;
+ totalFolderCount?: number;
+ totalDynamicSecretCount?: number;
+ totalCount: number;
+ totalUniqueSecretsInPage: number;
+ totalUniqueDynamicSecretsInPage: number;
+ totalUniqueFoldersInPage: number;
+};
+
+export type DashboardProjectSecretsDetailsResponse = {
+ imports?: TSecretImport[];
+ folders?: TSecretFolder[];
+ dynamicSecrets?: TDynamicSecret[];
+ secrets?: SecretV3Raw[];
+ totalImportCount?: number;
+ totalFolderCount?: number;
+ totalDynamicSecretCount?: number;
+ totalSecretCount?: number;
+ totalCount: number;
+};
+
+export type DashboardProjectSecretsByKeys = {
+ secrets: SecretV3Raw[];
+};
+
+export type DashboardProjectSecretsOverview = Omit<
+ DashboardProjectSecretsOverviewResponse,
+ "secrets"
+> & {
+ secrets?: SecretV3RawSanitized[];
+};
+
+export type DashboardProjectSecretsDetails = Omit<
+ DashboardProjectSecretsDetailsResponse,
+ "secrets"
+> & {
+ secrets?: SecretV3RawSanitized[];
+};
+
+export enum DashboardSecretsOrderBy {
+ Name = "name"
+}
+
+export type TGetDashboardProjectSecretsOverviewDTO = {
+ projectId: string;
+ secretPath: string;
+ offset?: number;
+ limit?: number;
+ orderBy?: DashboardSecretsOrderBy;
+ orderDirection?: OrderByDirection;
+ search?: string;
+ includeSecrets?: boolean;
+ includeFolders?: boolean;
+ includeDynamicSecrets?: boolean;
+ environments: string[];
+};
+
+export type TGetDashboardProjectSecretsDetailsDTO = Omit<
+ TGetDashboardProjectSecretsOverviewDTO,
+ "environments"
+> & {
+ environment: string;
+ includeImports?: boolean;
+ tags: Record;
+};
+
+export type TDashboardProjectSecretsQuickSearchResponse = {
+ folders: (TSecretFolder & { environment: string; path: string })[];
+ dynamicSecrets: (TDynamicSecret & { environment: string; path: string })[];
+ secrets: SecretV3Raw[];
+};
+
+export type TDashboardProjectSecretsQuickSearch = {
+ folders: Record;
+ secrets: Record;
+ dynamicSecrets: Record;
+};
+
+export type TGetDashboardProjectSecretsQuickSearchDTO = {
+ projectId: string;
+ secretPath: string;
+ tags: Record;
+ search: string;
+ environments: string[];
+};
+
+export type TGetDashboardProjectSecretsByKeys = {
+ projectId: string;
+ secretPath: string;
+ environment: string;
+ keys: string[];
+};
diff --git a/frontend-v2/src/hooks/api/dynamicSecret/index.ts b/frontend-v2/src/hooks/api/dynamicSecret/index.ts
new file mode 100644
index 000000000..3d6e403bc
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dynamicSecret/index.ts
@@ -0,0 +1,6 @@
+export { useCreateDynamicSecret, useDeleteDynamicSecret, useUpdateDynamicSecret } from "./mutation";
+export {
+ useGetDynamicSecretDetails,
+ useGetDynamicSecrets,
+ useGetDynamicSecretsOfAllEnv
+} from "./queries";
diff --git a/frontend-v2/src/hooks/api/dynamicSecret/mutation.ts b/frontend-v2/src/hooks/api/dynamicSecret/mutation.ts
new file mode 100644
index 000000000..f8fbb4d05
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dynamicSecret/mutation.ts
@@ -0,0 +1,69 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
+
+import { dynamicSecretKeys } from "./queries";
+import {
+ TCreateDynamicSecretDTO,
+ TDeleteDynamicSecretDTO,
+ TDynamicSecret,
+ TUpdateDynamicSecretDTO
+} from "./types";
+
+export const useCreateDynamicSecret = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TCreateDynamicSecretDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.post<{ dynamicSecret: TDynamicSecret }>(
+ "/api/v1/dynamic-secrets",
+ dto
+ );
+ return data.dynamicSecret;
+ },
+ onSuccess: (_, { path, environmentSlug, projectSlug }) => {
+ // TODO: optimize but we currently don't pass projectId
+ queryClient.invalidateQueries(dashboardKeys.all());
+ queryClient.invalidateQueries(dynamicSecretKeys.list({ path, projectSlug, environmentSlug }));
+ }
+ });
+};
+
+export const useUpdateDynamicSecret = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TUpdateDynamicSecretDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.patch<{ dynamicSecret: TDynamicSecret }>(
+ `/api/v1/dynamic-secrets/${dto.name}`,
+ dto
+ );
+ return data.dynamicSecret;
+ },
+ onSuccess: (_, { path, environmentSlug, projectSlug }) => {
+ // TODO: optimize but currently don't pass projectId
+ queryClient.invalidateQueries(dashboardKeys.all());
+ queryClient.invalidateQueries(dynamicSecretKeys.list({ path, projectSlug, environmentSlug }));
+ }
+ });
+};
+
+export const useDeleteDynamicSecret = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TDeleteDynamicSecretDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.delete<{ dynamicSecret: TDynamicSecret }>(
+ `/api/v1/dynamic-secrets/${dto.name}`,
+ { data: dto }
+ );
+ return data.dynamicSecret;
+ },
+ onSuccess: (_, { path, environmentSlug, projectSlug }) => {
+ // TODO: optimize but currently don't pass projectId
+ queryClient.invalidateQueries(dashboardKeys.all());
+ queryClient.invalidateQueries(dynamicSecretKeys.list({ path, projectSlug, environmentSlug }));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/dynamicSecret/queries.ts b/frontend-v2/src/hooks/api/dynamicSecret/queries.ts
new file mode 100644
index 000000000..f84fd0712
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dynamicSecret/queries.ts
@@ -0,0 +1,154 @@
+import { useCallback, useMemo } from "react";
+import { useQueries, useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import {
+ TDetailsDynamicSecretDTO,
+ TDynamicSecret,
+ TGetDynamicSecretsByEnvsDTO,
+ TListDynamicSecretDTO
+} from "./types";
+
+export const dynamicSecretKeys = {
+ list: ({
+ projectSlug,
+ environmentSlug,
+ path
+ }: Pick) =>
+ [{ projectSlug, environmentSlug, path }, "dynamic-secrets"] as const,
+ details: ({ path, environmentSlug, projectSlug, name }: TDetailsDynamicSecretDTO) =>
+ [{ projectSlug, path, environmentSlug, name }, "dynamic-secret-details"] as const
+};
+
+export const useGetDynamicSecrets = ({
+ projectSlug,
+ environmentSlug,
+ path
+}: TListDynamicSecretDTO) => {
+ return useQuery({
+ queryKey: dynamicSecretKeys.list({ path, environmentSlug, projectSlug }),
+ enabled: Boolean(projectSlug && environmentSlug && path),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<{ dynamicSecrets: TDynamicSecret[] }>(
+ "/api/v1/dynamic-secrets",
+ {
+ params: {
+ projectSlug,
+ environmentSlug,
+ path
+ }
+ }
+ );
+
+ return data.dynamicSecrets;
+ }
+ });
+};
+
+export const useGetDynamicSecretDetails = ({
+ projectSlug,
+ environmentSlug,
+ path,
+ name
+}: TDetailsDynamicSecretDTO) => {
+ return useQuery({
+ queryKey: dynamicSecretKeys.details({ path, environmentSlug, projectSlug, name }),
+ enabled: Boolean(projectSlug && environmentSlug && path && name),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<{
+ dynamicSecret: TDynamicSecret & { inputs: unknown };
+ }>(`/api/v1/dynamic-secrets/${name}`, {
+ params: {
+ projectSlug,
+ environmentSlug,
+ path
+ }
+ });
+
+ return data.dynamicSecret;
+ }
+ });
+};
+
+export const useGetDynamicSecretProviderData = ({
+ tenantId,
+ applicationId,
+ clientSecret,
+ enabled
+}: {
+ tenantId: string;
+ applicationId: string;
+ clientSecret: string;
+ enabled: boolean
+}) => {
+ return useQuery({
+ queryKey: ["users"],
+ queryFn: async () => {
+ const { data } = await apiRequest.post<{id:string, email: string, name:string}[]>(
+ "/api/v1/dynamic-secrets/entra-id/users",
+ {
+ tenantId,
+ applicationId,
+ clientSecret
+ }
+ );
+ return data;
+ },
+ enabled
+ });
+};
+
+export const useGetDynamicSecretsOfAllEnv = ({
+ path,
+ projectSlug,
+ environmentSlugs
+}: TGetDynamicSecretsByEnvsDTO) => {
+ const dynamicSecrets = useQueries({
+ queries: environmentSlugs.map((environment) => ({
+ queryKey: dynamicSecretKeys.list({ path, environmentSlug: environment, projectSlug }),
+ enabled: Boolean(projectSlug && environment && path),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<{ dynamicSecrets: TDynamicSecret[] }>(
+ "/api/v1/dynamic-secrets",
+ {
+ params: {
+ projectSlug,
+ environmentSlug: environment,
+ path
+ }
+ }
+ );
+
+ return data.dynamicSecrets;
+ }
+ }))
+ });
+
+ const dynamicSecretNames = useMemo(() => {
+ const names = new Set();
+ dynamicSecrets?.forEach(({ data }) => {
+ data?.forEach(({ name }) => {
+ names.add(name);
+ });
+ });
+ return [...names];
+ }, [(dynamicSecrets || []).map((dynamicSecret) => dynamicSecret.data)]);
+
+ const isDynamicSecretPresentInEnv = useCallback(
+ (name: string, env: string) => {
+ const selectedEnvIndex = environmentSlugs.indexOf(env);
+ if (selectedEnvIndex !== -1) {
+ return Boolean(
+ dynamicSecrets?.[selectedEnvIndex]?.data?.find(
+ ({ name: dynamicSecretName }) => dynamicSecretName === name
+ )
+ );
+ }
+ return false;
+ },
+ [(dynamicSecrets || []).map((el) => el.data)]
+ );
+
+ return { dynamicSecrets, isDynamicSecretPresentInEnv, dynamicSecretNames };
+};
diff --git a/frontend-v2/src/hooks/api/dynamicSecret/types.ts b/frontend-v2/src/hooks/api/dynamicSecret/types.ts
new file mode 100644
index 000000000..96fc3d6c8
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dynamicSecret/types.ts
@@ -0,0 +1,311 @@
+export enum DynamicSecretStatus {
+ Deleting = "Revocation in process",
+ FailedDeletion = "Failed to delete"
+}
+// TODO(akhilmhdh): When we switch to monorepo all the server api ts will be in a shared repo
+export type TDynamicSecret = {
+ id: string;
+ name: string;
+ type: DynamicSecretProviders;
+ createdAt: string;
+ updatedAt: string;
+ defaultTTL: string;
+ status?: DynamicSecretStatus;
+ statusDetails?: string;
+ maxTTL: string;
+};
+
+export enum DynamicSecretProviders {
+ SqlDatabase = "sql-database",
+ Cassandra = "cassandra",
+ AwsIam = "aws-iam",
+ Redis = "redis",
+ AwsElastiCache = "aws-elasticache",
+ MongoAtlas = "mongo-db-atlas",
+ ElasticSearch = "elastic-search",
+ MongoDB = "mongo-db",
+ RabbitMq = "rabbit-mq",
+ AzureEntraId = "azure-entra-id",
+ Ldap = "ldap",
+ SapHana = "sap-hana",
+ Snowflake = "snowflake",
+ Totp = "totp",
+ SapAse = "sap-ase"
+}
+
+export enum SqlProviders {
+ Postgres = "postgres",
+ MySql = "mysql2",
+ Oracle = "oracledb",
+ MsSQL = "mssql"
+}
+
+export type TDynamicSecretProvider =
+ | {
+ type: DynamicSecretProviders.SqlDatabase;
+ inputs: {
+ client: SqlProviders;
+ host: string;
+ port: number;
+ database: string;
+ username: string;
+ password: string;
+ creationStatement: string;
+ revocationStatement: string;
+ renewStatement?: string;
+ ca?: string | undefined;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.Cassandra;
+ inputs: {
+ host: string;
+ port: number;
+ keyspace?: string;
+ localDataCenter: string;
+ username: string;
+ password: string;
+ creationStatement: string;
+ revocationStatement: string;
+ renewStatement?: string;
+ ca?: string | undefined;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.AwsIam;
+ inputs: {
+ accessKey: string;
+ secretAccessKey: string;
+ region: string;
+ awsPath?: string;
+ policyDocument?: string;
+ userGroups?: string;
+ policyArns?: string;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.Redis;
+ inputs: {
+ host: string;
+ port: number;
+ username: string;
+ password?: string;
+ creationStatement: string;
+ renewStatement?: string;
+ revocationStatement: string;
+ ca?: string | undefined;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.AwsElastiCache;
+ inputs: {
+ clusterName: string;
+ accessKeyId: string;
+ secretAccessKey: string;
+ region: string;
+ creationStatement: string;
+ revocationStatement: string;
+ ca?: string | undefined;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.MongoAtlas;
+ inputs: {
+ adminPublicKey: string;
+ adminPrivateKey: string;
+ groupId: string;
+ roles: {
+ databaseName: string;
+ roleName: string;
+ collectionName?: string;
+ }[];
+ scopes?: {
+ name: string;
+ type: string;
+ }[];
+ };
+ }
+ | {
+ type: DynamicSecretProviders.MongoDB;
+ inputs: {
+ host: string;
+ port?: number;
+ database: string;
+ username: string;
+ password: string;
+ ca?: string | undefined;
+ roles: (
+ | {
+ databaseName: string;
+ roleName: string;
+ }
+ | string
+ )[];
+ };
+ }
+ | {
+ type: DynamicSecretProviders.ElasticSearch;
+ inputs: {
+ host: string;
+ port: number;
+ ca?: string | undefined;
+ roles: string[];
+ auth:
+ | {
+ type: "user";
+ username: string;
+ password: string;
+ }
+ | {
+ type: "api-key";
+ apiKey: string;
+ apiKeyId: string;
+ };
+ };
+ }
+ | {
+ type: DynamicSecretProviders.RabbitMq;
+ inputs: {
+ host: string;
+ port: number;
+
+ username: string;
+ password: string;
+
+ tags: string[];
+ virtualHost: {
+ name: string;
+ permissions: {
+ configure: string;
+ write: string;
+ read: string;
+ };
+ };
+ ca?: string;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.AzureEntraId;
+ inputs: {
+ tenantId: string;
+ userId: string;
+ email: string;
+ applicationId: string;
+ clientSecret: string;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.Ldap;
+ inputs: {
+ url: string;
+ binddn: string;
+ bindpass: string;
+ ca?: string | undefined;
+ credentialType: string;
+ creationLdif?: string;
+ revocationLdif?: string;
+ rollbackLdif?: string;
+ rotationLdif?: string;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.SapHana;
+ inputs: {
+ host: string;
+ port: number;
+ username: string;
+ password: string;
+ creationStatement: string;
+ revocationStatement: string;
+ renewStatement?: string;
+ ca?: string | undefined;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.SapAse;
+ inputs: {
+ host: string;
+ port: number;
+ username: string;
+ database: string;
+ password: string;
+ creationStatement: string;
+ revocationStatement: string;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.Snowflake;
+ inputs: {
+ orgId: string;
+ accountId: string;
+ username: string;
+ password: string;
+ creationStatement: string;
+ revocationStatement: string;
+ renewStatement?: string;
+ };
+ }
+ | {
+ type: DynamicSecretProviders.Totp;
+ inputs:
+ | {
+ configType: "url";
+ url: string;
+ }
+ | {
+ configType: "manual";
+ secret: string;
+ period?: number;
+ algorithm?: string;
+ digits?: number;
+ };
+ };
+export type TCreateDynamicSecretDTO = {
+ projectSlug: string;
+ provider: TDynamicSecretProvider;
+ defaultTTL: string;
+ maxTTL?: string;
+ path: string;
+ environmentSlug: string;
+ name: string;
+};
+
+export type TUpdateDynamicSecretDTO = {
+ name: string;
+ projectSlug: string;
+ path: string;
+ environmentSlug: string;
+ data: {
+ newName?: string;
+ defaultTTL?: string;
+ maxTTL?: string | null;
+ inputs?: unknown;
+ };
+};
+
+export type TListDynamicSecretDTO = {
+ projectSlug: string;
+ path: string;
+ environmentSlug: string;
+};
+
+export type TDeleteDynamicSecretDTO = {
+ projectSlug: string;
+ path: string;
+ environmentSlug: string;
+ name: string;
+ isForced?: boolean;
+};
+
+export type TDetailsDynamicSecretDTO = {
+ projectSlug: string;
+ path: string;
+ environmentSlug: string;
+ name: string;
+};
+
+export type TGetDynamicSecretsByEnvsDTO = {
+ projectSlug: string;
+ path: string;
+ environmentSlugs: string[];
+};
diff --git a/frontend-v2/src/hooks/api/dynamicSecretLease/index.ts b/frontend-v2/src/hooks/api/dynamicSecretLease/index.ts
new file mode 100644
index 000000000..7f2d6852f
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dynamicSecretLease/index.ts
@@ -0,0 +1,6 @@
+export {
+ useCreateDynamicSecretLease,
+ useRenewDynamicSecretLease,
+ useRevokeDynamicSecretLease
+} from "./mutation";
+export { useGetDynamicSecretLeases } from "./queries";
diff --git a/frontend-v2/src/hooks/api/dynamicSecretLease/mutation.ts b/frontend-v2/src/hooks/api/dynamicSecretLease/mutation.ts
new file mode 100644
index 000000000..3ed2b75d9
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dynamicSecretLease/mutation.ts
@@ -0,0 +1,72 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { dynamicSecretLeaseKeys } from "./queries";
+import {
+ TCreateDynamicSecretLeaseDTO,
+ TDynamicSecretLease,
+ TRenewDynamicSecretLeaseDTO,
+ TRevokeDynamicSecretLeaseDTO
+} from "./types";
+
+export const useCreateDynamicSecretLease = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<
+ { lease: TDynamicSecretLease; data: unknown },
+ {},
+ TCreateDynamicSecretLeaseDTO
+ >({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.post<{ lease: TDynamicSecretLease; data: unknown }>(
+ "/api/v1/dynamic-secrets/leases",
+ dto
+ );
+ return data;
+ },
+ onSuccess: (_, { path, environmentSlug, projectSlug, dynamicSecretName }) => {
+ queryClient.invalidateQueries(
+ dynamicSecretLeaseKeys.list({ path, projectSlug, environmentSlug, dynamicSecretName })
+ );
+ }
+ });
+};
+
+export const useRenewDynamicSecretLease = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TRenewDynamicSecretLeaseDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.post<{ lease: TDynamicSecretLease }>(
+ `/api/v1/dynamic-secrets/leases/${dto.leaseId}/renew`,
+ dto
+ );
+ return data.lease;
+ },
+ onSuccess: (_, { path, environmentSlug, projectSlug, dynamicSecretName }) => {
+ queryClient.invalidateQueries(
+ dynamicSecretLeaseKeys.list({ path, projectSlug, environmentSlug, dynamicSecretName })
+ );
+ }
+ });
+};
+
+export const useRevokeDynamicSecretLease = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, TRevokeDynamicSecretLeaseDTO>({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.delete<{ lease: TDynamicSecretLease }>(
+ `/api/v1/dynamic-secrets/leases/${dto.leaseId}`,
+ { data: dto }
+ );
+ return data.lease;
+ },
+ onSuccess: (_, { path, environmentSlug, projectSlug, dynamicSecretName }) => {
+ queryClient.invalidateQueries(
+ dynamicSecretLeaseKeys.list({ path, projectSlug, environmentSlug, dynamicSecretName })
+ );
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/dynamicSecretLease/queries.ts b/frontend-v2/src/hooks/api/dynamicSecretLease/queries.ts
new file mode 100644
index 000000000..556c0edbf
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dynamicSecretLease/queries.ts
@@ -0,0 +1,39 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { TDynamicSecretLease, TListDynamicSecretLeaseDTO } from "./types";
+
+export const dynamicSecretLeaseKeys = {
+ list: ({ projectSlug, environmentSlug, path, dynamicSecretName }: TListDynamicSecretLeaseDTO) =>
+ [{ projectSlug, environmentSlug, path, dynamicSecretName }, "dynamic-secret-leases"] as const
+};
+
+export const useGetDynamicSecretLeases = ({
+ projectSlug,
+ environmentSlug,
+ path,
+ dynamicSecretName,
+ enabled = true
+}: TListDynamicSecretLeaseDTO) => {
+ return useQuery({
+ queryKey: dynamicSecretLeaseKeys.list({ path, environmentSlug, projectSlug, dynamicSecretName }),
+ enabled: Boolean(projectSlug && environmentSlug && path && dynamicSecretName && enabled),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<{ leases: TDynamicSecretLease[] }>(
+ `/api/v1/dynamic-secrets/${dynamicSecretName}/leases`,
+ {
+ params: {
+ projectSlug,
+ environmentSlug,
+ path
+ }
+ }
+ );
+
+ return data.leases;
+ }
+ });
+};
+
+
diff --git a/frontend-v2/src/hooks/api/dynamicSecretLease/types.ts b/frontend-v2/src/hooks/api/dynamicSecretLease/types.ts
new file mode 100644
index 000000000..e6fbb0e75
--- /dev/null
+++ b/frontend-v2/src/hooks/api/dynamicSecretLease/types.ts
@@ -0,0 +1,49 @@
+export enum DynamicSecretLeaseStatus {
+ FailedDeletion = "Failed to delete"
+}
+
+export type TDynamicSecretLease = {
+ id: string;
+ version: number;
+ expireAt: string;
+ dynamicSecretId: string;
+ status?: DynamicSecretLeaseStatus;
+ statusDetails?: string;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type TCreateDynamicSecretLeaseDTO = {
+ dynamicSecretName: string;
+ projectSlug: string;
+ ttl?: string;
+ path: string;
+ environmentSlug: string;
+};
+
+export type TRenewDynamicSecretLeaseDTO = {
+ leaseId: string;
+ dynamicSecretName: string;
+ ttl?: string;
+ projectSlug: string;
+ path: string;
+ environmentSlug: string;
+};
+
+export type TListDynamicSecretLeaseDTO = {
+ dynamicSecretName: string;
+ projectSlug: string;
+ path: string;
+ environmentSlug: string;
+ enabled?: boolean;
+};
+
+export type TRevokeDynamicSecretLeaseDTO = {
+ leaseId: string;
+ dynamicSecretName: string;
+ projectSlug: string;
+ path: string;
+ environmentSlug: string;
+ isForced?: boolean;
+};
+
diff --git a/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/index.ts b/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/index.ts
new file mode 100644
index 000000000..177955438
--- /dev/null
+++ b/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/index.ts
@@ -0,0 +1,3 @@
+export * from "./mutations";
+export * from "./queries";
+export * from "./types";
diff --git a/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/mutations.tsx b/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/mutations.tsx
new file mode 100644
index 000000000..aac46726a
--- /dev/null
+++ b/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/mutations.tsx
@@ -0,0 +1,19 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+import { externalGroupOrgRoleMappingKeys } from "@app/hooks/api/externalGroupOrgRoleMappings/queries";
+import { TSyncExternalGroupOrgRoleMappingsDTO } from "@app/hooks/api/externalGroupOrgRoleMappings/types";
+
+export const useUpdateExternalGroupOrgRoleMappings = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (payload: TSyncExternalGroupOrgRoleMappingsDTO) => {
+ const { data } = await apiRequest.put("/api/v1/external-group-mappings", payload);
+
+ return data;
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries(externalGroupOrgRoleMappingKeys.list());
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/queries.tsx b/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/queries.tsx
new file mode 100644
index 000000000..620ec0447
--- /dev/null
+++ b/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/queries.tsx
@@ -0,0 +1,33 @@
+import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+import { TExternalGroupOrgRoleMappingList } from "@app/hooks/api/externalGroupOrgRoleMappings/types";
+
+export const externalGroupOrgRoleMappingKeys = {
+ all: ["external-group-org-role-mapping"] as const,
+ list: () => [...externalGroupOrgRoleMappingKeys.all, "list"] as const
+};
+
+export const useGetExternalGroupOrgRoleMappings = (
+ options?: Omit<
+ UseQueryOptions<
+ TExternalGroupOrgRoleMappingList,
+ unknown,
+ TExternalGroupOrgRoleMappingList,
+ ReturnType
+ >,
+ "queryKey" | "queryFn"
+ >
+) => {
+ return useQuery({
+ queryKey: externalGroupOrgRoleMappingKeys.list(),
+ queryFn: async () => {
+ const { data } = await apiRequest.get(
+ "/api/v1/external-group-mappings"
+ );
+
+ return data;
+ },
+ ...options
+ });
+};
diff --git a/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/types.ts b/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/types.ts
new file mode 100644
index 000000000..313045742
--- /dev/null
+++ b/frontend-v2/src/hooks/api/externalGroupOrgRoleMappings/types.ts
@@ -0,0 +1,18 @@
+export type TSyncExternalGroupOrgRoleMappingsDTO = {
+ mappings: {
+ groupName: string;
+ roleSlug: string;
+ }[];
+};
+
+export type TExternalGroupOrgRoleMapping = {
+ id: string;
+ groupName: string;
+ role: string;
+ roleId: string;
+ orgId: string;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type TExternalGroupOrgRoleMappingList = TExternalGroupOrgRoleMapping[];
diff --git a/frontend-v2/src/hooks/api/generic/types.ts b/frontend-v2/src/hooks/api/generic/types.ts
new file mode 100644
index 000000000..1f09c5c0a
--- /dev/null
+++ b/frontend-v2/src/hooks/api/generic/types.ts
@@ -0,0 +1,4 @@
+export enum OrderByDirection {
+ ASC = "asc",
+ DESC = "desc"
+}
diff --git a/frontend-v2/src/hooks/api/groups/index.tsx b/frontend-v2/src/hooks/api/groups/index.tsx
new file mode 100644
index 000000000..c23a55832
--- /dev/null
+++ b/frontend-v2/src/hooks/api/groups/index.tsx
@@ -0,0 +1,8 @@
+export {
+ useAddUserToGroup,
+ useCreateGroup,
+ useDeleteGroup,
+ useRemoveUserFromGroup,
+ useUpdateGroup
+} from "./mutations";
+export { useGetGroupById, useListGroupUsers } from "./queries";
diff --git a/frontend-v2/src/hooks/api/groups/mutations.tsx b/frontend-v2/src/hooks/api/groups/mutations.tsx
new file mode 100644
index 000000000..2f5c5984c
--- /dev/null
+++ b/frontend-v2/src/hooks/api/groups/mutations.tsx
@@ -0,0 +1,124 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { organizationKeys } from "../organization/queries";
+import { userKeys } from "../users/query-keys";
+import { groupKeys } from "./queries";
+import { TGroup } from "./types";
+
+export const useCreateGroup = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ name,
+ slug,
+ role
+ }: {
+ name: string;
+ slug: string;
+ organizationId: string;
+ role?: string;
+ }) => {
+ const { data: group } = await apiRequest.post("/api/v1/groups", {
+ name,
+ slug,
+ role
+ });
+
+ return group;
+ },
+ onSuccess: (_, { organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgGroups(organizationId));
+ }
+ });
+};
+
+export const useUpdateGroup = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ id,
+ name,
+ slug,
+ role
+ }: {
+ id: string;
+ name?: string;
+ slug?: string;
+ role?: string;
+ }) => {
+ const { data: group } = await apiRequest.patch(`/api/v1/groups/${id}`, {
+ name,
+ slug,
+ role
+ });
+
+ return group;
+ },
+ onSuccess: ({ orgId, id: groupId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
+ queryClient.invalidateQueries(groupKeys.getGroupById(groupId));
+ }
+ });
+};
+
+export const useDeleteGroup = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ id }: { id: string }) => {
+ const { data: group } = await apiRequest.delete(`/api/v1/groups/${id}`);
+
+ return group;
+ },
+ onSuccess: ({ orgId, id: groupId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgGroups(orgId));
+ queryClient.invalidateQueries(groupKeys.getGroupById(groupId));
+ }
+ });
+};
+
+export const useAddUserToGroup = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ groupId,
+ username
+ }: {
+ groupId: string;
+ username: string;
+ slug: string;
+ }) => {
+ const { data } = await apiRequest.post(`/api/v1/groups/${groupId}/users/${username}`);
+
+ return data;
+ },
+ onSuccess: (_, { slug }) => {
+ queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
+ }
+ });
+};
+
+export const useRemoveUserFromGroup = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ username,
+ groupId
+ }: {
+ slug: string;
+ username: string;
+ groupId: string;
+ }) => {
+ const { data } = await apiRequest.delete(
+ `/api/v1/groups/${groupId}/users/${username}`
+ );
+
+ return data;
+ },
+ onSuccess: (_, { slug, username }) => {
+ queryClient.invalidateQueries(groupKeys.forGroupUserMemberships(slug));
+ queryClient.invalidateQueries(userKeys.listUserGroupMemberships(username));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/groups/queries.tsx b/frontend-v2/src/hooks/api/groups/queries.tsx
new file mode 100644
index 000000000..dc3791db7
--- /dev/null
+++ b/frontend-v2/src/hooks/api/groups/queries.tsx
@@ -0,0 +1,82 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { EFilterReturnedUsers, TGroup, TGroupUser } from "./types";
+
+export const groupKeys = {
+ getGroupById: (groupId: string) => [{ groupId }, "group"] as const,
+ allGroupUserMemberships: () => ["group-user-memberships"] as const,
+ forGroupUserMemberships: (slug: string) =>
+ [...groupKeys.allGroupUserMemberships(), slug] as const,
+ specificGroupUserMemberships: ({
+ slug,
+ offset,
+ limit,
+ search,
+ filter
+ }: {
+ slug: string;
+ offset: number;
+ limit: number;
+ search: string;
+ filter?: EFilterReturnedUsers;
+ }) => [...groupKeys.forGroupUserMemberships(slug), { offset, limit, search, filter }] as const
+};
+
+export const useGetGroupById = (groupId: string) => {
+ return useQuery({
+ enabled: Boolean(groupId),
+ queryKey: groupKeys.getGroupById(groupId),
+ queryFn: async () => {
+ const { data } = await apiRequest.get(`/api/v1/groups/${groupId}`);
+
+ return { group: data };
+ }
+ });
+};
+
+export const useListGroupUsers = ({
+ id,
+ groupSlug,
+ offset = 0,
+ limit = 10,
+ search,
+ filter
+}: {
+ id: string;
+ groupSlug: string;
+ offset: number;
+ limit: number;
+ search: string;
+ filter?: EFilterReturnedUsers;
+}) => {
+ return useQuery({
+ queryKey: groupKeys.specificGroupUserMemberships({
+ slug: groupSlug,
+ offset,
+ limit,
+ search,
+ filter
+ }),
+ enabled: Boolean(groupSlug),
+ keepPreviousData: true,
+ queryFn: async () => {
+ const params = new URLSearchParams({
+ offset: String(offset),
+ limit: String(limit),
+ search,
+ ...(filter && { filter })
+ });
+
+ const { data } = await apiRequest.get<{ users: TGroupUser[]; totalCount: number }>(
+ `/api/v1/groups/${id}/users`,
+ {
+ params
+ }
+ );
+
+ return data;
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/groups/types.ts b/frontend-v2/src/hooks/api/groups/types.ts
new file mode 100644
index 000000000..6bc82b39e
--- /dev/null
+++ b/frontend-v2/src/hooks/api/groups/types.ts
@@ -0,0 +1,58 @@
+import { TOrgRole } from "../roles/types";
+
+// TODO: rectify/standardize types
+
+export type TGroupOrgMembership = TGroup & {
+ customRole?: TOrgRole;
+};
+
+export type TGroup = {
+ id: string;
+ name: string;
+ slug: string;
+ orgId: string;
+ createdAt: string;
+ updatedAt: string;
+ role: string;
+};
+
+export type TGroupMembership = {
+ id: string;
+ group: TGroup;
+ roles: {
+ id: string;
+ role: "owner" | "admin" | "member" | "no-access" | "custom";
+ customRoleId: string;
+ customRoleName: string;
+ customRoleSlug: string;
+ isTemporary: boolean;
+ temporaryMode: string | null;
+ temporaryRange: string | null;
+ temporaryAccessStartTime: string | null;
+ temporaryAccessEndTime: string | null;
+ }[];
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type TGroupWithProjectMemberships = {
+ id: string;
+ name: string;
+ slug: string;
+ orgId: string;
+};
+
+export type TGroupUser = {
+ id: string;
+ email: string;
+ username: string;
+ firstName: string;
+ lastName: string;
+ isPartOfGroup: boolean;
+ joinedGroupAt: Date;
+};
+
+export enum EFilterReturnedUsers {
+ EXISTING_MEMBERS = "existingMembers",
+ NON_MEMBERS = "nonMembers"
+}
diff --git a/frontend-v2/src/hooks/api/identities/constants.tsx b/frontend-v2/src/hooks/api/identities/constants.tsx
new file mode 100644
index 000000000..0c57ee82c
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identities/constants.tsx
@@ -0,0 +1,11 @@
+import { IdentityAuthMethod } from "./enums";
+
+export const identityAuthToNameMap: { [I in IdentityAuthMethod]: string } = {
+ [IdentityAuthMethod.TOKEN_AUTH]: "Token Auth",
+ [IdentityAuthMethod.UNIVERSAL_AUTH]: "Universal Auth",
+ [IdentityAuthMethod.KUBERNETES_AUTH]: "Kubernetes Auth",
+ [IdentityAuthMethod.GCP_AUTH]: "GCP Auth",
+ [IdentityAuthMethod.AWS_AUTH]: "AWS Auth",
+ [IdentityAuthMethod.AZURE_AUTH]: "Azure Auth",
+ [IdentityAuthMethod.OIDC_AUTH]: "OIDC Auth"
+};
diff --git a/frontend-v2/src/hooks/api/identities/enums.tsx b/frontend-v2/src/hooks/api/identities/enums.tsx
new file mode 100644
index 000000000..5e445521a
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identities/enums.tsx
@@ -0,0 +1,9 @@
+export enum IdentityAuthMethod {
+ TOKEN_AUTH = "token-auth",
+ UNIVERSAL_AUTH = "universal-auth",
+ KUBERNETES_AUTH = "kubernetes-auth",
+ GCP_AUTH = "gcp-auth",
+ AWS_AUTH = "aws-auth",
+ AZURE_AUTH = "azure-auth",
+ OIDC_AUTH = "oidc-auth"
+}
diff --git a/frontend-v2/src/hooks/api/identities/index.tsx b/frontend-v2/src/hooks/api/identities/index.tsx
new file mode 100644
index 000000000..5c7bcc3e7
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identities/index.tsx
@@ -0,0 +1,44 @@
+export { identityAuthToNameMap } from "./constants";
+export { IdentityAuthMethod } from "./enums";
+export {
+ useAddIdentityAwsAuth,
+ useAddIdentityAzureAuth,
+ useAddIdentityGcpAuth,
+ useAddIdentityKubernetesAuth,
+ useAddIdentityOidcAuth,
+ useAddIdentityTokenAuth,
+ useAddIdentityUniversalAuth,
+ useCreateIdentity,
+ useCreateIdentityUniversalAuthClientSecret,
+ useCreateTokenIdentityTokenAuth,
+ useDeleteIdentity,
+ useDeleteIdentityAwsAuth,
+ useDeleteIdentityAzureAuth,
+ useDeleteIdentityGcpAuth,
+ useDeleteIdentityKubernetesAuth,
+ useDeleteIdentityOidcAuth,
+ useDeleteIdentityTokenAuth,
+ useDeleteIdentityUniversalAuth,
+ useRevokeIdentityTokenAuthToken,
+ useRevokeIdentityUniversalAuthClientSecret,
+ useUpdateIdentity,
+ useUpdateIdentityAwsAuth,
+ useUpdateIdentityAzureAuth,
+ useUpdateIdentityGcpAuth,
+ useUpdateIdentityKubernetesAuth,
+ useUpdateIdentityOidcAuth,
+ useUpdateIdentityTokenAuth,
+ useUpdateIdentityTokenAuthToken,
+ useUpdateIdentityUniversalAuth} from "./mutations";
+export {
+ useGetIdentityAwsAuth,
+ useGetIdentityAzureAuth,
+ useGetIdentityById,
+ useGetIdentityGcpAuth,
+ useGetIdentityKubernetesAuth,
+ useGetIdentityOidcAuth,
+ useGetIdentityProjectMemberships,
+ useGetIdentityTokenAuth,
+ useGetIdentityTokensTokenAuth,
+ useGetIdentityUniversalAuth,
+ useGetIdentityUniversalAuthClientSecrets} from "./queries";
diff --git a/frontend-v2/src/hooks/api/identities/mutations.tsx b/frontend-v2/src/hooks/api/identities/mutations.tsx
new file mode 100644
index 000000000..21c4c560e
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identities/mutations.tsx
@@ -0,0 +1,855 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { organizationKeys } from "../organization/queries";
+import { identitiesKeys } from "./queries";
+import {
+ AddIdentityAwsAuthDTO,
+ AddIdentityAzureAuthDTO,
+ AddIdentityGcpAuthDTO,
+ AddIdentityKubernetesAuthDTO,
+ AddIdentityOidcAuthDTO,
+ AddIdentityTokenAuthDTO,
+ AddIdentityUniversalAuthDTO,
+ ClientSecretData,
+ CreateIdentityDTO,
+ CreateIdentityUniversalAuthClientSecretDTO,
+ CreateIdentityUniversalAuthClientSecretRes,
+ CreateTokenIdentityTokenAuthDTO,
+ CreateTokenIdentityTokenAuthRes,
+ DeleteIdentityAwsAuthDTO,
+ DeleteIdentityAzureAuthDTO,
+ DeleteIdentityDTO,
+ DeleteIdentityGcpAuthDTO,
+ DeleteIdentityKubernetesAuthDTO,
+ DeleteIdentityOidcAuthDTO,
+ DeleteIdentityTokenAuthDTO,
+ DeleteIdentityUniversalAuthClientSecretDTO,
+ DeleteIdentityUniversalAuthDTO,
+ Identity,
+ IdentityAccessToken,
+ IdentityAwsAuth,
+ IdentityAzureAuth,
+ IdentityGcpAuth,
+ IdentityKubernetesAuth,
+ IdentityOidcAuth,
+ IdentityTokenAuth,
+ IdentityUniversalAuth,
+ RevokeTokenDTO,
+ RevokeTokenRes,
+ UpdateIdentityAwsAuthDTO,
+ UpdateIdentityAzureAuthDTO,
+ UpdateIdentityDTO,
+ UpdateIdentityGcpAuthDTO,
+ UpdateIdentityKubernetesAuthDTO,
+ UpdateIdentityOidcAuthDTO,
+ UpdateIdentityTokenAuthDTO,
+ UpdateIdentityUniversalAuthDTO,
+ UpdateTokenIdentityTokenAuthDTO
+} from "./types";
+
+export const useCreateIdentity = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async (body) => {
+ const {
+ data: { identity }
+ } = await apiRequest.post("/api/v1/identities/", body);
+ return identity;
+ },
+ onSuccess: (_, { organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ }
+ });
+};
+
+export const useUpdateIdentity = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId, name, role, metadata }) => {
+ const {
+ data: { identity }
+ } = await apiRequest.patch(`/api/v1/identities/${identityId}`, {
+ name,
+ role,
+ metadata
+ });
+
+ return identity;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ }
+ });
+};
+
+export const useDeleteIdentity = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identity }
+ } = await apiRequest.delete(`/api/v1/identities/${identityId}`);
+ return identity;
+ },
+ onSuccess: (_, { organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ }
+ });
+};
+
+// TODO: move these to /auth
+
+export const useAddIdentityUniversalAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ clientSecretTrustedIps,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityUniversalAuth }
+ } = await apiRequest.post(`/api/v1/auth/universal-auth/identities/${identityId}`, {
+ clientSecretTrustedIps,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ });
+ return identityUniversalAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
+ }
+ });
+};
+
+export const useUpdateIdentityUniversalAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ clientSecretTrustedIps,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityUniversalAuth }
+ } = await apiRequest.patch(`/api/v1/auth/universal-auth/identities/${identityId}`, {
+ clientSecretTrustedIps,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ });
+ return identityUniversalAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
+ }
+ });
+};
+
+export const useDeleteIdentityUniversalAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identityUniversalAuth }
+ } = await apiRequest.delete(`/api/v1/auth/universal-auth/identities/${identityId}`);
+ return identityUniversalAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
+ }
+ });
+};
+
+export const useCreateIdentityUniversalAuthClientSecret = () => {
+ const queryClient = useQueryClient();
+ return useMutation<
+ CreateIdentityUniversalAuthClientSecretRes,
+ {},
+ CreateIdentityUniversalAuthClientSecretDTO
+ >({
+ mutationFn: async ({ identityId, description, ttl, numUsesLimit }) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/auth/universal-auth/identities/${identityId}/client-secrets`,
+ {
+ description,
+ ttl,
+ numUsesLimit
+ }
+ );
+ return data;
+ },
+ onSuccess: (_, { identityId }) => {
+ queryClient.invalidateQueries(
+ identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId)
+ );
+ }
+ });
+};
+
+export const useRevokeIdentityUniversalAuthClientSecret = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId, clientSecretId }) => {
+ const {
+ data: { clientSecretData }
+ } = await apiRequest.post<{ clientSecretData: ClientSecretData }>(
+ `/api/v1/auth/universal-auth/identities/${identityId}/client-secrets/${clientSecretId}/revoke`
+ );
+ return clientSecretData;
+ },
+ onSuccess: (_, { identityId }) => {
+ queryClient.invalidateQueries(
+ identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId)
+ );
+ }
+ });
+};
+
+export const useAddIdentityGcpAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ type,
+ allowedServiceAccounts,
+ allowedProjects,
+ allowedZones,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityGcpAuth }
+ } = await apiRequest.post<{ identityGcpAuth: IdentityGcpAuth }>(
+ `/api/v1/auth/gcp-auth/identities/${identityId}`,
+ {
+ type,
+ allowedServiceAccounts,
+ allowedProjects,
+ allowedZones,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityGcpAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityGcpAuth(identityId));
+ }
+ });
+};
+
+export const useUpdateIdentityGcpAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ type,
+ allowedServiceAccounts,
+ allowedProjects,
+ allowedZones,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityGcpAuth }
+ } = await apiRequest.patch<{ identityGcpAuth: IdentityGcpAuth }>(
+ `/api/v1/auth/gcp-auth/identities/${identityId}`,
+ {
+ type,
+ allowedServiceAccounts,
+ allowedProjects,
+ allowedZones,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityGcpAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityGcpAuth(identityId));
+ }
+ });
+};
+
+export const useDeleteIdentityGcpAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identityGcpAuth }
+ } = await apiRequest.delete(`/api/v1/auth/gcp-auth/identities/${identityId}`);
+ return identityGcpAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityGcpAuth(identityId));
+ }
+ });
+};
+
+export const useAddIdentityAwsAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ stsEndpoint,
+ allowedPrincipalArns,
+ allowedAccountIds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityAwsAuth }
+ } = await apiRequest.post<{ identityAwsAuth: IdentityAwsAuth }>(
+ `/api/v1/auth/aws-auth/identities/${identityId}`,
+ {
+ stsEndpoint,
+ allowedPrincipalArns,
+ allowedAccountIds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityAwsAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityAwsAuth(identityId));
+ }
+ });
+};
+
+export const useUpdateIdentityAwsAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ stsEndpoint,
+ allowedPrincipalArns,
+ allowedAccountIds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityAwsAuth }
+ } = await apiRequest.patch<{ identityAwsAuth: IdentityAwsAuth }>(
+ `/api/v1/auth/aws-auth/identities/${identityId}`,
+ {
+ stsEndpoint,
+ allowedPrincipalArns,
+ allowedAccountIds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityAwsAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityAwsAuth(identityId));
+ }
+ });
+};
+
+export const useDeleteIdentityAwsAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identityAwsAuth }
+ } = await apiRequest.delete(`/api/v1/auth/aws-auth/identities/${identityId}`);
+ return identityAwsAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityAwsAuth(identityId));
+ }
+ });
+};
+
+export const useUpdateIdentityOidcAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps,
+ oidcDiscoveryUrl,
+ caCert,
+ boundIssuer,
+ boundAudiences,
+ boundClaims,
+ boundSubject
+ }) => {
+ const {
+ data: { identityOidcAuth }
+ } = await apiRequest.patch<{ identityOidcAuth: IdentityOidcAuth }>(
+ `/api/v1/auth/oidc-auth/identities/${identityId}`,
+ {
+ oidcDiscoveryUrl,
+ caCert,
+ boundIssuer,
+ boundAudiences,
+ boundClaims,
+ boundSubject,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityOidcAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityOidcAuth(identityId));
+ }
+ });
+};
+
+export const useAddIdentityOidcAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ oidcDiscoveryUrl,
+ caCert,
+ boundIssuer,
+ boundAudiences,
+ boundClaims,
+ boundSubject,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityOidcAuth }
+ } = await apiRequest.post<{ identityOidcAuth: IdentityOidcAuth }>(
+ `/api/v1/auth/oidc-auth/identities/${identityId}`,
+ {
+ oidcDiscoveryUrl,
+ caCert,
+ boundIssuer,
+ boundAudiences,
+ boundClaims,
+ boundSubject,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityOidcAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityOidcAuth(identityId));
+ }
+ });
+};
+
+export const useDeleteIdentityOidcAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identityOidcAuth }
+ } = await apiRequest.delete(`/api/v1/auth/oidc-auth/identities/${identityId}`);
+ return identityOidcAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityOidcAuth(identityId));
+ }
+ });
+};
+
+export const useAddIdentityAzureAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ tenantId,
+ resource,
+ allowedServicePrincipalIds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityAzureAuth }
+ } = await apiRequest.post<{ identityAzureAuth: IdentityAzureAuth }>(
+ `/api/v1/auth/azure-auth/identities/${identityId}`,
+ {
+ tenantId,
+ resource,
+ allowedServicePrincipalIds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityAzureAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityKubernetesAuth(identityId));
+ }
+ });
+};
+
+export const useAddIdentityKubernetesAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ kubernetesHost,
+ tokenReviewerJwt,
+ allowedNames,
+ allowedNamespaces,
+ allowedAudience,
+ caCert,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityKubernetesAuth }
+ } = await apiRequest.post<{ identityKubernetesAuth: IdentityKubernetesAuth }>(
+ `/api/v1/auth/kubernetes-auth/identities/${identityId}`,
+ {
+ kubernetesHost,
+ tokenReviewerJwt,
+ allowedNames,
+ allowedNamespaces,
+ allowedAudience,
+ caCert,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityKubernetesAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityAzureAuth(identityId));
+ }
+ });
+};
+
+export const useUpdateIdentityAzureAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ tenantId,
+ resource,
+ allowedServicePrincipalIds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityAzureAuth }
+ } = await apiRequest.patch<{ identityAzureAuth: IdentityAzureAuth }>(
+ `/api/v1/auth/azure-auth/identities/${identityId}`,
+ {
+ tenantId,
+ resource,
+ allowedServicePrincipalIds,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityAzureAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityAzureAuth(identityId));
+ }
+ });
+};
+
+export const useDeleteIdentityAzureAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identityAzureAuth }
+ } = await apiRequest.delete(`/api/v1/auth/azure-auth/identities/${identityId}`);
+ return identityAzureAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityAzureAuth(identityId));
+ }
+ });
+};
+
+export const useUpdateIdentityKubernetesAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ kubernetesHost,
+ tokenReviewerJwt,
+ allowedNamespaces,
+ allowedNames,
+ allowedAudience,
+ caCert,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityKubernetesAuth }
+ } = await apiRequest.patch<{ identityKubernetesAuth: IdentityKubernetesAuth }>(
+ `/api/v1/auth/kubernetes-auth/identities/${identityId}`,
+ {
+ kubernetesHost,
+ tokenReviewerJwt,
+ allowedNames,
+ allowedNamespaces,
+ allowedAudience,
+ caCert,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityKubernetesAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityKubernetesAuth(identityId));
+ }
+ });
+};
+
+export const useDeleteIdentityKubernetesAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identityKubernetesAuth }
+ } = await apiRequest.delete(`/api/v1/auth/kubernetes-auth/identities/${identityId}`);
+ return identityKubernetesAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityKubernetesAuth(identityId));
+ }
+ });
+};
+
+export const useAddIdentityTokenAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityTokenAuth }
+ } = await apiRequest.post<{ identityTokenAuth: IdentityTokenAuth }>(
+ `/api/v1/auth/token-auth/identities/${identityId}`,
+ {
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityTokenAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
+ }
+ });
+};
+
+export const useUpdateIdentityTokenAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({
+ identityId,
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }) => {
+ const {
+ data: { identityTokenAuth }
+ } = await apiRequest.patch<{ identityTokenAuth: IdentityTokenAuth }>(
+ `/api/v1/auth/token-auth/identities/${identityId}`,
+ {
+ accessTokenTTL,
+ accessTokenMaxTTL,
+ accessTokenNumUsesLimit,
+ accessTokenTrustedIps
+ }
+ );
+
+ return identityTokenAuth;
+ },
+ onSuccess: (_, { identityId, organizationId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuth(identityId));
+ }
+ });
+};
+
+export const useDeleteIdentityTokenAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId }) => {
+ const {
+ data: { identityTokenAuth }
+ } = await apiRequest.delete(`/api/v1/auth/token-auth/identities/${identityId}`);
+ return identityTokenAuth;
+ },
+ onSuccess: (_, { organizationId, identityId }) => {
+ queryClient.invalidateQueries(organizationKeys.getOrgIdentityMemberships(organizationId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityById(identityId));
+ queryClient.invalidateQueries(identitiesKeys.getIdentityTokenAuth(identityId));
+ }
+ });
+};
+
+export const useCreateTokenIdentityTokenAuth = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ identityId, name }) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/auth/token-auth/identities/${identityId}/tokens`,
+ {
+ name
+ }
+ );
+
+ return data;
+ },
+ onSuccess: (_, { identityId }) => {
+ queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId));
+ }
+ });
+};
+
+export const useUpdateIdentityTokenAuthToken = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ tokenId, name }) => {
+ const {
+ data: { token }
+ } = await apiRequest.patch<{ token: IdentityAccessToken }>(
+ `/api/v1/auth/token-auth/tokens/${tokenId}`,
+ {
+ name
+ }
+ );
+
+ return token;
+ },
+ onSuccess: (_, { identityId }) => {
+ queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId));
+ }
+ });
+};
+
+export const useRevokeIdentityTokenAuthToken = () => {
+ const queryClient = useQueryClient();
+ return useMutation({
+ mutationFn: async ({ tokenId }) => {
+ const { data } = await apiRequest.post(
+ `/api/v1/auth/token-auth/tokens/${tokenId}/revoke`
+ );
+
+ return data;
+ },
+ onSuccess: (_, { identityId }) => {
+ queryClient.invalidateQueries(identitiesKeys.getIdentityTokensTokenAuth(identityId));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/identities/queries.tsx b/frontend-v2/src/hooks/api/identities/queries.tsx
new file mode 100644
index 000000000..c5c442407
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identities/queries.tsx
@@ -0,0 +1,278 @@
+import { useQuery, UseQueryOptions } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import {
+ ClientSecretData,
+ IdentityAccessToken,
+ IdentityAwsAuth,
+ IdentityAzureAuth,
+ IdentityGcpAuth,
+ IdentityKubernetesAuth,
+ IdentityMembership,
+ IdentityMembershipOrg,
+ IdentityOidcAuth,
+ IdentityTokenAuth,
+ IdentityUniversalAuth
+} from "./types";
+
+export const identitiesKeys = {
+ getIdentityById: (identityId: string) => [{ identityId }, "identity"] as const,
+ getIdentityUniversalAuth: (identityId: string) =>
+ [{ identityId }, "identity-universal-auth"] as const,
+ getIdentityUniversalAuthClientSecrets: (identityId: string) =>
+ [{ identityId }, "identity-universal-auth-client-secrets"] as const,
+ getIdentityKubernetesAuth: (identityId: string) =>
+ [{ identityId }, "identity-kubernetes-auth"] as const,
+ getIdentityGcpAuth: (identityId: string) => [{ identityId }, "identity-gcp-auth"] as const,
+ getIdentityOidcAuth: (identityId: string) => [{ identityId }, "identity-oidc-auth"] as const,
+ getIdentityAwsAuth: (identityId: string) => [{ identityId }, "identity-aws-auth"] as const,
+ getIdentityAzureAuth: (identityId: string) => [{ identityId }, "identity-azure-auth"] as const,
+ getIdentityTokenAuth: (identityId: string) => [{ identityId }, "identity-token-auth"] as const,
+ getIdentityTokensTokenAuth: (identityId: string) =>
+ [{ identityId }, "identity-tokens-token-auth"] as const,
+ getIdentityProjectMemberships: (identityId: string) =>
+ [{ identityId }, "identity-project-memberships"] as const
+};
+
+export const useGetIdentityById = (identityId: string) => {
+ return useQuery({
+ enabled: Boolean(identityId),
+ queryKey: identitiesKeys.getIdentityById(identityId),
+ queryFn: async () => {
+ const {
+ data: { identity }
+ } = await apiRequest.get<{ identity: IdentityMembershipOrg }>(
+ `/api/v1/identities/${identityId}`
+ );
+ return identity;
+ }
+ });
+};
+
+export const useGetIdentityProjectMemberships = (identityId: string) => {
+ return useQuery({
+ enabled: Boolean(identityId),
+ queryKey: identitiesKeys.getIdentityProjectMemberships(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityMemberships }
+ } = await apiRequest.get<{ identityMemberships: IdentityMembership[] }>(
+ `/api/v1/identities/${identityId}/identity-memberships`
+ );
+ return identityMemberships;
+ }
+ });
+};
+
+export const useGetIdentityUniversalAuth = (
+ identityId: string,
+ options?: UseQueryOptions<
+ IdentityUniversalAuth,
+ unknown,
+ IdentityUniversalAuth,
+ ReturnType
+ >
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityUniversalAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityUniversalAuth }
+ } = await apiRequest.get<{ identityUniversalAuth: IdentityUniversalAuth }>(
+ `/api/v1/auth/universal-auth/identities/${identityId}`
+ );
+ return identityUniversalAuth;
+ },
+ cacheTime: 0,
+ staleTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
+
+export const useGetIdentityUniversalAuthClientSecrets = (identityId: string) => {
+ return useQuery({
+ enabled: Boolean(identityId),
+ queryKey: identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId),
+ queryFn: async () => {
+ const {
+ data: { clientSecretData }
+ } = await apiRequest.get<{ clientSecretData: ClientSecretData[] }>(
+ `/api/v1/auth/universal-auth/identities/${identityId}/client-secrets`
+ );
+ return clientSecretData;
+ }
+ });
+};
+
+export const useGetIdentityGcpAuth = (
+ identityId: string,
+ options?: UseQueryOptions<
+ IdentityGcpAuth,
+ unknown,
+ IdentityGcpAuth,
+ ReturnType
+ >
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityGcpAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityGcpAuth }
+ } = await apiRequest.get<{ identityGcpAuth: IdentityGcpAuth }>(
+ `/api/v1/auth/gcp-auth/identities/${identityId}`
+ );
+ return identityGcpAuth;
+ },
+ staleTime: 0,
+ cacheTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
+
+export const useGetIdentityAwsAuth = (
+ identityId: string,
+ options?: UseQueryOptions<
+ IdentityAwsAuth,
+ unknown,
+ IdentityAwsAuth,
+ ReturnType
+ >
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityAwsAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityAwsAuth }
+ } = await apiRequest.get<{ identityAwsAuth: IdentityAwsAuth }>(
+ `/api/v1/auth/aws-auth/identities/${identityId}`
+ );
+ return identityAwsAuth;
+ },
+ staleTime: 0,
+ cacheTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
+
+export const useGetIdentityAzureAuth = (
+ identityId: string,
+ options?: UseQueryOptions<
+ IdentityAzureAuth,
+ unknown,
+ IdentityAzureAuth,
+ ReturnType
+ >
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityAzureAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityAzureAuth }
+ } = await apiRequest.get<{ identityAzureAuth: IdentityAzureAuth }>(
+ `/api/v1/auth/azure-auth/identities/${identityId}`
+ );
+ return identityAzureAuth;
+ },
+ staleTime: 0,
+ cacheTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
+
+export const useGetIdentityKubernetesAuth = (
+ identityId: string,
+ options?: UseQueryOptions<
+ IdentityKubernetesAuth,
+ unknown,
+ IdentityKubernetesAuth,
+ ReturnType
+ >
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityKubernetesAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityKubernetesAuth }
+ } = await apiRequest.get<{ identityKubernetesAuth: IdentityKubernetesAuth }>(
+ `/api/v1/auth/kubernetes-auth/identities/${identityId}`
+ );
+ return identityKubernetesAuth;
+ },
+ staleTime: 0,
+ cacheTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
+
+export const useGetIdentityTokenAuth = (
+ identityId: string,
+ options?: UseQueryOptions<
+ IdentityTokenAuth,
+ unknown,
+ IdentityTokenAuth,
+ ReturnType
+ >
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityTokenAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityTokenAuth }
+ } = await apiRequest.get<{ identityTokenAuth: IdentityTokenAuth }>(
+ `/api/v1/auth/token-auth/identities/${identityId}`
+ );
+ return identityTokenAuth;
+ },
+ staleTime: 0,
+ cacheTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
+
+export const useGetIdentityTokensTokenAuth = (identityId: string) => {
+ return useQuery({
+ enabled: Boolean(identityId),
+ queryKey: identitiesKeys.getIdentityTokensTokenAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { tokens }
+ } = await apiRequest.get<{ tokens: IdentityAccessToken[] }>(
+ `/api/v1/auth/token-auth/identities/${identityId}/tokens`
+ );
+ return tokens;
+ }
+ });
+};
+
+export const useGetIdentityOidcAuth = (
+ identityId: string,
+ options?: UseQueryOptions<
+ IdentityOidcAuth,
+ unknown,
+ IdentityOidcAuth,
+ ReturnType
+ >
+) => {
+ return useQuery({
+ queryKey: identitiesKeys.getIdentityOidcAuth(identityId),
+ queryFn: async () => {
+ const {
+ data: { identityOidcAuth }
+ } = await apiRequest.get<{ identityOidcAuth: IdentityOidcAuth }>(
+ `/api/v1/auth/oidc-auth/identities/${identityId}`
+ );
+ return identityOidcAuth;
+ },
+ staleTime: 0,
+ cacheTime: 0,
+ ...options,
+ enabled: Boolean(identityId) && (options?.enabled ?? true)
+ });
+};
diff --git a/frontend-v2/src/hooks/api/identities/types.ts b/frontend-v2/src/hooks/api/identities/types.ts
new file mode 100644
index 000000000..559a01974
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identities/types.ts
@@ -0,0 +1,479 @@
+import { TOrgRole } from "../roles/types";
+import { ProjectUserMembershipTemporaryMode, Workspace } from "../workspace/types";
+import { IdentityAuthMethod } from "./enums";
+
+export type IdentityTrustedIp = {
+ id: string;
+ ipAddress: string;
+ type: "ipv4" | "ipv6";
+ prefix?: number;
+};
+
+export type Identity = {
+ id: string;
+ name: string;
+ authMethods: IdentityAuthMethod[];
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type IdentityAccessToken = {
+ id: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUses: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenLastUsedAt: string | null;
+ accessTokenLastRenewedAt: string | null;
+ isAccessTokenRevoked: boolean;
+ identityUAClientSecretId: string | null;
+ identityId: string;
+ createdAt: string;
+ updatedAt: string;
+ name: string | null;
+};
+
+export type IdentityMembershipOrg = {
+ id: string;
+ identity: Identity;
+ organization: string;
+ metadata: { key: string; value: string; id: string }[];
+ role: "admin" | "member" | "viewer" | "no-access" | "custom";
+ customRole?: TOrgRole;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type IdentityMembership = {
+ id: string;
+ identity: Identity;
+ project: Pick;
+ roles: Array<
+ {
+ id: string;
+ role: "owner" | "admin" | "member" | "no-access" | "custom";
+ customRoleId: string;
+ customRoleName: string;
+ customRoleSlug: string;
+ } & (
+ | {
+ isTemporary: false;
+ temporaryRange: null;
+ temporaryMode: null;
+ temporaryAccessEndTime: null;
+ temporaryAccessStartTime: null;
+ }
+ | {
+ isTemporary: true;
+ temporaryRange: string;
+ temporaryMode: ProjectUserMembershipTemporaryMode;
+ temporaryAccessEndTime: string;
+ temporaryAccessStartTime: string;
+ }
+ )
+ >;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type CreateIdentityDTO = {
+ name: string;
+ organizationId: string;
+ role?: string;
+ metadata?: { key: string; value: string }[];
+};
+
+export type UpdateIdentityDTO = {
+ identityId: string;
+ name?: string;
+ role?: string;
+ organizationId: string;
+ metadata?: { key: string; value: string }[];
+};
+
+export type DeleteIdentityDTO = {
+ identityId: string;
+ organizationId: string;
+};
+
+export type IdentityUniversalAuth = {
+ identityId: string;
+ clientId: string;
+ clientSecretTrustedIps: IdentityTrustedIp[];
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentityUniversalAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ clientSecretTrustedIps: {
+ ipAddress: string;
+ }[];
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityUniversalAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ clientSecretTrustedIps?: {
+ ipAddress: string;
+ }[];
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityUniversalAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
+export type IdentityGcpAuth = {
+ identityId: string;
+ type: "iam" | "gce";
+ allowedServiceAccounts: string;
+ allowedProjects: string;
+ allowedZones: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentityGcpAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ type: "iam" | "gce";
+ allowedServiceAccounts: string;
+ allowedProjects: string;
+ allowedZones: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityGcpAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ type?: "iam" | "gce";
+ allowedServiceAccounts?: string;
+ allowedProjects?: string;
+ allowedZones?: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityGcpAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
+export type IdentityOidcAuth = {
+ identityId: string;
+ oidcDiscoveryUrl: string;
+ caCert: string;
+ boundIssuer: string;
+ boundAudiences: string;
+ boundClaims: Record;
+ boundSubject: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentityOidcAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ oidcDiscoveryUrl: string;
+ caCert: string;
+ boundIssuer: string;
+ boundAudiences: string;
+ boundClaims: Record;
+ boundSubject: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityOidcAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ oidcDiscoveryUrl?: string;
+ caCert?: string;
+ boundIssuer?: string;
+ boundAudiences?: string;
+ boundClaims?: Record;
+ boundSubject?: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityOidcAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
+export type IdentityAwsAuth = {
+ identityId: string;
+ type: "iam";
+ stsEndpoint: string;
+ allowedPrincipalArns: string;
+ allowedAccountIds: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentityAwsAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ stsEndpoint: string;
+ allowedPrincipalArns: string;
+ allowedAccountIds: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityAwsAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ stsEndpoint?: string;
+ allowedPrincipalArns?: string;
+ allowedAccountIds?: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityAwsAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
+export type IdentityAzureAuth = {
+ identityId: string;
+ tenantId: string;
+ resource: string;
+ allowedServicePrincipalIds: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentityAzureAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ tenantId: string;
+ resource: string;
+ allowedServicePrincipalIds: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityAzureAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ tenantId?: string;
+ resource?: string;
+ allowedServicePrincipalIds?: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityAzureAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
+export type IdentityKubernetesAuth = {
+ identityId: string;
+ kubernetesHost: string;
+ tokenReviewerJwt: string;
+ allowedNamespaces: string;
+ allowedNames: string;
+ allowedAudience: string;
+ caCert: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentityKubernetesAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ kubernetesHost: string;
+ tokenReviewerJwt: string;
+ allowedNamespaces: string;
+ allowedNames: string;
+ allowedAudience: string;
+ caCert: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityKubernetesAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ kubernetesHost?: string;
+ tokenReviewerJwt?: string;
+ allowedNamespaces?: string;
+ allowedNames?: string;
+ allowedAudience?: string;
+ caCert?: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityKubernetesAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
+export type CreateIdentityUniversalAuthClientSecretDTO = {
+ identityId: string;
+ description?: string;
+ ttl?: number;
+ numUsesLimit?: number;
+};
+
+export type ClientSecretData = {
+ id: string;
+ identityUniversalAuth: string;
+ isClientSecretRevoked: boolean;
+ description: string;
+ clientSecretPrefix: string;
+ clientSecretNumUses: number;
+ clientSecretNumUsesLimit: number;
+ clientSecretTTL: number;
+ createdAt: string;
+ updatedAt: string;
+};
+
+export type CreateIdentityUniversalAuthClientSecretRes = {
+ clientSecret: string;
+ clientSecretData: ClientSecretData;
+};
+
+export type DeleteIdentityUniversalAuthClientSecretDTO = {
+ identityId: string;
+ clientSecretId: string;
+};
+
+export type IdentityTokenAuth = {
+ identityId: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: IdentityTrustedIp[];
+};
+
+export type AddIdentityTokenAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ accessTokenTTL: number;
+ accessTokenMaxTTL: number;
+ accessTokenNumUsesLimit: number;
+ accessTokenTrustedIps: {
+ ipAddress: string;
+ }[];
+};
+
+export type UpdateIdentityTokenAuthDTO = {
+ organizationId: string;
+ identityId: string;
+ accessTokenTTL?: number;
+ accessTokenMaxTTL?: number;
+ accessTokenNumUsesLimit?: number;
+ accessTokenTrustedIps?: {
+ ipAddress: string;
+ }[];
+};
+
+export type DeleteIdentityTokenAuthDTO = {
+ organizationId: string;
+ identityId: string;
+};
+
+export type CreateTokenIdentityTokenAuthDTO = {
+ identityId: string;
+ name: string;
+};
+
+export type CreateTokenIdentityTokenAuthRes = {
+ accessToken: string;
+ tokenType: string;
+ expiresIn: number;
+ accessTokenMaxTTL: number;
+};
+
+export type UpdateTokenIdentityTokenAuthDTO = {
+ identityId: string;
+ tokenId: string;
+ name?: string;
+};
+
+export type RevokeTokenDTO = {
+ identityId: string;
+ tokenId: string;
+};
+
+export type RevokeTokenRes = {
+ message: string;
+};
+
+export type TProjectIdentitiesList = {
+ identityMemberships: IdentityMembership[];
+ totalCount: number;
+};
diff --git a/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/index.tsx b/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/index.tsx
new file mode 100644
index 000000000..401a06a72
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/index.tsx
@@ -0,0 +1,7 @@
+export {
+ useCreateIdentityProjectAdditionalPrivilege,
+ useDeleteIdentityProjectAdditionalPrivilege,
+ useUpdateIdentityProjectAdditionalPrivilege
+} from "./mutation";
+export { useGetIdentityProjectPrivilegeDetails } from "./queries";
+export type { TIdentityProjectPrivilege } from "./types";
diff --git a/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx b/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx
new file mode 100644
index 000000000..ab8a2e5d1
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/mutation.tsx
@@ -0,0 +1,72 @@
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { identitiyProjectPrivilegeKeys } from "./queries";
+import {
+ TCreateIdentityProjectPrivilegeDTO,
+ TDeleteIdentityProjectPrivilegeDTO,
+ TIdentityProjectPrivilege,
+ TUpdateIdentityProjectPrivlegeDTO
+} from "./types";
+
+export const useCreateIdentityProjectAdditionalPrivilege = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async (dto) => {
+ const { data } = await apiRequest.post("/api/v2/identity-project-additional-privilege", dto);
+ return data.privilege;
+ },
+ onSuccess: (_, { projectId, identityId }) => {
+ queryClient.invalidateQueries(identitiyProjectPrivilegeKeys.list({ projectId, identityId }));
+ }
+ });
+};
+
+export const useUpdateIdentityProjectAdditionalPrivilege = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ projectId, privilegeId, identityId, permissions, slug, type }) => {
+ const { data: res } = await apiRequest.patch(
+ `/api/v2/identity-project-additional-privilege/${privilegeId}`,
+ {
+ privilegeId,
+ projectId,
+ identityId,
+ permissions,
+ slug,
+ type
+ }
+ );
+ return res.privilege;
+ },
+ onSuccess: (_, { projectId, identityId }) => {
+ queryClient.invalidateQueries(identitiyProjectPrivilegeKeys.list({ projectId, identityId }));
+ }
+ });
+};
+
+export const useDeleteIdentityProjectAdditionalPrivilege = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation({
+ mutationFn: async ({ identityId, projectId, privilegeId }) => {
+ const { data } = await apiRequest.delete(
+ `/api/v2/identity-project-additional-privilege/${privilegeId}`,
+ {
+ data: {
+ identityId,
+ privilegeId,
+ projectId
+ }
+ }
+ );
+ return data.privilege;
+ },
+ onSuccess: (_, { projectId, identityId }) => {
+ queryClient.invalidateQueries(identitiyProjectPrivilegeKeys.list({ projectId, identityId }));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx b/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx
new file mode 100644
index 000000000..8504f0423
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/queries.tsx
@@ -0,0 +1,67 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import {
+ TGetIdentityProejctPrivilegeDetails as TGetIdentityProjectPrivilegeDetails,
+ TIdentityProjectPrivilege,
+ TListIdentityUserPrivileges as TListIdentityProjectPrivileges
+} from "./types";
+
+export const identitiyProjectPrivilegeKeys = {
+ details: ({ identityId, privilegeId, projectId }: TGetIdentityProjectPrivilegeDetails) =>
+ [
+ "identity-user-privilege",
+ {
+ identityId,
+ projectId,
+ privilegeId
+ }
+ ] as const,
+ list: ({ projectId, identityId }: TListIdentityProjectPrivileges) =>
+ ["identity-user-privileges", { identityId, projectId }] as const
+};
+
+export const useGetIdentityProjectPrivilegeDetails = ({
+ projectId,
+ identityId,
+ privilegeId
+}: TGetIdentityProjectPrivilegeDetails) => {
+ return useQuery({
+ enabled: Boolean(projectId && identityId && privilegeId),
+ queryKey: identitiyProjectPrivilegeKeys.details({ projectId, privilegeId, identityId }),
+ queryFn: async () => {
+ const {
+ data: { privilege }
+ } = await apiRequest.get<{
+ privilege: TIdentityProjectPrivilege;
+ }>(`/api/v2/identity-project-additional-privilege/${privilegeId}`, {
+ params: {
+ identityId,
+ projectId
+ }
+ });
+ return privilege;
+ }
+ });
+};
+
+export const useListIdentityProjectPrivileges = ({
+ projectId,
+ identityId
+}: TListIdentityProjectPrivileges) => {
+ return useQuery({
+ enabled: Boolean(projectId && identityId),
+ queryKey: identitiyProjectPrivilegeKeys.list({ projectId, identityId }),
+ queryFn: async () => {
+ const {
+ data: { privileges }
+ } = await apiRequest.get<{
+ privileges: Array;
+ }>("/api/v2/identity-project-additional-privilege", {
+ params: { identityId, projectId }
+ });
+ return privileges;
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx b/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx
new file mode 100644
index 000000000..1e070e2bb
--- /dev/null
+++ b/frontend-v2/src/hooks/api/identityProjectAdditionalPrivilege/types.tsx
@@ -0,0 +1,69 @@
+import { TProjectPermission } from "../roles/types";
+
+export enum IdentityProjectAdditionalPrivilegeTemporaryMode {
+ Relative = "relative"
+}
+
+export type TIdentityProjectPrivilege = {
+ projectMembershipId: string;
+ slug: string;
+ id: string;
+ createdAt: Date;
+ updatedAt: Date;
+ permissions?: TProjectPermission[];
+} & (
+ | {
+ isTemporary: true;
+ temporaryMode: string;
+ temporaryRange: string;
+ temporaryAccessStartTime: string;
+ temporaryAccessEndTime?: string;
+ }
+ | {
+ isTemporary: false;
+ temporaryMode?: null;
+ temporaryRange?: null;
+ temporaryAccessStartTime?: null;
+ temporaryAccessEndTime?: null;
+ }
+);
+
+export type TCreateIdentityProjectPrivilegeDTO = {
+ identityId: string;
+ projectId: string;
+ slug?: string;
+ type:
+ | {
+ isTemporary: true;
+ temporaryMode?: IdentityProjectAdditionalPrivilegeTemporaryMode;
+ temporaryRange?: string;
+ temporaryAccessStartTime?: string;
+ }
+ | {
+ isTemporary: false;
+ };
+ permissions: TProjectPermission[];
+};
+
+export type TUpdateIdentityProjectPrivlegeDTO = {
+ projectId: string;
+ identityId: string;
+ privilegeId: string;
+} & Partial>;
+
+export type TDeleteIdentityProjectPrivilegeDTO = {
+ projectId: string;
+ identityId: string;
+ privilegeId: string;
+};
+
+export type TListIdentityUserPrivileges = {
+ projectId: string;
+ identityId: string;
+};
+
+export type TGetIdentityProejctPrivilegeDetails = {
+ projectId: string;
+ identityId: string;
+ privilegeId: string;
+};
diff --git a/frontend-v2/src/hooks/api/incidentContacts/index.tsx b/frontend-v2/src/hooks/api/incidentContacts/index.tsx
new file mode 100644
index 000000000..b9b518848
--- /dev/null
+++ b/frontend-v2/src/hooks/api/incidentContacts/index.tsx
@@ -0,0 +1,5 @@
+export {
+ useAddIncidentContact,
+ useDeleteIncidentContact,
+ useGetOrgIncidentContact
+} from "./queries";
diff --git a/frontend-v2/src/hooks/api/incidentContacts/queries.tsx b/frontend-v2/src/hooks/api/incidentContacts/queries.tsx
new file mode 100644
index 000000000..98f95d785
--- /dev/null
+++ b/frontend-v2/src/hooks/api/incidentContacts/queries.tsx
@@ -0,0 +1,55 @@
+import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { AddIncidentContactDTO, DeleteIncidentContactDTO, IncidentContact } from "./types";
+
+const incidentContactKeys = {
+ getAllContact: (orgId: string) => ["org-incident-contacts", { orgId }] as const
+};
+
+export const useGetOrgIncidentContact = (orgId: string) =>
+ useQuery({
+ queryKey: incidentContactKeys.getAllContact(orgId),
+ queryFn: async () => {
+ const { data } = await apiRequest.get<{ incidentContactsOrg: IncidentContact[] }>(
+ `/api/v1/organization/${orgId}/incidentContactOrg`
+ );
+
+ return data.incidentContactsOrg;
+ },
+ enabled: Boolean(orgId)
+ });
+
+// mutation
+export const useAddIncidentContact = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, AddIncidentContactDTO>({
+ mutationFn: async ({ orgId, email }) => {
+ const { data } = await apiRequest.post(`/api/v1/organization/${orgId}/incidentContactOrg`, {
+ email
+ });
+ return data;
+ },
+ onSuccess: (_, { orgId }) => {
+ queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
+ }
+ });
+};
+
+export const useDeleteIncidentContact = () => {
+ const queryClient = useQueryClient();
+
+ return useMutation<{}, {}, DeleteIncidentContactDTO>({
+ mutationFn: async ({ orgId, incidentContactId }) => {
+ const { data } = await apiRequest.delete(
+ `/api/v1/organization/${orgId}/incidentContactOrg/${incidentContactId}`
+ );
+ return data;
+ },
+ onSuccess: (_, { orgId }) => {
+ queryClient.invalidateQueries(incidentContactKeys.getAllContact(orgId));
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/incidentContacts/types.ts b/frontend-v2/src/hooks/api/incidentContacts/types.ts
new file mode 100644
index 000000000..2c93fb7f9
--- /dev/null
+++ b/frontend-v2/src/hooks/api/incidentContacts/types.ts
@@ -0,0 +1,18 @@
+export type IncidentContact = {
+ id: string;
+ email: string;
+ organization: string;
+ __v: number;
+ createdAt: Date;
+ updatedAt: Date;
+};
+
+export type DeleteIncidentContactDTO = {
+ orgId: string;
+ incidentContactId: string;
+};
+
+export type AddIncidentContactDTO = {
+ orgId: string;
+ email: string;
+};
diff --git a/frontend-v2/src/hooks/api/index.tsx b/frontend-v2/src/hooks/api/index.tsx
new file mode 100644
index 000000000..551822f09
--- /dev/null
+++ b/frontend-v2/src/hooks/api/index.tsx
@@ -0,0 +1,48 @@
+export * from "./accessApproval";
+export * from "./admin";
+export * from "./apiKeys";
+export * from "./auditLogs";
+export * from "./auditLogStreams";
+export * from "./auth";
+export * from "./bots";
+export * from "./ca";
+export * from "./certificates";
+export * from "./certificateTemplates";
+export * from "./dynamicSecret";
+export * from "./dynamicSecretLease";
+export * from "./groups";
+export * from "./identities";
+export * from "./identityProjectAdditionalPrivilege";
+export * from "./incidentContacts";
+export * from "./integrationAuth";
+export * from "./integrations";
+export * from "./keys";
+export * from "./kms";
+export * from "./ldapConfig";
+export * from "./oidcConfig";
+export * from "./orgAdmin";
+export * from "./organization";
+export * from "./pkiAlerts";
+export * from "./pkiCollections";
+export * from "./projectUserAdditionalPrivilege";
+export * from "./rateLimit";
+export * from "./roles";
+export * from "./scim";
+export * from "./secretApproval";
+export * from "./secretApprovalRequest";
+export * from "./secretFolders";
+export * from "./secretImports";
+export * from "./secretRotation";
+export * from "./secrets";
+export * from "./secretSharing";
+export * from "./secretSnapshots";
+export * from "./serverDetails";
+export * from "./serviceTokens";
+export * from "./ssoConfig";
+export * from "./subscriptions";
+export * from "./tags";
+export * from "./trustedIps";
+export * from "./users";
+export * from "./webhooks";
+export * from "./workflowIntegrations";
+export * from "./workspace";
diff --git a/frontend-v2/src/hooks/api/integrationAuth/index.tsx b/frontend-v2/src/hooks/api/integrationAuth/index.tsx
new file mode 100644
index 000000000..0ae3511de
--- /dev/null
+++ b/frontend-v2/src/hooks/api/integrationAuth/index.tsx
@@ -0,0 +1,19 @@
+export { useDuplicateIntegrationAuth } from "./mutations";
+export {
+ useAuthorizeIntegration,
+ useDeleteIntegrationAuth,
+ useDeleteIntegrationAuths,
+ useGetIntegrationAuthApps,
+ useGetIntegrationAuthBitBucketWorkspaces,
+ useGetIntegrationAuthById,
+ useGetIntegrationAuthChecklyGroups,
+ useGetIntegrationAuthGithubEnvs,
+ useGetIntegrationAuthGithubOrgs,
+ useGetIntegrationAuthNorthflankSecretGroups,
+ useGetIntegrationAuthRailwayEnvironments,
+ useGetIntegrationAuthRailwayServices,
+ useGetIntegrationAuthTeamCityBuildConfigs,
+ useGetIntegrationAuthTeams,
+ useGetIntegrationAuthVercelBranches,
+ useSaveIntegrationAccessToken
+} from "./queries";
diff --git a/frontend-v2/src/hooks/api/integrationAuth/mutations.tsx b/frontend-v2/src/hooks/api/integrationAuth/mutations.tsx
new file mode 100644
index 000000000..b7a3f18bd
--- /dev/null
+++ b/frontend-v2/src/hooks/api/integrationAuth/mutations.tsx
@@ -0,0 +1,19 @@
+import { useMutation } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { IntegrationAuth, TDuplicateIntegrationAuthDTO } from "./types";
+
+// For now, this should only be used in the Github app integration flow.
+export const useDuplicateIntegrationAuth = () => {
+ return useMutation({
+ mutationFn: async (body) => {
+ const { data } = await apiRequest.post<{ integrationAuth: IntegrationAuth }>(
+ `/api/v1/integration-auth/${body.integrationAuthId}/duplicate`,
+ body
+ );
+
+ return data.integrationAuth;
+ }
+ });
+};
diff --git a/frontend-v2/src/hooks/api/integrationAuth/queries.tsx b/frontend-v2/src/hooks/api/integrationAuth/queries.tsx
new file mode 100644
index 000000000..e5f928158
--- /dev/null
+++ b/frontend-v2/src/hooks/api/integrationAuth/queries.tsx
@@ -0,0 +1,995 @@
+import { useMutation, useQuery, useQueryClient, UseQueryOptions } from "@tanstack/react-query";
+
+import { apiRequest } from "@app/config/request";
+
+import { workspaceKeys } from "../workspace";
+import {
+ App,
+ BitBucketEnvironment,
+ BitBucketWorkspace,
+ ChecklyGroup,
+ Environment,
+ HerokuPipelineCoupling,
+ IntegrationAuth,
+ KmsKey,
+ NorthflankSecretGroup,
+ Org,
+ Project,
+ Service,
+ Team,
+ TeamCityBuildConfig,
+ TGetIntegrationAuthOctopusDeployScopeValuesDTO,
+ TOctopusDeployVariableSetScopeValues
+} from "./types";
+
+const integrationAuthKeys = {
+ getIntegrationAuthById: (integrationAuthId: string) =>
+ [{ integrationAuthId }, "integrationAuth"] as const,
+ getIntegrationAuthApps: (integrationAuthId: string, teamId?: string, workspaceSlug?: string) =>
+ [{ integrationAuthId, teamId, workspaceSlug }, "integrationAuthApps"] as const,
+ getIntegrationAuthTeams: (integrationAuthId: string) =>
+ [{ integrationAuthId }, "integrationAuthTeams"] as const,
+ getIntegrationAuthVercelBranches: ({
+ integrationAuthId,
+ appId
+ }: {
+ integrationAuthId: string;
+ appId: string;
+ }) => [{ integrationAuthId, appId }, "integrationAuthVercelBranches"] as const,
+ getIntegrationAuthChecklyGroups: ({
+ integrationAuthId,
+ accountId
+ }: {
+ integrationAuthId: string;
+ accountId: string;
+ }) => [{ integrationAuthId, accountId }, "integrationAuthChecklyGroups"] as const,
+ getIntegrationAuthGithubOrgs: (integrationAuthId: string) =>
+ [{ integrationAuthId }, "integrationAuthGithubOrgs"] as const,
+ getIntegrationAuthGithubEnvs: (integrationAuthId: string, repoName: string, repoOwner: string) =>
+ [{ integrationAuthId, repoName, repoOwner }, "integrationAuthGithubOrgs"] as const,
+ getIntegrationAuthAwsKmsKeys: ({
+ integrationAuthId,
+ region
+ }: {
+ integrationAuthId: string;
+ region: string;
+ }) => [{ integrationAuthId, region }, "integrationAuthAwsKmsKeyIds"] as const,
+ getIntegrationAuthQoveryOrgs: (integrationAuthId: string) =>
+ [{ integrationAuthId }, "integrationAuthQoveryOrgs"] as const,
+ getIntegrationAuthQoveryProjects: ({
+ integrationAuthId,
+ orgId
+ }: {
+ integrationAuthId: string;
+ orgId: string;
+ }) => [{ integrationAuthId, orgId }, "integrationAuthQoveryProjects"] as const,
+ getIntegrationAuthQoveryEnvironments: ({
+ integrationAuthId,
+ projectId
+ }: {
+ integrationAuthId: string;
+ projectId: string;
+ }) => [{ integrationAuthId, projectId }, "integrationAuthQoveryEnvironments"] as const,
+ getIntegrationAuthQoveryScopes: ({
+ integrationAuthId,
+ environmentId,
+ scope
+ }: {
+ integrationAuthId: string;
+ environmentId: string;
+ scope: "job" | "application" | "container";
+ }) => [{ integrationAuthId, environmentId, scope }, "integrationAuthQoveryScopes"] as const,
+ getIntegrationAuthHerokuPipelines: ({ integrationAuthId }: { integrationAuthId: string }) =>
+ [{ integrationAuthId }, "integrationAuthHerokuPipelines"] as const,
+ getIntegrationAuthRailwayEnvironments: ({
+ integrationAuthId,
+ appId
+ }: {
+ integrationAuthId: string;
+ appId: string;
+ }) => [{ integrationAuthId, appId }, "integrationAuthRailwayEnvironments"] as const,
+ getIntegrationAuthRailwayServices: ({
+ integrationAuthId,
+ appId
+ }: {
+ integrationAuthId: string;
+ appId: string;
+ }) => [{ integrationAuthId, appId }, "integrationAuthRailwayServices"] as const,
+ getIntegrationAuthBitBucketWorkspaces: (integrationAuthId: string) =>
+ [{ integrationAuthId }, "integrationAuthBitbucketWorkspaces"] as const,
+ getIntegrationAuthBitBucketEnvironments: (
+ integrationAuthId: string,
+ workspaceSlug: string,
+ repoSlug: string
+ ) =>
+ [
+ { integrationAuthId },
+ workspaceSlug,
+ repoSlug,
+ "integrationAuthBitbucketEnvironments"
+ ] as const,
+ getIntegrationAuthNorthflankSecretGroups: ({
+ integrationAuthId,
+ appId
+ }: {
+ integrationAuthId: string;
+ appId: string;
+ }) => [{ integrationAuthId, appId }, "integrationAuthNorthflankSecretGroups"] as const,
+ getIntegrationAuthTeamCityBuildConfigs: ({
+ integrationAuthId,
+ appId
+ }: {
+ integrationAuthId: string;
+ appId: string;
+ }) => [{ integrationAuthId, appId }, "integrationAuthTeamCityBranchConfigs"] as const,
+ getIntegrationAuthOctopusDeploySpaces: (integrationAuthId: string) =>
+ [{ integrationAuthId }, "getIntegrationAuthOctopusDeploySpaces"] as const,
+ getIntegrationAuthOctopusDeployScopeValues: ({
+ integrationAuthId,
+ ...params
+ }: TGetIntegrationAuthOctopusDeployScopeValuesDTO) =>
+ [{ integrationAuthId }, "getIntegrationAuthOctopusDeployScopeValues", params] as const
+};
+
+const fetchIntegrationAuthById = async (integrationAuthId: string) => {
+ const { data } = await apiRequest.get<{ integrationAuth: IntegrationAuth }>(
+ `/api/v1/integration-auth/${integrationAuthId}`
+ );
+ return data.integrationAuth;
+};
+
+const fetchIntegrationAuthApps = async ({
+ integrationAuthId,
+ teamId,
+ azureDevOpsOrgName,
+ workspaceSlug
+}: {
+ integrationAuthId: string;
+ teamId?: string;
+ azureDevOpsOrgName?: string;
+ workspaceSlug?: string;
+}) => {
+ const params: Record