Merge branch 'main' into PAM-10

This commit is contained in:
x032205
2025-11-17 10:30:33 -05:00
132 changed files with 3035 additions and 963 deletions

View File

@@ -150,7 +150,8 @@
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
"integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@alloc/quick-lru": {
"version": "5.2.0",
@@ -195,7 +196,6 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -488,7 +488,6 @@
"resolved": "https://registry.npmjs.org/@casl/ability/-/ability-6.7.2.tgz",
"integrity": "sha512-KjKXlcjKbUz8dKw7PY56F7qlfOFgxTU6tnlJ8YrbDyWkJMIlHa6VRWzCD8RU20zbJUC1hExhOFggZjm6tf1mUw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ucast/mongo2js": "^1.3.0"
},
@@ -547,7 +546,6 @@
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
@@ -1325,7 +1323,6 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.1.tgz",
"integrity": "sha512-8dBIHbfsKlCk2jHQ9PoRBg2Z+4TwyE3vZICSnoDlnsHA6SiMlTwfmW6yX0lHsRmWJugkeb92sA0hZdkXJhuz+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.1"
},
@@ -2016,7 +2013,6 @@
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz",
"integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^5.0.0",
"@octokit/graphql": "^8.0.0",
@@ -4412,7 +4408,6 @@
"integrity": "sha512-RnO1SaiCFHn666wNz2QfZEFxvmiNRqhzaMXHXxXXKt+MEP7aajlPxUSMIQpKAaJfverpovEYqjBOXDq6dDcaOQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/utils": "^8.13.0",
"eslint-visitor-keys": "^4.2.0",
@@ -5038,7 +5033,6 @@
"resolved": "https://registry.npmjs.org/@tanstack/react-router/-/react-router-1.95.1.tgz",
"integrity": "sha512-P5x4yNhcdkYsCEoYeGZP8Q9Jlxf0WXJa4G/xvbmM905seZc9FqJqvCSRvX3dWTPOXRABhl4g+8DHqfft0c/AvQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@tanstack/history": "1.95.0",
"@tanstack/react-store": "^0.7.0",
@@ -5250,6 +5244,7 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -5270,6 +5265,7 @@
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
@@ -5280,6 +5276,7 @@
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
@@ -5299,7 +5296,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@testing-library/user-event": {
"version": "14.6.1",
@@ -5307,6 +5305,7 @@
"integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12",
"npm": ">=6"
@@ -5327,7 +5326,8 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -5380,6 +5380,7 @@
"integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/deep-eql": "*",
"assertion-error": "^2.0.1"
@@ -5453,7 +5454,8 @@
"resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
"integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@types/doctrine": {
"version": "0.0.9",
@@ -5589,7 +5591,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.16.tgz",
"integrity": "sha512-oh8AMIC4Y2ciKufU8hnKgs+ufgbA/dhPTACaZPM86AbwX9QwnFtSoPWEeRUj8fge+v6kFt78BXcDhAU1SrrAsw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -5601,7 +5602,6 @@
"integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -5649,7 +5649,6 @@
"integrity": "sha512-QXwAlHlbcAwNlEEMKQS2RCgJsgXrTJdjXT08xEgbPFa2yYQgVjBymxP5DrfrE7X7iodSzd9qBUHUycdyVJTW1w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.34.0",
@@ -5690,7 +5689,6 @@
"integrity": "sha512-vxXJV1hVFx3IXz/oy2sICsJukaBrtDEQSBiV48/YIV5KWjX1dO+bcIr/kCPrW6weKXvsaGKFNlwH0v2eYdRRbA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.34.0",
"@typescript-eslint/types": "8.34.0",
@@ -5962,6 +5960,7 @@
"integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/spy": "3.2.4",
@@ -5979,6 +5978,7 @@
"integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/spy": "3.2.4",
"estree-walker": "^3.0.3",
@@ -6006,6 +6006,7 @@
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -6016,6 +6017,7 @@
"integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tinyrainbow": "^2.0.0"
},
@@ -6029,6 +6031,7 @@
"integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tinyspy": "^4.0.3"
},
@@ -6042,6 +6045,7 @@
"integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/pretty-format": "3.2.4",
"loupe": "^3.1.4",
@@ -6115,7 +6119,6 @@
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -6232,6 +6235,7 @@
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -6280,6 +6284,7 @@
"integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
@@ -6360,6 +6365,7 @@
"integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
@@ -6447,6 +6453,7 @@
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -6457,6 +6464,7 @@
"integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.0.1"
},
@@ -6469,7 +6477,8 @@
"resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/asynckit": {
"version": "0.4.0",
@@ -6519,6 +6528,7 @@
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -6620,6 +6630,7 @@
"integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"open": "^8.0.4"
},
@@ -6856,7 +6867,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -7038,6 +7048,7 @@
"integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"assertion-error": "^2.0.1",
"check-error": "^2.1.1",
@@ -7108,6 +7119,7 @@
"integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 16"
}
@@ -7493,7 +7505,8 @@
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/cssesc": {
"version": "3.0.0",
@@ -7512,8 +7525,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/cva": {
"name": "class-variance-authority",
@@ -7585,7 +7597,6 @@
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -7639,7 +7650,8 @@
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
"dev": true,
"license": "BSD-2-Clause"
"license": "BSD-2-Clause",
"peer": true
},
"node_modules/data-view-buffer": {
"version": "1.0.1",
@@ -7749,6 +7761,7 @@
"integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=6"
}
@@ -7783,6 +7796,7 @@
"integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -7904,7 +7918,8 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/dom-helpers": {
"version": "5.2.1",
@@ -8114,6 +8129,7 @@
"integrity": "sha512-tpxqxncxnpw3c93u8n3VOzACmRFoVmWJqbWXvX/JfKbkhBw1oslgPrUfeSt2psuqyEJFD6N/9lg5i7bsKpoq+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
@@ -8197,7 +8213,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -8239,6 +8254,7 @@
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"debug": "^4.3.4"
},
@@ -8275,7 +8291,6 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -8332,7 +8347,6 @@
"integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0",
"object.assign": "^4.1.2",
@@ -8355,7 +8369,6 @@
"integrity": "sha512-xaX3z4ZZIcFLvh2oUNvcX5oEofXda7giYmuplVxoOg5A7EXJMrUyqRgR+mhDhPK8LZ4PttFOBvCYDbX3sUoUig==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"confusing-browser-globals": "^1.0.10",
"object.assign": "^4.1.2",
@@ -8386,7 +8399,6 @@
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@@ -8486,7 +8498,6 @@
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
@@ -8616,6 +8627,7 @@
"integrity": "sha512-EsTAnj9fLVr/GZleBLFbj/sSuXeWmp1eXIN60ceYnZveqEaUCyW4X+Vh4WTdUhCkW4xutXYqTXCUSyqD4rB75w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"array-includes": "^3.1.8",
"array.prototype.findlast": "^1.2.5",
@@ -8649,7 +8661,6 @@
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -8673,6 +8684,7 @@
"integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"esutils": "^2.0.2"
},
@@ -8686,6 +8698,7 @@
"integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
@@ -8704,6 +8717,7 @@
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"peer": true,
"bin": {
"semver": "bin/semver.js"
}
@@ -8865,6 +8879,7 @@
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"dev": true,
"license": "BSD-2-Clause",
"peer": true,
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
@@ -9895,7 +9910,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.2"
},
@@ -9989,6 +10003,7 @@
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=8"
}
@@ -10265,6 +10280,7 @@
"integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"is-docker": "cli.js"
},
@@ -10586,6 +10602,7 @@
"integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"is-docker": "^2.0.0"
},
@@ -10621,6 +10638,7 @@
"resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz",
"integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==",
"license": "MIT",
"peer": true,
"funding": {
"type": "GitHub Sponsors ❤",
"url": "https://github.com/sponsors/dmonad"
@@ -10632,6 +10650,7 @@
"integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"define-data-property": "^1.1.4",
"es-object-atoms": "^1.0.0",
@@ -10781,6 +10800,7 @@
"integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flat": "^1.3.1",
@@ -10815,7 +10835,8 @@
"resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz",
"integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==",
"dev": true,
"license": "CC0-1.0"
"license": "CC0-1.0",
"peer": true
},
"node_modules/language-tags": {
"version": "1.0.9",
@@ -10823,6 +10844,7 @@
"integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"language-subtag-registry": "^0.3.20"
},
@@ -10855,6 +10877,7 @@
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.102.tgz",
"integrity": "sha512-g70kydI0I1sZU0ChO8mBbhw0oUW/8U0GHzygpvEIx8k+jgOpqnTSb/E+70toYVqHxBhrERD21TwD5QcZJQ40ZQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"isomorphic.js": "^0.2.4"
},
@@ -11179,7 +11202,8 @@
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
"integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/lru-cache": {
"version": "5.1.1",
@@ -11206,6 +11230,7 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -11897,6 +11922,7 @@
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=4"
}
@@ -12245,6 +12271,7 @@
"integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"define-lazy-prop": "^2.0.0",
"is-docker": "^2.1.1",
@@ -12514,6 +12541,7 @@
"integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 14.16"
}
@@ -12665,7 +12693,6 @@
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -12782,6 +12809,7 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -12797,6 +12825,7 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -12809,7 +12838,8 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/prismjs": {
"version": "1.30.0",
@@ -13010,7 +13040,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -13038,7 +13067,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"object-assign": "^4.1.1",
@@ -13130,7 +13158,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -13181,7 +13208,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.56.3.tgz",
"integrity": "sha512-IK18V6GVbab4TAo1/cz3kqajxbDPGofdF0w7VHdCo0Nt8PrPlOZcuuDq9YYIV1BtjcX78x0XsldbQRQnQXWXmw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -13432,6 +13458,7 @@
"integrity": "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ast-types": "^0.16.1",
"esprima": "~4.0.0",
@@ -13449,6 +13476,7 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13465,6 +13493,7 @@
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
@@ -13479,6 +13508,7 @@
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"min-indent": "^1.0.0"
},
@@ -13693,7 +13723,6 @@
"integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -14204,6 +14233,7 @@
"integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
@@ -14219,6 +14249,7 @@
"integrity": "sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1",
@@ -14246,6 +14277,7 @@
"integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"define-properties": "^1.1.3",
"es-abstract": "^1.17.5"
@@ -14466,8 +14498,7 @@
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.2.1",
@@ -14571,6 +14602,7 @@
"integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@@ -14581,6 +14613,7 @@
"integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@@ -14865,7 +14898,6 @@
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -15251,7 +15283,6 @@
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -15669,6 +15700,7 @@
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10.0.0"
},
@@ -15713,7 +15745,6 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.6.1.tgz",
"integrity": "sha512-7r0XPzioN/Q9kXBro/XPnA6kznR73DHq+GXh5ON7ZozRO6aMjbmiBuKste2wslTFkC5d1dw0GooOCepZXJ2SAg==",
"license": "ISC",
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
@@ -15864,7 +15895,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -59,6 +59,8 @@ export type TViewSharedSecretResponse = {
tag: string;
accessType: SecretSharingAccessType;
orgName?: string;
expiresAt?: Date | string;
expiresAfterViews?: number | null;
};
};

View File

@@ -10,7 +10,7 @@ import {
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { useDeleteCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/mutations";
import { TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
import { type TCertificateTemplateV2WithPolicies } from "@app/hooks/api/certificateTemplates/types";
import { CreateTemplateModal } from "./CreateTemplateModal";
import { TemplateList } from "./TemplateList";
@@ -84,7 +84,12 @@ export const CertificateTemplatesV2Tab = () => {
<TemplateList onEditTemplate={handleEditTemplate} onDeleteTemplate={handleDeleteTemplate} />
<CreateTemplateModal isOpen={isCreateModalOpen} onClose={() => setIsCreateModalOpen(false)} />
<CreateTemplateModal
isOpen={isCreateModalOpen}
onClose={() => {
setIsCreateModalOpen(false);
}}
/>
{selectedTemplate && (
<>

View File

@@ -36,13 +36,18 @@ import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSanInclude,
CertSubjectAlternativeNameType,
CertSubjectAttributeInclude,
CertSubjectAttributeType,
SAN_INCLUDE_OPTIONS,
SAN_TYPE_OPTIONS,
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS,
SUBJECT_ATTRIBUTE_TYPE_OPTIONS
SUBJECT_ATTRIBUTE_TYPE_OPTIONS,
TEMPLATE_PRESET_IDS,
type TemplatePresetId
} from "./shared/certificate-constants";
import { CERTIFICATE_TEMPLATE_PRESETS } from "./shared/template-presets";
import { KeyUsagesSection, TemplateFormData, templateSchema } from "./shared";
export type FormData = TemplateFormData;
@@ -91,7 +96,7 @@ const SIGNATURE_ALGORITHMS = [
"SHA256-ECDSA",
"SHA384-ECDSA",
"SHA512-ECDSA"
];
] as const;
const KEY_ALGORITHMS = [
"RSA-2048",
@@ -100,7 +105,7 @@ const KEY_ALGORITHMS = [
"ECDSA-P256",
"ECDSA-P384",
"ECDSA-P521"
];
] as const;
export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create" }: Props) => {
const { currentProject } = useProject();
@@ -117,7 +122,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
subj.allowed.forEach((allowedValue) => {
attributes.push({
type: subj.type as CertSubjectAttributeType,
include: "optional",
include: CertSubjectAttributeInclude.OPTIONAL,
value: [allowedValue]
});
});
@@ -126,7 +131,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
subj.denied.forEach((deniedValue) => {
attributes.push({
type: subj.type as CertSubjectAttributeType,
include: "prohibit",
include: CertSubjectAttributeInclude.PROHIBIT,
value: [deniedValue]
});
});
@@ -141,7 +146,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
san.required.forEach((requiredValue) => {
subjectAlternativeNames.push({
type: san.type as CertSubjectAlternativeNameType,
include: "mandatory",
include: CertSanInclude.MANDATORY,
value: [requiredValue]
});
});
@@ -150,7 +155,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
san.allowed.forEach((allowedValue) => {
subjectAlternativeNames.push({
type: san.type as CertSubjectAlternativeNameType,
include: "optional",
include: CertSanInclude.OPTIONAL,
value: [allowedValue]
});
});
@@ -159,7 +164,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
san.denied.forEach((deniedValue) => {
subjectAlternativeNames.push({
type: san.type as CertSubjectAlternativeNameType,
include: "prohibit",
include: CertSanInclude.PROHIBIT,
value: [deniedValue]
});
});
@@ -219,6 +224,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
};
return {
preset: TEMPLATE_PRESET_IDS.CUSTOM,
name: templateData.name || "",
description: templateData.description || "",
attributes,
@@ -231,25 +237,30 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
};
};
const getDefaultValues = (): FormData => ({
name: "",
description: "",
attributes: [],
keyUsages: { requiredUsages: [], optionalUsages: [] },
extendedKeyUsages: { requiredUsages: [], optionalUsages: [] },
subjectAlternativeNames: [],
validity: {
maxDuration: { value: 365, unit: CertDurationUnit.DAYS }
},
signatureAlgorithm: {
allowedAlgorithms: []
},
keyAlgorithm: {
allowedKeyTypes: []
}
});
const getDefaultValues = (): FormData & { preset: TemplatePresetId } => {
return {
preset: TEMPLATE_PRESET_IDS.CUSTOM,
name: "",
description: "",
attributes: [],
keyUsages: { requiredUsages: [], optionalUsages: [] },
extendedKeyUsages: { requiredUsages: [], optionalUsages: [] },
subjectAlternativeNames: [],
validity: {
maxDuration: { value: 365, unit: CertDurationUnit.DAYS }
},
signatureAlgorithm: {
allowedAlgorithms: []
},
keyAlgorithm: {
allowedKeyTypes: []
}
};
};
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<FormData>({
const { control, handleSubmit, reset, watch, setValue, formState } = useForm<
FormData & { preset: TemplatePresetId }
>({
resolver: zodResolver(templateSchema),
defaultValues: getDefaultValues(),
mode: "onChange",
@@ -260,7 +271,7 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
useEffect(() => {
if (isEdit && template) {
const convertedData = convertApiToUiFormat(template);
reset(convertedData);
reset({ ...convertedData, preset: TEMPLATE_PRESET_IDS.CUSTOM });
} else if (!isEdit) {
reset(getDefaultValues());
}
@@ -273,6 +284,37 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
requiredUsages: [],
optionalUsages: []
};
const watchedPreset = watch("preset") || TEMPLATE_PRESET_IDS.CUSTOM;
const handlePresetChange = (presetId: TemplatePresetId) => {
setValue("preset", presetId);
if (presetId === TEMPLATE_PRESET_IDS.CUSTOM) {
return;
}
const selectedPreset = CERTIFICATE_TEMPLATE_PRESETS.find((p) => p.id === presetId);
if (selectedPreset) {
if (selectedPreset.formData.keyUsages) {
setValue("keyUsages", selectedPreset.formData.keyUsages);
}
if (selectedPreset.formData.extendedKeyUsages) {
setValue("extendedKeyUsages", selectedPreset.formData.extendedKeyUsages);
}
if (selectedPreset.formData.attributes) {
setValue("attributes", selectedPreset.formData.attributes);
}
if (selectedPreset.formData.subjectAlternativeNames) {
setValue("subjectAlternativeNames", selectedPreset.formData.subjectAlternativeNames);
}
if (selectedPreset.formData.signatureAlgorithm) {
setValue("signatureAlgorithm", selectedPreset.formData.signatureAlgorithm);
}
if (selectedPreset.formData.keyAlgorithm) {
setValue("keyAlgorithm", selectedPreset.formData.keyAlgorithm);
}
}
};
const consolidateByType = <
T extends { type: string; allowed?: string[]; required?: string[]; denied?: string[] }
@@ -309,9 +351,17 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
data.attributes?.map((attr) => {
const result: AttributeTransform = { type: attr.type };
if (attr.include === "optional" && attr.value && attr.value.length > 0) {
if (
attr.include === CertSubjectAttributeInclude.OPTIONAL &&
attr.value &&
attr.value.length > 0
) {
result.allowed = attr.value;
} else if (attr.include === "prohibit" && attr.value && attr.value.length > 0) {
} else if (
attr.include === CertSubjectAttributeInclude.PROHIBIT &&
attr.value &&
attr.value.length > 0
) {
result.denied = attr.value;
}
@@ -322,11 +372,11 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
data.subjectAlternativeNames?.map((san) => {
const result: SanTransform = { type: san.type };
if (san.include === "mandatory" && san.value && san.value.length > 0) {
if (san.include === CertSanInclude.MANDATORY && san.value && san.value.length > 0) {
result.required = san.value;
} else if (san.include === "optional" && san.value && san.value.length > 0) {
} else if (san.include === CertSanInclude.OPTIONAL && san.value && san.value.length > 0) {
result.allowed = san.value;
} else if (san.include === "prohibit" && san.value && san.value.length > 0) {
} else if (san.include === CertSanInclude.PROHIBIT && san.value && san.value.length > 0) {
result.denied = san.value;
}
@@ -464,11 +514,19 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value: ["*"]
};
setValue("attributes", [...watchedAttributes, newAttribute]);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
};
const removeAttribute = (index: number) => {
const newAttributes = watchedAttributes.filter((_, i) => i !== index);
setValue("attributes", newAttributes);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
};
const addSan = () => {
@@ -478,11 +536,19 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value: ["*"]
};
setValue("subjectAlternativeNames", [...watchedSans, newSan]);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
};
const removeSan = (index: number) => {
const newSans = watchedSans.filter((_, i) => i !== index);
setValue("subjectAlternativeNames", newSans);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
};
const handleKeyUsagesChange = (usages: {
@@ -493,6 +559,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
requiredUsages: usages.requiredUsages,
optionalUsages: usages.optionalUsages
});
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
};
const handleExtendedKeyUsagesChange = (usages: {
@@ -503,6 +573,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
requiredUsages: usages.requiredUsages,
optionalUsages: usages.optionalUsages
});
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
};
return (
@@ -555,6 +629,33 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
</FormControl>
)}
/>
<Controller
control={control}
name="preset"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Template Preset"
isError={Boolean(error)}
errorText={error?.message}
>
<Select
value={field.value}
onValueChange={handlePresetChange}
className="w-full"
position="popper"
dropdownContainerClassName="max-w-none"
>
<SelectItem value={TEMPLATE_PRESET_IDS.CUSTOM}>Custom</SelectItem>
{CERTIFICATE_TEMPLATE_PRESETS.map((preset) => (
<SelectItem key={preset.id} value={preset.id}>
{preset.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
</div>
<AccordionItem value="attributes">
<AccordionTrigger>Subject Attributes</AccordionTrigger>
@@ -595,6 +696,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
type: value as CertSubjectAttributeType
};
setValue("attributes", newAttributes);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
}}
className="w-48"
>
@@ -615,6 +720,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value as (typeof SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS)[number]
};
setValue("attributes", newAttributes);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
}}
className="w-32"
>
@@ -635,6 +744,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value: e.target.value.trim() ? [e.target.value.trim()] : []
};
setValue("attributes", newAttributes);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
}}
className={`flex-1 ${
attr.value && attr.value.length > 0 && attr.value[0] === ""
@@ -701,6 +814,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
type: value as CertSubjectAlternativeNameType
};
setValue("subjectAlternativeNames", newSans);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
}}
className="w-36"
>
@@ -720,6 +837,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
include: value as (typeof SAN_INCLUDE_OPTIONS)[number]
};
setValue("subjectAlternativeNames", newSans);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
}}
className="w-32"
>
@@ -740,6 +861,10 @@ export const CreateTemplateModal = ({ isOpen, onClose, template, mode = "create"
value: e.target.value.trim() ? [e.target.value.trim()] : []
};
setValue("subjectAlternativeNames", newSans);
if (watchedPreset !== TEMPLATE_PRESET_IDS.CUSTOM) {
setValue("preset", TEMPLATE_PRESET_IDS.CUSTOM);
}
}}
className={`flex-1 ${
san.value && san.value.length > 0 && san.value[0] === ""

View File

@@ -150,8 +150,19 @@ export const SUBJECT_ATTRIBUTE_TYPE_OPTIONS = Object.values(CertSubjectAttribute
export const ATTRIBUTE_RULE_OPTIONS = Object.values(CertAttributeRule);
export const SAN_EFFECT_OPTIONS = Object.values(CertSanEffect);
export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = ["optional", "prohibit"] as const;
export const SAN_INCLUDE_OPTIONS = ["mandatory", "optional", "prohibit"] as const;
export enum CertSubjectAttributeInclude {
OPTIONAL = "optional",
PROHIBIT = "prohibit"
}
export enum CertSanInclude {
MANDATORY = "mandatory",
OPTIONAL = "optional",
PROHIBIT = "prohibit"
}
export const SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS = Object.values(CertSubjectAttributeInclude);
export const SAN_INCLUDE_OPTIONS = Object.values(CertSanInclude);
export const USAGE_STATES = {
REQUIRED: "required",
@@ -239,3 +250,27 @@ export const mapTemplateKeyAlgorithmToApi = (templateFormat: string): string =>
};
return mapping[templateFormat] || templateFormat;
};
export const TEMPLATE_PRESET_IDS = {
CUSTOM: "custom",
TLS_SERVER: "tls-server",
TLS_CLIENT: "tls-client",
CODE_SIGNING: "code-signing",
DEVICE: "device",
USER: "user",
EMAIL_PROTECTION: "email-protection",
DUAL_PURPOSE_SERVER: "dual-purpose-server"
} as const;
export type TemplatePresetId = (typeof TEMPLATE_PRESET_IDS)[keyof typeof TEMPLATE_PRESET_IDS];
export const ALGORITHM_FAMILIES = {
ECDSA: {
signature: ["SHA256-ECDSA", "SHA384-ECDSA", "SHA512-ECDSA"] as const,
key: ["ECDSA-P256", "ECDSA-P384", "ECDSA-P521"] as const
},
RSA: {
signature: ["SHA256-RSA", "SHA384-RSA", "SHA512-RSA"] as const,
key: ["RSA-2048", "RSA-3072", "RSA-4096"] as const
}
} as const;

View File

@@ -4,21 +4,22 @@ import {
CertDurationUnit,
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSanInclude,
CertSubjectAlternativeNameType,
CertSubjectAttributeInclude,
CertSubjectAttributeType,
SAN_INCLUDE_OPTIONS,
SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS
TEMPLATE_PRESET_IDS
} from "./certificate-constants";
export const uiAttributeSchema = z.object({
type: z.nativeEnum(CertSubjectAttributeType),
include: z.enum(SUBJECT_ATTRIBUTE_INCLUDE_OPTIONS),
include: z.nativeEnum(CertSubjectAttributeInclude),
value: z.array(z.string().min(1, "Value cannot be empty"))
});
export const uiSanSchema = z.object({
type: z.nativeEnum(CertSubjectAlternativeNameType),
include: z.enum(SAN_INCLUDE_OPTIONS),
include: z.nativeEnum(CertSanInclude),
value: z.array(z.string().min(1, "Value cannot be empty"))
});
@@ -51,7 +52,21 @@ export const uiKeyAlgorithmSchema = z.object({
.min(1, "At least one key type must be selected")
});
export const uiPresetSchema = z
.enum([
TEMPLATE_PRESET_IDS.CUSTOM,
TEMPLATE_PRESET_IDS.TLS_SERVER,
TEMPLATE_PRESET_IDS.TLS_CLIENT,
TEMPLATE_PRESET_IDS.CODE_SIGNING,
TEMPLATE_PRESET_IDS.DEVICE,
TEMPLATE_PRESET_IDS.USER,
TEMPLATE_PRESET_IDS.EMAIL_PROTECTION,
TEMPLATE_PRESET_IDS.DUAL_PURPOSE_SERVER
])
.default(TEMPLATE_PRESET_IDS.CUSTOM);
export const templateSchema = z.object({
preset: uiPresetSchema,
name: z
.string()
.trim()

View File

@@ -0,0 +1,385 @@
import {
ALGORITHM_FAMILIES,
CertDurationUnit,
CertExtendedKeyUsageType,
CertKeyUsageType,
CertSanInclude,
CertSubjectAlternativeNameType,
CertSubjectAttributeInclude,
CertSubjectAttributeType,
TEMPLATE_PRESET_IDS,
type TemplatePresetId
} from "./certificate-constants";
import { TemplateFormData } from ".";
export interface CertificateTemplatePreset {
readonly id: TemplatePresetId;
readonly name: string;
readonly description: string;
readonly useCase: string;
readonly formData: Omit<TemplateFormData, "preset">;
}
export const CERTIFICATE_TEMPLATE_PRESETS: CertificateTemplatePreset[] = [
{
id: TEMPLATE_PRESET_IDS.TLS_SERVER,
name: "TLS Server Certificate",
description: "Standard TLS/SSL server certificate for HTTPS services and API endpoints.",
useCase: "Web servers, API endpoints, HTTPS services",
formData: {
name: "TLS Server Certificate",
description: "Standard TLS/SSL server certificate for HTTPS services and API endpoints.",
keyUsages: {
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_ENCIPHERMENT]
},
extendedKeyUsages: {
requiredUsages: [CertExtendedKeyUsageType.SERVER_AUTH],
optionalUsages: [CertExtendedKeyUsageType.SERVER_AUTH]
},
validity: {
maxDuration: {
value: 365,
unit: CertDurationUnit.DAYS
}
},
attributes: [
{
type: CertSubjectAttributeType.COMMON_NAME,
include: CertSubjectAttributeInclude.OPTIONAL,
value: ["*"]
}
],
subjectAlternativeNames: [
{
type: CertSubjectAlternativeNameType.DNS_NAME,
include: CertSanInclude.OPTIONAL,
value: ["*"]
},
{
type: CertSubjectAlternativeNameType.IP_ADDRESS,
include: CertSanInclude.OPTIONAL,
value: ["*"]
}
],
signatureAlgorithm: {
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
},
keyAlgorithm: {
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
}
}
},
{
id: TEMPLATE_PRESET_IDS.TLS_CLIENT,
name: "TLS Client Certificate",
description: "Client certificate for mutual TLS authentication and API access.",
useCase: "Client authentication, mTLS, API authentication",
formData: {
name: "TLS Client Certificate",
description: "Client certificate for mutual TLS authentication and API access.",
keyUsages: {
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_AGREEMENT]
},
extendedKeyUsages: {
requiredUsages: [CertExtendedKeyUsageType.CLIENT_AUTH],
optionalUsages: [CertExtendedKeyUsageType.CLIENT_AUTH]
},
validity: {
maxDuration: {
value: 365,
unit: CertDurationUnit.DAYS
}
},
attributes: [
{
type: CertSubjectAttributeType.COMMON_NAME,
include: CertSubjectAttributeInclude.OPTIONAL,
value: ["*"]
}
],
subjectAlternativeNames: [
{
type: CertSubjectAlternativeNameType.EMAIL,
include: CertSanInclude.OPTIONAL,
value: ["*"]
},
{
type: CertSubjectAlternativeNameType.DNS_NAME,
include: CertSanInclude.OPTIONAL,
value: ["*"]
}
],
signatureAlgorithm: {
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
},
keyAlgorithm: {
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
}
}
},
{
id: TEMPLATE_PRESET_IDS.CODE_SIGNING,
name: "Code Signing Certificate",
description:
"Certificate for signing software, executables, and packages. Requires hardware security modules.",
useCase: "Software signing, executable authentication, package validation",
formData: {
name: "Code Signing Certificate",
description:
"Certificate for signing software, executables, and packages. Requires hardware security modules.",
keyUsages: {
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.NON_REPUDIATION],
optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.NON_REPUDIATION]
},
extendedKeyUsages: {
requiredUsages: [CertExtendedKeyUsageType.CODE_SIGNING],
optionalUsages: [
CertExtendedKeyUsageType.CODE_SIGNING,
CertExtendedKeyUsageType.TIME_STAMPING
]
},
validity: {
maxDuration: {
value: 365,
unit: CertDurationUnit.DAYS
}
},
attributes: [
{
type: CertSubjectAttributeType.COMMON_NAME,
include: CertSubjectAttributeInclude.OPTIONAL,
value: ["*"]
}
],
subjectAlternativeNames: [
{
type: CertSubjectAlternativeNameType.EMAIL,
include: CertSanInclude.OPTIONAL,
value: ["*"]
}
],
signatureAlgorithm: {
allowedAlgorithms: [...ALGORITHM_FAMILIES.RSA.signature]
},
keyAlgorithm: {
allowedKeyTypes: [...ALGORITHM_FAMILIES.RSA.key]
}
}
},
{
id: TEMPLATE_PRESET_IDS.DEVICE,
name: "Device Certificate",
description:
"Certificate for IoT devices and embedded systems authentication. IEEE 802.1AR compliant.",
useCase: "Device authentication, IoT security, embedded systems",
formData: {
name: "Device Certificate",
description:
"Certificate for IoT devices and embedded systems authentication. IEEE 802.1AR compliant.",
keyUsages: {
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
optionalUsages: [CertKeyUsageType.DIGITAL_SIGNATURE, CertKeyUsageType.KEY_AGREEMENT]
},
extendedKeyUsages: {
requiredUsages: [CertExtendedKeyUsageType.CLIENT_AUTH],
optionalUsages: [CertExtendedKeyUsageType.CLIENT_AUTH, CertExtendedKeyUsageType.SERVER_AUTH]
},
validity: {
maxDuration: {
value: 365,
unit: CertDurationUnit.DAYS
}
},
attributes: [
{
type: CertSubjectAttributeType.COMMON_NAME,
include: CertSubjectAttributeInclude.OPTIONAL,
value: ["*"]
}
],
subjectAlternativeNames: [
{
type: CertSubjectAlternativeNameType.DNS_NAME,
include: CertSanInclude.OPTIONAL,
value: ["*"]
},
{
type: CertSubjectAlternativeNameType.IP_ADDRESS,
include: CertSanInclude.OPTIONAL,
value: ["*"]
}
],
signatureAlgorithm: {
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
},
keyAlgorithm: {
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
}
}
},
{
id: TEMPLATE_PRESET_IDS.USER,
name: "User Certificate",
description:
"Personal certificate for user authentication and email signing. FIPS 201 PIV compliant.",
useCase: "Personal authentication, smart cards, email protection",
formData: {
name: "User Certificate",
description:
"Personal certificate for user authentication and email signing. FIPS 201 PIV compliant.",
keyUsages: {
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
optionalUsages: [
CertKeyUsageType.DIGITAL_SIGNATURE,
CertKeyUsageType.KEY_ENCIPHERMENT,
CertKeyUsageType.KEY_AGREEMENT
]
},
extendedKeyUsages: {
requiredUsages: [
CertExtendedKeyUsageType.CLIENT_AUTH,
CertExtendedKeyUsageType.EMAIL_PROTECTION
],
optionalUsages: [
CertExtendedKeyUsageType.CLIENT_AUTH,
CertExtendedKeyUsageType.EMAIL_PROTECTION
]
},
validity: {
maxDuration: {
value: 365,
unit: CertDurationUnit.DAYS
}
},
attributes: [
{
type: CertSubjectAttributeType.COMMON_NAME,
include: CertSubjectAttributeInclude.OPTIONAL,
value: ["*"]
}
],
subjectAlternativeNames: [
{
type: CertSubjectAlternativeNameType.EMAIL,
include: CertSanInclude.OPTIONAL,
value: ["*"]
}
],
signatureAlgorithm: {
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
},
keyAlgorithm: {
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
}
}
},
{
id: TEMPLATE_PRESET_IDS.EMAIL_PROTECTION,
name: "Email Protection Certificate",
description: "S/MIME certificate for email encryption and digital signing. RFC 8550 compliant.",
useCase: "Email encryption, digital signing, secure messaging",
formData: {
name: "Email Protection Certificate",
description:
"S/MIME certificate for email encryption and digital signing. RFC 8550 compliant.",
keyUsages: {
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
optionalUsages: [
CertKeyUsageType.DIGITAL_SIGNATURE,
CertKeyUsageType.KEY_ENCIPHERMENT,
CertKeyUsageType.KEY_AGREEMENT
]
},
extendedKeyUsages: {
requiredUsages: [CertExtendedKeyUsageType.EMAIL_PROTECTION],
optionalUsages: [CertExtendedKeyUsageType.EMAIL_PROTECTION]
},
validity: {
maxDuration: {
value: 365,
unit: CertDurationUnit.DAYS
}
},
attributes: [
{
type: CertSubjectAttributeType.COMMON_NAME,
include: CertSubjectAttributeInclude.OPTIONAL,
value: ["*"]
}
],
subjectAlternativeNames: [
{
type: CertSubjectAlternativeNameType.EMAIL,
include: CertSanInclude.OPTIONAL,
value: ["*"]
}
],
signatureAlgorithm: {
allowedAlgorithms: [...ALGORITHM_FAMILIES.RSA.signature]
},
keyAlgorithm: {
allowedKeyTypes: [...ALGORITHM_FAMILIES.RSA.key]
}
}
},
{
id: TEMPLATE_PRESET_IDS.DUAL_PURPOSE_SERVER,
name: "Dual-Purpose Server Certificate",
description:
"Certificate for services requiring both server and client authentication capabilities",
useCase: "Microservices, service mesh, dual authentication",
formData: {
name: "Dual-Purpose Server Certificate",
description:
"Certificate for services requiring both server and client authentication capabilities",
keyUsages: {
requiredUsages: [CertKeyUsageType.DIGITAL_SIGNATURE],
optionalUsages: [
CertKeyUsageType.DIGITAL_SIGNATURE,
CertKeyUsageType.KEY_ENCIPHERMENT,
CertKeyUsageType.KEY_AGREEMENT
]
},
extendedKeyUsages: {
requiredUsages: [
CertExtendedKeyUsageType.SERVER_AUTH,
CertExtendedKeyUsageType.CLIENT_AUTH
],
optionalUsages: [CertExtendedKeyUsageType.SERVER_AUTH, CertExtendedKeyUsageType.CLIENT_AUTH]
},
validity: {
maxDuration: {
value: 365,
unit: CertDurationUnit.DAYS
}
},
attributes: [
{
type: CertSubjectAttributeType.COMMON_NAME,
include: CertSubjectAttributeInclude.OPTIONAL,
value: ["*"]
}
],
subjectAlternativeNames: [
{
type: CertSubjectAlternativeNameType.DNS_NAME,
include: CertSanInclude.OPTIONAL,
value: ["*"]
},
{
type: CertSubjectAlternativeNameType.IP_ADDRESS,
include: CertSanInclude.OPTIONAL,
value: ["*"]
}
],
signatureAlgorithm: {
allowedAlgorithms: [...ALGORITHM_FAMILIES.ECDSA.signature]
},
keyAlgorithm: {
allowedKeyTypes: [...ALGORITHM_FAMILIES.ECDSA.key]
}
}
}
];

View File

@@ -4,8 +4,10 @@ import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tab } from "@headlessui/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { z } from "zod";
import { OrgPermissionCan } from "@app/components/permissions";
import {
Button,
FormControl,
@@ -18,8 +20,11 @@ import {
TextArea,
Tooltip
} from "@app/components/v2";
import { OrgPermissionSubjects, useSubscription } from "@app/context";
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
import { APP_CONNECTION_MAP, getAppConnectionMethodDetails } from "@app/helpers/appConnections";
import { DistinguishedNameRegex, UserPrincipalNameRegex } from "@app/helpers/string";
import { gatewaysQueryKeys } from "@app/hooks/api";
import {
LdapConnectionMethod,
LdapConnectionProvider,
@@ -84,6 +89,7 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
defaultValues: appConnection ?? {
app: AppConnection.LDAP,
method: LdapConnectionMethod.SimpleBind,
gatewayId: null,
credentials: {
provider: LdapConnectionProvider.ActiveDirectory,
url: "",
@@ -104,6 +110,8 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
const selectedProvider = watch("credentials.provider");
const sslEnabled = watch("credentials.url")?.startsWith("ldaps://") ?? false;
const { subscription } = useSubscription();
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
return (
<FormProvider {...form}>
@@ -114,6 +122,57 @@ export const LdapConnectionForm = ({ appConnection, onSubmit }: Props) => {
}}
>
{!isUpdate && <GenericAppConnectionsFields />}
{subscription.gateway && (
<OrgPermissionCan
I={OrgGatewayPermissionActions.AttachGateways}
a={OrgPermissionSubjects.Gateway}
>
{(isAllowed) => (
<Controller
control={control}
name="gatewayId"
defaultValue=""
render={({ field: { value, onChange }, fieldState: { error } }) => (
<FormControl
isError={Boolean(error?.message)}
errorText={error?.message}
label="Gateway"
>
<Tooltip
isDisabled={isAllowed}
content="Restricted access. You don't have permission to attach gateways to resources."
>
<div>
<Select
isDisabled={!isAllowed}
value={value as string}
onValueChange={onChange}
className="w-full border border-mineshaft-500"
dropdownContainerClassName="max-w-none"
isLoading={isGatewaysLoading}
placeholder="Default: Internet Gateway"
position="popper"
>
<SelectItem
value={null as unknown as string}
onClick={() => onChange(undefined)}
>
Internet Gateway
</SelectItem>
{gateways?.map((el) => (
<SelectItem value={el.id} key={el.id}>
{el.name}
</SelectItem>
))}
</Select>
</div>
</Tooltip>
</FormControl>
)}
/>
)}
</OrgPermissionCan>
)}
<div className="grid grid-cols-2 items-center gap-2">
<Controller
name="method"

View File

@@ -301,7 +301,7 @@ export const GatewayCliDeploymentMethod = () => {
<>
<FormLabel
label="Identity Token"
tooltipText="The identity token that your relay will use for authentication."
tooltipText="The identity token that your gateway will use for authentication."
className="mt-4"
/>
<Input

View File

@@ -0,0 +1,389 @@
import { useMemo, useState } from "react";
import { SingleValue } from "react-select";
import { faCopy, faQuestionCircle, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNavigate } from "@tanstack/react-router";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormLabel,
IconButton,
Input,
ModalClose,
Tooltip
} from "@app/components/v2";
import { ROUTE_PATHS } from "@app/const/routes";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import {
useAddIdentityTokenAuth,
useCreateTokenIdentityTokenAuth,
useGetIdentityMembershipOrgs,
useGetIdentityTokenAuth,
useGetRelays
} from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
import { RelayOption } from "./RelayOption";
const baseFormSchema = z.object({
name: slugSchema({ field: "name" }),
relay: z
.object(
{
id: z.string(),
name: z.string()
},
{ required_error: "Relay is required" }
)
.nullable()
.refine((val) => val !== null, { message: "Relay is required" })
});
const formSchemaWithIdentity = baseFormSchema.extend({
identity: z
.object(
{
id: z.string(),
name: z.string()
},
{ required_error: "Identity is required" }
)
.nullable()
.refine((val) => val !== null, { message: "Identity is required" })
});
const formSchemaWithToken = baseFormSchema.extend({
identityToken: z.string().min(1, "Token is required")
});
export const GatewayCliSystemdDeploymentMethod = () => {
const { protocol, hostname, port } = window.location;
const portSuffix = port && port !== "80" ? `:${port}` : "";
const siteURL = `${protocol}//${hostname}${portSuffix}`;
const navigate = useNavigate({
from: ROUTE_PATHS.Organization.NetworkingPage.path
});
const [autogenerateToken, setAutogenerateToken] = useState(true);
const [step, setStep] = useState<"form" | "command">("form");
const [name, setName] = useState("");
const [relay, setRelay] = useState<null | {
id: string;
name: string;
}>({ id: "_auto", name: "Auto Select Relay" });
const [identity, setIdentity] = useState<null | {
id: string;
name: string;
}>(null);
const [identityToken, setIdentityToken] = useState("");
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
const errors = useMemo(() => {
const errorMap: Record<string, string | undefined> = {};
formErrors.forEach((issue) => {
if (issue.path.length > 0) {
errorMap[String(issue.path[0])] = issue.message;
}
});
return errorMap;
}, [formErrors]);
const { data: relays, isPending: isRelaysLoading } = useGetRelays();
const { currentOrg } = useOrganization();
const organizationId = currentOrg?.id || "";
const { permission } = useOrgPermission();
const canCreateToken = permission.can(
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
);
const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } =
useGetIdentityMembershipOrgs({
organizationId,
limit: 20000
});
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || [];
const { mutateAsync: createToken, isPending: isCreatingToken } =
useCreateTokenIdentityTokenAuth();
const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } =
useAddIdentityTokenAuth();
const { refetch } = useGetIdentityTokenAuth(identity?.id ?? "");
const handleGenerateCommand = async () => {
setFormErrors([]);
if (canCreateToken && autogenerateToken) {
const validation = formSchemaWithIdentity.safeParse({
name,
relay,
identity
});
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
const validatedIdentity = validation.data.identity;
try {
const { data: identityTokenAuth } = await refetch();
if (!identityTokenAuth) {
await addIdentityTokenAuth({
identityId: validatedIdentity.id,
organizationId,
accessTokenTTL: 2592000,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0,
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
createNotification({
text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.",
type: "warning"
});
}
const token = await createToken({
identityId: validatedIdentity.id,
name: `gateway token for ${name} (autogenerated)`
});
setIdentityToken(token.accessToken);
createNotification({
text: "Automatically generated a token for the selected identity.",
type: "info"
});
setStep("command");
} catch {
setIdentityToken("");
}
} else {
const validation = formSchemaWithToken.safeParse({
name,
relay,
identityToken
});
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
setStep("command");
}
};
const installCommand = useMemo(() => {
const relayPart = relay?.id !== "_auto" ? ` --relay=${relay?.name || ""}` : "";
return `sudo infisical gateway systemd install --name=${name}${relayPart} --domain=${siteURL} --token=${identityToken}`;
}, [name, relay, identityToken, siteURL]);
const startServiceCommand = "sudo systemctl start infisical-gateway";
if (step === "command") {
return (
<>
<FormLabel label="Installation Command" />
<div className="flex gap-2">
<Input value={installCommand} isDisabled />
<IconButton
ariaLabel="copy install command"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(installCommand);
createNotification({
text: "Installation command copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</div>
<FormLabel label="Start the Gateway Service" className="mt-4" />
<div className="mb-2 flex gap-2">
<Input value={startServiceCommand} isDisabled />
<IconButton
ariaLabel="copy start service command"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(startServiceCommand);
createNotification({
text: "Start service command copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</div>
<a
href="https://infisical.com/docs/cli/overview"
target="_blank"
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
rel="noreferrer"
>
<span>Install the Infisical CLI</span>
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
</a>
<div className="mt-6 flex items-center">
<ModalClose asChild>
<Button className="mr-4" size="sm" colorSchema="secondary">
Done
</Button>
</ModalClose>
</div>
</>
);
}
return (
<>
<FormLabel label="Name" tooltipText="The name for your gateway." />
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter gateway name..."
isError={Boolean(errors.name)}
/>
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
<FormLabel label="Relay" tooltipText="The relay to use with your gateway." className="mt-4" />
<FilterableSelect
value={relay}
onChange={(newValue) => {
if ((newValue as SingleValue<{ id: string }>)?.id === "_create") {
navigate({
search: (prev) => ({ ...prev, selectedTab: "relays", action: "deploy-relay" })
});
return;
}
setRelay(newValue as SingleValue<{ id: string; name: string }>);
}}
isLoading={isRelaysLoading}
options={[
{
id: "_auto",
name: "Auto Select Relay"
},
{
id: "_create",
name: "Deploy New Relay"
},
...(relays || [])
]}
placeholder="Select relay..."
getOptionLabel={(option) => option.name}
getOptionValue={(option) => option.id}
components={{ Option: RelayOption }}
/>
{errors.relay && <p className="mt-1 text-sm text-red">{errors.relay}</p>}
{canCreateToken && autogenerateToken ? (
<>
<FormLabel
label="Identity"
tooltipText="The identity that your gateway will use for authentication."
className="mt-4"
/>
<FilterableSelect
value={identity}
onChange={(e) =>
setIdentity(
e as SingleValue<{
id: string;
name: string;
}>
)
}
isLoading={isIdentitiesLoading}
placeholder="Select identity..."
options={identityMembershipOrgs.map((membership) => membership.identity)}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
{errors.identity && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
</>
) : (
<>
<FormLabel
label="Identity Token"
tooltipText="The identity token that your gateway will use for authentication."
className="mt-4"
/>
<Input
value={identityToken}
onChange={(e) => setIdentityToken(e.target.value)}
placeholder="Enter identity token..."
isError={Boolean(errors.identityToken)}
/>
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
</>
)}
{canCreateToken && (
<div className="mt-2">
<Checkbox
isChecked={autogenerateToken}
onCheckedChange={(e) => {
setAutogenerateToken(Boolean(e));
}}
id="autogenerate-token"
className="mr-2"
>
<div className="flex items-center">
<span>Automatically enable token auth and generate a token for identity</span>
<Tooltip
className="max-w-md"
content={
<>
Token authentication will be automatically enabled for the selected identity if
it isn&apos;t already configured. By default, it will be configured to allow all
IP addresses with a token TTL of 30 days. You can manage these settings in
Access Control.
<br />
<br />A token will automatically be generated to be used with the CLI command.
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
</Tooltip>
</div>
</Checkbox>
</div>
)}
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
colorSchema="secondary"
onClick={handleGenerateCommand}
isLoading={isCreatingToken || isAddingTokenAuth}
>
Continue
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};

View File

@@ -4,6 +4,7 @@ import { Modal, ModalContent } from "@app/components/v2";
import { GatewayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/GatewayTab/components/GatewayDeploymentMethodSelect";
import { GatewayCliDeploymentMethod } from "./GatewayCliDeploymentMethod";
import { GatewayCliSystemdDeploymentMethod } from "./GatewayCliSystemdDeploymentMethod";
type Props = {
isOpen: boolean;
@@ -11,7 +12,8 @@ type Props = {
};
export const GatewayDeploymentInfoMap = {
cli: { name: "CLI", image: "SSH.png", component: GatewayCliDeploymentMethod }
cli: { name: "CLI", image: "SSH.png", component: GatewayCliDeploymentMethod },
systemd: { name: "CLI (systemd)", image: "SSH.png", component: GatewayCliSystemdDeploymentMethod }
} as const;
export type GatewayDeploymentMethod = keyof typeof GatewayDeploymentInfoMap;

View File

@@ -156,15 +156,6 @@ export const RelayCliDeploymentMethod = () => {
}
};
const handleIdentityChange = (
selectedIdentity: SingleValue<{
id: string;
name: string;
}>
) => {
setIdentity(selectedIdentity);
};
const command = useMemo(() => {
return `infisical relay start --name=${name} --domain=${siteURL} --host=${host} --token=${identityToken}`;
}, [name, siteURL, host, identityToken]);
@@ -245,7 +236,7 @@ export const RelayCliDeploymentMethod = () => {
<FilterableSelect
value={identity}
onChange={(e) =>
handleIdentityChange(
setIdentity(
e as SingleValue<{
id: string;
name: string;

View File

@@ -0,0 +1,362 @@
import { useMemo, useState } from "react";
import { SingleValue } from "react-select";
import { faCopy, faQuestionCircle, faUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import {
Button,
Checkbox,
FilterableSelect,
FormLabel,
IconButton,
Input,
ModalClose,
Tooltip
} from "@app/components/v2";
import {
OrgPermissionIdentityActions,
OrgPermissionSubjects,
useOrganization,
useOrgPermission
} from "@app/context";
import {
useAddIdentityTokenAuth,
useCreateTokenIdentityTokenAuth,
useGetIdentityMembershipOrgs,
useGetIdentityTokenAuth
} from "@app/hooks/api";
import { slugSchema } from "@app/lib/schemas";
const baseFormSchema = z.object({
name: slugSchema({ field: "name" }),
host: z.string().min(1, "Host is required")
});
const formSchemaWithIdentity = baseFormSchema.extend({
identity: z
.object(
{
id: z.string(),
name: z.string()
},
{ required_error: "Identity is required" }
)
.nullable()
.refine((val) => val !== null, { message: "Identity is required" })
});
const formSchemaWithToken = baseFormSchema.extend({
identityToken: z.string().min(1, "Token is required")
});
export const RelayCliSystemdDeploymentMethod = () => {
const { protocol, hostname, port } = window.location;
const portSuffix = port && port !== "80" ? `:${port}` : "";
const siteURL = `${protocol}//${hostname}${portSuffix}`;
const [autogenerateToken, setAutogenerateToken] = useState(true);
const [step, setStep] = useState<"form" | "command">("form");
const [name, setName] = useState("");
const [host, setHost] = useState("");
const [identity, setIdentity] = useState<null | {
id: string;
name: string;
}>(null);
const [identityToken, setIdentityToken] = useState("");
const [formErrors, setFormErrors] = useState<z.ZodIssue[]>([]);
const errors = useMemo(() => {
const errorMap: Record<string, string | undefined> = {};
formErrors.forEach((issue) => {
if (issue.path.length > 0) {
errorMap[String(issue.path[0])] = issue.message;
}
});
return errorMap;
}, [formErrors]);
const { currentOrg } = useOrganization();
const organizationId = currentOrg?.id || "";
const { permission } = useOrgPermission();
const canCreateToken = permission.can(
OrgPermissionIdentityActions.CreateToken,
OrgPermissionSubjects.Identity
);
const { data: identityMembershipOrgsData, isPending: isIdentitiesLoading } =
useGetIdentityMembershipOrgs({
organizationId,
limit: 20000
});
const identityMembershipOrgs = identityMembershipOrgsData?.identityMemberships || [];
const { mutateAsync: createToken, isPending: isCreatingToken } =
useCreateTokenIdentityTokenAuth();
const { mutateAsync: addIdentityTokenAuth, isPending: isAddingTokenAuth } =
useAddIdentityTokenAuth();
const { refetch } = useGetIdentityTokenAuth(identity?.id ?? "");
const handleGenerateCommand = async () => {
setFormErrors([]);
if (canCreateToken && autogenerateToken) {
const validation = formSchemaWithIdentity.safeParse({ name, host, identity });
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
const validatedIdentity = validation.data.identity;
try {
const { data: identityTokenAuth } = await refetch();
if (!identityTokenAuth) {
await addIdentityTokenAuth({
identityId: validatedIdentity.id,
organizationId,
accessTokenTTL: 2592000,
accessTokenMaxTTL: 2592000,
accessTokenNumUsesLimit: 0,
accessTokenTrustedIps: [{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]
});
createNotification({
text: "Token authentication has been automatically enabled for the selected identity. By default, it is configured to allow all IP addresses with a default token TTL of 30 days. You can manage these settings in Access Control.",
type: "warning"
});
}
const token = await createToken({
identityId: validatedIdentity.id,
name: `relay token for ${name} (autogenerated)`
});
setIdentityToken(token.accessToken);
createNotification({
text: "Automatically generated a token for the selected identity.",
type: "info"
});
setStep("command");
} catch {
setIdentityToken("");
}
} else {
const validation = formSchemaWithToken.safeParse({
name,
host,
identityToken
});
if (!validation.success) {
setFormErrors(validation.error.issues);
return;
}
setStep("command");
}
};
const installCommand = useMemo(() => {
return `sudo infisical relay systemd install --name=${name} --domain=${siteURL} --host=${host} --token=${identityToken}`;
}, [name, siteURL, host, identityToken]);
const startServiceCommand = "sudo systemctl start infisical-relay";
const enableServiceCommand = "sudo systemctl enable infisical-relay";
if (step === "command") {
return (
<>
<FormLabel label="Installation Command" />
<div className="flex gap-2">
<Input value={installCommand} isDisabled />
<IconButton
ariaLabel="copy install command"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(installCommand);
createNotification({
text: "Installation command copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</div>
<FormLabel label="Start the Relay Service" className="mt-4" />
<div className="mb-2 flex gap-2">
<Input value={startServiceCommand} isDisabled />
<IconButton
ariaLabel="copy start service command"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(startServiceCommand);
createNotification({
text: "Start service command copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</div>
<div className="flex gap-2">
<Input value={enableServiceCommand} isDisabled />
<IconButton
ariaLabel="copy enable service command"
variant="outline_bg"
colorSchema="secondary"
onClick={() => {
navigator.clipboard.writeText(enableServiceCommand);
createNotification({
text: "Enable service command copied to clipboard",
type: "info"
});
}}
className="w-10"
>
<FontAwesomeIcon icon={faCopy} />
</IconButton>
</div>
<a
href="https://infisical.com/docs/cli/overview"
target="_blank"
className="mt-2 flex h-4 w-fit items-center gap-2 border-b border-mineshaft-400 text-sm text-mineshaft-400 transition-colors duration-100 hover:border-yellow-400 hover:text-yellow-400"
rel="noreferrer"
>
<span>Install the Infisical CLI</span>
<FontAwesomeIcon icon={faUpRightFromSquare} className="size-3" />
</a>
<div className="mt-6 flex items-center">
<ModalClose asChild>
<Button className="mr-4" size="sm" colorSchema="secondary">
Done
</Button>
</ModalClose>
</div>
</>
);
}
return (
<>
<FormLabel label="Name" tooltipText="The name for your relay." />
<Input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter relay name..."
isError={Boolean(errors.name)}
/>
{errors.name && <p className="mt-1 text-sm text-red">{errors.name}</p>}
<FormLabel
label="Host"
tooltipText="The public IP address of the system you're deploying the relay to."
className="mt-4"
/>
<Input
value={host}
onChange={(e) => setHost(e.target.value)}
placeholder="0.0.0.0"
isError={Boolean(errors.host)}
/>
{errors.host && <p className="mt-1 text-sm text-red">{errors.host}</p>}
{canCreateToken && autogenerateToken ? (
<>
<FormLabel
label="Identity"
tooltipText="The identity that your relay will use for authentication."
className="mt-4"
/>
<FilterableSelect
value={identity}
onChange={(e) =>
setIdentity(
e as SingleValue<{
id: string;
name: string;
}>
)
}
isLoading={isIdentitiesLoading}
placeholder="Select identity..."
options={identityMembershipOrgs.map((membership) => membership.identity)}
getOptionValue={(option) => option.id}
getOptionLabel={(option) => option.name}
/>
{errors.identity && <p className="mt-1 text-sm text-red">{errors.identity}</p>}
</>
) : (
<>
<FormLabel
label="Identity Token"
tooltipText="The identity token that your relay will use for authentication."
className="mt-4"
/>
<Input
value={identityToken}
onChange={(e) => setIdentityToken(e.target.value)}
placeholder="Enter identity token..."
isError={Boolean(errors.identityToken)}
/>
{errors.identityToken && <p className="mt-1 text-sm text-red">{errors.identityToken}</p>}
</>
)}
{canCreateToken && (
<div className="mt-2">
<Checkbox
isChecked={autogenerateToken}
onCheckedChange={(e) => {
setAutogenerateToken(Boolean(e));
}}
id="autogenerate-token"
className="mr-2"
>
<div className="flex items-center">
<span>Automatically enable token auth and generate a token for identity</span>
<Tooltip
className="max-w-md"
content={
<>
Token authentication will be automatically enabled for the selected identity if
it isn&apos;t already configured. By default, it will be configured to allow all
IP addresses with a token TTL of 30 days. You can manage these settings in
Access Control.
<br />
<br />A token will automatically be generated to be used with the CLI command.
</>
}
>
<FontAwesomeIcon icon={faQuestionCircle} size="sm" className="mt-0.5 ml-1" />
</Tooltip>
</div>
</Checkbox>
</div>
)}
<div className="mt-6 flex items-center">
<Button
className="mr-4"
size="sm"
colorSchema="secondary"
onClick={handleGenerateCommand}
isLoading={isCreatingToken || isAddingTokenAuth}
>
Continue
</Button>
<ModalClose asChild>
<Button colorSchema="secondary" variant="plain">
Cancel
</Button>
</ModalClose>
</div>
</>
);
};

View File

@@ -4,6 +4,7 @@ import { Modal, ModalContent } from "@app/components/v2";
import { RelayDeploymentMethodSelect } from "@app/pages/organization/NetworkingPage/components/RelayTab/components/RelayDeploymentMethodSelect";
import { RelayCliDeploymentMethod } from "./RelayCliDeploymentMethod";
import { RelayCliSystemdDeploymentMethod } from "./RelayCliSystemdDeploymentMethod";
import { RelayTerraformDeploymentMethod } from "./RelayTerraformDeploymentMethod";
type Props = {
@@ -13,6 +14,7 @@ type Props = {
export const RelayDeploymentInfoMap = {
cli: { name: "CLI", image: "SSH.png", component: RelayCliDeploymentMethod },
systemd: { name: "CLI (systemd)", image: "SSH.png", component: RelayCliSystemdDeploymentMethod },
terraform: {
name: "Terraform",
image: "Terraform.png",

View File

@@ -190,15 +190,6 @@ export const RelayTerraformDeploymentMethod = () => {
}
};
const handleIdentityChange = (
selectedIdentity: SingleValue<{
id: string;
name: string;
}>
) => {
setIdentity(selectedIdentity);
};
const terraformCommand = useMemo(() => {
return `terraform {
required_providers {
@@ -365,7 +356,7 @@ resource "aws_eip_association" "eip_assoc" {
<FilterableSelect
value={identity}
onChange={(e) =>
handleIdentityChange(
setIdentity(
e as SingleValue<{
id: string;
name: string;

View File

@@ -13,6 +13,8 @@ import { Button, IconButton } from "@app/components/v2";
import { useTimedReset, useToggle } from "@app/hooks";
import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
import { SecretShareInfo } from "./SecretShareInfo";
type Props = {
secret: TViewSharedSecretResponse["secret"];
secretKey: string | null;
@@ -71,6 +73,7 @@ export const SecretContainer = ({ secret, secretKey: key }: Props) => {
</IconButton>
</div>
</div>
<SecretShareInfo secret={secret} />
<Button
className="mt-4 w-full bg-mineshaft-700 py-3 text-bunker-200"
colorSchema="primary"

View File

@@ -0,0 +1,52 @@
import { faClock, faEye } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { format } from "date-fns";
import { TViewSharedSecretResponse } from "@app/hooks/api/secretSharing";
type Props = {
secret: TViewSharedSecretResponse["secret"];
};
export const SecretShareInfo = ({ secret }: Props) => {
let timeRemaining: string | null = null;
if (secret.expiresAt) {
try {
timeRemaining = format(new Date(secret.expiresAt), "yyyy-MM-dd 'at' HH:mm a");
} catch {
timeRemaining = null;
}
}
let viewsRemaining: number | null = null;
if (secret.expiresAfterViews) {
viewsRemaining = secret.expiresAfterViews - 1;
}
if (!timeRemaining && viewsRemaining === null) {
return null;
}
return (
<div className="mt-4 flex flex-col gap-2 rounded-md border border-mineshaft-600 bg-mineshaft-700/50 p-3 text-sm text-gray-300">
{timeRemaining && (
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faClock} className="text-mineshaft-400" />
<span>Expires on {timeRemaining}</span>
</div>
)}
{viewsRemaining !== null && (
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faEye} className="text-mineshaft-400" />
<span>
{viewsRemaining === 0
? "This is the last time you can view this secret"
: `${viewsRemaining} more view${viewsRemaining === 1 ? "" : "s"} remaining`}
</span>
</div>
)}
</div>
);
};

View File

@@ -39,7 +39,7 @@ const CommitItem = ({
<div className="flex gap-2 px-4 py-3 transition-colors duration-200 hover:bg-zinc-800">
<div className="flex min-w-0 flex-1 flex-col items-start">
<p className="block w-full truncate text-left text-sm text-mineshaft-100">
{commit.message}
{commit.message || <span className="text-mineshaft-400 italic">No message</span>}
</p>
<p className="text-left text-xs text-mineshaft-300">
{commit.actorMetadata?.email || commit.actorMetadata?.name || commit.actorType}{" "}

View File

@@ -293,9 +293,6 @@ export const CommitForm: React.FC<CommitFormProps> = ({
}
const handleCommit = async () => {
if (!commitMessage.trim()) {
return;
}
await onCommit(pendingChanges, commitMessage);
clearAllPendingChanges({
projectId,
@@ -432,7 +429,7 @@ export const CommitForm: React.FC<CommitFormProps> = ({
{/* Commit Message */}
<div>
<label className="mb-2 block text-sm font-medium text-mineshaft-200">
Commit Message <span className="text-red-400">*</span>
Commit Message
</label>
<Input
value={commitMessage}
@@ -440,7 +437,6 @@ export const CommitForm: React.FC<CommitFormProps> = ({
placeholder="Describe your changes..."
className="w-full"
autoFocus
required
/>
</div>
@@ -457,7 +453,7 @@ export const CommitForm: React.FC<CommitFormProps> = ({
<Button
onClick={handleCommit}
isLoading={isCommitting}
isDisabled={isCommitting || !commitMessage.trim()}
isDisabled={isCommitting}
leftIcon={<FontAwesomeIcon icon={faCodeCommit} />}
colorSchema="primary"
variant="outline_bg"