Compare commits

..

5 Commits

Author SHA1 Message Date
dependabot[bot]
97a9e5d9e9 Bump the prod-patch-updates group across 1 directory with 6 updates
Bumps the prod-patch-updates group with 6 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi) | `8.4.0` | `8.4.1` |
| [@react-email/components](https://github.com/resend/react-email/tree/HEAD/packages/components) | `1.0.7` | `1.0.8` |
| [@react-email/tailwind](https://github.com/resend/react-email/tree/HEAD/packages/tailwind) | `2.0.4` | `2.0.5` |
| [glob](https://github.com/isaacs/node-glob) | `13.0.3` | `13.0.5` |
| [next-intl](https://github.com/amannn/next-intl) | `4.8.2` | `4.8.3` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.24.15` | `5.24.17` |



Updates `@asteasolutions/zod-to-openapi` from 8.4.0 to 8.4.1
- [Release notes](https://github.com/asteasolutions/zod-to-openapi/releases)
- [Commits](https://github.com/asteasolutions/zod-to-openapi/compare/v8.4.0...v8.4.1)

Updates `@react-email/components` from 1.0.7 to 1.0.8
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/components/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/components@1.0.8/packages/components)

Updates `@react-email/tailwind` from 2.0.4 to 2.0.5
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/tailwind/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/tailwind@2.0.5/packages/tailwind)

Updates `glob` from 13.0.3 to 13.0.5
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v13.0.3...v13.0.5)

Updates `next-intl` from 4.8.2 to 4.8.3
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.8.2...v4.8.3)

Updates `posthog-node` from 5.24.15 to 5.24.17
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.24.17/packages/node)

---
updated-dependencies:
- dependency-name: "@asteasolutions/zod-to-openapi"
  dependency-version: 8.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/components"
  dependency-version: 1.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: "@react-email/tailwind"
  dependency-version: 2.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: glob
  dependency-version: 13.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: next-intl
  dependency-version: 4.8.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
- dependency-name: posthog-node
  dependency-version: 5.24.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: prod-patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-19 01:37:57 +00:00
Owen
756f3f32ca Merge branch 'dev' 2026-02-17 21:57:49 -08:00
Owen
5987f6b2cd Allow enterprise 2026-02-17 21:55:57 -08:00
Owen
09a9457021 Fix transaction issue 2026-02-17 21:27:23 -08:00
Owen Schwartz
ca4643ec36 Merge pull request #2494 from fosrl/dev
1.15.4-s.3
2026-02-17 21:07:04 -08:00
34 changed files with 784 additions and 1361 deletions

View File

@@ -649,7 +649,7 @@
"resourcesUsersRolesAccess": "User and role-based access control",
"resourcesErrorUpdate": "Failed to toggle resource",
"resourcesErrorUpdateDescription": "An error occurred while updating the resource",
"access": "Access Control",
"access": "Access",
"shareLink": "{resource} Share Link",
"resourceSelect": "Select resource",
"shareLinks": "Share Links",
@@ -790,7 +790,6 @@
"accessRoleRemoved": "Role removed",
"accessRoleRemovedDescription": "The role has been successfully removed.",
"accessRoleRequiredRemove": "Before deleting this role, please select a new role to transfer existing members to.",
"network": "Network",
"manage": "Manage",
"sitesNotFound": "No sites found.",
"pangolinServerAdmin": "Server Admin - Pangolin",
@@ -1268,7 +1267,6 @@
"sidebarLogAndAnalytics": "Log & Analytics",
"sidebarBluePrints": "Blueprints",
"sidebarOrganization": "Organization",
"sidebarManagement": "Management",
"sidebarBillingAndLicenses": "Billing & Licenses",
"sidebarLogsAnalytics": "Analytics",
"blueprints": "Blueprints",
@@ -1645,24 +1643,6 @@
"timeIsInSeconds": "Time is in seconds",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
"sshAccess": "SSH Access",
"roleAllowSsh": "Allow SSH",
"roleAllowSshAllow": "Allow",
"roleAllowSshDisallow": "Disallow",
"roleAllowSshDescription": "Allow users with this role to connect to resources via SSH. When disabled, the role cannot use SSH access.",
"sshSudoMode": "Sudo Access",
"sshSudoModeNone": "None",
"sshSudoModeNoneDescription": "User cannot run commands with sudo.",
"sshSudoModeFull": "Full Sudo",
"sshSudoModeFullDescription": "User can run any command with sudo.",
"sshSudoModeCommands": "Commands",
"sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.",
"sshSudo": "Allow sudo",
"sshSudoCommands": "Sudo Commands",
"sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo.",
"sshCreateHomeDir": "Create Home Directory",
"sshUnixGroups": "Unix Groups",
"sshUnixGroupsDescription": "Unix groups to add the user to on the target host.",
"retryAttempts": "Retry Attempts",
"expectedResponseCodes": "Expected Response Codes",
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",

189
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "SEE LICENSE IN LICENSE AND README.md",
"dependencies": {
"@asteasolutions/zod-to-openapi": "8.4.0",
"@asteasolutions/zod-to-openapi": "8.4.1",
"@aws-sdk/client-s3": "3.989.0",
"@faker-js/faker": "10.3.0",
"@headlessui/react": "2.2.9",
@@ -36,9 +36,9 @@
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "1.2.8",
"@react-email/components": "1.0.7",
"@react-email/components": "1.0.8",
"@react-email/render": "2.0.4",
"@react-email/tailwind": "2.0.4",
"@react-email/tailwind": "2.0.5",
"@simplewebauthn/browser": "13.2.2",
"@simplewebauthn/server": "13.2.2",
"@tailwindcss/forms": "0.5.11",
@@ -58,7 +58,7 @@
"drizzle-orm": "0.45.1",
"express": "5.2.1",
"express-rate-limit": "8.2.1",
"glob": "13.0.3",
"glob": "13.0.5",
"helmet": "8.1.0",
"http-errors": "2.0.1",
"input-otp": "1.4.2",
@@ -70,14 +70,14 @@
"maxmind": "5.0.5",
"moment": "2.30.1",
"next": "15.5.12",
"next-intl": "4.8.2",
"next-intl": "4.8.3",
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
"node-cache": "5.1.2",
"nodemailer": "8.0.1",
"oslo": "1.2.1",
"pg": "8.18.0",
"posthog-node": "5.24.15",
"posthog-node": "5.24.17",
"qrcode.react": "4.2.0",
"react": "19.2.4",
"react-day-picker": "9.13.2",
@@ -179,9 +179,9 @@
}
},
"node_modules/@asteasolutions/zod-to-openapi": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.4.0.tgz",
"integrity": "sha512-Ckp971tmTw4pnv+o7iK85ldBHBKk6gxMaoNyLn3c2Th/fKoTG8G3jdYuOanpdGqwlDB0z01FOjry2d32lfTqrA==",
"version": "8.4.1",
"resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.4.1.tgz",
"integrity": "sha512-WmJUsFINbnWxGvHSd16aOjgKf+5GsfdxruO2YDLcgplsidakCauik1lhlk83YDH06265Yd1XtUyF24o09uygpw==",
"license": "MIT",
"dependencies": {
"openapi3-ts": "^4.1.2"
@@ -1086,7 +1086,6 @@
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
@@ -2662,16 +2661,6 @@
"tslib": "^2.8.1"
}
},
"node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz",
"integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "3.1.0",
"tslib": "^2.8.1"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz",
@@ -2703,12 +2692,13 @@
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.5.10",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz",
"integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==",
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz",
"integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==",
"license": "MIT",
"dependencies": {
"tslib": "2"
"@formatjs/fast-memoize": "3.1.0",
"tslib": "^2.8.1"
}
},
"node_modules/@headlessui/react": {
@@ -2818,7 +2808,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2841,7 +2830,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -2864,7 +2852,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2881,7 +2868,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2898,7 +2884,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2915,7 +2900,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2932,7 +2916,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2949,7 +2932,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2966,7 +2948,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -2983,7 +2964,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3000,7 +2980,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3017,7 +2996,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3040,7 +3018,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3063,7 +3040,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3086,7 +3062,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3109,7 +3084,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3132,7 +3106,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3155,7 +3128,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3178,7 +3150,6 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
@@ -3198,7 +3169,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3218,7 +3188,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3238,7 +3207,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3284,6 +3252,7 @@
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
"dev": true,
"engines": {
"node": ">=18"
}
@@ -3520,7 +3489,6 @@
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": "^14.21.3 || >=16"
},
@@ -4653,9 +4621,10 @@
}
},
"node_modules/@posthog/core": {
"version": "1.22.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.22.0.tgz",
"integrity": "sha512-WkmOnq95aAOu6yk6r5LWr5cfXsQdpVbWDCwOxQwxSne8YV6GuZET1ziO5toSQXgrgbdcjrSz2/GopAfiL6iiAA==",
"version": "1.23.1",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.1.tgz",
"integrity": "sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6"
}
@@ -6959,9 +6928,9 @@
}
},
"node_modules/@react-email/components": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.7.tgz",
"integrity": "sha512-mY+v4C1SMaGOKuKp0QWDQLGK+3fvH06ZE10EVavv+T6tQneDHq9cpQ9NdCrvuO1nWZnWrA/0tRpvyqyF0uo93w==",
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.8.tgz",
"integrity": "sha512-zY81ED6o5MWMzBkr9uZFuT24lWarT+xIbOZxI6C9dsFmCWBczM8IE1BgOI8rhpUK4JcYVDy1uKxYAFqsx2Bc4w==",
"license": "MIT",
"dependencies": {
"@react-email/body": "0.2.1",
@@ -6982,7 +6951,7 @@
"@react-email/render": "2.0.4",
"@react-email/row": "0.0.13",
"@react-email/section": "0.0.17",
"@react-email/tailwind": "2.0.4",
"@react-email/tailwind": "2.0.5",
"@react-email/text": "0.1.6"
},
"engines": {
@@ -7888,9 +7857,9 @@
}
},
"node_modules/@react-email/tailwind": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.4.tgz",
"integrity": "sha512-cDp8Ss6LJKI8zBLKE+tsXFurn6I2nnQNg1qqjfZuNPNoToN1Uyx3egW0bwSVk1JjrNWx/Xnme7ZxvNLRrU9K0Q==",
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.5.tgz",
"integrity": "sha512-7Ey+kiWliJdxPMCLYsdDts8ffp4idlP//w4Ui3q/A5kokVaLSNKG8DOg/8qAuzWmRiGwNQVOKBk7PXNlK5W+sg==",
"license": "MIT",
"dependencies": {
"tailwindcss": "^4.1.18"
@@ -7950,7 +7919,6 @@
"resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.6.tgz",
"integrity": "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=20.0.0"
},
@@ -9369,7 +9337,6 @@
"version": "5.90.21",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
"peer": true,
"dependencies": {
"@tanstack/query-core": "5.90.20"
},
@@ -9485,7 +9452,6 @@
"integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "*"
}
@@ -9826,7 +9792,6 @@
"integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0",
@@ -9921,7 +9886,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz",
"integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==",
"devOptional": true,
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -9949,7 +9913,6 @@
"integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
@@ -9975,7 +9938,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"devOptional": true,
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -9986,7 +9948,6 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -10073,7 +10034,8 @@
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
"optional": true,
"peer": true
},
"node_modules/@types/ws": {
"version": "8.18.1",
@@ -10144,7 +10106,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz",
"integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==",
"dev": true,
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.55.0",
"@typescript-eslint/types": "8.55.0",
@@ -10634,7 +10595,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -11099,7 +11059,6 @@
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/types": "^7.26.0"
}
@@ -11166,7 +11125,6 @@
"integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
@@ -11293,7 +11251,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -12247,7 +12204,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"
}
@@ -12688,6 +12644,7 @@
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz",
"integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==",
"license": "(MPL-2.0 OR Apache-2.0)",
"peer": true,
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -13801,7 +13758,6 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -13900,7 +13856,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -14086,7 +14041,6 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -14406,7 +14360,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -15063,11 +15016,12 @@
"license": "MIT"
},
"node_modules/glob": {
"version": "13.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.3.tgz",
"integrity": "sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==",
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.5.tgz",
"integrity": "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.2.0",
"minimatch": "^10.2.1",
"minipass": "^7.1.2",
"path-scurry": "^2.0.0"
},
@@ -15092,12 +15046,10 @@
}
},
"node_modules/glob/node_modules/balanced-match": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz",
"integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==",
"dependencies": {
"jackspeak": "^4.2.3"
},
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.3.tgz",
"integrity": "sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==",
"license": "MIT",
"engines": {
"node": "20 || >=22"
}
@@ -15106,6 +15058,7 @@
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz",
"integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
@@ -15114,9 +15067,10 @@
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "10.2.0",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz",
"integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==",
"version": "10.2.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.1.tgz",
"integrity": "sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==",
"license": "BlueOak-1.0.0",
"dependencies": {
"brace-expansion": "^5.0.2"
},
@@ -15389,9 +15343,9 @@
}
},
"node_modules/icu-minify": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.2.tgz",
"integrity": "sha512-LHBQV+skKkjZSPd590pZ7ZAHftUgda3eFjeuNwA8/15L8T8loCNBktKQyTlkodAU86KovFXeg/9WntlAo5wA5A==",
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.3.tgz",
"integrity": "sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==",
"funding": [
{
"type": "individual",
@@ -16028,6 +15982,7 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^9.0.0"
},
@@ -16916,6 +16871,7 @@
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
@@ -16926,6 +16882,7 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
"peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -17013,7 +16970,6 @@
"version": "15.5.12",
"resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz",
"integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==",
"peer": true,
"dependencies": {
"@next/env": "15.5.12",
"@swc/helpers": "0.5.15",
@@ -17062,9 +17018,9 @@
}
},
"node_modules/next-intl": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.2.tgz",
"integrity": "sha512-GuuwyvyEI49/oehQbBXEoY8KSIYCzmfMLhmIwhMXTb+yeBmly1PnJcpgph3KczQ+HTJMXwXCmkizgtT8jBMf3A==",
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.3.tgz",
"integrity": "sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==",
"funding": [
{
"type": "individual",
@@ -17073,14 +17029,14 @@
],
"license": "MIT",
"dependencies": {
"@formatjs/intl-localematcher": "^0.5.4",
"@formatjs/intl-localematcher": "^0.8.1",
"@parcel/watcher": "^2.4.1",
"@swc/core": "^1.15.2",
"icu-minify": "^4.8.2",
"icu-minify": "^4.8.3",
"negotiator": "^1.0.0",
"next-intl-swc-plugin-extractor": "^4.8.2",
"next-intl-swc-plugin-extractor": "^4.8.3",
"po-parser": "^2.1.1",
"use-intl": "^4.8.2"
"use-intl": "^4.8.3"
},
"peerDependencies": {
"next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0",
@@ -17094,9 +17050,9 @@
}
},
"node_modules/next-intl-swc-plugin-extractor": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.2.tgz",
"integrity": "sha512-sHDs36L1VZmFHj3tPHsD+KZJtnsRudHlNvT0ieIe3iFVn5OpGLTxW3d/Zc/2LXSj5GpGuR6wQeikbhFjU9tMQQ==",
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.3.tgz",
"integrity": "sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg==",
"license": "MIT"
},
"node_modules/next-themes": {
@@ -17948,7 +17904,6 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.11.0",
"pg-pool": "^3.11.0",
@@ -18167,11 +18122,12 @@
}
},
"node_modules/posthog-node": {
"version": "5.24.15",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.24.15.tgz",
"integrity": "sha512-0QnWVOZAPwEAlp+r3r0jIGfk2IaNYM/2YnEJJhBMJZXs4LpHcTu7mX42l+e95o9xX87YpVuZU0kOkmtQUxgnOA==",
"version": "5.24.17",
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.24.17.tgz",
"integrity": "sha512-mdb8TKt+YCRbGQdYar3AKNUPCyEiqcprScF4unYpGALF6HlBaEuO6wPuIqXXpCWkw4VclJYCKbb6lq6pH6bJeA==",
"license": "MIT",
"dependencies": {
"@posthog/core": "1.22.0"
"@posthog/core": "1.23.1"
},
"engines": {
"node": "^20.20.0 || >=22.22.0"
@@ -18443,7 +18399,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -18473,7 +18428,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -19290,7 +19244,6 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -20760,8 +20713,7 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -21235,7 +21187,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -21425,9 +21376,9 @@
}
},
"node_modules/use-intl": {
"version": "4.8.2",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.2.tgz",
"integrity": "sha512-3VNXZgDnPFqhIYosQ9W1Hc6K5q+ZelMfawNbexdwL/dY7BTHbceLUBX5Eeex9lgogxTp0pf1SjHuhYNAjr9H3g==",
"version": "4.8.3",
"resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz",
"integrity": "sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA==",
"funding": [
{
"type": "individual",
@@ -21438,7 +21389,7 @@
"dependencies": {
"@formatjs/fast-memoize": "^3.1.0",
"@schummar/icu-type-parser": "1.21.5",
"icu-minify": "^4.8.2",
"icu-minify": "^4.8.3",
"intl-messageformat": "^11.1.0"
},
"peerDependencies": {
@@ -21662,7 +21613,6 @@
"resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz",
"integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8",
@@ -21869,7 +21819,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -32,7 +32,7 @@
"format": "prettier --write ."
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "8.4.0",
"@asteasolutions/zod-to-openapi": "8.4.1",
"@aws-sdk/client-s3": "3.989.0",
"@faker-js/faker": "10.3.0",
"@headlessui/react": "2.2.9",
@@ -59,9 +59,9 @@
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "1.2.8",
"@react-email/components": "1.0.7",
"@react-email/components": "1.0.8",
"@react-email/render": "2.0.4",
"@react-email/tailwind": "2.0.4",
"@react-email/tailwind": "2.0.5",
"@simplewebauthn/browser": "13.2.2",
"@simplewebauthn/server": "13.2.2",
"@tailwindcss/forms": "0.5.11",
@@ -81,7 +81,7 @@
"drizzle-orm": "0.45.1",
"express": "5.2.1",
"express-rate-limit": "8.2.1",
"glob": "13.0.3",
"glob": "13.0.5",
"helmet": "8.1.0",
"http-errors": "2.0.1",
"input-otp": "1.4.2",
@@ -93,14 +93,14 @@
"maxmind": "5.0.5",
"moment": "2.30.1",
"next": "15.5.12",
"next-intl": "4.8.2",
"next-intl": "4.8.3",
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
"node-cache": "5.1.2",
"nodemailer": "8.0.1",
"oslo": "1.2.1",
"pg": "8.18.0",
"posthog-node": "5.24.15",
"posthog-node": "5.24.17",
"qrcode.react": "4.2.0",
"react": "19.2.4",
"react-day-picker": "9.13.2",

View File

@@ -372,11 +372,7 @@ export const roles = pgTable("roles", {
isAdmin: boolean("isAdmin"),
name: varchar("name").notNull(),
description: varchar("description"),
requireDeviceApproval: boolean("requireDeviceApproval").default(false),
sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("none"), // "none" | "full" | "commands"
sshSudoCommands: text("sshSudoCommands").default("[]"),
sshCreateHomeDir: boolean("sshCreateHomeDir").default(false),
sshUnixGroups: text("sshUnixGroups").default("[]")
requireDeviceApproval: boolean("requireDeviceApproval").default(false)
});
export const roleActions = pgTable("roleActions", {

View File

@@ -679,13 +679,7 @@ export const roles = sqliteTable("roles", {
description: text("description"),
requireDeviceApproval: integer("requireDeviceApproval", {
mode: "boolean"
}).default(false),
sshSudoMode: text("sshSudoMode").default("none"), // "none" | "full" | "commands"
sshSudoCommands: text("sshSudoCommands").default("[]"),
sshCreateHomeDir: integer("sshCreateHomeDir", { mode: "boolean" }).default(
false
),
sshUnixGroups: text("sshUnixGroups").default("[]")
}).default(false)
});
export const roleActions = sqliteTable("roleActions", {

View File

@@ -48,5 +48,5 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
"enterprise"
],
[TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"],
[TierFeature.SshPam]: ["tier1", "tier3", "enterprise"]
[TierFeature.SshPam]: ["enterprise"]
};

View File

@@ -46,8 +46,6 @@ export class UsageService {
return null;
}
let orgIdToUse = await this.getBillingOrg(orgId, transaction);
// Truncate value to 11 decimal places
value = this.truncateValue(value);
@@ -59,6 +57,7 @@ export class UsageService {
try {
let usage;
if (transaction) {
const orgIdToUse = await this.getBillingOrg(orgId, transaction);
usage = await this.internalAddUsage(
orgIdToUse,
featureId,
@@ -67,6 +66,7 @@ export class UsageService {
);
} else {
await db.transaction(async (trx) => {
const orgIdToUse = await this.getBillingOrg(orgId, trx);
usage = await this.internalAddUsage(
orgIdToUse,
featureId,
@@ -92,7 +92,7 @@ export class UsageService {
const delay = baseDelay + jitter;
logger.warn(
`Deadlock detected for ${orgIdToUse}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
);
await new Promise((resolve) => setTimeout(resolve, delay));
@@ -100,7 +100,7 @@ export class UsageService {
}
logger.error(
`Failed to add usage for ${orgIdToUse}/${featureId} after ${attempt} attempts:`,
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
error
);
break;
@@ -169,7 +169,7 @@ export class UsageService {
return;
}
let orgIdToUse = await this.getBillingOrg(orgId);
const orgIdToUse = await this.getBillingOrg(orgId);
try {
// Truncate value to 11 decimal places if provided
@@ -227,7 +227,7 @@ export class UsageService {
orgId: string,
featureId: FeatureId
): Promise<string | null> {
let orgIdToUse = await this.getBillingOrg(orgId);
const orgIdToUse = await this.getBillingOrg(orgId);
const cacheKey = `customer_${orgIdToUse}_${featureId}`;
const cached = cache.get<string>(cacheKey);
@@ -274,7 +274,7 @@ export class UsageService {
return null;
}
let orgIdToUse = await this.getBillingOrg(orgId, trx);
const orgIdToUse = await this.getBillingOrg(orgId, trx);
const usageId = `${orgIdToUse}-${featureId}`;
@@ -382,7 +382,7 @@ export class UsageService {
return false;
}
let orgIdToUse = await this.getBillingOrg(orgId, trx);
const orgIdToUse = await this.getBillingOrg(orgId, trx);
// This method should check the current usage against the limits set for the organization
// and kick out all of the sites on the org

View File

@@ -23,14 +23,9 @@ export async function verifyApiKeyRoleAccess(
);
}
let allRoleIds: number[] = [];
if (!isNaN(singleRoleId)) {
// If roleId is provided in URL params, query params, or body (single), use it exclusively
allRoleIds = [singleRoleId];
} else if (req.body?.roleIds) {
// Only use body.roleIds if no single roleId was provided
allRoleIds = req.body.roleIds;
}
const { roleIds } = req.body;
const allRoleIds =
roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
if (allRoleIds.length === 0) {
return next();

View File

@@ -23,14 +23,8 @@ export async function verifyRoleAccess(
);
}
let allRoleIds: number[] = [];
if (!isNaN(singleRoleId)) {
// If roleId is provided in URL params, query params, or body (single), use it exclusively
allRoleIds = [singleRoleId];
} else if (req.body?.roleIds) {
// Only use body.roleIds if no single roleId was provided
allRoleIds = req.body.roleIds;
}
const roleIds = req.body?.roleIds;
const allRoleIds = roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
if (allRoleIds.length === 0) {
return next();

View File

@@ -78,7 +78,8 @@ export async function getOrgTierData(
if (
subscription.type === "tier1" ||
subscription.type === "tier2" ||
subscription.type === "tier3"
subscription.type === "tier3" ||
subscription.type === "enterprise"
) {
tier = subscription.type;
active = true;

View File

@@ -286,10 +286,6 @@ async function disableFeature(
await disableAutoProvisioning(orgId);
break;
case TierFeature.SshPam:
await disableSshPam(orgId);
break;
default:
logger.warn(
`Unknown feature ${feature} for org ${orgId}, skipping`
@@ -319,20 +315,6 @@ async function disableDeviceApprovals(orgId: string): Promise<void> {
logger.info(`Disabled device approvals on all roles for org ${orgId}`);
}
async function disableSshPam(orgId: string): Promise<void> {
await db
.update(roles)
.set({
sshSudoMode: "none",
sshSudoCommands: "[]",
sshCreateHomeDir: false,
sshUnixGroups: "[]"
})
.where(eq(roles.orgId, orgId));
logger.info(`Disabled SSH PAM options on all roles for org ${orgId}`);
}
async function disableLoginPageBranding(orgId: string): Promise<void> {
const [existingBranding] = await db
.select()

View File

@@ -13,17 +13,7 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import {
db,
newts,
roles,
roundTripMessageTracker,
siteResources,
sites,
userOrgs
} from "@server/db";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import { db, newts, orgs, roundTripMessageTracker, siteResources, sites, userOrgs } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -35,8 +25,6 @@ import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResourc
import { signPublicKey, getOrgCAKeys } from "#private/lib/sshCA";
import config from "@server/lib/config";
import { sendToClient } from "#private/routers/ws";
import { groups } from "d3";
import { homedir } from "os";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty()
@@ -147,26 +135,11 @@ export async function signSshKey(
);
}
const isLicensed = await isLicensedOrSubscribed(
orgId,
tierMatrix.sshPam
);
if (!isLicensed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"SSH key signing requires a paid plan"
)
);
}
let usernameToUse;
if (!userOrg.pamUsername) {
if (req.user?.email) {
// Extract username from email (first part before @)
usernameToUse = req.user?.email
.split("@")[0]
.replace(/[^a-zA-Z0-9_-]/g, "");
usernameToUse = req.user?.email.split("@")[0];
if (!usernameToUse) {
return next(
createHttpError(
@@ -328,29 +301,6 @@ export async function signSshKey(
);
}
const [roleRow] = await db
.select()
.from(roles)
.where(eq(roles.roleId, roleId))
.limit(1);
let parsedSudoCommands: string[] = [];
let parsedGroups: string[] = [];
try {
parsedSudoCommands = JSON.parse(roleRow?.sshSudoCommands ?? "[]");
if (!Array.isArray(parsedSudoCommands)) parsedSudoCommands = [];
} catch {
parsedSudoCommands = [];
}
try {
parsedGroups = JSON.parse(roleRow?.sshUnixGroups ?? "[]");
if (!Array.isArray(parsedGroups)) parsedGroups = [];
} catch {
parsedGroups = [];
}
const homedir = roleRow?.sshCreateHomeDir ?? null;
const sudoMode = roleRow?.sshSudoMode ?? "none";
// get the site
const [newt] = await db
.select()
@@ -384,7 +334,7 @@ export async function signSshKey(
.values({
wsClientId: newt.newtId,
messageType: `newt/pam/connection`,
sentAt: Math.floor(Date.now() / 1000)
sentAt: Math.floor(Date.now() / 1000),
})
.returning();
@@ -408,10 +358,8 @@ export async function signSshKey(
username: usernameToUse,
niceId: resource.niceId,
metadata: {
sudoMode: sudoMode,
sudoCommands: parsedSudoCommands,
homedir: homedir,
groups: parsedGroups
sudo: true, // we are hardcoding these for now but should make configurable from the role or something
homedir: true
}
}
});

View File

@@ -6,7 +6,7 @@ import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { eq, and, ne } from "drizzle-orm";
import { eq, and } from "drizzle-orm";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
@@ -93,8 +93,7 @@ export async function updateClient(
.where(
and(
eq(clients.niceId, niceId),
eq(clients.orgId, clients.orgId),
ne(clients.clientId, clientId)
eq(clients.orgId, clients.orgId)
)
)
.limit(1);

View File

@@ -197,7 +197,6 @@ export async function updateSiteBandwidth(
usageService
.checkLimitSet(
orgId,
FeatureId.EGRESS_DATA_MB,
bandwidthUsage
)

View File

@@ -9,7 +9,7 @@ import {
Resource,
resources
} from "@server/db";
import { eq, and, ne } from "drizzle-orm";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -33,15 +33,7 @@ const updateResourceParamsSchema = z.strictObject({
const updateHttpResourceBodySchema = z
.strictObject({
name: z.string().min(1).max(255).optional(),
niceId: z
.string()
.min(1)
.max(255)
.regex(
/^[a-zA-Z0-9-]+$/,
"niceId can only contain letters, numbers, and dashes"
)
.optional(),
niceId: z.string().min(1).max(255).regex(/^[a-zA-Z0-9-]+$/, "niceId can only contain letters, numbers, and dashes").optional(),
subdomain: subdomainSchema.nullable().optional(),
ssl: z.boolean().optional(),
sso: z.boolean().optional(),
@@ -256,13 +248,14 @@ async function updateHttpResource(
.where(
and(
eq(resources.niceId, updateData.niceId),
eq(resources.orgId, resource.orgId),
ne(resources.resourceId, resource.resourceId) // exclude the current resource from the search
eq(resources.orgId, resource.orgId)
)
)
.limit(1);
);
if (existingResource) {
if (
existingResource &&
existingResource.resourceId !== resource.resourceId
) {
return next(
createHttpError(
HttpCode.CONFLICT,
@@ -350,10 +343,7 @@ async function updateHttpResource(
headers = null;
}
const isLicensed = await isLicensedOrSubscribed(
resource.orgId,
tierMatrix.maintencePage
);
const isLicensed = await isLicensedOrSubscribed(resource.orgId, tierMatrix.maintencePage);
if (!isLicensed) {
updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined;

View File

@@ -18,17 +18,10 @@ const createRoleParamsSchema = z.strictObject({
orgId: z.string()
});
const sshSudoModeSchema = z.enum(["none", "full", "commands"]);
const createRoleSchema = z.strictObject({
name: z.string().min(1).max(255),
description: z.string().optional(),
requireDeviceApproval: z.boolean().optional(),
allowSsh: z.boolean().optional(),
sshSudoMode: sshSudoModeSchema.optional(),
sshSudoCommands: z.array(z.string()).optional(),
sshCreateHomeDir: z.boolean().optional(),
sshUnixGroups: z.array(z.string()).optional()
requireDeviceApproval: z.boolean().optional()
});
export const defaultRoleAllowedActions: ActionsEnum[] = [
@@ -108,40 +101,24 @@ export async function createRole(
);
}
const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
if (!isLicensedDeviceApprovals) {
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
if (!isLicensed) {
roleData.requireDeviceApproval = undefined;
}
const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam);
const roleInsertValues: Record<string, unknown> = {
name: roleData.name,
orgId
};
if (roleData.description !== undefined) roleInsertValues.description = roleData.description;
if (roleData.requireDeviceApproval !== undefined) roleInsertValues.requireDeviceApproval = roleData.requireDeviceApproval;
if (isLicensedSshPam) {
if (roleData.sshSudoMode !== undefined) roleInsertValues.sshSudoMode = roleData.sshSudoMode;
if (roleData.sshSudoCommands !== undefined) roleInsertValues.sshSudoCommands = JSON.stringify(roleData.sshSudoCommands);
if (roleData.sshCreateHomeDir !== undefined) roleInsertValues.sshCreateHomeDir = roleData.sshCreateHomeDir;
if (roleData.sshUnixGroups !== undefined) roleInsertValues.sshUnixGroups = JSON.stringify(roleData.sshUnixGroups);
}
await db.transaction(async (trx) => {
const newRole = await trx
.insert(roles)
.values(roleInsertValues as typeof roles.$inferInsert)
.values({
...roleData,
orgId
})
.returning();
const actionsToInsert = [...defaultRoleAllowedActions];
if (roleData.allowSsh) {
actionsToInsert.push(ActionsEnum.signSshKey);
}
await trx
.insert(roleActions)
.values(
actionsToInsert.map((action) => ({
defaultRoleAllowedActions.map((action) => ({
roleId: newRole[0].roleId,
actionId: action,
orgId

View File

@@ -1,10 +1,9 @@
import { db, orgs, roleActions, roles } from "@server/db";
import { db, orgs, roles } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi";
import HttpCode from "@server/types/HttpCode";
import { and, eq, inArray, sql } from "drizzle-orm";
import { ActionsEnum } from "@server/auth/actions";
import { eq, sql } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
@@ -38,11 +37,7 @@ async function queryRoles(orgId: string, limit: number, offset: number) {
name: roles.name,
description: roles.description,
orgName: orgs.name,
requireDeviceApproval: roles.requireDeviceApproval,
sshSudoMode: roles.sshSudoMode,
sshSudoCommands: roles.sshSudoCommands,
sshCreateHomeDir: roles.sshCreateHomeDir,
sshUnixGroups: roles.sshUnixGroups
requireDeviceApproval: roles.requireDeviceApproval
})
.from(roles)
.leftJoin(orgs, eq(roles.orgId, orgs.orgId))
@@ -111,28 +106,9 @@ export async function listRoles(
const totalCountResult = await countQuery;
const totalCount = totalCountResult[0].count;
let rolesWithAllowSsh = rolesList;
if (rolesList.length > 0) {
const roleIds = rolesList.map((r) => r.roleId);
const signSshKeyRows = await db
.select({ roleId: roleActions.roleId })
.from(roleActions)
.where(
and(
inArray(roleActions.roleId, roleIds),
eq(roleActions.actionId, ActionsEnum.signSshKey)
)
);
const roleIdsWithSsh = new Set(signSshKeyRows.map((r) => r.roleId));
rolesWithAllowSsh = rolesList.map((r) => ({
...r,
allowSsh: roleIdsWithSsh.has(r.roleId)
}));
}
return response(res, {
data: {
roles: rolesWithAllowSsh,
roles: rolesList,
pagination: {
total: totalCount,
limit,

View File

@@ -1,9 +1,8 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db, type Role } from "@server/db";
import { roleActions, roles } from "@server/db";
import { and, eq } from "drizzle-orm";
import { ActionsEnum } from "@server/auth/actions";
import { roles } from "@server/db";
import { eq } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -17,18 +16,11 @@ const updateRoleParamsSchema = z.strictObject({
roleId: z.string().transform(Number).pipe(z.int().positive())
});
const sshSudoModeSchema = z.enum(["none", "full", "commands"]);
const updateRoleBodySchema = z
.strictObject({
name: z.string().min(1).max(255).optional(),
description: z.string().optional(),
requireDeviceApproval: z.boolean().optional(),
allowSsh: z.boolean().optional(),
sshSudoMode: sshSudoModeSchema.optional(),
sshSudoCommands: z.array(z.string()).optional(),
sshCreateHomeDir: z.boolean().optional(),
sshUnixGroups: z.array(z.string()).optional()
requireDeviceApproval: z.boolean().optional()
})
.refine((data) => Object.keys(data).length > 0, {
error: "At least one field must be provided for update"
@@ -83,9 +75,7 @@ export async function updateRole(
}
const { roleId } = parsedParams.data;
const body = parsedBody.data;
const { allowSsh, ...restBody } = body;
const updateData: Record<string, unknown> = { ...restBody };
const updateData = parsedBody.data;
const role = await db
.select()
@@ -121,70 +111,18 @@ export async function updateRole(
);
}
const isLicensedDeviceApprovals = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
if (!isLicensedDeviceApprovals) {
const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.deviceApprovals);
if (!isLicensed) {
updateData.requireDeviceApproval = undefined;
}
const isLicensedSshPam = await isLicensedOrSubscribed(orgId, tierMatrix.sshPam);
if (!isLicensedSshPam) {
delete updateData.sshSudoMode;
delete updateData.sshSudoCommands;
delete updateData.sshCreateHomeDir;
delete updateData.sshUnixGroups;
} else {
if (Array.isArray(updateData.sshSudoCommands)) {
updateData.sshSudoCommands = JSON.stringify(updateData.sshSudoCommands);
}
if (Array.isArray(updateData.sshUnixGroups)) {
updateData.sshUnixGroups = JSON.stringify(updateData.sshUnixGroups);
}
}
const updatedRole = await db
.update(roles)
.set(updateData)
.where(eq(roles.roleId, roleId))
.returning();
const updatedRole = await db.transaction(async (trx) => {
const result = await trx
.update(roles)
.set(updateData as typeof roles.$inferInsert)
.where(eq(roles.roleId, roleId))
.returning();
if (result.length === 0) {
return null;
}
if (allowSsh === true) {
const existing = await trx
.select()
.from(roleActions)
.where(
and(
eq(roleActions.roleId, roleId),
eq(roleActions.actionId, ActionsEnum.signSshKey)
)
)
.limit(1);
if (existing.length === 0) {
await trx.insert(roleActions).values({
roleId,
actionId: ActionsEnum.signSshKey,
orgId: orgId!
});
}
} else if (allowSsh === false) {
await trx
.delete(roleActions)
.where(
and(
eq(roleActions.roleId, roleId),
eq(roleActions.actionId, ActionsEnum.signSshKey)
)
);
}
return result[0];
});
if (!updatedRole) {
if (updatedRole.length === 0) {
return next(
createHttpError(
HttpCode.NOT_FOUND,
@@ -194,7 +132,7 @@ export async function updateRole(
}
return response(res, {
data: updatedRole,
data: updatedRole[0],
success: true,
error: false,
message: "Role updated successfully",

View File

@@ -2,7 +2,7 @@ import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { db } from "@server/db";
import { sites } from "@server/db";
import { eq, and, ne } from "drizzle-orm";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -19,8 +19,8 @@ const updateSiteBodySchema = z
.strictObject({
name: z.string().min(1).max(255).optional(),
niceId: z.string().min(1).max(255).optional(),
dockerSocketEnabled: z.boolean().optional()
// remoteSubnets: z.string().optional()
dockerSocketEnabled: z.boolean().optional(),
remoteSubnets: z.string().optional()
// subdomain: z
// .string()
// .min(1)
@@ -86,19 +86,18 @@ export async function updateSite(
// if niceId is provided, check if it's already in use by another site
if (updateData.niceId) {
const [existingSite] = await db
const existingSite = await db
.select()
.from(sites)
.where(
and(
eq(sites.niceId, updateData.niceId),
eq(sites.orgId, sites.orgId),
ne(sites.siteId, siteId)
eq(sites.orgId, sites.orgId)
)
)
.limit(1);
if (existingSite) {
if (existingSite.length > 0 && existingSite[0].siteId !== siteId) {
return next(
createHttpError(
HttpCode.CONFLICT,
@@ -108,22 +107,22 @@ export async function updateSite(
}
}
// // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
// if (updateData.remoteSubnets) {
// const subnets = updateData.remoteSubnets
// .split(",")
// .map((s) => s.trim());
// for (const subnet of subnets) {
// if (!isValidCIDR(subnet)) {
// return next(
// createHttpError(
// HttpCode.BAD_REQUEST,
// `Invalid CIDR format: ${subnet}`
// )
// );
// }
// }
// }
// if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
if (updateData.remoteSubnets) {
const subnets = updateData.remoteSubnets
.split(",")
.map((s) => s.trim());
for (const subnet of subnets) {
if (!isValidCIDR(subnet)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Invalid CIDR format: ${subnet}`
)
);
}
}
}
const updatedSite = await db
.update(sites)

View File

@@ -1,28 +0,0 @@
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
import Database from "better-sqlite3";
import path from "path";
const version = "1.16.0";
export default async function migration() {
console.log(`Running setup script ${version}...`);
const location = path.join(APP_PATH, "db", "db.sqlite");
const db = new Database(location);
// set all admin role sudo to "full"; all other roles to "none"
// all roles set hoemdir to true
// generate ca certs for all orgs?
try {
db.transaction(() => {})();
console.log(`Migrated database`);
} catch (e) {
console.log("Failed to migrate db:", e);
throw e;
}
console.log(`${version} migration complete`);
}

View File

@@ -2,7 +2,6 @@ import { SidebarNavItem } from "@app/components/SidebarNav";
import { Env } from "@app/lib/types/env";
import { build } from "@server/build";
import {
Building2,
ChartLine,
Combine,
CreditCard,
@@ -12,11 +11,10 @@ import {
KeyRound,
Laptop,
Link as LinkIcon,
Logs,
Logs, // Added from 'dev' branch
MonitorUp,
Plug,
ReceiptText,
ScanEye,
ScanEye, // Added from 'dev' branch
Server,
Settings,
SquareMousePointer,
@@ -51,12 +49,12 @@ export const orgNavSections = (
options?: OrgNavSectionsOptions
): SidebarNavSection[] => [
{
heading: "network",
heading: "sidebarGeneral",
items: [
{
title: "sidebarSites",
href: "/{orgId}/settings/sites",
icon: <Plug className="size-4 flex-none" />
icon: <Combine className="size-4 flex-none" />
},
{
title: "sidebarResources",
@@ -159,88 +157,92 @@ export const orgNavSections = (
}
]
},
{
heading: "sidebarLogsAndAnalytics",
items: (() => {
const logItems: SidebarNavItem[] = [
{
title: "sidebarLogsRequest",
href: "/{orgId}/settings/logs/request",
icon: <SquareMousePointer className="size-4 flex-none" />
},
...(!env?.flags.disableEnterpriseFeatures
? [
{
title: "sidebarLogsAccess",
href: "/{orgId}/settings/logs/access",
icon: <ScanEye className="size-4 flex-none" />
},
{
title: "sidebarLogsAction",
href: "/{orgId}/settings/logs/action",
icon: <Logs className="size-4 flex-none" />
}
]
: [])
];
const analytics = {
title: "sidebarLogsAnalytics",
href: "/{orgId}/settings/logs/analytics",
icon: <ChartLine className="h-4 w-4" />
};
// If only one log item, return it directly without grouping
if (logItems.length === 1) {
return [analytics, ...logItems];
}
// If multiple log items, create a group
return [
analytics,
{
title: "sidebarLogs",
icon: <Logs className="size-4 flex-none" />,
items: logItems
}
];
})()
},
{
heading: "sidebarOrganization",
items: [
{
title: "sidebarLogsAndAnalytics",
icon: <ChartLine className="size-4 flex-none" />,
items: [
{
title: "sidebarLogsAnalytics",
href: "/{orgId}/settings/logs/analytics",
icon: <ChartLine className="size-4 flex-none" />
},
{
title: "sidebarLogsRequest",
href: "/{orgId}/settings/logs/request",
icon: (
<SquareMousePointer className="size-4 flex-none" />
)
},
...(!env?.flags.disableEnterpriseFeatures
? [
{
title: "sidebarLogsAccess",
href: "/{orgId}/settings/logs/access",
icon: <ScanEye className="size-4 flex-none" />
},
{
title: "sidebarLogsAction",
href: "/{orgId}/settings/logs/action",
icon: <Logs className="size-4 flex-none" />
}
]
: [])
]
title: "sidebarApiKeys",
href: "/{orgId}/settings/api-keys",
icon: <KeyRound className="size-4 flex-none" />
},
{
title: "sidebarManagement",
icon: <Building2 className="size-4 flex-none" />,
items: [
{
title: "sidebarApiKeys",
href: "/{orgId}/settings/api-keys",
icon: <KeyRound className="size-4 flex-none" />
},
{
title: "sidebarBluePrints",
href: "/{orgId}/settings/blueprints",
icon: <ReceiptText className="size-4 flex-none" />
}
]
title: "sidebarBluePrints",
href: "/{orgId}/settings/blueprints",
icon: <ReceiptText className="size-4 flex-none" />
},
...(build == "saas" && options?.isPrimaryOrg
? [
{
title: "sidebarBillingAndLicenses",
icon: <CreditCard className="size-4 flex-none" />,
items: [
{
title: "sidebarBilling",
href: "/{orgId}/settings/billing",
icon: (
<CreditCard className="size-4 flex-none" />
)
},
{
title: "sidebarEnterpriseLicenses",
href: "/{orgId}/settings/license",
icon: (
<TicketCheck className="size-4 flex-none" />
)
}
]
}
]
: []),
{
title: "sidebarSettings",
href: "/{orgId}/settings/general",
icon: <Settings className="size-4 flex-none" />
}
]
}
},
...(build == "saas" && options?.isPrimaryOrg
? [
{
heading: "sidebarBillingAndLicenses",
items: [
{
title: "sidebarBilling",
href: "/{orgId}/settings/billing",
icon: <CreditCard className="size-4 flex-none" />
},
{
title: "sidebarEnterpriseLicenses",
href: "/{orgId}/settings/license",
icon: <TicketCheck className="size-4 flex-none" />
}
]
}
]
: [])
];
export const adminNavSections = (env?: Env): SidebarNavSection[] => [

View File

@@ -11,19 +11,31 @@ import {
CredenzaTitle
} from "@app/components/Credenza";
import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import type {
CreateRoleBody,
CreateRoleResponse
} from "@server/routers/role";
import { zodResolver } from "@hookform/resolvers/zod";
import { build } from "@server/build";
import type { CreateRoleBody, CreateRoleResponse } from "@server/routers/role";
import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl";
import { useTransition } from "react";
import { RoleForm, type RoleFormValues } from "./RoleForm";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { CheckboxWithLabel } from "./ui/checkbox";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
type CreateRoleFormProps = {
@@ -40,39 +52,35 @@ export default function CreateRoleForm({
const { org } = useOrgContext();
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const { env } = useEnvContext();
const formSchema = z.object({
name: z
.string({ message: t("nameRequired") })
.min(1)
.max(32),
description: z.string().max(255).optional(),
requireDeviceApproval: z.boolean().optional()
});
const api = createApiClient(useEnvContext());
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
description: "",
requireDeviceApproval: false
}
});
const [loading, startTransition] = useTransition();
async function onSubmit(values: RoleFormValues) {
const payload: CreateRoleBody = {
name: values.name,
description: values.description || undefined,
requireDeviceApproval: values.requireDeviceApproval,
allowSsh: values.allowSsh
};
if (isPaidUser(tierMatrix.sshPam)) {
payload.sshSudoMode = values.sshSudoMode;
payload.sshCreateHomeDir = values.sshCreateHomeDir;
payload.sshSudoCommands =
values.sshSudoMode === "commands" &&
values.sshSudoCommands?.trim()
? values.sshSudoCommands
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [];
if (values.sshUnixGroups?.trim()) {
payload.sshUnixGroups = values.sshUnixGroups
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
}
async function onSubmit(values: z.infer<typeof formSchema>) {
const res = await api
.put<AxiosResponse<CreateRoleResponse>>(
`/org/${org?.org.orgId}/role`,
payload
)
.put<
AxiosResponse<CreateRoleResponse>
>(`/org/${org?.org.orgId}/role`, values satisfies CreateRoleBody)
.catch((e) => {
toast({
variant: "destructive",
@@ -90,42 +98,143 @@ export default function CreateRoleForm({
title: t("accessRoleCreated"),
description: t("accessRoleCreatedDescription")
});
if (open) setOpen(false);
if (open) {
setOpen(false);
}
afterCreate?.(res.data.data);
}
}
return (
<Credenza open={open} onOpenChange={setOpen}>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t("accessRoleCreate")}</CredenzaTitle>
<CredenzaDescription>
{t("accessRoleCreateDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<RoleForm
variant="create"
onSubmit={(values) =>
startTransition(() => onSubmit(values))
}
/>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="submit"
form="create-role-form"
loading={loading}
disabled={loading}
>
{t("accessRoleCreateSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
<>
<Credenza
open={open}
onOpenChange={(val) => {
setOpen(val);
form.reset();
}}
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t("accessRoleCreate")}</CredenzaTitle>
<CredenzaDescription>
{t("accessRoleCreateDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) =>
startTransition(() => onSubmit(values))
)}
className="space-y-4"
id="create-role-form"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("accessRoleName")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("description")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!env.flags.disableEnterpriseFeatures && (
<>
<PaidFeaturesAlert
tiers={tierMatrix.deviceApprovals}
/>
<FormField
control={form.control}
name="requireDeviceApproval"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
disabled={
!isPaidUser(
tierMatrix.deviceApprovals
)
}
value="on"
checked={form.watch(
"requireDeviceApproval"
)}
onCheckedChange={(
checked
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
checked
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
<FormDescription>
{t(
"requireDeviceApprovalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="submit"
form="create-role-form"
loading={loading}
disabled={loading}
>
{t("accessRoleCreateSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
);
}

View File

@@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => {
return (
<CredenzaContent
className={cn(
"overflow-y-auto max-h-[100dvh] md:max-h-screen md:top-[200px] md:translate-y-0",
"overflow-y-auto max-h-[100dvh] md:max-h-screen",
className
)}
{...props}

View File

@@ -11,26 +11,44 @@ import {
CredenzaTitle
} from "@app/components/Credenza";
import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { useOrgContext } from "@app/hooks/useOrgContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { build } from "@server/build";
import type { Role } from "@server/db";
import type {
CreateRoleBody,
CreateRoleResponse,
UpdateRoleBody,
UpdateRoleResponse
} from "@server/routers/role";
import { AxiosResponse } from "axios";
import { useTranslations } from "next-intl";
import { useTransition } from "react";
import { RoleForm, type RoleFormValues } from "./RoleForm";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { CheckboxWithLabel } from "./ui/checkbox";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
type EditRoleFormProps = {
type CreateRoleFormProps = {
role: Role;
open: boolean;
setOpen: (open: boolean) => void;
onSuccess?: (res: UpdateRoleResponse) => void;
onSuccess?: (res: CreateRoleResponse) => void;
};
export default function EditRoleForm({
@@ -38,42 +56,39 @@ export default function EditRoleForm({
role,
setOpen,
onSuccess
}: EditRoleFormProps) {
}: CreateRoleFormProps) {
const { org } = useOrgContext();
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const { env } = useEnvContext();
const formSchema = z.object({
name: z
.string({ message: t("nameRequired") })
.min(1)
.max(32),
description: z.string().max(255).optional(),
requireDeviceApproval: z.boolean().optional()
});
const api = createApiClient(useEnvContext());
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: role.name,
description: role.description ?? "",
requireDeviceApproval: role.requireDeviceApproval ?? false
}
});
const [loading, startTransition] = useTransition();
async function onSubmit(values: RoleFormValues) {
const payload: UpdateRoleBody = {
name: values.name,
description: values.description || undefined,
requireDeviceApproval: values.requireDeviceApproval,
allowSsh: values.allowSsh
};
if (isPaidUser(tierMatrix.sshPam)) {
payload.sshSudoMode = values.sshSudoMode;
payload.sshCreateHomeDir = values.sshCreateHomeDir;
payload.sshSudoCommands =
values.sshSudoMode === "commands" &&
values.sshSudoCommands?.trim()
? values.sshSudoCommands
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [];
if (values.sshUnixGroups !== undefined) {
payload.sshUnixGroups = values.sshUnixGroups
.split(",")
.map((s) => s.trim())
.filter(Boolean);
}
}
async function onSubmit(values: z.infer<typeof formSchema>) {
const res = await api
.post<AxiosResponse<UpdateRoleResponse>>(
`/role/${role.roleId}`,
payload
)
.post<
AxiosResponse<UpdateRoleResponse>
>(`/role/${role.roleId}`, values satisfies UpdateRoleBody)
.catch((e) => {
toast({
variant: "destructive",
@@ -91,43 +106,143 @@ export default function EditRoleForm({
title: t("accessRoleUpdated"),
description: t("accessRoleUpdatedDescription")
});
if (open) setOpen(false);
if (open) {
setOpen(false);
}
onSuccess?.(res.data.data);
}
}
return (
<Credenza open={open} onOpenChange={setOpen}>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t("accessRoleEdit")}</CredenzaTitle>
<CredenzaDescription>
{t("accessRoleEditDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<RoleForm
variant="edit"
role={role}
onSubmit={(values) =>
startTransition(() => onSubmit(values))
}
/>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="submit"
form="create-role-form"
loading={loading}
disabled={loading}
>
{t("accessRoleUpdateSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
<>
<Credenza
open={open}
onOpenChange={(val) => {
setOpen(val);
form.reset();
}}
>
<CredenzaContent>
<CredenzaHeader>
<CredenzaTitle>{t("accessRoleEdit")}</CredenzaTitle>
<CredenzaDescription>
{t("accessRoleEditDescription")}
</CredenzaDescription>
</CredenzaHeader>
<CredenzaBody>
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) =>
startTransition(() => onSubmit(values))
)}
className="space-y-4"
id="create-role-form"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("accessRoleName")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("description")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{!env.flags.disableEnterpriseFeatures && (
<>
<PaidFeaturesAlert
tiers={tierMatrix.deviceApprovals}
/>
<FormField
control={form.control}
name="requireDeviceApproval"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
disabled={
!isPaidUser(
tierMatrix.deviceApprovals
)
}
value="on"
checked={form.watch(
"requireDeviceApproval"
)}
onCheckedChange={(
checked
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
checked
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
<FormDescription>
{t(
"requireDeviceApprovalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
</form>
</Form>
</CredenzaBody>
<CredenzaFooter>
<CredenzaClose asChild>
<Button variant="outline">{t("close")}</Button>
</CredenzaClose>
<Button
type="submit"
form="create-role-form"
loading={loading}
disabled={loading}
>
{t("accessRoleUpdateSubmit")}
</Button>
</CredenzaFooter>
</CredenzaContent>
</Credenza>
</>
);
}

View File

@@ -18,7 +18,7 @@ import { approvalQueries } from "@app/lib/queries";
import { build } from "@server/build";
import { useQuery } from "@tanstack/react-query";
import { ListUserOrgsResponse } from "@server/routers/org";
import { ArrowRight, ExternalLink, Server } from "lucide-react";
import { ExternalLink, Server } from "lucide-react";
import { useTranslations } from "next-intl";
import dynamic from "next/dynamic";
import Link from "next/link";
@@ -146,6 +146,36 @@ export function LayoutSidebar({
/>
<div className="flex-1 overflow-y-auto relative">
<div className="px-2 pt-1">
{!isAdminPage && user.serverAdmin && (
<div className="py-2">
<Link
href="/admin"
className={cn(
"flex items-center transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/80 dark:hover:bg-secondary/50 rounded-md",
isSidebarCollapsed
? "px-2 py-2 justify-center"
: "px-3 py-1.5"
)}
title={
isSidebarCollapsed
? t("serverAdmin")
: undefined
}
>
<span
className={cn(
"shrink-0",
!isSidebarCollapsed && "mr-2"
)}
>
<Server className="h-4 w-4" />
</span>
{!isSidebarCollapsed && (
<span>{t("serverAdmin")}</span>
)}
</Link>
</div>
)}
<SidebarNav
sections={navItems}
isCollapsed={isSidebarCollapsed}
@@ -156,40 +186,6 @@ export function LayoutSidebar({
<div className="sticky bottom-0 left-0 right-0 h-8 pointer-events-none bg-gradient-to-t from-card to-transparent" />
</div>
{!isAdminPage && user.serverAdmin && (
<div className="shrink-0 px-2 pb-2">
<Link
href="/admin"
className={cn(
"flex items-center transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/80 dark:hover:bg-secondary/50 rounded-md",
isSidebarCollapsed
? "px-2 py-2 justify-center"
: "px-3 py-1.5"
)}
title={
isSidebarCollapsed
? t("serverAdmin")
: undefined
}
>
<span
className={cn(
"shrink-0",
!isSidebarCollapsed && "mr-2"
)}
>
<Server className="h-4 w-4" />
</span>
{!isSidebarCollapsed && (
<>
<span className="flex-1">{t("serverAdmin")}</span>
<ArrowRight className="h-4 w-4 shrink-0 ml-auto opacity-70" />
</>
)}
</Link>
</div>
)}
<div className="w-full border-t border-border" />
<div className="p-4 pt-1 flex flex-col shrink-0">

View File

@@ -1,70 +0,0 @@
"use client";
import { Button } from "@app/components/ui/button";
import { cn } from "@app/lib/cn";
import type { ReactNode } from "react";
export type OptionSelectOption<TValue extends string> = {
value: TValue;
label: string;
icon?: ReactNode;
};
type OptionSelectProps<TValue extends string> = {
options: ReadonlyArray<OptionSelectOption<TValue>>;
value: TValue;
onChange: (value: TValue) => void;
label?: string;
/** Grid columns: 2, 3, 4, 5, etc. Default 5 on md+. */
cols?: number;
className?: string;
disabled?: boolean;
};
export function OptionSelect<TValue extends string>({
options,
value,
onChange,
label,
cols = 5,
className,
disabled = false
}: OptionSelectProps<TValue>) {
return (
<div className={className}>
{label && (
<p className="font-bold mb-3">{label}</p>
)}
<div
className={cn(
"grid gap-2",
cols === 2 && "grid-cols-2",
cols === 3 && "grid-cols-2 md:grid-cols-3",
cols === 4 && "grid-cols-2 md:grid-cols-4",
cols === 5 && "grid-cols-2 md:grid-cols-5",
cols === 6 && "grid-cols-2 md:grid-cols-3 lg:grid-cols-6"
)}
>
{options.map((option) => {
const isSelected = value === option.value;
return (
<Button
key={option.value}
type="button"
variant={isSelected ? "squareOutlinePrimary" : "squareOutline"}
className={cn(
"flex-1 min-w-30 shadow-none",
isSelected && "bg-primary/10"
)}
onClick={() => onChange(option.value)}
disabled={disabled}
>
{option.icon}
{option.label}
</Button>
);
})}
</div>
</div>
);
}

View File

@@ -71,7 +71,7 @@ export function OrgSelector({
"cursor-pointer transition-colors",
isCollapsed
? "w-full h-16 flex items-center justify-center hover:bg-muted"
: "w-full px-5 py-4 hover:bg-muted"
: "w-full px-4 py-4 hover:bg-muted"
)}
>
{isCollapsed ? (

View File

@@ -1,441 +0,0 @@
"use client";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@app/components/ui/form";
import { Input } from "@app/components/ui/input";
import {
OptionSelect,
type OptionSelectOption
} from "@app/components/OptionSelect";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { usePaidStatus } from "@app/hooks/usePaidStatus";
import { useTranslations } from "next-intl";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { HorizontalTabs } from "@app/components/HorizontalTabs";
import { PaidFeaturesAlert } from "./PaidFeaturesAlert";
import { CheckboxWithLabel } from "./ui/checkbox";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
import type { Role } from "@server/db";
export const SSH_SUDO_MODE_VALUES = ["none", "full", "commands"] as const;
export type SshSudoMode = (typeof SSH_SUDO_MODE_VALUES)[number];
function parseRoleJsonArray(value: string | null | undefined): string[] {
if (value == null || value === "") return [];
try {
const parsed = JSON.parse(value);
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
}
function toSshSudoMode(value: string | null | undefined): SshSudoMode {
if (value === "none" || value === "full" || value === "commands")
return value;
return "none";
}
export type RoleFormValues = {
name: string;
description?: string;
requireDeviceApproval?: boolean;
allowSsh?: boolean;
sshSudoMode: SshSudoMode;
sshSudoCommands?: string;
sshCreateHomeDir?: boolean;
sshUnixGroups?: string;
};
type RoleFormProps = {
variant: "create" | "edit";
role?: Role;
onSubmit: (values: RoleFormValues) => void | Promise<void>;
formId?: string;
};
export function RoleForm({
variant,
role,
onSubmit,
formId = "create-role-form"
}: RoleFormProps) {
const t = useTranslations();
const { isPaidUser } = usePaidStatus();
const { env } = useEnvContext();
const formSchema = z.object({
name: z
.string({ message: t("nameRequired") })
.min(1)
.max(32),
description: z.string().max(255).optional(),
requireDeviceApproval: z.boolean().optional(),
allowSsh: z.boolean().optional(),
sshSudoMode: z.enum(SSH_SUDO_MODE_VALUES),
sshSudoCommands: z.string().optional(),
sshCreateHomeDir: z.boolean().optional(),
sshUnixGroups: z.string().optional()
});
const defaultValues: RoleFormValues = role
? {
name: role.name,
description: role.description ?? "",
requireDeviceApproval: role.requireDeviceApproval ?? false,
allowSsh:
(role as Role & { allowSsh?: boolean }).allowSsh ?? false,
sshSudoMode: toSshSudoMode(role.sshSudoMode),
sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join(
", "
),
sshCreateHomeDir: role.sshCreateHomeDir ?? false,
sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ")
}
: {
name: "",
description: "",
requireDeviceApproval: false,
allowSsh: false,
sshSudoMode: "none",
sshSudoCommands: "",
sshCreateHomeDir: true,
sshUnixGroups: ""
};
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues
});
useEffect(() => {
if (variant === "edit" && role) {
form.reset({
name: role.name,
description: role.description ?? "",
requireDeviceApproval: role.requireDeviceApproval ?? false,
allowSsh:
(role as Role & { allowSsh?: boolean }).allowSsh ?? false,
sshSudoMode: toSshSudoMode(role.sshSudoMode),
sshSudoCommands: parseRoleJsonArray(role.sshSudoCommands).join(
", "
),
sshCreateHomeDir: role.sshCreateHomeDir ?? false,
sshUnixGroups: parseRoleJsonArray(role.sshUnixGroups).join(", ")
});
}
}, [variant, role, form]);
const sshDisabled = !isPaidUser(tierMatrix.sshPam);
const sshSudoMode = form.watch("sshSudoMode");
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit((values) => onSubmit(values))}
className="space-y-4"
id={formId}
>
{env.flags.disableEnterpriseFeatures ? (
<div className="space-y-4 mt-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("accessRoleName")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>{t("description")}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
) : (
<HorizontalTabs
clientSide={true}
defaultTab={0}
items={[
{ title: t("general"), href: "#" },
...(env.flags.disableEnterpriseFeatures
? []
: [{ title: t("sshAccess"), href: "#" }])
]}
>
{/* General tab */}
<div className="space-y-4 mt-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("accessRoleName")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("description")}
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<PaidFeaturesAlert
tiers={tierMatrix.deviceApprovals}
/>
<FormField
control={form.control}
name="requireDeviceApproval"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
disabled={
!isPaidUser(
tierMatrix.deviceApprovals
)
}
value="on"
checked={form.watch(
"requireDeviceApproval"
)}
onCheckedChange={(checked) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"requireDeviceApproval",
checked
);
}
}}
label={t(
"requireDeviceApproval"
)}
/>
</FormControl>
<FormDescription>
{t(
"requireDeviceApprovalDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* SSH tab - hidden when enterprise features are disabled */}
{!env.flags.disableEnterpriseFeatures && (
<div className="space-y-4 mt-4">
<PaidFeaturesAlert tiers={tierMatrix.sshPam} />
<FormField
control={form.control}
name="allowSsh"
render={({ field }) => {
const allowSshOptions: OptionSelectOption<"allow" | "disallow">[] = [
{
value: "allow",
label: t("roleAllowSshAllow")
},
{
value: "disallow",
label: t("roleAllowSshDisallow")
}
];
return (
<FormItem>
<FormLabel>
{t("roleAllowSsh")}
</FormLabel>
<OptionSelect<"allow" | "disallow">
options={allowSshOptions}
value={
field.value
? "allow"
: "disallow"
}
onChange={(v) =>
field.onChange(v === "allow")
}
cols={2}
/>
<FormDescription>
{t(
"roleAllowSshDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
);
}}
/>
<FormField
control={form.control}
name="sshSudoMode"
render={({ field }) => {
const sudoOptions: OptionSelectOption<SshSudoMode>[] =
[
{
value: "none",
label: t("sshSudoModeNone")
},
{
value: "full",
label: t("sshSudoModeFull")
},
{
value: "commands",
label: t(
"sshSudoModeCommands"
)
}
];
return (
<FormItem>
<FormLabel>
{t("sshSudoMode")}
</FormLabel>
<OptionSelect<SshSudoMode>
options={sudoOptions}
value={field.value}
onChange={field.onChange}
cols={3}
disabled={sshDisabled}
/>
<FormMessage />
</FormItem>
);
}}
/>
{sshSudoMode === "commands" && (
<FormField
control={form.control}
name="sshSudoCommands"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("sshSudoCommands")}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={sshDisabled}
/>
</FormControl>
<FormDescription>
{t(
"sshSudoCommandsDescription"
)}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="sshUnixGroups"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("sshUnixGroups")}
</FormLabel>
<FormControl>
<Input
{...field}
disabled={sshDisabled}
/>
</FormControl>
<FormDescription>
{t("sshUnixGroupsDescription")}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="sshCreateHomeDir"
render={({ field }) => (
<FormItem className="my-2">
<FormControl>
<CheckboxWithLabel
{...field}
value="on"
checked={form.watch(
"sshCreateHomeDir"
)}
onCheckedChange={(
checked
) => {
if (
checked !==
"indeterminate"
) {
form.setValue(
"sshCreateHomeDir",
checked
);
}
}}
label={t(
"sshCreateHomeDir"
)}
disabled={sshDisabled}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
)}
</HorizontalTabs>
)}
</form>
</Form>
);
}

View File

@@ -103,47 +103,45 @@ export default function UsersTable({ roles }: RolesTableProps) {
header: () => <span className="p-3"></span>,
cell: ({ row }) => {
const roleRow = row.original;
const isAdmin = roleRow.isAdmin;
return (
<div className="flex items-center gap-2 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
disabled={isAdmin}
>
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
disabled={isAdmin}
onClick={() => {
setRoleToRemove(roleRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant={"outline"}
disabled={isAdmin}
onClick={() => {
setEditingRole(roleRow);
setIsEditDialogOpen(true);
}}
>
{t("edit")}
</Button>
</div>
!roleRow.isAdmin && (
<div className="flex items-center gap-2 justify-end">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
>
<span className="sr-only">
{t("openMenu")}
</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem
onClick={() => {
setRoleToRemove(roleRow);
setIsDeleteModalOpen(true);
}}
>
<span className="text-red-500">
{t("delete")}
</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<Button
variant={"outline"}
onClick={() => {
setEditingRole(roleRow);
setIsEditDialogOpen(true);
}}
>
{t("edit")}
</Button>
</div>
)
);
}
}

View File

@@ -119,7 +119,7 @@ function CollapsibleNavItem({
<button
className={cn(
"flex items-center w-full rounded-md transition-colors",
"px-3 py-1.5",
level === 0 ? "px-3 py-1.5" : "px-3 py-1",
isActive
? "bg-secondary font-medium"
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
@@ -128,7 +128,7 @@ function CollapsibleNavItem({
disabled={isDisabled}
>
{item.icon && (
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center opacity-50">
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">
{item.icon}
</span>
)}
@@ -167,25 +167,16 @@ function CollapsibleNavItem({
</div>
</button>
</CollapsibleTrigger>
<CollapsibleContent forceMount>
<CollapsibleContent>
<div
className={cn(
"grid overflow-hidden transition-[grid-template-rows] duration-200 ease-in-out",
isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
"border-l ml-3 pl-3 mt-0 space-y-0",
"border-border"
)}
>
<div className="min-h-0">
<div
className={cn(
"border-l ml-[22px] pl-[9px] mt-0 space-y-0",
"border-border"
)}
>
{item.items!.map((childItem) =>
renderNavItem(childItem, level + 1)
)}
</div>
</div>
{item.items!.map((childItem) =>
renderNavItem(childItem, level + 1)
)}
</div>
</CollapsibleContent>
</Collapsible>
@@ -287,7 +278,11 @@ export function SidebarNav({
href={isDisabled ? "#" : hydratedHref}
className={cn(
"flex items-center rounded-md transition-colors relative",
isCollapsed ? "px-2 py-2 justify-center" : "px-3 py-1.5",
isCollapsed
? "px-2 py-2 justify-center"
: level === 0
? "px-3 py-1.5"
: "px-3 py-1",
isActive
? "bg-secondary font-medium"
: "text-muted-foreground hover:bg-secondary/80 dark:hover:bg-secondary/50 hover:text-foreground",
@@ -303,10 +298,10 @@ export function SidebarNav({
tabIndex={isDisabled ? -1 : undefined}
aria-disabled={isDisabled}
>
{item.icon && level === 0 && (
{item.icon && (
<span
className={cn(
"flex-shrink-0 w-5 h-5 flex items-center justify-center opacity-50",
"flex-shrink-0 w-5 h-5 flex items-center justify-center",
!isCollapsed && "mr-3"
)}
>
@@ -360,13 +355,13 @@ export function SidebarNav({
<div
className={cn(
"flex items-center rounded-md transition-colors",
"px-3 py-1.5",
level === 0 ? "px-3 py-1.5" : "px-3 py-1",
"text-muted-foreground",
isDisabled && "cursor-not-allowed opacity-60"
)}
>
{item.icon && level === 0 && (
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center opacity-50">
{item.icon && (
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">
{item.icon}
</span>
)}
@@ -423,7 +418,7 @@ export function SidebarNav({
disabled={isDisabled}
>
{item.icon && (
<span className="flex-shrink-0 w-5 h-5 flex items-center justify-center opacity-50">
<span className="flex-shrink-0 w-5 h-5 flex items-center justify-center">
{item.icon}
</span>
)}
@@ -485,6 +480,11 @@ export function SidebarNav({
}
}}
>
{childItem.icon && (
<span className="flex-shrink-0 mr-3 w-5 h-5 flex items-center justify-center">
{childItem.icon}
</span>
)}
<div className="flex items-center gap-2 flex-1 min-w-0">
<span className="truncate">
{t(childItem.title)}

View File

@@ -8,7 +8,7 @@ import {
SettingsSectionTitle
} from "./Settings";
import { CheckboxWithLabel } from "./ui/checkbox";
import { OptionSelect, type OptionSelectOption } from "./OptionSelect";
import { Button } from "./ui/button";
import { useState } from "react";
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa";
import { Terminal } from "lucide-react";
@@ -138,14 +138,6 @@ WantedBy=default.target`
const commands = commandList[platform][architecture];
const platformOptions: OptionSelectOption<Platform>[] = PLATFORMS.map(
(os) => ({
value: os,
label: getPlatformName(os),
icon: getPlatformIcon(os)
})
);
return (
<SettingsSection>
<SettingsSectionHeader>
@@ -157,33 +149,53 @@ WantedBy=default.target`
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<OptionSelect<Platform>
label={t("operatingSystem")}
options={platformOptions}
value={platform}
onChange={(os) => {
setPlatform(os);
const architectures = getArchitectures(os);
setArchitecture(architectures[0]);
}}
cols={5}
/>
<div>
<p className="font-bold mb-3">{t("operatingSystem")}</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{PLATFORMS.map((os) => (
<Button
key={os}
variant={
platform === os
? "squareOutlinePrimary"
: "squareOutline"
}
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
onClick={() => {
setPlatform(os);
const architectures = getArchitectures(os);
setArchitecture(architectures[0]);
}}
>
{getPlatformIcon(os)}
{getPlatformName(os)}
</Button>
))}
</div>
</div>
<OptionSelect<string>
label={
["docker", "podman"].includes(platform)
<div>
<p className="font-bold mb-3">
{["docker", "podman"].includes(platform)
? t("method")
: t("architecture")
}
options={getArchitectures(platform).map((arch) => ({
value: arch,
label: arch
}))}
value={architecture}
onChange={setArchitecture}
cols={5}
className="mt-4"
/>
: t("architecture")}
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{getArchitectures(platform).map((arch) => (
<Button
key={arch}
variant={
architecture === arch
? "squareOutlinePrimary"
: "squareOutline"
}
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
onClick={() => setArchitecture(arch)}
>
{arch}
</Button>
))}
</div>
<div className="pt-4">
<p className="font-bold mb-3">
@@ -238,6 +250,7 @@ WantedBy=default.target`
})}
</div>
</div>
</div>
</SettingsSectionBody>
</SettingsSection>
);

View File

@@ -10,7 +10,7 @@ import {
SettingsSectionHeader,
SettingsSectionTitle
} from "./Settings";
import { OptionSelect, type OptionSelectOption } from "./OptionSelect";
import { Button } from "./ui/button";
export type CommandItem = string | { title: string; command: string };
@@ -88,15 +88,6 @@ curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/ol
};
const commands = commandList[platform][architecture];
const platformOptions: OptionSelectOption<Platform>[] = PLATFORMS.map(
(os) => ({
value: os,
label: getPlatformName(os),
icon: getPlatformIcon(os)
})
);
return (
<SettingsSection>
<SettingsSectionHeader>
@@ -108,35 +99,54 @@ curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/ol
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<OptionSelect<Platform>
label={t("operatingSystem")}
options={platformOptions}
value={platform}
onChange={(os) => {
setPlatform(os);
const architectures = getArchitectures(os);
setArchitecture(architectures[0]);
}}
cols={5}
/>
<div>
<p className="font-bold mb-3">{t("operatingSystem")}</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{PLATFORMS.map((os) => (
<Button
key={os}
variant={
platform === os
? "squareOutlinePrimary"
: "squareOutline"
}
className={`flex-1 min-w-30 ${platform === os ? "bg-primary/10" : ""} shadow-none`}
onClick={() => {
setPlatform(os);
const architectures = getArchitectures(os);
setArchitecture(architectures[0]);
}}
>
{getPlatformIcon(os)}
{getPlatformName(os)}
</Button>
))}
</div>
</div>
<OptionSelect<string>
label={
platform === "docker"
<div>
<p className="font-bold mb-3">
{["docker", "podman"].includes(platform)
? t("method")
: t("architecture")
}
options={getArchitectures(platform).map((arch) => ({
value: arch,
label: arch
}))}
value={architecture}
onChange={setArchitecture}
cols={5}
className="mt-4"
/>
<div className="pt-4">
: t("architecture")}
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{getArchitectures(platform).map((arch) => (
<Button
key={arch}
variant={
architecture === arch
? "squareOutlinePrimary"
: "squareOutline"
}
className={`flex-1 min-w-30 ${architecture === arch ? "bg-primary/10" : ""} shadow-none`}
onClick={() => setArchitecture(arch)}
>
{arch}
</Button>
))}
</div>
<div className="pt-4">
<p className="font-bold mb-3">{t("commands")}</p>
<div className="mt-2 space-y-3">
{commands.map((item, index) => {
@@ -164,6 +174,7 @@ curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/ol
);
})}
</div>
</div>
</div>
</SettingsSectionBody>
</SettingsSection>

View File

@@ -20,7 +20,7 @@ export const isOrgSubscribed = cache(async (orgId: string) => {
try {
const subRes = await getCachedSubscription(orgId);
subscribed =
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3") &&
(subRes.data.data.tier == "tier1" || subRes.data.data.tier == "tier2" || subRes.data.data.tier == "tier3" || subRes.data.data.tier == "enterprise") &&
subRes.data.data.active;
} catch {}
}

View File

@@ -42,7 +42,8 @@ export function SubscriptionStatusProvider({
if (
subscription.type == "tier1" ||
subscription.type == "tier2" ||
subscription.type == "tier3"
subscription.type == "tier3" ||
subscription.type == "enterprise"
) {
return {
tier: subscription.type,
@@ -61,7 +62,7 @@ export function SubscriptionStatusProvider({
const isSubscribed = () => {
const { tier, active } = getTier();
return (
(tier == "tier1" || tier == "tier2" || tier == "tier3") &&
(tier == "tier1" || tier == "tier2" || tier == "tier3" || tier == "enterprise") &&
active
);
};