From 19d32a1a3b1bf5d566787e0b177e59b1bef22002 Mon Sep 17 00:00:00 2001 From: = Date: Sun, 29 Dec 2024 22:29:41 +0530 Subject: [PATCH] feat: completed migration of ssh product --- ...m-regular-126-verified-hover-verified.json | 1 + frontend-v2/src/const/routes.ts | 6 + .../context/ProjectPermissionContext/types.ts | 6 + frontend-v2/src/helpers/project.ts | 3 +- frontend-v2/src/hooks/api/ca/constants.tsx | 4 +- frontend-v2/src/hooks/api/index.tsx | 2 + frontend-v2/src/hooks/api/sshCa/constants.tsx | 14 + frontend-v2/src/hooks/api/sshCa/index.tsx | 9 + frontend-v2/src/hooks/api/sshCa/mutations.tsx | 101 +++++ frontend-v2/src/hooks/api/sshCa/queries.tsx | 37 ++ frontend-v2/src/hooks/api/sshCa/types.ts | 74 ++++ .../api/sshCertificateTemplates/index.tsx | 7 + .../api/sshCertificateTemplates/mutations.tsx | 59 +++ .../api/sshCertificateTemplates/queries.tsx | 22 + .../api/sshCertificateTemplates/types.ts | 47 ++ frontend-v2/src/hooks/api/workspace/index.tsx | 3 + .../src/hooks/api/workspace/queries.tsx | 63 +++ .../src/hooks/api/workspace/query-keys.tsx | 16 +- frontend-v2/src/hooks/api/workspace/types.ts | 3 +- .../OrganizationLayout/OrganizationLayout.tsx | 10 + .../layouts/ProjectLayout/ProjectLayout.tsx | 15 + .../SecretManagerOverviewPage.tsx | 7 +- .../SshOverviewPage/SshOverviewPage.tsx | 5 + .../organization/SshOverviewPage/route.tsx | 9 + .../project/AccessControlPage/route-ssh.tsx | 18 + .../IdentityDetailsByIDPage/route-ssh.tsx | 9 + .../MemberDetailsByIDPage/route-ssh.tsx | 9 + .../ProjectRoleModifySection.utils.tsx | 32 ++ .../RoleDetailsBySlugPage/route-ssh.tsx | 9 + .../pages/ssh/OverviewPage/OverviewPage.tsx | 75 ++++ .../OverviewPage/components/SshCaModal.tsx | 198 +++++++++ .../OverviewPage/components/SshCaSection.tsx | 120 +++++ .../OverviewPage/components/SshCaTable.tsx | 160 +++++++ .../components/SshCertificatesSection.tsx | 39 ++ .../components/SshCertificatesTable.tsx | 87 ++++ .../components/SshCertificatesTable.utils.ts | 17 + .../ssh/OverviewPage/components/index.tsx | 2 + .../src/pages/ssh/OverviewPage/route.tsx | 9 + .../pages/ssh/SettingsPage/SettingsPage.tsx | 39 ++ .../AuditLogsRetentionSection.tsx | 129 ++++++ .../AuditLogsRetentionSection/index.tsx | 1 + .../DeleteProjectSection.tsx | 192 ++++++++ .../components/DeleteProjectSection/index.tsx | 1 + .../ProjectGeneralTab/ProjectGeneralTab.tsx | 13 + .../components/ProjectGeneralTab/index.tsx | 1 + .../CopyButton.tsx | 51 +++ .../ProjectOverviewChangeSection.tsx | 168 +++++++ .../ProjectOverviewChangeSection/index.tsx | 1 + .../ssh/SettingsPage/components/index.tsx | 2 + .../src/pages/ssh/SettingsPage/route.tsx | 9 + .../pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx | 167 +++++++ .../components/SshCaDetailsSection.tsx | 124 ++++++ .../components/SshCertificateContent.tsx | 174 ++++++++ .../components/SshCertificateModal.tsx | 373 ++++++++++++++++ .../SshCertificateTemplateModal.tsx | 411 ++++++++++++++++++ .../SshCertificateTemplatesSection.tsx | 161 +++++++ .../SshCertificateTemplatesTable.tsx | 193 ++++++++ .../ssh/SshCaByIDPage/components/index.tsx | 3 + .../src/pages/ssh/SshCaByIDPage/route.tsx | 9 + frontend-v2/src/pages/ssh/layout.tsx | 25 ++ frontend-v2/src/routeTree.gen.ts | 305 ++++++++++++- frontend-v2/src/routes.ts | 16 +- 62 files changed, 3867 insertions(+), 8 deletions(-) create mode 100644 frontend-v2/src/assets/lotties/system-regular-126-verified-hover-verified.json create mode 100644 frontend-v2/src/hooks/api/sshCa/constants.tsx create mode 100644 frontend-v2/src/hooks/api/sshCa/index.tsx create mode 100644 frontend-v2/src/hooks/api/sshCa/mutations.tsx create mode 100644 frontend-v2/src/hooks/api/sshCa/queries.tsx create mode 100644 frontend-v2/src/hooks/api/sshCa/types.ts create mode 100644 frontend-v2/src/hooks/api/sshCertificateTemplates/index.tsx create mode 100644 frontend-v2/src/hooks/api/sshCertificateTemplates/mutations.tsx create mode 100644 frontend-v2/src/hooks/api/sshCertificateTemplates/queries.tsx create mode 100644 frontend-v2/src/hooks/api/sshCertificateTemplates/types.ts create mode 100644 frontend-v2/src/pages/organization/SshOverviewPage/SshOverviewPage.tsx create mode 100644 frontend-v2/src/pages/organization/SshOverviewPage/route.tsx create mode 100644 frontend-v2/src/pages/project/AccessControlPage/route-ssh.tsx create mode 100644 frontend-v2/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx create mode 100644 frontend-v2/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx create mode 100644 frontend-v2/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx create mode 100644 frontend-v2/src/pages/ssh/OverviewPage/OverviewPage.tsx create mode 100644 frontend-v2/src/pages/ssh/OverviewPage/components/SshCaModal.tsx create mode 100644 frontend-v2/src/pages/ssh/OverviewPage/components/SshCaSection.tsx create mode 100644 frontend-v2/src/pages/ssh/OverviewPage/components/SshCaTable.tsx create mode 100644 frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesSection.tsx create mode 100644 frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesTable.tsx create mode 100644 frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesTable.utils.ts create mode 100644 frontend-v2/src/pages/ssh/OverviewPage/components/index.tsx create mode 100644 frontend-v2/src/pages/ssh/OverviewPage/route.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/SettingsPage.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/AuditLogsRetentionSection/index.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/DeleteProjectSection/index.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/index.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/index.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/components/index.tsx create mode 100644 frontend-v2/src/pages/ssh/SettingsPage/route.tsx create mode 100644 frontend-v2/src/pages/ssh/SshCaByIDPage/SshCaByIDPage.tsx create mode 100644 frontend-v2/src/pages/ssh/SshCaByIDPage/components/SshCaDetailsSection.tsx create mode 100644 frontend-v2/src/pages/ssh/SshCaByIDPage/components/SshCertificateContent.tsx create mode 100644 frontend-v2/src/pages/ssh/SshCaByIDPage/components/SshCertificateModal.tsx create mode 100644 frontend-v2/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplateModal.tsx create mode 100644 frontend-v2/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplatesSection.tsx create mode 100644 frontend-v2/src/pages/ssh/SshCaByIDPage/components/SshCertificateTemplatesTable.tsx create mode 100644 frontend-v2/src/pages/ssh/SshCaByIDPage/components/index.tsx create mode 100644 frontend-v2/src/pages/ssh/SshCaByIDPage/route.tsx create mode 100644 frontend-v2/src/pages/ssh/layout.tsx diff --git a/frontend-v2/src/assets/lotties/system-regular-126-verified-hover-verified.json b/frontend-v2/src/assets/lotties/system-regular-126-verified-hover-verified.json new file mode 100644 index 000000000..ce8777c0a --- /dev/null +++ b/frontend-v2/src/assets/lotties/system-regular-126-verified-hover-verified.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":60,"ip":0,"op":60,"w":500,"h":500,"nm":"system-regular-126-verified","ddd":0,"assets":[{"id":"comp_1","nm":"hover-verified","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[6.109,-6.116],[-6.115,-6.108],[0,0],[-4.002,0],[-3.055,3.051],[0,0],[6.107,6.115],[6.115,-6.107],[0,0]],"o":[[-6.115,-6.107],[-6.107,6.116],[0,0],[3.056,3.052],[4.002,0],[0,0],[6.115,-6.108],[-6.109,-6.116],[0,0],[0,0]],"v":[[-69.803,-8.539],[-91.936,-8.526],[-91.922,13.607],[-39.704,65.762],[-28.644,70.339],[-17.584,65.762],[91.922,-43.616],[91.936,-65.749],[69.803,-65.762],[-28.644,32.57]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[263.158,242.187],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[73.521,-47.165],[0,0],[0,0],[11.506,86.895],[0,0],[-39.84,20.453],[-33.146,-7.159]],"o":[[-11.51,86.919],[0,0],[0,0],[-73.503,-47.153],[0,0],[33.146,-7.159],[39.84,20.453],[0,0]],"v":[[149.277,-67.68],[15.49,143.298],[0,153.266],[-15.509,143.287],[-149.274,-67.655],[-154.479,-107.449],[0,-153.936],[154.479,-107.449]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.073,1.615],[32.173,20.107],[5.074,-3.172],[35.445,-7.092],[-1.068,-8.164],[0,0],[-81.252,-52.124],[0,0],[-2.944,0],[-2.579,1.66],[0,0],[-12.724,96.093],[0,0]],"o":[[-35.445,-7.092],[-5.074,-3.172],[-32.173,20.107],[-8.073,1.615],[0,0],[12.72,96.068],[0,0],[2.579,1.66],[2.944,0],[0,0],[81.27,-52.136],[0,0],[1.068,-8.164]],"v":[[174.946,-135.138],[8.294,-185.147],[-8.294,-185.147],[-174.946,-135.138],[-187.394,-117.763],[-180.307,-63.572],[-32.428,169.62],[-8.469,185.036],[0,187.526],[8.469,185.036],[32.409,169.632],[180.31,-63.596],[187.394,-117.763]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":60,"op":300,"st":1,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250.004,250.003,0],"ix":2,"l":2},"a":{"a":0,"k":[250.004,250.003,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[6.109,-6.116],[-6.115,-6.108],[0,0],[-4.002,0],[-3.055,3.051],[0,0],[6.107,6.115],[6.115,-6.107],[0,0]],"o":[[-6.115,-6.107],[-6.107,6.116],[0,0],[3.056,3.052],[4.002,0],[0,0],[6.115,-6.108],[-6.109,-6.116],[0,0],[0,0]],"v":[[-69.803,-8.539],[-91.936,-8.526],[-91.922,13.607],[-39.704,65.762],[-28.644,70.339],[-17.584,65.762],[91.922,-43.616],[91.936,-65.749],[69.803,-65.762],[-28.644,32.57]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[263.158,242.187],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[73.521,-47.165],[0,0],[0,0],[11.506,86.895],[0,0],[-39.84,20.453],[-33.146,-7.159]],"o":[[-11.51,86.919],[0,0],[0,0],[-73.503,-47.153],[0,0],[33.146,-7.159],[39.84,20.453],[0,0]],"v":[[149.277,-67.68],[15.49,143.298],[0,153.266],[-15.509,143.287],[-149.274,-67.655],[-154.479,-107.449],[0,-153.936],[154.479,-107.449]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[8.073,1.615],[32.173,20.107],[5.074,-3.172],[35.445,-7.092],[-1.068,-8.164],[0,0],[-81.252,-52.124],[0,0],[-2.944,0],[-2.579,1.66],[0,0],[-12.724,96.093],[0,0]],"o":[[-35.445,-7.092],[-5.074,-3.172],[-32.173,20.107],[-8.073,1.615],[0,0],[12.72,96.068],[0,0],[2.579,1.66],[2.944,0],[0,0],[81.27,-52.136],[0,0],[1.068,-8.164]],"v":[[174.946,-135.138],[8.294,-185.147],[-8.294,-185.147],[-174.946,-135.138],[-187.394,-117.763],[-180.307,-63.572],[-32.428,169.62],[-8.469,185.036],[0,187.526],[8.469,185.036],[32.409,169.632],[180.31,-63.596],[187.394,-117.763]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Fill","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[250.004,250.003],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":1,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".primary.design","cl":"primary design","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.131],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.628],"y":[0]},"t":30,"s":[27]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":46,"s":[-11]},{"t":60,"s":[0]}],"ix":10},"p":{"a":0,"k":[249.998,250.004,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.131,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0],[77.333,-49.716],[0,0],[0,0],[0,0],[11.993,91.145],[0,0],[-41.666,26.039]],"o":[[0,0],[-11.99,91.149],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039]],"v":[[171.875,-119.795],[164.746,-65.6],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-164.743,-65.592],[-171.875,-119.795],[0,-171.875]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.628,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[99.436,-63.925],[0,0],[0,0],[0,0],[15.421,117.196],[0,0],[-53.575,33.481]],"o":[[0,0],[-15.417,117.201],[0,0],[0,0],[0,0],[-99.432,-63.922],[0,0],[0,0],[53.576,33.481]],"v":[[191.412,-122.573],[182.245,-52.889],[1.15,232.704],[-29.579,252.458],[-29.588,252.46],[-60.324,232.701],[-241.417,-52.879],[-250.588,-122.573],[-29.588,-189.539]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41.801,"s":[{"i":[[0,0],[0,0],[56.69,-36.445],[0,0],[0,0],[0,0],[8.792,66.816],[0,0],[-30.544,19.088]],"o":[[0,0],[-8.79,66.819],[0,0],[0,0],[0,0],[-56.688,-36.443],[0,0],[0,0],[30.545,19.088]],"v":[[125.996,-87.818],[120.77,-48.09],[17.524,114.733],[0.005,125.995],[0,125.996],[-17.523,114.731],[-120.768,-48.083],[-125.996,-87.818],[0,-125.996]],"c":true}]},{"t":60,"s":[{"i":[[0,0],[0,0],[77.333,-49.716],[0,0],[0,0],[0,0],[11.993,91.145],[0,0],[-41.666,26.039]],"o":[[0,0],[-11.99,91.149],[0,0],[0,0],[0,0],[-77.33,-49.714],[0,0],[0,0],[41.667,26.039]],"v":[[171.875,-119.795],[164.746,-65.6],[23.905,156.51],[0.007,171.873],[0,171.875],[-23.904,156.508],[-164.743,-65.592],[-171.875,-119.795],[0,-171.875]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1,"op":60,"st":-239,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".primary.design","cl":"primary design","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0.006,-0.001,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.157,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[153.363,-119.689],[-14.644,46.689],[-66.863,-5.466]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.6,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[167.916,-123.188],[-50.252,92.864],[-118.061,25.138]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":42,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[114.144,-89.081],[-10.899,34.749],[-49.764,-4.068]],"c":false}]},{"t":60,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[153.363,-119.689],[-14.644,46.689],[-66.863,-5.466]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3,"x":"var $bm_rt;\n$bm_rt = comp('system-regular-126-verified').layer('control').effect('primary')('Color');"},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":31.3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":".primary","mn":"ADBE Vector Graphic - Stroke","hd":false,"cl":"primary"},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[100]},{"t":20,"s":[100],"h":1},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0]},{"t":60,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":1,"s":[26.2]},{"t":20,"s":[100],"h":1},{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":28,"s":[0]},{"t":60,"s":[26.2]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":1,"op":60,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"control","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[0,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"ef":[{"ty":5,"nm":"primary","np":3,"mn":"ADBE Color Control","ix":1,"en":1,"ef":[{"ty":2,"nm":"Color","mn":"ADBE Color Control-0001","ix":1,"v":{"a":0,"k":[1,1,1],"ix":1}}]}],"ip":0,"op":131,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"hover-verified","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[250,250,0],"ix":2,"l":2},"a":{"a":0,"k":[250,250,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"w":500,"h":500,"ip":0,"op":70,"st":0,"bm":0}],"markers":[{"tm":0,"cm":"default:hover-verified","dr":60}],"props":{}} \ No newline at end of file diff --git a/frontend-v2/src/const/routes.ts b/frontend-v2/src/const/routes.ts index 35fa7e444..8a63c4f1b 100644 --- a/frontend-v2/src/const/routes.ts +++ b/frontend-v2/src/const/routes.ts @@ -79,6 +79,12 @@ export const ROUTE_PATHS = Object.freeze({ "/_authenticate/_inject-org-details/cert-manager/$projectId/_cert-manager-layout/pki-collections/$collectionId" ) }, + Ssh: { + SshCaByIDPage: setRoute( + "/ssh/$projectId/ca/$caId", + "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/ca/$caId" + ) + }, Public: { ViewSharedSecretByIDPage: setRoute("/shared/secret/$secretId", "/shared/secret/$secretId") } diff --git a/frontend-v2/src/context/ProjectPermissionContext/types.ts b/frontend-v2/src/context/ProjectPermissionContext/types.ts index 673b2b41a..b0c3aa463 100644 --- a/frontend-v2/src/context/ProjectPermissionContext/types.ts +++ b/frontend-v2/src/context/ProjectPermissionContext/types.ts @@ -85,6 +85,9 @@ export enum ProjectPermissionSub { CertificateAuthorities = "certificate-authorities", Certificates = "certificates", CertificateTemplates = "certificate-templates", + SshCertificateAuthorities = "ssh-certificate-authorities", + SshCertificateTemplates = "ssh-certificate-templates", + SshCertificates = "ssh-certificates", PkiAlerts = "pki-alerts", PkiCollections = "pki-collections", Kms = "kms", @@ -165,6 +168,9 @@ export type ProjectPermissionSet = | [ProjectPermissionActions, ProjectPermissionSub.CertificateAuthorities] | [ProjectPermissionActions, ProjectPermissionSub.Certificates] | [ProjectPermissionActions, ProjectPermissionSub.CertificateTemplates] + | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateAuthorities] + | [ProjectPermissionActions, ProjectPermissionSub.SshCertificateTemplates] + | [ProjectPermissionActions, ProjectPermissionSub.SshCertificates] | [ProjectPermissionActions, ProjectPermissionSub.PkiAlerts] | [ProjectPermissionActions, ProjectPermissionSub.PkiCollections] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Project] diff --git a/frontend-v2/src/helpers/project.ts b/frontend-v2/src/helpers/project.ts index 6bdac3230..cc5c6fa91 100644 --- a/frontend-v2/src/helpers/project.ts +++ b/frontend-v2/src/helpers/project.ts @@ -68,7 +68,8 @@ export const getProjectTitle = (type: ProjectType) => { const titleConvert = { [ProjectType.SecretManager]: "Secret Management", [ProjectType.KMS]: "Key Management", - [ProjectType.CertificateManager]: "Cert Management" + [ProjectType.CertificateManager]: "Cert Management", + [ProjectType.SSH]: "SSH" }; return titleConvert[type]; }; diff --git a/frontend-v2/src/hooks/api/ca/constants.tsx b/frontend-v2/src/hooks/api/ca/constants.tsx index 9bb7b89d5..8838ef748 100644 --- a/frontend-v2/src/hooks/api/ca/constants.tsx +++ b/frontend-v2/src/hooks/api/ca/constants.tsx @@ -1,3 +1,5 @@ +import { SshCaStatus } from "../sshCa"; +import { SshCertTemplateStatus } from "../sshCertificateTemplates"; import { CaStatus, CaType } from "./enums"; export const caTypeToNameMap: { [K in CaType]: string } = { @@ -11,7 +13,7 @@ export const caStatusToNameMap: { [K in CaStatus]: string } = { [CaStatus.PENDING_CERTIFICATE]: "Pending Certificate" }; -export const getCaStatusBadgeVariant = (status: CaStatus) => { +export const getCaStatusBadgeVariant = (status: CaStatus | SshCaStatus | SshCertTemplateStatus) => { switch (status) { case CaStatus.ACTIVE: return "success"; diff --git a/frontend-v2/src/hooks/api/index.tsx b/frontend-v2/src/hooks/api/index.tsx index 551822f09..441b5ba86 100644 --- a/frontend-v2/src/hooks/api/index.tsx +++ b/frontend-v2/src/hooks/api/index.tsx @@ -38,6 +38,8 @@ export * from "./secretSharing"; export * from "./secretSnapshots"; export * from "./serverDetails"; export * from "./serviceTokens"; +export * from "./sshCa"; +export * from "./sshCertificateTemplates"; export * from "./ssoConfig"; export * from "./subscriptions"; export * from "./tags"; diff --git a/frontend-v2/src/hooks/api/sshCa/constants.tsx b/frontend-v2/src/hooks/api/sshCa/constants.tsx new file mode 100644 index 000000000..2742a7bfa --- /dev/null +++ b/frontend-v2/src/hooks/api/sshCa/constants.tsx @@ -0,0 +1,14 @@ +export enum SshCaStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export enum SshCertType { + USER = "user", + HOST = "host" +} + +export const sshCertTypeToNameMap: { [K in SshCertType]: string } = { + [SshCertType.USER]: "User", + [SshCertType.HOST]: "Host" +}; diff --git a/frontend-v2/src/hooks/api/sshCa/index.tsx b/frontend-v2/src/hooks/api/sshCa/index.tsx new file mode 100644 index 000000000..8fc57654b --- /dev/null +++ b/frontend-v2/src/hooks/api/sshCa/index.tsx @@ -0,0 +1,9 @@ +export { SshCaStatus } from "./constants"; +export { + useCreateSshCa, + useDeleteSshCa, + useIssueSshCreds, + useSignSshKey, + useUpdateSshCa +} from "./mutations"; +export { useGetSshCaById, useGetSshCaCertTemplates } from "./queries"; diff --git a/frontend-v2/src/hooks/api/sshCa/mutations.tsx b/frontend-v2/src/hooks/api/sshCa/mutations.tsx new file mode 100644 index 000000000..c7e90c3b9 --- /dev/null +++ b/frontend-v2/src/hooks/api/sshCa/mutations.tsx @@ -0,0 +1,101 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { workspaceKeys } from "../workspace/query-keys"; +import { + TCreateSshCaDTO, + TDeleteSshCaDTO, + TIssueSshCredsDTO, + TIssueSshCredsResponse, + TSignSshKeyDTO, + TSignSshKeyResponse, + TSshCertificateAuthority, + TUpdateSshCaDTO +} from "./types"; + +export const sshCaKeys = { + getSshCaById: (caId: string) => [{ caId }, "ssh-ca"] +}; + +export const useCreateSshCa = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { + data: { ca } + } = await apiRequest.post<{ ca: TSshCertificateAuthority }>("/api/v1/ssh/ca/", body); + return ca; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshCas(projectId) }); + } + }); +}; + +export const useUpdateSshCa = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ caId, ...body }) => { + const { + data: { ca } + } = await apiRequest.patch<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`, body); + return ca; + }, + onSuccess: ({ projectId }, { caId }) => { + queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshCas(projectId) }); + queryClient.invalidateQueries({ queryKey: sshCaKeys.getSshCaById(caId) }); + } + }); +}; + +export const useDeleteSshCa = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ caId }) => { + const { + data: { ca } + } = await apiRequest.delete<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`); + return ca; + }, + onSuccess: ({ projectId }) => { + queryClient.invalidateQueries({ queryKey: workspaceKeys.getWorkspaceSshCas(projectId) }); + } + }); +}; + +export const useSignSshKey = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { data } = await apiRequest.post( + "/api/v1/ssh/certificates/sign", + body + ); + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.allWorkspaceSshCertificates(projectId) + }); + } + }); +}; + +export const useIssueSshCreds = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (body) => { + const { data } = await apiRequest.post( + "/api/v1/ssh/certificates/issue", + body + ); + return data; + }, + onSuccess: (_, { projectId }) => { + queryClient.invalidateQueries({ + queryKey: workspaceKeys.allWorkspaceSshCertificates(projectId) + }); + } + }); +}; diff --git a/frontend-v2/src/hooks/api/sshCa/queries.tsx b/frontend-v2/src/hooks/api/sshCa/queries.tsx new file mode 100644 index 000000000..b85bbcb35 --- /dev/null +++ b/frontend-v2/src/hooks/api/sshCa/queries.tsx @@ -0,0 +1,37 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TSshCertificateTemplate } from "../sshCertificateTemplates/types"; +import { TSshCertificateAuthority } from "./types"; + +export const sshCaKeys = { + getSshCaById: (caId: string) => [{ caId }, "ssh-ca"], + getSshCaCertTemplates: (caId: string) => [{ caId }, "ssh-ca-cert-templates"] +}; + +export const useGetSshCaById = (caId: string) => { + return useQuery({ + queryKey: sshCaKeys.getSshCaById(caId), + queryFn: async () => { + const { + data: { ca } + } = await apiRequest.get<{ ca: TSshCertificateAuthority }>(`/api/v1/ssh/ca/${caId}`); + return ca; + }, + enabled: Boolean(caId) + }); +}; + +export const useGetSshCaCertTemplates = (caId: string) => { + return useQuery({ + queryKey: sshCaKeys.getSshCaCertTemplates(caId), + queryFn: async () => { + const { data } = await apiRequest.get<{ + certificateTemplates: TSshCertificateTemplate[]; + }>(`/api/v1/ssh/ca/${caId}/certificate-templates`); + return data; + }, + enabled: Boolean(caId) + }); +}; diff --git a/frontend-v2/src/hooks/api/sshCa/types.ts b/frontend-v2/src/hooks/api/sshCa/types.ts new file mode 100644 index 000000000..6e5f02c4d --- /dev/null +++ b/frontend-v2/src/hooks/api/sshCa/types.ts @@ -0,0 +1,74 @@ +import { CertKeyAlgorithm } from "../certificates/enums"; +import { SshCaStatus, SshCertType } from "./constants"; + +export type TSshCertificate = { + id: string; + sshCaId: string; + sshCertificateTemplateId: string; + serialNumber: string; + certType: SshCertType; + principals: string[]; + keyId: string; + notBefore: string; + notAfter: string; +}; + +export type TSshCertificateAuthority = { + id: string; + projectId: string; + status: SshCaStatus; + friendlyName: string; + keyAlgorithm: CertKeyAlgorithm; + createdAt: string; + updatedAt: string; + publicKey: string; +}; + +export type TCreateSshCaDTO = { + projectId: string; + friendlyName?: string; + keyAlgorithm: CertKeyAlgorithm; +}; + +export type TUpdateSshCaDTO = { + caId: string; + friendlyName?: string; + status?: SshCaStatus; +}; + +export type TDeleteSshCaDTO = { + caId: string; +}; + +export type TSignSshKeyDTO = { + projectId: string; + certificateTemplateId: string; + publicKey?: string; + certType: SshCertType; + principals: string[]; + ttl?: string; + keyId?: string; +}; + +export type TSignSshKeyResponse = { + serialNumber: string; + signedKey: string; +}; + +export type TIssueSshCredsDTO = { + projectId: string; + certificateTemplateId: string; + keyAlgorithm: CertKeyAlgorithm; + certType: SshCertType; + principals: string[]; + ttl?: string; + keyId?: string; +}; + +export type TIssueSshCredsResponse = { + serialNumber: string; + signedKey: string; + privateKey: string; + publicKey: string; + keyAlgorithm: CertKeyAlgorithm; +}; diff --git a/frontend-v2/src/hooks/api/sshCertificateTemplates/index.tsx b/frontend-v2/src/hooks/api/sshCertificateTemplates/index.tsx new file mode 100644 index 000000000..9efe99c81 --- /dev/null +++ b/frontend-v2/src/hooks/api/sshCertificateTemplates/index.tsx @@ -0,0 +1,7 @@ +export { + useCreateSshCertTemplate, + useDeleteSshCertTemplate, + useUpdateSshCertTemplate +} from "./mutations"; +export { useGetSshCertTemplate } from "./queries"; +export * from "./types"; diff --git a/frontend-v2/src/hooks/api/sshCertificateTemplates/mutations.tsx b/frontend-v2/src/hooks/api/sshCertificateTemplates/mutations.tsx new file mode 100644 index 000000000..73e109f21 --- /dev/null +++ b/frontend-v2/src/hooks/api/sshCertificateTemplates/mutations.tsx @@ -0,0 +1,59 @@ +import { useMutation, useQueryClient } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { sshCaKeys } from "../sshCa/queries"; +import { + TCreateSshCertificateTemplateDTO, + TDeleteSshCertificateTemplateDTO, + TSshCertificateTemplate, + TUpdateSshCertificateTemplateDTO +} from "./types"; + +export const useCreateSshCertTemplate = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (data) => { + const { data: certificateTemplate } = await apiRequest.post( + "/api/v1/ssh/certificate-templates", + data + ); + return certificateTemplate; + }, + onSuccess: ({ sshCaId }) => { + queryClient.invalidateQueries({ queryKey: sshCaKeys.getSshCaCertTemplates(sshCaId) }); + } + }); +}; + +export const useUpdateSshCertTemplate = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (data) => { + const { data: certificateTemplate } = await apiRequest.patch( + `/api/v1/ssh/certificate-templates/${data.id}`, + data + ); + + return certificateTemplate; + }, + onSuccess: ({ sshCaId }) => { + queryClient.invalidateQueries({ queryKey: sshCaKeys.getSshCaCertTemplates(sshCaId) }); + } + }); +}; + +export const useDeleteSshCertTemplate = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async (data) => { + const { data: certificateTemplate } = await apiRequest.delete( + `/api/v1/ssh/certificate-templates/${data.id}` + ); + return certificateTemplate; + }, + onSuccess: ({ sshCaId }) => { + queryClient.invalidateQueries({ queryKey: sshCaKeys.getSshCaCertTemplates(sshCaId) }); + } + }); +}; diff --git a/frontend-v2/src/hooks/api/sshCertificateTemplates/queries.tsx b/frontend-v2/src/hooks/api/sshCertificateTemplates/queries.tsx new file mode 100644 index 000000000..b9b535916 --- /dev/null +++ b/frontend-v2/src/hooks/api/sshCertificateTemplates/queries.tsx @@ -0,0 +1,22 @@ +import { useQuery } from "@tanstack/react-query"; + +import { apiRequest } from "@app/config/request"; + +import { TSshCertificateTemplate } from "./types"; + +export const certTemplateKeys = { + getSshCertTemplateById: (id: string) => [{ id }, "ssh-cert-template"] +}; + +export const useGetSshCertTemplate = (id: string) => { + return useQuery({ + queryKey: certTemplateKeys.getSshCertTemplateById(id), + queryFn: async () => { + const { data: certificateTemplate } = await apiRequest.get( + `/api/v1/ssh/certificate-templates/${id}` + ); + return certificateTemplate; + }, + enabled: Boolean(id) + }); +}; diff --git a/frontend-v2/src/hooks/api/sshCertificateTemplates/types.ts b/frontend-v2/src/hooks/api/sshCertificateTemplates/types.ts new file mode 100644 index 000000000..4099b4b8a --- /dev/null +++ b/frontend-v2/src/hooks/api/sshCertificateTemplates/types.ts @@ -0,0 +1,47 @@ +export enum SshCertTemplateStatus { + ACTIVE = "active", + DISABLED = "disabled" +} + +export type TSshCertificateTemplate = { + id: string; + sshCaId: string; + status: SshCertTemplateStatus; + name: string; + ttl: string; + maxTTL: string; + allowedUsers: string[]; + allowedHosts: string[]; + allowUserCertificates: boolean; + allowHostCertificates: boolean; + allowCustomKeyIds: boolean; +}; + +export type TCreateSshCertificateTemplateDTO = { + sshCaId: string; + name: string; + ttl: string; + maxTTL: string; + allowedUsers: string[]; + allowedHosts: string[]; + allowUserCertificates: boolean; + allowHostCertificates: boolean; + allowCustomKeyIds: boolean; +}; + +export type TUpdateSshCertificateTemplateDTO = { + id: string; + status?: SshCertTemplateStatus; + name?: string; + ttl?: string; + maxTTL?: string; + allowedUsers?: string[]; + allowedHosts?: string[]; + allowUserCertificates?: boolean; + allowHostCertificates?: boolean; + allowCustomKeyIds?: boolean; +}; + +export type TDeleteSshCertificateTemplateDTO = { + id: string; +}; diff --git a/frontend-v2/src/hooks/api/workspace/index.tsx b/frontend-v2/src/hooks/api/workspace/index.tsx index cd652970a..665462e7d 100644 --- a/frontend-v2/src/hooks/api/workspace/index.tsx +++ b/frontend-v2/src/hooks/api/workspace/index.tsx @@ -32,6 +32,9 @@ export { useListWorkspaceGroups, useListWorkspacePkiAlerts, useListWorkspacePkiCollections, + useListWorkspaceSshCas, + useListWorkspaceSshCertificates, + useListWorkspaceSshCertificateTemplates, useNameWorkspaceSecrets, useToggleAutoCapitalization, useUpdateIdentityWorkspaceRole, diff --git a/frontend-v2/src/hooks/api/workspace/queries.tsx b/frontend-v2/src/hooks/api/workspace/queries.tsx index be6821f99..af692b390 100644 --- a/frontend-v2/src/hooks/api/workspace/queries.tsx +++ b/frontend-v2/src/hooks/api/workspace/queries.tsx @@ -15,6 +15,8 @@ import { TIntegration } from "../integrations/types"; import { TPkiAlert } from "../pkiAlerts/types"; import { TPkiCollection } from "../pkiCollections/types"; import { EncryptedSecret } from "../secrets/types"; +import { TSshCertificate, TSshCertificateAuthority } from "../sshCa/types"; +import { TSshCertificateTemplate } from "../sshCertificateTemplates/types"; import { userKeys } from "../users/query-keys"; import { TWorkspaceUser } from "../users/types"; import { ProjectSlackConfig } from "../workflowIntegrations/types"; @@ -758,6 +760,67 @@ export const useListWorkspaceCertificateTemplates = ({ workspaceId }: { workspac }); }; +export const useListWorkspaceSshCertificates = ({ + offset, + limit, + projectId +}: { + offset: number; + limit: number; + projectId: string; +}) => { + return useQuery({ + queryKey: workspaceKeys.specificWorkspaceSshCertificates({ + offset, + limit, + projectId + }), + queryFn: async () => { + const params = new URLSearchParams({ + offset: String(offset), + limit: String(limit) + }); + + const { data } = await apiRequest.get<{ + certificates: TSshCertificate[]; + totalCount: number; + }>(`/api/v2/workspace/${projectId}/ssh-certificates`, { + params + }); + return data; + }, + enabled: Boolean(projectId) + }); +}; + +export const useListWorkspaceSshCas = (projectId: string) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspaceSshCas(projectId), + queryFn: async () => { + const { + data: { cas } + } = await apiRequest.get<{ cas: Omit[] }>( + `/api/v2/workspace/${projectId}/ssh-cas` + ); + return cas; + }, + enabled: Boolean(projectId) + }); +}; + +export const useListWorkspaceSshCertificateTemplates = (projectId: string) => { + return useQuery({ + queryKey: workspaceKeys.getWorkspaceSshCertificateTemplates(projectId), + queryFn: async () => { + const { data } = await apiRequest.get<{ certificateTemplates: TSshCertificateTemplate[] }>( + `/api/v2/workspace/${projectId}/ssh-certificate-templates` + ); + return data; + }, + enabled: Boolean(projectId) + }); +}; + export const useGetWorkspaceSlackConfig = ({ workspaceId }: { workspaceId: string }) => { return useQuery({ queryKey: workspaceKeys.getWorkspaceSlackConfig(workspaceId), diff --git a/frontend-v2/src/hooks/api/workspace/query-keys.tsx b/frontend-v2/src/hooks/api/workspace/query-keys.tsx index a1c5770e9..1005fe8bc 100644 --- a/frontend-v2/src/hooks/api/workspace/query-keys.tsx +++ b/frontend-v2/src/hooks/api/workspace/query-keys.tsx @@ -53,5 +53,19 @@ export const workspaceKeys = { getWorkspaceCertificateTemplates: (workspaceId: string) => [{ workspaceId }, "workspace-certificate-templates"] as const, getWorkspaceSlackConfig: (workspaceId: string) => - [{ workspaceId }, "workspace-slack-config"] as const + [{ workspaceId }, "workspace-slack-config"] as const, + getWorkspaceSshCas: (projectId: string) => [{ projectId }, "workspace-ssh-cas"] as const, + allWorkspaceSshCertificates: (projectId: string) => + [{ projectId }, "workspace-ssh-certificates"] as const, + specificWorkspaceSshCertificates: ({ + offset, + limit, + projectId + }: { + offset: number; + limit: number; + projectId: string; + }) => [...workspaceKeys.allWorkspaceSshCertificates(projectId), { offset, limit }] as const, + getWorkspaceSshCertificateTemplates: (projectId: string) => + [{ projectId }, "workspace-ssh-certificate-templates"] as const }; diff --git a/frontend-v2/src/hooks/api/workspace/types.ts b/frontend-v2/src/hooks/api/workspace/types.ts index 558982d80..0510bdbe7 100644 --- a/frontend-v2/src/hooks/api/workspace/types.ts +++ b/frontend-v2/src/hooks/api/workspace/types.ts @@ -11,7 +11,8 @@ export enum ProjectVersion { export enum ProjectType { SecretManager = "secret-manager", CertificateManager = "cert-manager", - KMS = "kms" + KMS = "kms", + SSH = "ssh" } export enum ProjectUserMembershipTemporaryMode { diff --git a/frontend-v2/src/layouts/OrganizationLayout/OrganizationLayout.tsx b/frontend-v2/src/layouts/OrganizationLayout/OrganizationLayout.tsx index e0a596d77..616ee3d01 100644 --- a/frontend-v2/src/layouts/OrganizationLayout/OrganizationLayout.tsx +++ b/frontend-v2/src/layouts/OrganizationLayout/OrganizationLayout.tsx @@ -92,6 +92,16 @@ export const OrganizationLayout = () => { )} + + {({ isActive }) => ( + + SSH + + )} + {({ isActive }) => ( diff --git a/frontend-v2/src/layouts/ProjectLayout/ProjectLayout.tsx b/frontend-v2/src/layouts/ProjectLayout/ProjectLayout.tsx index 3f34cb0c3..641d3b8c2 100644 --- a/frontend-v2/src/layouts/ProjectLayout/ProjectLayout.tsx +++ b/frontend-v2/src/layouts/ProjectLayout/ProjectLayout.tsx @@ -78,6 +78,7 @@ export const ProjectLayout = () => { const isSecretManager = currentWorkspace?.type === ProjectType.SecretManager; const isCertManager = currentWorkspace?.type === ProjectType.CertificateManager; const isCmek = currentWorkspace?.type === ProjectType.KMS; + const isSSH = currentWorkspace?.type === ProjectType.SSH; return ( <> @@ -133,6 +134,20 @@ export const ProjectLayout = () => { )} )} + {isSSH && ( + + {({ isActive }) => ( + + Overview + + )} + + )} { if (type === ProjectType.SecretManager) return "Secret Management"; if (type === ProjectType.CertificateManager) return "Cert Management"; - return "Key Management"; + if (type === ProjectType.KMS) return "Key Management"; + return "SSH"; }; const formatDescription = (type: ProjectType) => { @@ -61,7 +62,9 @@ const formatDescription = (type: ProjectType) => { return "Securely store, manage, and rotate various application secrets, such as database credentials, API keys, etc."; if (type === ProjectType.CertificateManager) return "Manage your PKI infrastructure and issue digital certificates for services, applications, and devices."; - return "Centralize the management of keys for cryptographic operations, such as encryption and decryption."; + if (type === ProjectType.KMS) + return "Centralize the management of keys for cryptographic operations, such as encryption and decryption."; + return "Generate SSH credentials to provide secure and centralized SSH access control for your infrastructure."; }; type Props = { diff --git a/frontend-v2/src/pages/organization/SshOverviewPage/SshOverviewPage.tsx b/frontend-v2/src/pages/organization/SshOverviewPage/SshOverviewPage.tsx new file mode 100644 index 000000000..e381b6fc0 --- /dev/null +++ b/frontend-v2/src/pages/organization/SshOverviewPage/SshOverviewPage.tsx @@ -0,0 +1,5 @@ +import { ProjectType } from "@app/hooks/api/workspace/types"; + +import { ProductOverviewPage } from "../SecretManagerOverviewPage/SecretManagerOverviewPage"; + +export const SshOverviewPage = () => ; diff --git a/frontend-v2/src/pages/organization/SshOverviewPage/route.tsx b/frontend-v2/src/pages/organization/SshOverviewPage/route.tsx new file mode 100644 index 000000000..8d08e049d --- /dev/null +++ b/frontend-v2/src/pages/organization/SshOverviewPage/route.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from '@tanstack/react-router' + +import { SshOverviewPage } from './SshOverviewPage' + +export const Route = createFileRoute( + '/_authenticate/_inject-org-details/organization/_layout/ssh/overview', +)({ + component: SshOverviewPage, +}) diff --git a/frontend-v2/src/pages/project/AccessControlPage/route-ssh.tsx b/frontend-v2/src/pages/project/AccessControlPage/route-ssh.tsx new file mode 100644 index 000000000..49e53aa63 --- /dev/null +++ b/frontend-v2/src/pages/project/AccessControlPage/route-ssh.tsx @@ -0,0 +1,18 @@ +import { createFileRoute } from "@tanstack/react-router"; +import { zodValidator } from "@tanstack/zod-adapter"; +import { z } from "zod"; + +import { ProjectAccessControlTabs } from "@app/types/project"; + +import { AccessControlPage } from "./AccessControlPage"; + +const AccessControlPageQuerySchema = z.object({ + selectedTab: z.nativeEnum(ProjectAccessControlTabs).catch(ProjectAccessControlTabs.Member) +}); + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/access-management" +)({ + component: AccessControlPage, + validateSearch: zodValidator(AccessControlPageQuerySchema) +}); diff --git a/frontend-v2/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx b/frontend-v2/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx new file mode 100644 index 000000000..8159205c9 --- /dev/null +++ b/frontend-v2/src/pages/project/IdentityDetailsByIDPage/route-ssh.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { IdentityDetailsByIDPage } from "./IdentityDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/identities/$identityId" +)({ + component: IdentityDetailsByIDPage +}); diff --git a/frontend-v2/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx b/frontend-v2/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx new file mode 100644 index 000000000..94b597179 --- /dev/null +++ b/frontend-v2/src/pages/project/MemberDetailsByIDPage/route-ssh.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { MemberDetailsByIDPage } from "./MemberDetailsByIDPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/members/$membershipId" +)({ + component: MemberDetailsByIDPage +}); diff --git a/frontend-v2/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx b/frontend-v2/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx index e7f4fc91a..3a109ceb0 100644 --- a/frontend-v2/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx +++ b/frontend-v2/src/pages/project/RoleDetailsBySlugPage/components/ProjectRoleModifySection.utils.tsx @@ -126,6 +126,11 @@ export const projectRoleFormSchema = z.object({ [ProjectPermissionSub.PkiAlerts]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.PkiCollections]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.CertificateTemplates]: GeneralPolicyActionSchema.array().default([]), + [ProjectPermissionSub.SshCertificateAuthorities]: GeneralPolicyActionSchema.array().default( + [] + ), + [ProjectPermissionSub.SshCertificates]: GeneralPolicyActionSchema.array().default([]), + [ProjectPermissionSub.SshCertificateTemplates]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretApproval]: GeneralPolicyActionSchema.array().default([]), [ProjectPermissionSub.SecretRollback]: SecretRollbackPolicyActionSchema.array().default([]), [ProjectPermissionSub.Project]: WorkspacePolicyActionSchema.array().default([]), @@ -596,6 +601,33 @@ export const PROJECT_PERMISSION_OBJECT: TProjectPermissionObject = { { label: "Remove", value: "delete" } ] }, + [ProjectPermissionSub.SshCertificateAuthorities]: { + title: "SSH Certificate Authorities", + actions: [ + { label: "Read", value: "read" }, + { label: "Create", value: "create" }, + { label: "Modify", value: "edit" }, + { label: "Remove", value: "delete" } + ] + }, + [ProjectPermissionSub.SshCertificates]: { + title: "SSH Certificates", + actions: [ + { label: "Read", value: "read" }, + { label: "Create", value: "create" }, + { label: "Modify", value: "edit" }, + { label: "Remove", value: "delete" } + ] + }, + [ProjectPermissionSub.SshCertificateTemplates]: { + title: "SSH Certificate Templates", + actions: [ + { label: "Read", value: "read" }, + { label: "Create", value: "create" }, + { label: "Modify", value: "edit" }, + { label: "Remove", value: "delete" } + ] + }, [ProjectPermissionSub.PkiCollections]: { title: "PKI Collections", actions: [ diff --git a/frontend-v2/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx b/frontend-v2/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx new file mode 100644 index 000000000..73b5826d4 --- /dev/null +++ b/frontend-v2/src/pages/project/RoleDetailsBySlugPage/route-ssh.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { RoleDetailsBySlugPage } from "./RoleDetailsBySlugPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/roles/$roleSlug" +)({ + component: RoleDetailsBySlugPage +}); diff --git a/frontend-v2/src/pages/ssh/OverviewPage/OverviewPage.tsx b/frontend-v2/src/pages/ssh/OverviewPage/OverviewPage.tsx new file mode 100644 index 000000000..13c3d8ea3 --- /dev/null +++ b/frontend-v2/src/pages/ssh/OverviewPage/OverviewPage.tsx @@ -0,0 +1,75 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; +import { motion } from "framer-motion"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; + +import { SshCaSection, SshCertificatesSection } from "./components"; + +enum TabSections { + SshCa = "ssh-certificate-authorities", + SshCertificates = "ssh-certificates" +} + +export const OverviewPage = () => { + const { t } = useTranslation(); + return ( + <> + + {t("common.head-title", { title: "Certificates" })} + + +
+
+
+

SSH

+ + + SSH Certificates + Certificate Authorities + + + + + + + + + + + + + + + + +
+
+
+ + ); +}; diff --git a/frontend-v2/src/pages/ssh/OverviewPage/components/SshCaModal.tsx b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCaModal.tsx new file mode 100644 index 000000000..4ddbacaba --- /dev/null +++ b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCaModal.tsx @@ -0,0 +1,198 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useNavigate } from "@tanstack/react-router"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useCreateSshCa, useGetSshCaById, useUpdateSshCa } from "@app/hooks/api"; +import { certKeyAlgorithms } from "@app/hooks/api/certificates/constants"; +import { CertKeyAlgorithm } from "@app/hooks/api/certificates/enums"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + popUp: UsePopUpState<["sshCa"]>; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["sshCa"]>, state?: boolean) => void; +}; + +const schema = z + .object({ + friendlyName: z.string(), + keyAlgorithm: z.enum([ + CertKeyAlgorithm.RSA_2048, + CertKeyAlgorithm.RSA_4096, + CertKeyAlgorithm.ECDSA_P256, + CertKeyAlgorithm.ECDSA_P384 + ]) + }) + .required(); + +export type FormData = z.infer; + +export const SshCaModal = ({ popUp, handlePopUpToggle }: Props) => { + const navigate = useNavigate(); + const { currentWorkspace } = useWorkspace(); + const projectId = currentWorkspace?.id || ""; + const { data: ca } = useGetSshCaById((popUp?.sshCa?.data as { caId: string })?.caId || ""); + + const { mutateAsync: createMutateAsync } = useCreateSshCa(); + const { mutateAsync: updateMutateAsync } = useUpdateSshCa(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + friendlyName: "", + keyAlgorithm: CertKeyAlgorithm.RSA_2048 + } + }); + + useEffect(() => { + if (ca) { + reset({ + friendlyName: ca.friendlyName, + keyAlgorithm: ca.keyAlgorithm + }); + } else { + reset({ + friendlyName: "", + keyAlgorithm: CertKeyAlgorithm.RSA_2048 + }); + } + }, [ca]); + + const onFormSubmit = async ({ friendlyName, keyAlgorithm }: FormData) => { + try { + if (!projectId) return; + + if (ca) { + await updateMutateAsync({ + caId: ca.id, + friendlyName + }); + } else { + const { id: newCaId } = await createMutateAsync({ + projectId, + friendlyName, + keyAlgorithm + }); + + navigate({ + to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const, + params: { + projectId, + caId: newCaId + } + }); + } + + reset(); + handlePopUpToggle("sshCa", false); + + createNotification({ + text: `Successfully ${ca ? "updated" : "created"} SSH CA`, + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to create SSH CA", + type: "error" + }); + } + }; + + return ( + { + reset(); + handlePopUpToggle("sshCa", isOpen); + }} + > + +
+ {ca && ( + + + + )} + ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +}; diff --git a/frontend-v2/src/pages/ssh/OverviewPage/components/SshCaSection.tsx b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCaSection.tsx new file mode 100644 index 000000000..e51bff42d --- /dev/null +++ b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCaSection.tsx @@ -0,0 +1,120 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, DeleteActionModal } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { SshCaStatus, useDeleteSshCa, useUpdateSshCa } from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { SshCaModal } from "./SshCaModal"; +import { SshCaTable } from "./SshCaTable"; + +export const SshCaSection = () => { + const { mutateAsync: deleteSshCa } = useDeleteSshCa(); + const { mutateAsync: updateSshCa } = useUpdateSshCa(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "sshCa", + "deleteSshCa", + "sshCaStatus", // enable / disable + "upgradePlan" + ] as const); + + const onRemoveSshCaSubmit = async (caId: string) => { + try { + await deleteSshCa({ caId }); + + createNotification({ + text: "Successfully deleted SSH CA", + type: "success" + }); + + handlePopUpClose("deleteSshCa"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete SSH CA", + type: "error" + }); + } + }; + + const onUpdateSshCaStatus = async ({ caId, status }: { caId: string; status: SshCaStatus }) => { + try { + await updateSshCa({ caId, status }); + + createNotification({ + text: `Successfully ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`, + type: "success" + }); + + handlePopUpClose("sshCaStatus"); + } catch (err) { + console.error(err); + createNotification({ + text: `Failed to ${status === SshCaStatus.ACTIVE ? "enabled" : "disabled"} SSH CA`, + type: "error" + }); + } + }; + + return ( +
+
+

Certificate Authorities

+ + {(isAllowed) => ( + + )} + +
+ + + handlePopUpToggle("deleteSshCa", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onRemoveSshCaSubmit((popUp?.deleteSshCa?.data as { caId: string })?.caId) + } + /> + handlePopUpToggle("sshCaStatus", isOpen)} + deleteKey="confirm" + onDeleteApproved={() => + onUpdateSshCaStatus(popUp?.sshCaStatus?.data as { caId: string; status: SshCaStatus }) + } + /> + {/* handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> */} +
+ ); +}; diff --git a/frontend-v2/src/pages/ssh/OverviewPage/components/SshCaTable.tsx b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCaTable.tsx new file mode 100644 index 000000000..3f97e4e1c --- /dev/null +++ b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCaTable.tsx @@ -0,0 +1,160 @@ +import { faBan, faCertificate, faEllipsis, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useNavigate } from "@tanstack/react-router"; +import { twMerge } from "tailwind-merge"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { + Badge, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tooltip, + Tr +} from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { SshCaStatus, useListWorkspaceSshCas } from "@app/hooks/api"; +import { caStatusToNameMap, getCaStatusBadgeVariant } from "@app/hooks/api/ca/constants"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +type Props = { + handlePopUpOpen: ( + popUpName: keyof UsePopUpState<["deleteSshCa", "sshCaStatus"]>, + data?: object + ) => void; +}; + +export const SshCaTable = ({ handlePopUpOpen }: Props) => { + const navigate = useNavigate(); + const { currentWorkspace } = useWorkspace(); + const { data, isPending } = useListWorkspaceSshCas(currentWorkspace?.id || ""); + + return ( +
+ + + + + + + + + + {isPending && } + {!isPending && + data && + data.length > 0 && + data.map((ca) => { + return ( + + navigate({ + to: `/${ProjectType.SSH}/$projectId/ca/$caId` as const, + params: { + projectId: currentWorkspace.id, + caId: ca.id + } + }) + } + > + + + + + ); + })} + +
Friendly NameStatus +
{ca.friendlyName} + + {caStatusToNameMap[ca.status]} + + + + +
+ + + +
+
+ + {(ca.status === SshCaStatus.ACTIVE || + ca.status === SshCaStatus.DISABLED) && ( + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("sshCaStatus", { + caId: ca.id, + status: + ca.status === SshCaStatus.ACTIVE + ? SshCaStatus.DISABLED + : SshCaStatus.ACTIVE + }); + }} + disabled={!isAllowed} + icon={} + > + {`${ + ca.status === SshCaStatus.ACTIVE ? "Disable" : "Enable" + } SSH CA`} + + )} + + )} + + {(isAllowed) => ( + { + e.stopPropagation(); + handlePopUpOpen("deleteSshCa", { + caId: ca.id + }); + }} + disabled={!isAllowed} + icon={} + > + Delete SSH CA + + )} + + +
+
+ {!isPending && data?.length === 0 && ( + + )} +
+
+ ); +}; diff --git a/frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesSection.tsx b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesSection.tsx new file mode 100644 index 000000000..680b0d487 --- /dev/null +++ b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesSection.tsx @@ -0,0 +1,39 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { SshCertificateModal } from "../../SshCaByIDPage/components/SshCertificateModal"; +import { SshCertificatesTable } from "./SshCertificatesTable"; + +export const SshCertificatesSection = () => { + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["sshCertificate"] as const); + return ( +
+
+

Certificates

+ + {(isAllowed) => ( + + )} + +
+ + +
+ ); +}; diff --git a/frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesTable.tsx b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesTable.tsx new file mode 100644 index 000000000..158d1de80 --- /dev/null +++ b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesTable.tsx @@ -0,0 +1,87 @@ +import { useState } from "react"; +import { faCertificate } from "@fortawesome/free-solid-svg-icons"; +import { format } from "date-fns"; + +import { + Badge, + EmptyState, + Pagination, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { useWorkspace } from "@app/context"; +import { useListWorkspaceSshCertificates } from "@app/hooks/api"; + +import { getSshCertStatusBadgeDetails } from "./SshCertificatesTable.utils"; + +const PER_PAGE_INIT = 25; + +export const SshCertificatesTable = () => { + const { currentWorkspace } = useWorkspace(); + const [page, setPage] = useState(1); + const [perPage, setPerPage] = useState(PER_PAGE_INIT); + + const { data, isPending } = useListWorkspaceSshCertificates({ + projectId: currentWorkspace?.id || "", + offset: (page - 1) * perPage, + limit: perPage + }); + + return ( + + + + + + + + + + + + {isPending && } + {!isPending && + data?.certificates?.map((certificate) => { + const { variant, label } = getSshCertStatusBadgeDetails(certificate.notAfter); + return ( + + + + + + + ); + })} + +
PrincipalsStatusNot BeforeNot After
{certificate.principals.join(", ")} + {label} + + {certificate.notBefore + ? format(new Date(certificate.notBefore), "yyyy-MM-dd") + : "-"} + + {certificate.notAfter + ? format(new Date(certificate.notAfter), "yyyy-MM-dd") + : "-"} +
+ {!isPending && data?.totalCount !== undefined && data.totalCount >= PER_PAGE_INIT && ( + setPage(newPage)} + onChangePerPage={(newPerPage) => setPerPage(newPerPage)} + /> + )} + {!isPending && !data?.certificates?.length && ( + + )} +
+ ); +}; diff --git a/frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesTable.utils.ts b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesTable.utils.ts new file mode 100644 index 000000000..12b75aa08 --- /dev/null +++ b/frontend-v2/src/pages/ssh/OverviewPage/components/SshCertificatesTable.utils.ts @@ -0,0 +1,17 @@ +export const getSshCertStatusBadgeDetails = (notAfter: string) => { + const currentDate = new Date().getTime(); + const notAfterDate = new Date(notAfter).getTime(); + + let variant: "success" | "primary" | "danger" = "success"; + let label = "Active"; + + if (notAfterDate > currentDate) { + variant = "success"; + label = "Active"; + } else { + variant = "danger"; + label = "Expired"; + } + + return { variant, label }; +}; diff --git a/frontend-v2/src/pages/ssh/OverviewPage/components/index.tsx b/frontend-v2/src/pages/ssh/OverviewPage/components/index.tsx new file mode 100644 index 000000000..efb307725 --- /dev/null +++ b/frontend-v2/src/pages/ssh/OverviewPage/components/index.tsx @@ -0,0 +1,2 @@ +export { SshCaSection } from "./SshCaSection"; +export { SshCertificatesSection } from "./SshCertificatesSection"; diff --git a/frontend-v2/src/pages/ssh/OverviewPage/route.tsx b/frontend-v2/src/pages/ssh/OverviewPage/route.tsx new file mode 100644 index 000000000..427492518 --- /dev/null +++ b/frontend-v2/src/pages/ssh/OverviewPage/route.tsx @@ -0,0 +1,9 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { OverviewPage } from "./OverviewPage"; + +export const Route = createFileRoute( + "/_authenticate/_inject-org-details/ssh/$projectId/_ssh-layout/overview" +)({ + component: OverviewPage +}); diff --git a/frontend-v2/src/pages/ssh/SettingsPage/SettingsPage.tsx b/frontend-v2/src/pages/ssh/SettingsPage/SettingsPage.tsx new file mode 100644 index 000000000..674b97009 --- /dev/null +++ b/frontend-v2/src/pages/ssh/SettingsPage/SettingsPage.tsx @@ -0,0 +1,39 @@ +import { Helmet } from "react-helmet"; +import { useTranslation } from "react-i18next"; + +import { Tab, TabList, TabPanel, Tabs } from "@app/components/v2"; + +import { ProjectGeneralTab } from "./components/ProjectGeneralTab"; + +const tabs = [{ name: "General", key: "tab-project-general", Component: ProjectGeneralTab }]; + +export const SettingsPage = () => { + const { t } = useTranslation(); + + return ( +
+ + {t("common.head-title", { title: t("settings.project.title") })} + +
+
+

{t("settings.project.title")}

+
+ + + {tabs.map((tab) => ( + + {tab.name} + + ))} + + {tabs.map(({ key, Component }) => ( + + + + ))} + +
+
+ ); +}; diff --git a/frontend-v2/src/pages/ssh/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx b/frontend-v2/src/pages/ssh/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx new file mode 100644 index 000000000..7c65b1857 --- /dev/null +++ b/frontend-v2/src/pages/ssh/SettingsPage/components/AuditLogsRetentionSection/AuditLogsRetentionSection.tsx @@ -0,0 +1,129 @@ +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal"; +import { createNotification } from "@app/components/notifications"; +import { Button, FormControl, Input } from "@app/components/v2"; +import { useProjectPermission, useSubscription, useWorkspace } from "@app/context"; +import { usePopUp } from "@app/hooks"; +import { ProjectMembershipRole } from "@app/hooks/api/roles/types"; +import { useUpdateWorkspaceAuditLogsRetention } from "@app/hooks/api/workspace/queries"; + +const formSchema = z.object({ + auditLogsRetentionDays: z.coerce.number().min(0) +}); + +type TForm = z.infer; + +export const AuditLogsRetentionSection = () => { + const { mutateAsync: updateAuditLogsRetention } = useUpdateWorkspaceAuditLogsRetention(); + + const { currentWorkspace } = useWorkspace(); + const { membership } = useProjectPermission(); + const { subscription } = useSubscription(); + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp(["upgradePlan"] as const); + + const { + control, + formState: { isSubmitting, isDirty }, + handleSubmit + } = useForm({ + resolver: zodResolver(formSchema), + values: { + auditLogsRetentionDays: + currentWorkspace?.auditLogsRetentionDays ?? subscription?.auditLogsRetentionDays ?? 0 + } + }); + + if (!currentWorkspace) return null; + + const handleAuditLogsRetentionSubmit = async ({ auditLogsRetentionDays }: TForm) => { + try { + if (!subscription?.auditLogs) { + handlePopUpOpen("upgradePlan", { + description: "You can only configure audit logs retention if you upgrade your plan." + }); + + return; + } + + if (subscription && auditLogsRetentionDays > subscription?.auditLogsRetentionDays) { + handlePopUpOpen("upgradePlan", { + description: + "To update your audit logs retention period to a higher value, upgrade your plan." + }); + + return; + } + + await updateAuditLogsRetention({ + auditLogsRetentionDays, + projectSlug: currentWorkspace.slug + }); + + createNotification({ + text: "Successfully updated audit logs retention period", + type: "success" + }); + } catch { + createNotification({ + text: "Failed updating audit logs retention period", + type: "error" + }); + } + }; + + // render only for dedicated/self-hosted instances of Infisical + if ( + window.location.origin.includes("https://app.infisical.com") || + window.location.origin.includes("https://gamma.infisical.com") + ) { + return null; + } + + const isAdmin = membership.roles.includes(ProjectMembershipRole.Admin); + return ( + <> +
+
+

Audit Logs Retention

+
+

+ Set the number of days to keep your project audit logs. +

+
+
+ ( + + + + )} + /> +
+ +
+
+ handlePopUpToggle("upgradePlan", isOpen)} + text={(popUp.upgradePlan?.data as { description: string })?.description} + /> + + ); +}; diff --git a/frontend-v2/src/pages/ssh/SettingsPage/components/AuditLogsRetentionSection/index.tsx b/frontend-v2/src/pages/ssh/SettingsPage/components/AuditLogsRetentionSection/index.tsx new file mode 100644 index 000000000..38a07f53b --- /dev/null +++ b/frontend-v2/src/pages/ssh/SettingsPage/components/AuditLogsRetentionSection/index.tsx @@ -0,0 +1 @@ +export { AuditLogsRetentionSection } from "./AuditLogsRetentionSection"; diff --git a/frontend-v2/src/pages/ssh/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx b/frontend-v2/src/pages/ssh/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx new file mode 100644 index 000000000..042adff1f --- /dev/null +++ b/frontend-v2/src/pages/ssh/SettingsPage/components/DeleteProjectSection/DeleteProjectSection.tsx @@ -0,0 +1,192 @@ +import { useMemo } from "react"; +import { useNavigate } from "@tanstack/react-router"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, DeleteActionModal } from "@app/components/v2"; +import { LeaveProjectModal } from "@app/components/v2/LeaveProjectModal"; +import { + ProjectPermissionActions, + ProjectPermissionSub, + useOrganization, + useProjectPermission, + useWorkspace +} from "@app/context"; +import { useToggle } from "@app/hooks"; +import { useDeleteWorkspace, useGetWorkspaceUsers, useLeaveProject } from "@app/hooks/api"; +import { ProjectType } from "@app/hooks/api/workspace/types"; +import { usePopUp } from "@app/hooks/usePopUp"; + +export const DeleteProjectSection = () => { + const navigate = useNavigate(); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "deleteWorkspace", + "leaveWorkspace" + ] as const); + + const { currentOrg } = useOrganization(); + const { hasProjectRole, membership } = useProjectPermission(); + const { currentWorkspace } = useWorkspace(); + const [isDeleting, setIsDeleting] = useToggle(); + const [isLeaving, setIsLeaving] = useToggle(); + const deleteWorkspace = useDeleteWorkspace(); + const leaveProject = useLeaveProject(); + const { data: members, isPending: isMembersLoading } = useGetWorkspaceUsers( + currentWorkspace?.id || "" + ); + + // If isNoAccessMember is true, then the user can't read the workspace members. So we need to handle this case separately. + const isNoAccessMember = hasProjectRole("no-access"); + + const isOnlyAdminMember = useMemo(() => { + if (!members || !membership || !hasProjectRole("admin")) return false; + + const adminMembers = members.filter( + (member) => member.roles.map((r) => r.role).includes("admin") && member.id !== membership.id // exclude the current user + ); + + return !adminMembers.length; + }, [members, membership]); + + const handleDeleteWorkspaceSubmit = async () => { + setIsDeleting.on(); + try { + if (!currentWorkspace?.id) return; + + await deleteWorkspace.mutateAsync({ + workspaceID: currentWorkspace?.id + }); + + createNotification({ + text: "Successfully deleted project", + type: "success" + }); + + navigate({ + to: `/organization/${ProjectType.SecretManager}/overview` as const + }); + handlePopUpClose("deleteWorkspace"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to delete project", + type: "error" + }); + } finally { + setIsDeleting.off(); + } + }; + + const handleLeaveWorkspaceSubmit = async () => { + console.log({ + currentWorkspace, + currentOrg, + members, + isNoAccessMember, + membership + }); + + try { + setIsLeaving.on(); + + if (!currentWorkspace?.id || !currentOrg?.id) return; + + // If there's no members, and the user has access to read members, something went wrong. + if (!members && !isNoAccessMember) return; + + // If the user has elevated permissions and can read members: + if (!isNoAccessMember) { + if (!members) return; + + if (members.length < 2) { + createNotification({ + text: "You can't leave the project as you are the only member", + type: "error" + }); + return; + } + // If the user has access to read members, and there's less than 1 admin member excluding the current user, they can't leave the project. + if (isOnlyAdminMember) { + createNotification({ + text: "You can't leave a project with no admin members left. Promote another member to admin first.", + type: "error" + }); + return; + } + } + + // If it's actually a no-access member, then we don't really care about the members. + + await leaveProject.mutateAsync({ + workspaceId: currentWorkspace.id + }); + + navigate({ + to: `/organization/${ProjectType.SecretManager}/overview` as const + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to leave project", + type: "error" + }); + } finally { + setIsLeaving.off(); + } + }; + + return ( +
+

Danger Zone

+
+ + {(isAllowed) => ( + + )} + + {!isOnlyAdminMember && ( + + )} +
+ + handlePopUpToggle("deleteWorkspace", isOpen)} + deleteKey="confirm" + buttonText="Delete Project" + onDeleteApproved={handleDeleteWorkspaceSubmit} + /> + + handlePopUpToggle("leaveWorkspace", isOpen)} + deleteKey="confirm" + buttonText="Leave Project" + onLeaveApproved={handleLeaveWorkspaceSubmit} + /> +
+ ); +}; diff --git a/frontend-v2/src/pages/ssh/SettingsPage/components/DeleteProjectSection/index.tsx b/frontend-v2/src/pages/ssh/SettingsPage/components/DeleteProjectSection/index.tsx new file mode 100644 index 000000000..04a624ec2 --- /dev/null +++ b/frontend-v2/src/pages/ssh/SettingsPage/components/DeleteProjectSection/index.tsx @@ -0,0 +1 @@ +export { DeleteProjectSection } from "./DeleteProjectSection"; diff --git a/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx b/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx new file mode 100644 index 000000000..ef684a21b --- /dev/null +++ b/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/ProjectGeneralTab.tsx @@ -0,0 +1,13 @@ +import { AuditLogsRetentionSection } from "../AuditLogsRetentionSection"; +import { DeleteProjectSection } from "../DeleteProjectSection"; +import { ProjectOverviewChangeSection } from "../ProjectOverviewChangeSection"; + +export const ProjectGeneralTab = () => { + return ( +
+ + + +
+ ); +}; diff --git a/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/index.tsx b/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/index.tsx new file mode 100644 index 000000000..c6e611e0f --- /dev/null +++ b/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectGeneralTab/index.tsx @@ -0,0 +1 @@ +export { ProjectGeneralTab } from "./ProjectGeneralTab"; diff --git a/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx b/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx new file mode 100644 index 000000000..34fe365ec --- /dev/null +++ b/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/CopyButton.tsx @@ -0,0 +1,51 @@ +import { useCallback } from "react"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { createNotification } from "@app/components/notifications"; +import { Button } from "@app/components/v2"; +import { useToggle } from "@app/hooks"; + +type Props = { + value: string; + hoverText: string; + notificationText: string; + children: React.ReactNode; +}; + +export const CopyButton = ({ value, children, hoverText, notificationText }: Props) => { + const [isProjectIdCopied, setIsProjectIdCopied] = useToggle(false); + + const copyToClipboard = useCallback(() => { + if (isProjectIdCopied) { + return; + } + + setIsProjectIdCopied.on(); + navigator.clipboard.writeText(value); + + createNotification({ + text: notificationText, + type: "success" + }); + + const timer = setTimeout(() => setIsProjectIdCopied.off(), 2000); + + // eslint-disable-next-line consistent-return + return () => clearTimeout(timer); + }, [isProjectIdCopied]); + + return ( + + ); +}; diff --git a/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx b/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx new file mode 100644 index 000000000..d82415c0d --- /dev/null +++ b/frontend-v2/src/pages/ssh/SettingsPage/components/ProjectOverviewChangeSection/ProjectOverviewChangeSection.tsx @@ -0,0 +1,168 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { ProjectPermissionCan } from "@app/components/permissions"; +import { Button, FormControl, Input, TextArea } from "@app/components/v2"; +import { ProjectPermissionActions, ProjectPermissionSub, useWorkspace } from "@app/context"; +import { useUpdateProject } from "@app/hooks/api"; + +import { CopyButton } from "./CopyButton"; + +const formSchema = z.object({ + name: z.string().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() +}); + +type FormData = z.infer; + +export const ProjectOverviewChangeSection = () => { + const { currentWorkspace } = useWorkspace(); + const { mutateAsync, isPending } = useUpdateProject(); + + const { handleSubmit, control, reset } = useForm({ resolver: zodResolver(formSchema) }); + + useEffect(() => { + if (currentWorkspace) { + reset({ + name: currentWorkspace.name, + description: currentWorkspace.description ?? "" + }); + } + }, [currentWorkspace]); + + const onFormSubmit = async ({ name, description }: FormData) => { + try { + if (!currentWorkspace?.id) return; + + await mutateAsync({ + projectID: currentWorkspace.id, + newProjectName: name, + newProjectDescription: description + }); + + createNotification({ + text: "Successfully updated project overview", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update project overview", + type: "error" + }); + } + }; + + return ( +
+
+

Project Overview

+
+ + Copy Project Slug + + + Copy Project ID + +
+
+
+
+
+
+ + {(isAllowed) => ( + ( + + + + )} + control={control} + name="name" + /> + )} + +
+
+
+
+ + {(isAllowed) => ( + ( + +