Compare commits

...

107 Commits

Author SHA1 Message Date
Owen Schwartz 40f2262f3e Merge pull request #2309 from fosrl/dev
1.15.0
2026-01-23 10:40:16 -08:00
miloschwartz 643d56958d fix saas private import 2026-01-23 10:07:05 -08:00
miloschwartz f378d6f040 fix input border 2026-01-22 21:24:28 -08:00
Milo Schwartz bb57794388 Merge pull request #2306 from Fredkiss3/fix/tab-from-host-port
fix: tab between host & port in resource target address column
2026-01-22 21:14:20 -08:00
miloschwartz a9ca49b8a2 Merge branch 'main' into dev 2026-01-22 21:10:40 -08:00
Fred KISSIE c1b473294e 🔥 remove useless useEffect 2026-01-23 04:54:24 +01:00
Fred KISSIE e3e4bdfe09 🚸 fix target item tabbing by memoizing the getColumns (and its dependencies) 2026-01-23 04:40:19 +01:00
miloschwartz bfbeace2e2 fix import in list approvals 2026-01-22 17:54:53 -08:00
miloschwartz efcf46ce8a fix policy check on olm register 2026-01-22 16:28:15 -08:00
miloschwartz 2085715965 fix wrong redirect url when idp login with custom auth domain 2026-01-22 15:46:48 -08:00
Owen d227db7b7b Show the source in the UI 2026-01-22 15:18:27 -08:00
Owen 2af67ad355 Fix the source of the cli blueprint 2026-01-22 15:18:27 -08:00
miloschwartz f100854423 add ios and android to readme 2026-01-22 15:18:27 -08:00
miloschwartz 92331d7a33 clean up paid features check 2026-01-22 15:18:27 -08:00
Owen 9a5bcb9099 Hiring 2026-01-22 15:18:27 -08:00
miloschwartz 8eb6bb2a95 dont include posture in repsonse if not licensed or subscribed 2026-01-22 15:18:27 -08:00
miloschwartz 2aa65ccab3 add mobile links to download banner 2026-01-22 15:18:27 -08:00
miloschwartz be1577a3e7 remove biometric support from ios 2026-01-22 15:18:27 -08:00
miloschwartz c8e1b3bf29 rename windowsDefenderEnabled 2026-01-22 15:18:27 -08:00
Owen e17b986628 Dont show bio info on android 2026-01-22 15:18:27 -08:00
Owen 5f19918ca0 Show the source in the UI 2026-01-22 15:16:41 -08:00
Owen 2959ad0e70 Fix the source of the cli blueprint 2026-01-22 15:03:04 -08:00
miloschwartz a76eec7bb7 add ios and android to readme 2026-01-22 11:27:24 -08:00
miloschwartz 068b2a0dcd clean up paid features check 2026-01-22 11:16:27 -08:00
Owen 316b7e5653 Hiring 2026-01-22 10:38:32 -08:00
miloschwartz 00fc1da33c dont include posture in repsonse if not licensed or subscribed 2026-01-22 10:36:52 -08:00
miloschwartz 9ef93df54f add mobile links to download banner 2026-01-21 18:16:16 -08:00
miloschwartz fd9fdf6399 remove biometric support from ios 2026-01-21 18:13:12 -08:00
miloschwartz 8fa1701e06 rename windowsDefenderEnabled 2026-01-21 17:57:20 -08:00
Owen 4abe83f8a9 Dont show bio info on android 2026-01-21 16:36:35 -08:00
Owen 0a7564acb6 Fix not detecting rc release in sign and package 2026-01-21 16:14:00 -08:00
miloschwartz db0f7cfbae add autoupdates to macos 2026-01-21 14:43:48 -08:00
Owen Schwartz 1724885371 New translations en-us.json (Norwegian Bokmal) 2026-01-21 14:41:55 -08:00
Owen Schwartz a97e9ea8b1 New translations en-us.json (Chinese Simplified) 2026-01-21 14:41:55 -08:00
Owen Schwartz 9d30e97526 New translations en-us.json (Turkish) 2026-01-21 14:41:55 -08:00
Owen Schwartz b91330a27a New translations en-us.json (Russian) 2026-01-21 14:41:55 -08:00
Owen Schwartz 744bc9ebe9 New translations en-us.json (Portuguese) 2026-01-21 14:41:55 -08:00
Owen Schwartz 89ed9e6d7f New translations en-us.json (Polish) 2026-01-21 14:41:55 -08:00
Owen Schwartz b007e7f54a New translations en-us.json (Dutch) 2026-01-21 14:41:55 -08:00
Owen Schwartz 6651a6df42 New translations en-us.json (Korean) 2026-01-21 14:41:55 -08:00
Owen Schwartz 3f29b165aa New translations en-us.json (Italian) 2026-01-21 14:41:55 -08:00
Owen Schwartz b13b91face New translations en-us.json (German) 2026-01-21 14:41:55 -08:00
Owen Schwartz 63c14fe2d5 New translations en-us.json (Czech) 2026-01-21 14:41:55 -08:00
Owen Schwartz 14e74ed02d New translations en-us.json (Bulgarian) 2026-01-21 14:41:55 -08:00
Owen Schwartz 7e30750618 New translations en-us.json (Spanish) 2026-01-21 14:41:55 -08:00
Owen Schwartz 4d1dd16be5 New translations en-us.json (French) 2026-01-21 14:41:55 -08:00
Owen Schwartz fa49cf5eba New translations en-us.json (Norwegian Bokmal) 2026-01-21 14:41:55 -08:00
Owen Schwartz 26b39fc1c6 New translations en-us.json (Chinese Simplified) 2026-01-21 14:41:55 -08:00
Owen Schwartz 0d36e368ea New translations en-us.json (Turkish) 2026-01-21 14:41:55 -08:00
Owen Schwartz 859f265c68 New translations en-us.json (Russian) 2026-01-21 14:41:55 -08:00
Owen Schwartz 3219f520ba New translations en-us.json (Portuguese) 2026-01-21 14:41:55 -08:00
Owen Schwartz 97e27b6caf New translations en-us.json (Polish) 2026-01-21 14:41:55 -08:00
Owen Schwartz 09da83a72b New translations en-us.json (Dutch) 2026-01-21 14:41:55 -08:00
Owen Schwartz d13b210e2f New translations en-us.json (Korean) 2026-01-21 14:41:55 -08:00
Owen Schwartz 09fb672718 New translations en-us.json (Italian) 2026-01-21 14:41:55 -08:00
Owen Schwartz 9797ad0e17 New translations en-us.json (German) 2026-01-21 14:41:55 -08:00
Owen Schwartz 8b3d61ac36 New translations en-us.json (Czech) 2026-01-21 14:41:55 -08:00
Owen Schwartz 7161c9547a New translations en-us.json (Bulgarian) 2026-01-21 14:41:55 -08:00
Owen Schwartz 60d4362a87 New translations en-us.json (Spanish) 2026-01-21 14:41:55 -08:00
Owen Schwartz 1836e0c8fc New translations en-us.json (French) 2026-01-21 14:41:55 -08:00
Owen Schwartz d3344aeb34 New translations en-us.json (Norwegian Bokmal) 2026-01-21 14:41:55 -08:00
Owen Schwartz cfeb093fa6 New translations en-us.json (Chinese Simplified) 2026-01-21 14:41:55 -08:00
Owen Schwartz a469b3ffcc New translations en-us.json (Turkish) 2026-01-21 14:41:55 -08:00
Owen Schwartz 14b3a3fdd8 New translations en-us.json (Russian) 2026-01-21 14:41:55 -08:00
Owen Schwartz 94367ce387 New translations en-us.json (Portuguese) 2026-01-21 14:41:55 -08:00
Owen Schwartz 5be518aa50 New translations en-us.json (Polish) 2026-01-21 14:41:55 -08:00
Owen Schwartz d059a8da9e New translations en-us.json (Dutch) 2026-01-21 14:41:55 -08:00
Owen Schwartz 1dcacbef7a New translations en-us.json (Korean) 2026-01-21 14:41:55 -08:00
Owen Schwartz a25edeccf7 New translations en-us.json (Italian) 2026-01-21 14:41:55 -08:00
Owen Schwartz 315f73c77d New translations en-us.json (German) 2026-01-21 14:41:55 -08:00
Owen Schwartz 666288fccc New translations en-us.json (Czech) 2026-01-21 14:41:55 -08:00
Owen Schwartz 0ccf61c2a9 New translations en-us.json (Bulgarian) 2026-01-21 14:41:55 -08:00
Owen Schwartz c16b1b27a3 New translations en-us.json (Spanish) 2026-01-21 14:41:55 -08:00
Owen Schwartz ed9ba60be6 New translations en-us.json (French) 2026-01-21 14:41:55 -08:00
Owen 24d047e3d8 Merge branch 'main' into dev 2026-01-21 14:39:15 -08:00
miloschwartz 9671079ffb show alias address in private resources table 2026-01-21 14:30:42 -08:00
Owen 688892523c Bump version 2026-01-21 14:24:29 -08:00
miloschwartz b02c341f62 add delete client/device cli command 2026-01-21 12:47:04 -08:00
miloschwartz 3e9bcada1e dont show pending/deny approvals in oss 2026-01-21 12:33:33 -08:00
Milo Schwartz 93d4bd6438 Merge pull request #2280 from Fredkiss3/feat/show-newt-install-command
feat: show install commands in sites & clients details page
2026-01-21 12:27:03 -08:00
miloschwartz 5146498b33 remove extra logs 2026-01-21 12:11:29 -08:00
miloschwartz 72da4f39a8 show disk encryption for mac 2026-01-21 11:49:25 -08:00
miloschwartz a2b2fb804b show device model for ios and android 2026-01-21 11:49:25 -08:00
Owen 3eac80e666 We dont care if its archived when recovering the olm 2026-01-20 20:47:35 -08:00
Fred KISSIE 718d2122a4 ♻️ move olm install command to its own component 2026-01-21 05:22:49 +01:00
Fred KISSIE 310c6c90a3 Merge branch 'dev' into feat/show-newt-install-command 2026-01-21 03:26:52 +01:00
Fred KISSIE 9d80f62d58 ♻️ move newt install commands to its own component for reusing in details 2026-01-21 03:21:35 +01:00
Owen 77032fc989 Remove extranious file 2026-01-20 18:07:28 -08:00
miloschwartz 64e6086f0c set docs link for approvals 2026-01-20 17:50:07 -08:00
Fred KISSIE fe729ec762 🚧wip: command component 2026-01-20 05:21:18 +01:00
Fred KISSIE d6a829abc2 Merge branch 'dev' into feat/show-newt-install-command 2026-01-20 03:36:38 +01:00
Kolin 75005ccf81 Fix WireGuard QR code layout on mobile in site creation page 2026-01-19 17:52:21 -08:00
Kolin fd6c600531 Fix WireGuard QR code layout on mobile in site credentials page 2026-01-19 17:52:21 -08:00
ThanatosDi 6996c2501e feat: zh-TW for v1.14.1 2026-01-19 17:51:30 -08:00
Owen efbd9bdb56 Remove faker 2026-01-19 17:50:46 -08:00
dependabot[bot] 0d34213647 Bump the prod-minor-updates group across 1 directory with 12 updates
Bumps the prod-minor-updates group with 12 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi) | `8.2.0` | `8.4.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.955.0` | `3.971.0` |
| [@faker-js/faker](https://github.com/faker-js/faker) | `10.1.0` | `10.2.0` |
| [ioredis](https://github.com/luin/ioredis) | `5.8.2` | `5.9.2` |
| [next-intl](https://github.com/amannn/next-intl) | `4.6.1` | `4.7.0` |
| [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) | `8.16.3` | `8.17.1` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.17.4` | `5.21.1` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.68.0` | `7.71.1` |
| [resend](https://github.com/resend/resend-node) | `6.6.0` | `6.7.0` |
| [stripe](https://github.com/stripe/stripe-node) | `20.1.0` | `20.2.0` |
| [ws](https://github.com/websockets/ws) | `8.18.3` | `8.19.0` |
| [zod](https://github.com/colinhacks/zod) | `4.2.1` | `4.3.5` |



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

Updates `@aws-sdk/client-s3` from 3.955.0 to 3.971.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.971.0/clients/client-s3)

Updates `@faker-js/faker` from 10.1.0 to 10.2.0
- [Release notes](https://github.com/faker-js/faker/releases)
- [Changelog](https://github.com/faker-js/faker/blob/next/CHANGELOG.md)
- [Commits](https://github.com/faker-js/faker/compare/v10.1.0...v10.2.0)

Updates `ioredis` from 5.8.2 to 5.9.2
- [Release notes](https://github.com/luin/ioredis/releases)
- [Changelog](https://github.com/redis/ioredis/blob/main/CHANGELOG.md)
- [Commits](https://github.com/luin/ioredis/compare/v5.8.2...v5.9.2)

Updates `next-intl` from 4.6.1 to 4.7.0
- [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.6.1...v4.7.0)

Updates `pg` from 8.16.3 to 8.17.1
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.17.1/packages/pg)

Updates `posthog-node` from 5.17.4 to 5.21.1
- [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.21.1/packages/node)

Updates `react-hook-form` from 7.68.0 to 7.71.1
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.68.0...v7.71.1)

Updates `resend` from 6.6.0 to 6.7.0
- [Release notes](https://github.com/resend/resend-node/releases)
- [Commits](https://github.com/resend/resend-node/compare/v6.6.0...v6.7.0)

Updates `stripe` from 20.1.0 to 20.2.0
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v20.1.0...v20.2.0)

Updates `ws` from 8.18.3 to 8.19.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.3...8.19.0)

Updates `zod` from 4.2.1 to 4.3.5
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.2.1...v4.3.5)

---
updated-dependencies:
- dependency-name: "@asteasolutions/zod-to-openapi"
  dependency-version: 8.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.971.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: "@faker-js/faker"
  dependency-version: 10.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: ioredis
  dependency-version: 5.9.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: next-intl
  dependency-version: 4.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: pg
  dependency-version: 8.17.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: posthog-node
  dependency-version: 5.21.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: react-hook-form
  dependency-version: 7.71.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: resend
  dependency-version: 6.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: stripe
  dependency-version: 20.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: ws
  dependency-version: 8.19.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
- dependency-name: zod
  dependency-version: 4.3.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:50:46 -08:00
dependabot[bot] 870b85d71b Bump the dev-minor-updates group across 1 directory with 3 updates
Bumps the dev-minor-updates group with 3 updates in the / directory: [prettier](https://github.com/prettier/prettier), [react-email](https://github.com/resend/react-email/tree/HEAD/packages/react-email) and [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint).


Updates `prettier` from 3.7.4 to 3.8.0
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.7.4...3.8.0)

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

Updates `typescript-eslint` from 8.49.0 to 8.53.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.53.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: prettier
  dependency-version: 3.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: react-email
  dependency-version: 5.2.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
- dependency-name: typescript-eslint
  dependency-version: 8.53.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:42:02 -08:00
dependabot[bot] 86ba6b6f86 Bump golang.org/x/term in /install in the prod-minor-updates group
Bumps the prod-minor-updates group in /install with 1 update: [golang.org/x/term](https://github.com/golang/term).


Updates `golang.org/x/term` from 0.38.0 to 0.39.0
- [Commits](https://github.com/golang/term/compare/v0.38.0...v0.39.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.39.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: prod-minor-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:39:29 -08:00
dependabot[bot] 02be3cd0c4 Bump qs from 6.14.0 to 6.14.1
Bumps [qs](https://github.com/ljharb/qs) from 6.14.0 to 6.14.1.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.0...v6.14.1)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.14.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:39:00 -08:00
dependabot[bot] 1b756ef9a0 Bump aws-actions/configure-aws-credentials from 2 to 5
Bumps [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) from 2 to 5.
- [Release notes](https://github.com/aws-actions/configure-aws-credentials/releases)
- [Changelog](https://github.com/aws-actions/configure-aws-credentials/blob/main/CHANGELOG.md)
- [Commits](https://github.com/aws-actions/configure-aws-credentials/compare/v2...v5)

---
updated-dependencies:
- dependency-name: aws-actions/configure-aws-credentials
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:38:03 -08:00
dependabot[bot] ceda06f9ae Bump actions/setup-go from 6.1.0 to 6.2.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/4dc6199c7b1a012772edbd06daecab0f50c9053c...7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:37:55 -08:00
dependabot[bot] 068eba015b Bump actions/setup-node from 6.1.0 to 6.2.0
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/395ad3262231945c25e8478fd5baf05154b1d79f...6044e13b5dc448c55e2357c09f80417699197238)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-19 17:37:46 -08:00
Owen 90f66baf85 Update config dir values to match install 2026-01-19 13:55:22 -08:00
Kolin 5743c0bb72 chore: add ru-RU, bg-BG and cs-CZ locales 2026-01-19 10:11:50 -08:00
Fred KISSIE ec10c37468 🚧wip 2026-01-17 02:09:59 +01:00
Fred KISSIE 5ee3e140ed 🔇 fix logs 2026-01-17 02:07:43 +01:00
miloschwartz 8e1fd4474f fix whitelist hyrdration closes #2190 2026-01-14 22:09:56 -08:00
82 changed files with 5080 additions and 4996 deletions
+19 -19
View File
@@ -29,7 +29,7 @@ jobs:
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
@@ -264,7 +264,7 @@ jobs:
shell: bash
- name: Install Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: 1.24
@@ -339,37 +339,37 @@ jobs:
TAG=${{ env.TAG }}
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
echo "Waiting for multi-arch manifests to be ready..."
sleep 30
# Determine if this is an RC release
IS_RC="false"
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
if [[ "$TAG" == *"-rc."* ]]; then
IS_RC="true"
fi
if [ "$IS_RC" = "true" ]; then
echo "RC release detected - copying version-specific tags only"
# SQLite OSS
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:$TAG \
docker://$GHCR_IMAGE:$TAG
# PostgreSQL OSS
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:postgresql-$TAG \
docker://$GHCR_IMAGE:postgresql-$TAG
# SQLite Enterprise
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-${TAG}"
skopeo copy --all --retry-times 3 \
docker://$DOCKERHUB_IMAGE:ee-$TAG \
docker://$GHCR_IMAGE:ee-$TAG
# PostgreSQL Enterprise
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG}"
skopeo copy --all --retry-times 3 \
@@ -377,7 +377,7 @@ jobs:
docker://$GHCR_IMAGE:ee-postgresql-$TAG
else
echo "Regular release detected - copying all tags (latest, major, minor, full version)"
# SQLite OSS - all tags
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:${TAG_SUFFIX}"
@@ -385,7 +385,7 @@ jobs:
docker://$DOCKERHUB_IMAGE:$TAG_SUFFIX \
docker://$GHCR_IMAGE:$TAG_SUFFIX
done
# PostgreSQL OSS - all tags
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG_SUFFIX}"
@@ -393,7 +393,7 @@ jobs:
docker://$DOCKERHUB_IMAGE:postgresql-$TAG_SUFFIX \
docker://$GHCR_IMAGE:postgresql-$TAG_SUFFIX
done
# SQLite Enterprise - all tags
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-${TAG_SUFFIX}"
@@ -401,7 +401,7 @@ jobs:
docker://$DOCKERHUB_IMAGE:ee-$TAG_SUFFIX \
docker://$GHCR_IMAGE:ee-$TAG_SUFFIX
done
# PostgreSQL Enterprise - all tags
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG_SUFFIX}"
@@ -410,7 +410,7 @@ jobs:
docker://$GHCR_IMAGE:ee-postgresql-$TAG_SUFFIX
done
fi
echo "All images copied successfully to GHCR!"
shell: bash
@@ -442,7 +442,7 @@ jobs:
# Determine if this is an RC release
IS_RC="false"
if echo "$TAG" | grep -qE "rc[0-9]+$"; then
if [[ "$TAG" == *"-rc."* ]]; then
IS_RC="true"
fi
@@ -490,11 +490,11 @@ jobs:
--certificate-oidc-issuer "${issuer}" \
--certificate-identity-regexp "${id_regex}" \
"${REF}" -o text
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
done
done
echo "All images signed and verified successfully!"
shell: bash
@@ -513,7 +513,7 @@ jobs:
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '22'
+3 -3
View File
@@ -23,7 +23,7 @@ jobs:
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
@@ -69,7 +69,7 @@ jobs:
fi
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
@@ -110,7 +110,7 @@ jobs:
permissions: write-all
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: '22'
+3 -3
View File
@@ -4,13 +4,13 @@
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.json-language-features"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
@@ -19,4 +19,4 @@
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true
}
}
+8
View File
@@ -35,6 +35,12 @@
</div>
<p align="center">
<a href="https://docs.pangolin.net/careers/join-us">
<img src="https://img.shields.io/badge/🚀_We're_Hiring!-Join_Our_Team-brightgreen?style=for-the-badge" alt="We're Hiring!" />
</a>
</p>
<p align="center">
<strong>
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
@@ -74,6 +80,8 @@ Download the Pangolin client for your platform:
- [Mac](https://pangolin.net/downloads/mac)
- [Windows](https://pangolin.net/downloads/windows)
- [Linux](https://pangolin.net/downloads/linux)
- [iOS](https://pangolin.net/downloads/ios)
- [Android](https://pangolin.net/downloads/android)
## Get Started
-72
View File
@@ -1,72 +0,0 @@
import requests
import yaml
import json
import base64
# The file path for the YAML file to be read
# You can change this to the path of your YAML file
YAML_FILE_PATH = 'blueprint.yaml'
# The API endpoint and headers from the curl request
API_URL = 'http://api.pangolin.net/v1/org/test/blueprint'
HEADERS = {
'accept': '*/*',
'Authorization': 'Bearer <your_token_here>',
'Content-Type': 'application/json'
}
def convert_and_send(file_path, url, headers):
"""
Reads a YAML file, converts its content to a JSON payload,
and sends it via a PUT request to a specified URL.
"""
try:
# Read the YAML file content
with open(file_path, 'r') as file:
yaml_content = file.read()
# Parse the YAML string to a Python dictionary
# This will be used to ensure the YAML is valid before sending
parsed_yaml = yaml.safe_load(yaml_content)
# convert the parsed YAML to a JSON string
json_payload = json.dumps(parsed_yaml)
print("Converted JSON payload:")
print(json_payload)
# Encode the JSON string to Base64
encoded_json = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8')
# Create the final payload with the base64 encoded data
final_payload = {
"blueprint": encoded_json
}
print("Sending the following Base64 encoded JSON payload:")
print(final_payload)
print("-" * 20)
# Make the PUT request with the base64 encoded payload
response = requests.put(url, headers=headers, json=final_payload)
# Print the API response for debugging
print(f"API Response Status Code: {response.status_code}")
print("API Response Content:")
print(response.text)
# Raise an exception for bad status codes (4xx or 5xx)
response.raise_for_status()
except FileNotFoundError:
print(f"Error: The file '{file_path}' was not found.")
except yaml.YAMLError as e:
print(f"Error parsing YAML file: {e}")
except requests.exceptions.RequestException as e:
print(f"An error occurred during the API request: {e}")
except Exception as e:
print(f"An unexpected error occurred: {e}")
# Run the function
if __name__ == "__main__":
convert_and_send(YAML_FILE_PATH, API_URL, HEADERS)
-70
View File
@@ -1,70 +0,0 @@
client-resources:
client-resource-nice-id-uno:
name: this is my resource
protocol: tcp
proxy-port: 3001
hostname: localhost
internal-port: 3000
site: lively-yosemite-toad
client-resource-nice-id-duce:
name: this is my resource
protocol: udp
proxy-port: 3000
hostname: localhost
internal-port: 3000
site: lively-yosemite-toad
proxy-resources:
resource-nice-id-uno:
name: this is my resource
protocol: http
full-domain: duce.test.example.com
host-header: example.com
tls-server-name: example.com
# auth:
# pincode: 123456
# password: sadfasdfadsf
# sso-enabled: true
# sso-roles:
# - Member
# sso-users:
# - owen@pangolin.net
# whitelist-users:
# - owen@pangolin.net
# auto-login-idp: 1
headers:
- name: X-Example-Header
value: example-value
- name: X-Another-Header
value: another-value
rules:
- action: allow
match: ip
value: 1.1.1.1
- action: deny
match: cidr
value: 2.2.2.2/32
- action: pass
match: path
value: /admin
targets:
- site: lively-yosemite-toad
path: /path
pathMatchType: prefix
hostname: localhost
method: http
port: 8000
- site: slim-alpine-chipmunk
hostname: localhost
path: /yoman
pathMatchType: exact
method: http
port: 8001
resource-nice-id-duce:
name: this is other resource
protocol: tcp
proxy-port: 3000
targets:
- site: lively-yosemite-toad
hostname: localhost
port: 3000
+123
View File
@@ -0,0 +1,123 @@
import { CommandModule } from "yargs";
import { db, clients, olms, currentFingerprint, userClients, approvals } from "@server/db";
import { eq, and, inArray } from "drizzle-orm";
type DeleteClientArgs = {
orgId: string;
niceId: string;
};
export const deleteClient: CommandModule<{}, DeleteClientArgs> = {
command: "delete-client",
describe:
"Delete a client and all associated data (OLMs, current fingerprint, userClients, approvals). Snapshots are preserved.",
builder: (yargs) => {
return yargs
.option("orgId", {
type: "string",
demandOption: true,
describe: "The organization ID"
})
.option("niceId", {
type: "string",
demandOption: true,
describe: "The client niceId (identifier)"
});
},
handler: async (argv: { orgId: string; niceId: string }) => {
try {
const { orgId, niceId } = argv;
console.log(
`Deleting client with orgId: ${orgId}, niceId: ${niceId}...`
);
// Find the client
const [client] = await db
.select()
.from(clients)
.where(and(eq(clients.orgId, orgId), eq(clients.niceId, niceId)))
.limit(1);
if (!client) {
console.error(
`Error: Client with orgId "${orgId}" and niceId "${niceId}" not found.`
);
process.exit(1);
}
const clientId = client.clientId;
console.log(`Found client with clientId: ${clientId}`);
// Find all OLMs associated with this client
const associatedOlms = await db
.select()
.from(olms)
.where(eq(olms.clientId, clientId));
console.log(`Found ${associatedOlms.length} OLM(s) associated with this client`);
// Delete in a transaction to ensure atomicity
await db.transaction(async (trx) => {
// Delete currentFingerprint entries for the associated OLMs
// Note: We delete these explicitly before deleting OLMs to ensure
// we have control, even though cascade would handle it
let fingerprintCount = 0;
if (associatedOlms.length > 0) {
const olmIds = associatedOlms.map((olm) => olm.olmId);
const deletedFingerprints = await trx
.delete(currentFingerprint)
.where(inArray(currentFingerprint.olmId, olmIds))
.returning();
fingerprintCount = deletedFingerprints.length;
}
console.log(`Deleted ${fingerprintCount} current fingerprint(s)`);
// Delete OLMs
// Note: OLMs have onDelete: "set null" for clientId, so we need to delete them explicitly
const deletedOlms = await trx
.delete(olms)
.where(eq(olms.clientId, clientId))
.returning();
console.log(`Deleted ${deletedOlms.length} OLM(s)`);
// Delete approvals
// Note: Approvals have onDelete: "cascade" but we delete explicitly for clarity
const deletedApprovals = await trx
.delete(approvals)
.where(eq(approvals.clientId, clientId))
.returning();
console.log(`Deleted ${deletedApprovals.length} approval(s)`);
// Delete userClients
// Note: userClients have onDelete: "cascade" but we delete explicitly for clarity
const deletedUserClients = await trx
.delete(userClients)
.where(eq(userClients.clientId, clientId))
.returning();
console.log(`Deleted ${deletedUserClients.length} userClient association(s)`);
// Finally, delete the client itself
const deletedClients = await trx
.delete(clients)
.where(eq(clients.clientId, clientId))
.returning();
console.log(`Deleted client: ${deletedClients[0]?.name || niceId}`);
});
console.log("\nClient deletion completed successfully!");
console.log("\nSummary:");
console.log(` - Client: ${niceId} (clientId: ${clientId})`);
console.log(` - Olm(s): ${associatedOlms.length}`);
console.log(` - Current fingerprints: deleted`);
console.log(` - Approvals: deleted`);
console.log(` - UserClients: deleted`);
console.log(` - Snapshots: preserved (not deleted)`);
process.exit(0);
} catch (error) {
console.error("Error deleting client:", error);
process.exit(1);
}
}
};
+2
View File
@@ -7,6 +7,7 @@ import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
import { clearExitNodes } from "./commands/clearExitNodes";
import { rotateServerSecret } from "./commands/rotateServerSecret";
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
import { deleteClient } from "./commands/deleteClient";
yargs(hideBin(process.argv))
.scriptName("pangctl")
@@ -15,5 +16,6 @@ yargs(hideBin(process.argv))
.command(clearExitNodes)
.command(rotateServerSecret)
.command(clearLicenseKeys)
.command(deleteClient)
.demandCommand()
.help().argv;
+24 -21
View File
@@ -1,27 +1,30 @@
# To see all available options, please visit the docs:
# https://docs.pangolin.net/self-host/advanced/config-file
app:
dashboard_url: http://localhost:3002
log_level: debug
domains:
domain1:
base_domain: example.com
server:
secret: my_secret_key
# https://docs.pangolin.net/
gerbil:
base_endpoint: example.com
start_port: 51820
base_endpoint: "{{.DashboardDomain}}"
orgs:
block_size: 24
subnet_group: 100.90.137.0/20
app:
dashboard_url: "https://{{.DashboardDomain}}"
log_level: "info"
telemetry:
anonymous_usage: true
domains:
domain1:
base_domain: "{{.BaseDomain}}"
server:
secret: "{{.Secret}}"
cors:
origins: ["https://{{.DashboardDomain}}"]
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false
flags:
require_email_verification: false
disable_signup_without_invite: true
disable_user_create_org: true
allow_raw_resources: true
enable_integration_api: true
require_email_verification: false
disable_signup_without_invite: true
disable_user_create_org: false
allow_raw_resources: true
+10 -3
View File
@@ -21,9 +21,8 @@ http:
# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "Host(`{{.DashboardDomain}}`)"
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
service: next-service
priority: 10
entryPoints:
- websecure
middlewares:
@@ -35,7 +34,6 @@ http:
api-router:
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
service: api-service
priority: 100
entryPoints:
- websecure
middlewares:
@@ -53,3 +51,12 @@ http:
loadBalancer:
servers:
- url: "http://pangolin:3000" # API/WebSocket server
tcp:
serversTransports:
pp-transport-v1:
proxyProtocol:
version: 1
pp-transport-v2:
proxyProtocol:
version: 2
+25 -5
View File
@@ -3,32 +3,52 @@ api:
dashboard: true
providers:
http:
endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s"
file:
directory: "/var/dynamic"
watch: true
filename: "/etc/traefik/dynamic_config.yml"
experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.3.0"
version: "{{.BadgerVersion}}"
log:
level: "DEBUG"
level: "INFO"
format: "common"
maxSize: 100
maxBackups: 3
maxAge: 3
compress: true
certificatesResolvers:
letsencrypt:
acme:
httpChallenge:
entryPoint: web
email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
entryPoints:
web:
address: ":80"
websecure:
address: ":9443"
address: ":443"
transport:
respondingTimeouts:
readTimeout: "30m"
http:
tls:
certResolver: "letsencrypt"
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true
serversTransport:
insecureSkipVerify: true
ping:
entryPoint: "web"
+2 -2
View File
@@ -3,8 +3,8 @@ module installer
go 1.24.0
require (
golang.org/x/term v0.38.0
golang.org/x/term v0.39.0
gopkg.in/yaml.v3 v3.0.1
)
require golang.org/x/sys v0.39.0 // indirect
require golang.org/x/sys v0.40.0 // indirect
+4 -4
View File
@@ -1,7 +1,7 @@
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.",
"resourcesTableDestination": "Дестинация",
"resourcesTableAlias": "Псевдоним",
"resourcesTableAliasAddress": "Адрес на псевдоним.",
"resourcesTableAliasAddressInfo": "Този адрес е част от подсистемата на организацията. Използва се за разрешаване на псевдонимни записи чрез вътрешно DNS разрешаване.",
"resourcesTableClients": "Клиенти",
"resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.",
"resourcesTableNoTargets": "Без цели",
@@ -2489,6 +2491,8 @@
"logIn": "Вход",
"deviceInformation": "Информация за устройството",
"deviceInformationDescription": "Информация за устройството и агента",
"deviceSecurity": "Защита на устройството.",
"deviceSecurityDescription": "Информация за състоянието на защитата на устройството.",
"platform": "Платформа",
"macosVersion": "Версия на macOS",
"windowsVersion": "Версия на Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Име на хост",
"firstSeen": "Видян за първи път",
"lastSeen": "Последно видян",
"biometricsEnabled": "Активирани биометрични данни.",
"diskEncrypted": "Криптиран диск.",
"firewallEnabled": "Активирана защитна стена.",
"autoUpdatesEnabled": "Активирани автоматични актуализации.",
"tpmAvailable": "TPM е на разположение.",
"macosSipEnabled": "Protection на системната цялост (SIP).",
"macosGatekeeperEnabled": "Gatekeeper.",
"macosFirewallStealthMode": "Скрит режим на защитната стена.",
"linuxAppArmorEnabled": "AppArmor.",
"linuxSELinuxEnabled": "SELinux.",
"deviceSettingsDescription": "Разгледайте информация и настройки на устройството",
"devicePendingApprovalDescription": "Това устройство чака одобрение",
"deviceBlockedDescription": "Това устройство е в момента блокирано. Няма да може да се свърже с никакви ресурси, освен ако не бъде деблокирано.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné vnitřní zdroje.",
"resourcesTableDestination": "Místo určení",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Adresa aliasu",
"resourcesTableAliasAddressInfo": "Tato adresa je součástí subsítě veřejných služeb organizace. Používá se k řešení záznamů aliasů pomocí interního rozlišení DNS.",
"resourcesTableClients": "Klienti",
"resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.",
"resourcesTableNoTargets": "Žádné cíle",
@@ -2489,6 +2491,8 @@
"logIn": "Přihlásit se",
"deviceInformation": "Informace o zařízení",
"deviceInformationDescription": "Informace o zařízení a agentovi",
"deviceSecurity": "Zabezpečení zařízení",
"deviceSecurityDescription": "Informace o bezpečnostní pozici zařízení",
"platform": "Platforma",
"macosVersion": "macOS verze",
"windowsVersion": "Verze Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "První vidění",
"lastSeen": "Naposledy viděno",
"biometricsEnabled": "Biometrie povolena",
"diskEncrypted": "Šifrovaný disk",
"firewallEnabled": "Firewall povolen",
"autoUpdatesEnabled": "Automatické aktualizace povoleny",
"tpmAvailable": "TPM k dispozici",
"macosSipEnabled": "Ochrana systémové integrity (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Režim neviditelnosti firewallu",
"linuxAppArmorEnabled": "Pancíř aplikace",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Zobrazit informace o zařízení a nastavení",
"devicePendingApprovalDescription": "Toto zařízení čeká na schválení",
"deviceBlockedDescription": "Toto zařízení je momentálně blokováno. Nebude se moci připojit k žádným zdrojům, dokud nebude odblokováno.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.",
"resourcesTableDestination": "Ziel",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias-Adresse",
"resourcesTableAliasAddressInfo": "Diese Adresse ist Teil des Utility-Subnetzes der Organisation. Sie wird verwendet, um Alias-Einträge mit interner DNS-Auflösung aufzulösen.",
"resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
"resourcesTableNoTargets": "Keine Ziele",
@@ -2489,6 +2491,8 @@
"logIn": "Anmelden",
"deviceInformation": "Geräteinformationen",
"deviceInformationDescription": "Informationen über das Gerät und den Agent",
"deviceSecurity": "Gerätesicherheit",
"deviceSecurityDescription": "Informationen zur Gerätesicherheit",
"platform": "Plattform",
"macosVersion": "macOS-Version",
"windowsVersion": "Windows-Version",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "Erster Blick",
"lastSeen": "Zuletzt gesehen",
"biometricsEnabled": "Biometrie aktiviert",
"diskEncrypted": "Festplatte verschlüsselt",
"firewallEnabled": "Firewall aktiviert",
"autoUpdatesEnabled": "Automatische Updates aktiviert",
"tpmAvailable": "TPM verfügbar",
"macosSipEnabled": "Schutz der Systemintegrität (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Stealth-Modus",
"linuxAppArmorEnabled": "AppRüstung",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Geräteinformationen und -einstellungen anzeigen",
"devicePendingApprovalDescription": "Dieses Gerät wartet auf Freigabe",
"deviceBlockedDescription": "Dieses Gerät ist derzeit gesperrt. Es kann keine Verbindung zu anderen Ressourcen herstellen, es sei denn, es entsperrt.",
+3 -1
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
"resourcesTableDestination": "Destination",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias Address",
"resourcesTableAliasAddressInfo": "This address is part of the organization's utility subnet. It's used to resolve alias records using internal DNS resolution.",
"resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
"resourcesTableNoTargets": "No targets",
@@ -2508,7 +2510,7 @@
"firewallEnabled": "Firewall Enabled",
"autoUpdatesEnabled": "Auto Updates Enabled",
"tpmAvailable": "TPM Available",
"windowsDefenderEnabled": "Windows Defender Enabled",
"windowsAntivirusEnabled": "Antivirus Enabled",
"macosSipEnabled": "System Integrity Protection (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Stealth Mode",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.",
"resourcesTableDestination": "Destino",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Dirección del alias",
"resourcesTableAliasAddressInfo": "Esta dirección es parte de la subred de utilidad de la organización. Se utiliza para resolver registros de alias usando resolución DNS interna.",
"resourcesTableClients": "Clientes",
"resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.",
"resourcesTableNoTargets": "Sin objetivos",
@@ -2489,6 +2491,8 @@
"logIn": "Iniciar sesión",
"deviceInformation": "Información del dispositivo",
"deviceInformationDescription": "Información sobre el dispositivo y el agente",
"deviceSecurity": "Seguridad del dispositivo",
"deviceSecurityDescription": "Información de postura de seguridad del dispositivo",
"platform": "Plataforma",
"macosVersion": "versión macOS",
"windowsVersion": "Versión de Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "Primer detectado",
"lastSeen": "Último Visto",
"biometricsEnabled": "Biometría habilitada",
"diskEncrypted": "Disco cifrado",
"firewallEnabled": "Cortafuegos activado",
"autoUpdatesEnabled": "Actualizaciones automáticas habilitadas",
"tpmAvailable": "TPM disponible",
"macosSipEnabled": "Protección de integridad del sistema (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Sigilo Firewall",
"linuxAppArmorEnabled": "AppArmor",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Ver información y ajustes del dispositivo",
"devicePendingApprovalDescription": "Este dispositivo está esperando su aprobación",
"deviceBlockedDescription": "Este dispositivo está actualmente bloqueado. No podrá conectarse a ningún recurso a menos que sea desbloqueado.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.",
"resourcesTableDestination": "Destination",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Adresse de l'alias",
"resourcesTableAliasAddressInfo": "Cette adresse fait partie du sous-réseau utilitaire de l'organisation. Elle est utilisée pour résoudre les enregistrements d'alias en utilisant une résolution DNS interne.",
"resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.",
"resourcesTableNoTargets": "Aucune cible",
@@ -2489,6 +2491,8 @@
"logIn": "Se connecter",
"deviceInformation": "Informations sur l'appareil",
"deviceInformationDescription": "Informations sur l'appareil et l'agent",
"deviceSecurity": "Sécurité de l'appareil",
"deviceSecurityDescription": "Informations sur la posture de sécurité de l'appareil",
"platform": "Plateforme",
"macosVersion": "Version macOS",
"windowsVersion": "Version de Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "Première vue",
"lastSeen": "Dernière vue",
"biometricsEnabled": "biométrique activée",
"diskEncrypted": "Disque chiffré",
"firewallEnabled": "Pare-feu activé",
"autoUpdatesEnabled": "Mises à jour automatiques activées",
"tpmAvailable": "TPM disponible",
"macosSipEnabled": "Protection contre l'intégrité du système (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Mode furtif du pare-feu",
"linuxAppArmorEnabled": "Armure d'application",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Afficher les informations et les paramètres de l'appareil",
"devicePendingApprovalDescription": "Cet appareil est en attente d'approbation",
"deviceBlockedDescription": "Cet appareil est actuellement bloqué. Il ne pourra se connecter à aucune ressource à moins d'être débloqué.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.",
"resourcesTableDestination": "Destinazione",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Indirizzo Alias",
"resourcesTableAliasAddressInfo": "Questo indirizzo fa parte della subnet di utilità dell'organizzazione. È usato per risolvere i record alias usando la risoluzione DNS interna.",
"resourcesTableClients": "Client",
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
"resourcesTableNoTargets": "Nessun obiettivo",
@@ -2489,6 +2491,8 @@
"logIn": "Log In",
"deviceInformation": "Informazioni Sul Dispositivo",
"deviceInformationDescription": "Informazioni sul dispositivo e sull'agente",
"deviceSecurity": "Sicurezza Del Dispositivo",
"deviceSecurityDescription": "Informazioni postura sicurezza dispositivo",
"platform": "Piattaforma",
"macosVersion": "versione macOS",
"windowsVersion": "Versione Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "Prima Visto",
"lastSeen": "Visto L'Ultima",
"biometricsEnabled": "Biometria Abilitata",
"diskEncrypted": "Cifratura Del Disco",
"firewallEnabled": "Firewall Abilitato",
"autoUpdatesEnabled": "Aggiornamenti Automatici Abilitati",
"tpmAvailable": "TPM Disponibile",
"macosSipEnabled": "Protezione Dell'Integrità Del Sistema (Sip)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Furtivo Del Firewall",
"linuxAppArmorEnabled": "AppArmor",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Visualizza informazioni e impostazioni del dispositivo",
"devicePendingApprovalDescription": "Questo dispositivo è in attesa di approvazione",
"deviceBlockedDescription": "Questo dispositivo è attualmente bloccato. Non sarà in grado di connettersi a nessuna risorsa a meno che non sia sbloccato.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.",
"resourcesTableDestination": "대상지",
"resourcesTableAlias": "별칭",
"resourcesTableAliasAddress": "별칭 주소",
"resourcesTableAliasAddressInfo": "이 주소는 조직의 유틸리티 서브넷의 일부로, 내부 DNS 해석을 사용하여 별칭 레코드를 해석하는 데 사용됩니다.",
"resourcesTableClients": "클라이언트",
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
"resourcesTableNoTargets": "대상 없음",
@@ -2489,6 +2491,8 @@
"logIn": "로그인",
"deviceInformation": "장치 정보",
"deviceInformationDescription": "장치와 에이전트 정보",
"deviceSecurity": "디바이스 보안",
"deviceSecurityDescription": "디바이스 보안 상태 정보",
"platform": "플랫폼",
"macosVersion": "macOS 버전",
"windowsVersion": "Windows 버전",
@@ -2501,6 +2505,16 @@
"hostname": "호스트 이름",
"firstSeen": "처음 발견됨",
"lastSeen": "마지막으로 발견됨",
"biometricsEnabled": "생체 인식 활성화",
"diskEncrypted": "디스크 암호화됨",
"firewallEnabled": "방화벽 활성화",
"autoUpdatesEnabled": "자동 업데이트 활성화",
"tpmAvailable": "TPM 사용 가능",
"macosSipEnabled": "시스템 무결성 보호 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "방화벽 스텔스 모드",
"linuxAppArmorEnabled": "AppArmor",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "장치 정보 및 설정 보기",
"devicePendingApprovalDescription": "이 장치는 승인을 기다리고 있습니다.",
"deviceBlockedDescription": "이 장치는 현재 차단되었습니다. 차단이 해제되지 않으면 리소스에 연결할 수 없습니다.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.",
"resourcesTableDestination": "Destinasjon",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias adresse",
"resourcesTableAliasAddressInfo": "Denne adressen er en del av organisasjonens undernettverk. Den brukes til å løse aliasposter ved hjelp av intern DNS-oppløsning.",
"resourcesTableClients": "Klienter",
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
"resourcesTableNoTargets": "Ingen mål",
@@ -2489,6 +2491,8 @@
"logIn": "Logg inn",
"deviceInformation": "Enhetens informasjon",
"deviceInformationDescription": "Informasjon om enheten og agenten",
"deviceSecurity": "Enhetens sikkerhet",
"deviceSecurityDescription": "Sikkerhetsstillings informasjon om utstyr",
"platform": "Plattform",
"macosVersion": "macOS versjon",
"windowsVersion": "Windows versjon",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "Først sett",
"lastSeen": "Sist sett",
"biometricsEnabled": "Biometri aktivert",
"diskEncrypted": "Disk kryptert",
"firewallEnabled": "Brannmur aktivert",
"autoUpdatesEnabled": "Automatiske oppdateringer aktivert",
"tpmAvailable": "TPM tilgjengelig",
"macosSipEnabled": "System Integritetsbeskyttelse (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Brannmur Usynlig Modus",
"linuxAppArmorEnabled": "Rustning",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Vis enhetsinformasjon og innstillinger",
"devicePendingApprovalDescription": "Denne enheten venter på godkjenning",
"deviceBlockedDescription": "Denne enheten er blokkert. Det kan ikke kobles til noen ressurser med mindre de ikke blir blokkert.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.",
"resourcesTableDestination": "Bestemming",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias adres",
"resourcesTableAliasAddressInfo": "Dit adres is onderdeel van het hulpprogramma subnet van de organisatie. Het wordt gebruikt om aliasrecords op te lossen met behulp van interne DNS-resolutie.",
"resourcesTableClients": "Clienten",
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
"resourcesTableNoTargets": "Geen doelen",
@@ -2489,6 +2491,8 @@
"logIn": "Log in",
"deviceInformation": "Apparaat informatie",
"deviceInformationDescription": "Informatie over het apparaat en de agent",
"deviceSecurity": "Apparaat beveiliging",
"deviceSecurityDescription": "Apparaat beveiligingsinformatie",
"platform": "Platform",
"macosVersion": "macOS versie",
"windowsVersion": "Windows versie",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "Eerst gezien",
"lastSeen": "Laatst gezien op",
"biometricsEnabled": "Biometrie ingeschakeld",
"diskEncrypted": "Schijf versleuteld",
"firewallEnabled": "Firewall ingeschakeld",
"autoUpdatesEnabled": "Auto Updates Ingeschakeld",
"tpmAvailable": "TPM beschikbaar",
"macosSipEnabled": "Systeemintegriteitsbescherming (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Verberg Modus",
"linuxAppArmorEnabled": "Appharnas",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Apparaatinformatie en -instellingen bekijken",
"devicePendingApprovalDescription": "Dit apparaat wacht op goedkeuring",
"deviceBlockedDescription": "Dit apparaat is momenteel geblokkeerd. Het kan geen verbinding maken met bronnen tenzij het wordt gedeblokkeerd.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.",
"resourcesTableDestination": "Miejsce docelowe",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Adres aliasu",
"resourcesTableAliasAddressInfo": "Ten adres jest częścią podsieci użyteczności organizacji. Jest używany do rozwiązywania rekordów aliasu przy użyciu wewnętrznej rozdzielczości DNS.",
"resourcesTableClients": "Klientami",
"resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.",
"resourcesTableNoTargets": "Brak celów",
@@ -2489,6 +2491,8 @@
"logIn": "Zaloguj się",
"deviceInformation": "Informacje o urządzeniu",
"deviceInformationDescription": "Informacje o urządzeniu i agentach",
"deviceSecurity": "Bezpieczeństwo urządzenia",
"deviceSecurityDescription": "Informacje o bezpieczeństwie urządzenia",
"platform": "Platforma",
"macosVersion": "Wersja macOS",
"windowsVersion": "Wersja Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "Widziany po raz pierwszy",
"lastSeen": "Ostatnio widziane",
"biometricsEnabled": "Biometria włączona",
"diskEncrypted": "Dysk zaszyfrowany",
"firewallEnabled": "Zapora włączona",
"autoUpdatesEnabled": "Automatyczne aktualizacje włączone",
"tpmAvailable": "TPM dostępne",
"macosSipEnabled": "Ochrona integralności systemu (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Tryb Stealth zapory",
"linuxAppArmorEnabled": "Zbroja aplikacji",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Wyświetl informacje o urządzeniu i ustawienia",
"devicePendingApprovalDescription": "To urządzenie czeka na zatwierdzenie",
"deviceBlockedDescription": "To urządzenie jest obecnie zablokowane. Nie będzie można połączyć się z żadnymi zasobami, chyba że zostanie odblokowane.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.",
"resourcesTableDestination": "Destino",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Endereço do Pseudônimo",
"resourcesTableAliasAddressInfo": "Este endereço faz parte da sub-rede de utilitários da organização. É usado para resolver registros de alias usando resolução de DNS interno.",
"resourcesTableClients": "Clientes",
"resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.",
"resourcesTableNoTargets": "Nenhum alvo",
@@ -2489,6 +2491,8 @@
"logIn": "Iniciar sessão",
"deviceInformation": "Informações do dispositivo",
"deviceInformationDescription": "Informações sobre o dispositivo e o agente",
"deviceSecurity": "Segurança do dispositivo",
"deviceSecurityDescription": "Informações sobre postagem de segurança",
"platform": "Plataforma",
"macosVersion": "Versão do macOS",
"windowsVersion": "Versão do Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "Visto primeiro",
"lastSeen": "Visto por último",
"biometricsEnabled": "Biometria habilitada",
"diskEncrypted": "Disco criptografado",
"firewallEnabled": "Firewall habilitado",
"autoUpdatesEnabled": "Atualizações Automáticas Habilitadas",
"tpmAvailable": "TPM disponível",
"macosSipEnabled": "Proteção da Integridade do Sistema (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Modo Furtivo do Firewall",
"linuxAppArmorEnabled": "AppArmor",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Ver informações e configurações do dispositivo",
"devicePendingApprovalDescription": "Este dispositivo está aguardando aprovação",
"deviceBlockedDescription": "Este dispositivo está bloqueado no momento. Ele não será capaz de se conectar a qualquer recurso a menos que seja desbloqueado.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Внутренних ресурсов не найдено.",
"resourcesTableDestination": "Пункт назначения",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Псевдоним адреса",
"resourcesTableAliasAddressInfo": "Этот адрес является частью вспомогательной подсети организации. Он используется для разрешения псевдонимов с использованием внутреннего разрешения DNS.",
"resourcesTableClients": "Клиенты",
"resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.",
"resourcesTableNoTargets": "Нет ярлыков",
@@ -2489,6 +2491,8 @@
"logIn": "Войти",
"deviceInformation": "Информация об устройстве",
"deviceInformationDescription": "Информация о устройстве и агенте",
"deviceSecurity": "Безопасность устройства",
"deviceSecurityDescription": "Информация о позе безопасности устройства",
"platform": "Платформа",
"macosVersion": "Версия macOS",
"windowsVersion": "Версия Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "Первый раз виден",
"lastSeen": "Последнее посещение",
"biometricsEnabled": "Включены биометрические данные",
"diskEncrypted": "Диск зашифрован",
"firewallEnabled": "Брандмауэр включен",
"autoUpdatesEnabled": "Автоматические обновления включены",
"tpmAvailable": "Доступно TPM",
"macosSipEnabled": "Защита целостности системы (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Стилс-режим брандмауэра",
"linuxAppArmorEnabled": "Броня",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Просмотр информации и настроек устройства",
"devicePendingApprovalDescription": "Это устройство ожидает одобрения",
"deviceBlockedDescription": "Это устройство заблокировано. Оно не сможет подключаться к ресурсам, если не разблокировано.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.",
"resourcesTableDestination": "Hedef",
"resourcesTableAlias": "Takma Ad",
"resourcesTableAliasAddress": "Alias Adresi",
"resourcesTableAliasAddressInfo": "Bu adres, kuruluşun yardımcı ağ alt bantının bir parçasıdır. Alias kayıtlarını çözümlemek için dahili DNS çözümlemesi kullanılır.",
"resourcesTableClients": "İstemciler",
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
"resourcesTableNoTargets": "Hedef yok",
@@ -2489,6 +2491,8 @@
"logIn": "Giriş Yap",
"deviceInformation": "Cihaz Bilgisi",
"deviceInformationDescription": "Cihaz ve temsilci hakkında bilgi",
"deviceSecurity": "Cihaz Güvenliği",
"deviceSecurityDescription": "Cihaz güvenliği durumu bilgisi",
"platform": "Platform",
"macosVersion": "macOS Sürümü",
"windowsVersion": "Windows Sürümü",
@@ -2501,6 +2505,16 @@
"hostname": "Ana Makine Adı",
"firstSeen": "İlk Görüldü",
"lastSeen": "Son Görüldü",
"biometricsEnabled": "Biyometri Etkin",
"diskEncrypted": "Disk Şifrelenmiş",
"firewallEnabled": "Güvenlik Duvarı Etkin",
"autoUpdatesEnabled": "Otomatik Güncellemeler Etkin",
"tpmAvailable": "TPM Mevcut",
"macosSipEnabled": "Sistem Bütünlüğü Koruması (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Güvenlik Duvarı Gizlilik Modu",
"linuxAppArmorEnabled": "AppArmor",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Cihaz bilgilerini ve ayarlarını görüntüleyin",
"devicePendingApprovalDescription": "Bu cihaz onay bekliyor",
"deviceBlockedDescription": "Bu cihaz şu anda engellidir. Engeli kaldırılmadığı sürece hiçbir kaynağa bağlanamayacaktır.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "未找到内部资源。",
"resourcesTableDestination": "目标",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "别名地址",
"resourcesTableAliasAddressInfo": "此地址是组织实用子网的一部分。它用来使用内部DNS解析来解析别名记录。",
"resourcesTableClients": "客户端",
"resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。",
"resourcesTableNoTargets": "没有目标",
@@ -2489,6 +2491,8 @@
"logIn": "登录",
"deviceInformation": "设备信息",
"deviceInformationDescription": "关于设备和代理的信息",
"deviceSecurity": "设备安全",
"deviceSecurityDescription": "设备安全态势信息",
"platform": "平台",
"macosVersion": "macOS 版本",
"windowsVersion": "Windows 版本",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname",
"firstSeen": "第一次查看",
"lastSeen": "上次查看时间",
"biometricsEnabled": "生物计已启用",
"diskEncrypted": "磁盘加密",
"firewallEnabled": "防火墙已启用",
"autoUpdatesEnabled": "启用自动更新",
"tpmAvailable": "TPM 可用",
"macosSipEnabled": "系统完整性保护 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "防火墙隐形模式",
"linuxAppArmorEnabled": "AppArmor",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "查看设备信息和设置",
"devicePendingApprovalDescription": "此设备正在等待批准",
"deviceBlockedDescription": "此设备目前已被屏蔽。除非解除屏蔽,否则无法连接到任何资源。",
+2392 -2095
View File
File diff suppressed because it is too large Load Diff
+856 -920
View File
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -33,9 +33,9 @@
"format": "prettier --write ."
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "8.2.0",
"@aws-sdk/client-s3": "3.955.0",
"@faker-js/faker": "10.1.0",
"@asteasolutions/zod-to-openapi": "8.4.0",
"@aws-sdk/client-s3": "3.971.0",
"@faker-js/faker": "10.2.0",
"@headlessui/react": "2.2.9",
"@hookform/resolvers": "5.2.2",
"@monaco-editor/react": "4.7.0",
@@ -92,7 +92,7 @@
"http-errors": "2.0.1",
"i": "0.3.7",
"input-otp": "1.4.2",
"ioredis": "5.8.2",
"ioredis": "5.9.2",
"jmespath": "0.16.0",
"js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3",
@@ -100,7 +100,7 @@
"maxmind": "5.0.1",
"moment": "2.30.1",
"next": "15.5.9",
"next-intl": "4.6.1",
"next-intl": "4.7.0",
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
"node-cache": "5.1.2",
@@ -109,21 +109,21 @@
"npm": "11.7.0",
"nprogress": "0.2.0",
"oslo": "1.2.1",
"pg": "8.16.3",
"posthog-node": "5.17.4",
"pg": "8.17.1",
"posthog-node": "5.23.0",
"qrcode.react": "4.2.0",
"react": "19.2.3",
"react-day-picker": "9.13.0",
"react-dom": "19.2.3",
"react-easy-sort": "1.8.0",
"react-hook-form": "7.68.0",
"react-hook-form": "7.71.1",
"react-icons": "5.5.0",
"rebuild": "0.1.2",
"recharts": "2.15.4",
"reodotdev": "1.0.0",
"resend": "6.6.0",
"resend": "6.8.0",
"semver": "7.7.3",
"stripe": "20.1.0",
"stripe": "20.2.0",
"swagger-ui-express": "5.0.1",
"tailwind-merge": "3.4.0",
"topojson-client": "3.1.0",
@@ -133,10 +133,10 @@
"visionscarto-world-atlas": "1.0.0",
"winston": "3.19.0",
"winston-daily-rotate-file": "5.0.0",
"ws": "8.18.3",
"ws": "8.19.0",
"yaml": "2.8.2",
"yargs": "18.0.0",
"zod": "4.2.1",
"zod": "4.3.5",
"zod-validation-error": "5.0.0"
},
"devDependencies": {
@@ -170,12 +170,12 @@
"esbuild": "0.27.2",
"esbuild-node-externals": "1.20.1",
"postcss": "8.5.6",
"prettier": "3.7.4",
"react-email": "5.0.7",
"prettier": "3.8.0",
"react-email": "5.2.5",
"tailwindcss": "4.1.18",
"tsc-alias": "1.8.16",
"tsx": "4.21.0",
"typescript": "5.9.3",
"typescript-eslint": "8.49.0"
"typescript-eslint": "8.53.1"
}
}
+2 -2
View File
@@ -778,7 +778,7 @@ export const currentFingerprint = pgTable("currentFingerprint", {
// Windows-specific posture check information
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
.notNull()
.default(false),
@@ -830,7 +830,7 @@ export const fingerprintSnapshots = pgTable("fingerprintSnapshots", {
// Windows-specific posture check information
windowsDefenderEnabled: boolean("windowsDefenderEnabled")
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
.notNull()
.default(false),
+2 -2
View File
@@ -475,7 +475,7 @@ export const currentFingerprint = sqliteTable("currentFingerprint", {
// Windows-specific posture check information
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
mode: "boolean"
})
.notNull()
@@ -549,7 +549,7 @@ export const fingerprintSnapshots = sqliteTable("fingerprintSnapshots", {
// Windows-specific posture check information
windowsDefenderEnabled: integer("windowsDefenderEnabled", {
windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
mode: "boolean"
})
.notNull()
+4 -12
View File
@@ -31,7 +31,7 @@ import { pickPort } from "@server/routers/target/helpers";
import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { build } from "@server/build";
export type ProxyResourcesResults = {
@@ -213,11 +213,7 @@ export async function updateProxyResources(
// Update existing resource
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
if (!isLicensed) {
resourceData.maintenance = undefined;
}
@@ -594,7 +590,7 @@ export async function updateProxyResources(
existingRule.action !== getRuleAction(rule.action) ||
existingRule.match !== rule.match.toUpperCase() ||
existingRule.value !==
getRuleValue(rule.match.toUpperCase(), rule.value) ||
getRuleValue(rule.match.toUpperCase(), rule.value) ||
existingRule.priority !== intendedPriority
) {
validateRule(rule);
@@ -653,11 +649,7 @@ export async function updateProxyResources(
}
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
if (!isLicensed) {
resourceData.maintenance = undefined;
}
+1 -1
View File
@@ -14,7 +14,7 @@ import {
} from "@server/db";
import { getUniqueClientName } from "@server/db/names";
import { getNextAvailableClientSubnet } from "@server/lib/ip";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import logger from "@server/logger";
import { sendTerminateClient } from "@server/routers/client/terminate";
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm";
+1 -1
View File
@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process
export const APP_VERSION = "1.14.0";
export const APP_VERSION = "1.15.0";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);
+2 -16
View File
@@ -1,17 +1,3 @@
import { build } from "@server/build";
import license from "#dynamic/license/license";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
if (build === "enterprise") {
return await license.isUnlocked();
}
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
return tier === TierId.STANDARD;
}
return true;
}
return false;
}
@@ -0,0 +1,30 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { build } from "@server/build";
import license from "#private/license/license";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
if (build === "enterprise") {
return await license.isUnlocked();
}
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
return tier === TierId.STANDARD;
}
return false;
}
@@ -1,51 +0,0 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { build } from "@server/build";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
export async function verifyValidLicense(
req: Request,
res: Response,
next: NextFunction
) {
try {
if (build != "saas") {
return next();
}
const { tier, active } = await getOrgTierData(orgId);
const subscribed = tier === TierId.STANDARD;
if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
"This organization's current plan does not support this feature."
)
);
}
return next();
} catch (e) {
return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
"Error verifying subscription"
)
);
}
}
@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
import type { Request, Response, NextFunction } from "express";
import { build } from "@server/build";
import { getOrgTierData } from "@server/lib/billing";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import {
approvals,
@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
import { build } from "@server/build";
import { approvals, clients, db, orgs, type Approval } from "@server/db";
import { getOrgTierData } from "@server/lib/billing";
import { getOrgTierData } from "#private/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import response from "@server/lib/response";
import { and, eq, type InferInsertModel } from "drizzle-orm";
@@ -26,7 +26,8 @@ const applyBlueprintSchema = z
message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}`
});
}
})
}),
source: z.enum(["API", "UI", "CLI"]).optional()
})
.strict();
@@ -84,7 +85,7 @@ export async function applyYAMLBlueprint(
);
}
const { blueprint: contents, name } = parsedBody.data;
const { blueprint: contents, name, source = "UI" } = parsedBody.data;
logger.debug(`Received blueprint:`, contents);
@@ -107,7 +108,7 @@ export async function applyYAMLBlueprint(
blueprint = await applyBlueprint({
orgId,
name,
source: "UI",
source,
configData: parsedConfig
});
} catch (err) {
+1 -1
View File
@@ -1,6 +1,6 @@
import type { Blueprint } from "@server/db";
export type BlueprintSource = "API" | "UI" | "NEWT";
export type BlueprintSource = "API" | "UI" | "NEWT" | "CLI";
export type BlueprintData = Omit<Blueprint, "source"> & {
source: BlueprintSource;
+96 -43
View File
@@ -12,6 +12,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
const getClientSchema = z.strictObject({
clientId: z
@@ -58,7 +59,7 @@ type PostureData = {
firewallEnabled?: boolean | null;
autoUpdatesEnabled?: boolean | null;
tpmAvailable?: boolean | null;
windowsDefenderEnabled?: boolean | null;
windowsAntivirusEnabled?: boolean | null;
macosSipEnabled?: boolean | null;
macosGatekeeperEnabled?: boolean | null;
macosFirewallStealthMode?: boolean | null;
@@ -75,75 +76,123 @@ function getPlatformPostureData(
const normalizedPlatform = platform?.toLowerCase() || "unknown";
const posture: PostureData = {};
// Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Defender
// Windows: Hard drive encryption, Firewall, Auto updates, TPM availability, Windows Antivirus status
if (normalizedPlatform === "windows") {
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
if (
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted;
}
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
if (
fingerprint.firewallEnabled !== null &&
fingerprint.firewallEnabled !== undefined
) {
posture.firewallEnabled = fingerprint.firewallEnabled;
}
if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) {
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
}
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
if (
fingerprint.tpmAvailable !== null &&
fingerprint.tpmAvailable !== undefined
) {
posture.tpmAvailable = fingerprint.tpmAvailable;
}
if (fingerprint.windowsDefenderEnabled !== null && fingerprint.windowsDefenderEnabled !== undefined) {
posture.windowsDefenderEnabled = fingerprint.windowsDefenderEnabled;
if (
fingerprint.windowsAntivirusEnabled !== null &&
fingerprint.windowsAntivirusEnabled !== undefined
) {
posture.windowsAntivirusEnabled =
fingerprint.windowsAntivirusEnabled;
}
}
// macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode
else if (normalizedPlatform === "macos") {
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
if (
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted;
}
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
if (
fingerprint.biometricsEnabled !== null &&
fingerprint.biometricsEnabled !== undefined
) {
posture.biometricsEnabled = fingerprint.biometricsEnabled;
}
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
if (
fingerprint.firewallEnabled !== null &&
fingerprint.firewallEnabled !== undefined
) {
posture.firewallEnabled = fingerprint.firewallEnabled;
}
if (fingerprint.macosSipEnabled !== null && fingerprint.macosSipEnabled !== undefined) {
if (
fingerprint.macosSipEnabled !== null &&
fingerprint.macosSipEnabled !== undefined
) {
posture.macosSipEnabled = fingerprint.macosSipEnabled;
}
if (fingerprint.macosGatekeeperEnabled !== null && fingerprint.macosGatekeeperEnabled !== undefined) {
if (
fingerprint.macosGatekeeperEnabled !== null &&
fingerprint.macosGatekeeperEnabled !== undefined
) {
posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled;
}
if (fingerprint.macosFirewallStealthMode !== null && fingerprint.macosFirewallStealthMode !== undefined) {
posture.macosFirewallStealthMode = fingerprint.macosFirewallStealthMode;
if (
fingerprint.macosFirewallStealthMode !== null &&
fingerprint.macosFirewallStealthMode !== undefined
) {
posture.macosFirewallStealthMode =
fingerprint.macosFirewallStealthMode;
}
if (
fingerprint.autoUpdatesEnabled !== null &&
fingerprint.autoUpdatesEnabled !== undefined
) {
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled;
}
}
// Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability
else if (normalizedPlatform === "linux") {
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
if (
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted;
}
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) {
if (
fingerprint.firewallEnabled !== null &&
fingerprint.firewallEnabled !== undefined
) {
posture.firewallEnabled = fingerprint.firewallEnabled;
}
if (fingerprint.linuxAppArmorEnabled !== null && fingerprint.linuxAppArmorEnabled !== undefined) {
if (
fingerprint.linuxAppArmorEnabled !== null &&
fingerprint.linuxAppArmorEnabled !== undefined
) {
posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled;
}
if (fingerprint.linuxSELinuxEnabled !== null && fingerprint.linuxSELinuxEnabled !== undefined) {
if (
fingerprint.linuxSELinuxEnabled !== null &&
fingerprint.linuxSELinuxEnabled !== undefined
) {
posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled;
}
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) {
if (
fingerprint.tpmAvailable !== null &&
fingerprint.tpmAvailable !== undefined
) {
posture.tpmAvailable = fingerprint.tpmAvailable;
}
}
// iOS: Biometric configuration
else if (normalizedPlatform === "ios") {
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
posture.biometricsEnabled = fingerprint.biometricsEnabled;
}
// none supported yet
}
// Android: Screen lock, Biometric configuration, Hard drive encryption
else if (normalizedPlatform === "android") {
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) {
posture.biometricsEnabled = fingerprint.biometricsEnabled;
}
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) {
if (
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted;
}
}
@@ -240,27 +289,31 @@ export async function getClient(
// Build fingerprint data if available
const fingerprintData = client.currentFingerprint
? {
username: client.currentFingerprint.username || null,
hostname: client.currentFingerprint.hostname || null,
platform: client.currentFingerprint.platform || null,
osVersion: client.currentFingerprint.osVersion || null,
kernelVersion:
client.currentFingerprint.kernelVersion || null,
arch: client.currentFingerprint.arch || null,
deviceModel: client.currentFingerprint.deviceModel || null,
serialNumber: client.currentFingerprint.serialNumber || null,
firstSeen: client.currentFingerprint.firstSeen || null,
lastSeen: client.currentFingerprint.lastSeen || null
}
username: client.currentFingerprint.username || null,
hostname: client.currentFingerprint.hostname || null,
platform: client.currentFingerprint.platform || null,
osVersion: client.currentFingerprint.osVersion || null,
kernelVersion:
client.currentFingerprint.kernelVersion || null,
arch: client.currentFingerprint.arch || null,
deviceModel: client.currentFingerprint.deviceModel || null,
serialNumber: client.currentFingerprint.serialNumber || null,
firstSeen: client.currentFingerprint.firstSeen || null,
lastSeen: client.currentFingerprint.lastSeen || null
}
: null;
// Build posture data if available (platform-specific)
// Only return posture data if org is licensed/subscribed
let postureData: PostureData | null = null;
if (build !== "oss") {
const isOrgLicensed = await isLicensedOrSubscribed(
client.clients.orgId
);
if (isOrgLicensed) {
postureData = getPlatformPostureData(
client.currentFingerprint?.platform || null,
client.currentFingerprint
);
);
}
const data: GetClientResponse = {
+2 -6
View File
@@ -175,10 +175,7 @@ async function getSiteAssociations(clientIds: number[]) {
.where(inArray(clientSitesAssociationsCache.clientId, clientIds));
}
type ClientWithSites = Omit<
Awaited<ReturnType<typeof queryClients>>[0],
"deviceModel"
> & {
type ClientWithSites = Awaited<ReturnType<typeof queryClients>>[0] & {
sites: Array<{
siteId: number;
siteName: string | null;
@@ -324,9 +321,8 @@ export async function listClients(
const clientsWithSites = clientsList.map((client) => {
const model = client.deviceModel || null;
const newName = getUserDeviceName(model, client.name);
const { deviceModel, ...clientWithoutDeviceModel } = client;
return {
...clientWithoutDeviceModel,
...client,
name: newName,
sites: sitesByClient[client.clientId] || []
};
+5 -5
View File
@@ -22,7 +22,7 @@ function fingerprintSnapshotHash(fingerprint: any, postures: any): string {
autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false,
tpmAvailable: postures.tpmAvailable ?? false,
windowsDefenderEnabled: postures.windowsDefenderEnabled ?? false,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled ?? false,
macosSipEnabled: postures.macosSipEnabled ?? false,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false,
@@ -87,7 +87,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable,
windowsDefenderEnabled: postures.windowsDefenderEnabled,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -117,7 +117,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable,
windowsDefenderEnabled: postures.windowsDefenderEnabled,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -162,7 +162,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable,
windowsDefenderEnabled: postures.windowsDefenderEnabled,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -197,7 +197,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable,
windowsDefenderEnabled: postures.windowsDefenderEnabled,
windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -46,6 +46,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return;
}
logger.debug("Handling fingerprint insertion for olm register...", {
olmId: olm.olmId,
fingerprint,
postures
});
await handleFingerprintInsertion(olm, fingerprint, postures);
if (
@@ -143,7 +149,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return;
}
if (!policyCheck.policies?.passwordAge?.compliant === false) {
if (policyCheck.policies?.passwordAge?.compliant === false) {
logger.warn(
`Olm user ${olm.userId} has non-compliant password age for org ${orgId}`
);
@@ -153,7 +159,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
);
return;
} else if (
!policyCheck.policies?.maxSessionLength?.compliant === false
policyCheck.policies?.maxSessionLength?.compliant === false
) {
logger.warn(
`Olm user ${olm.userId} has non-compliant session length for org ${orgId}`
@@ -65,7 +65,6 @@ export async function recoverOlmWithFingerprint(
.where(
and(
eq(olms.userId, userId),
eq(olms.archived, false),
eq(
currentFingerprint.platformFingerprint,
platformFingerprint
+2 -2
View File
@@ -13,7 +13,7 @@ import { build } from "@server/build";
import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers";
import { cache } from "@server/lib/cache";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
const updateOrgParamsSchema = z.strictObject({
orgId: z.string()
@@ -89,7 +89,7 @@ export async function updateOrg(
const { orgId } = parsedParams.data;
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) {
if (!isLicensed) {
parsedBody.data.requireTwoFactor = undefined;
parsedBody.data.maxSessionLengthHours = undefined;
parsedBody.data.passwordExpiryDays = undefined;
+2 -6
View File
@@ -23,7 +23,7 @@ import { OpenAPITags } from "@server/openApi";
import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
const updateResourceParamsSchema = z.strictObject({
resourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -342,11 +342,7 @@ async function updateHttpResource(
}
const isLicensed = await isLicensedOrSubscribed(resource.orgId);
if (build == "enterprise" && !isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
if (!isLicensed) {
updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined;
updateData.maintenanceTitle = undefined;
+2 -2
View File
@@ -11,7 +11,7 @@ import { ActionsEnum } from "@server/auth/actions";
import { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
const createRoleParamsSchema = z.strictObject({
orgId: z.string()
@@ -101,7 +101,7 @@ export async function createRole(
}
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build === "oss" || !isLicensed) {
if (!isLicensed) {
roleData.requireDeviceApproval = undefined;
}
+2 -3
View File
@@ -8,8 +8,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { build } from "@server/build";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { OpenAPITags, registry } from "@server/openApi";
const updateRoleParamsSchema = z.strictObject({
@@ -112,7 +111,7 @@ export async function updateRole(
}
const isLicensed = await isLicensedOrSubscribed(orgId);
if (build === "oss" || !isLicensed) {
if (!isLicensed) {
updateData.requireDeviceApproval = undefined;
}
@@ -97,6 +97,7 @@ export async function listAllSiteResourcesByOrg(
destination: siteResources.destination,
enabled: siteResources.enabled,
alias: siteResources.alias,
aliasAddress: siteResources.aliasAddress,
tcpPortRangeString: siteResources.tcpPortRangeString,
udpPortRangeString: siteResources.udpPortRangeString,
disableIcmp: siteResources.disableIcmp,
+2 -2
View File
@@ -49,7 +49,7 @@ export default async function migration() {
"firewallEnabled" boolean DEFAULT false NOT NULL,
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
"tpmAvailable" boolean DEFAULT false NOT NULL,
"windowsDefenderEnabled" boolean DEFAULT false NOT NULL,
"windowsAntivirusEnabled" boolean DEFAULT false NOT NULL,
"macosSipEnabled" boolean DEFAULT false NOT NULL,
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
@@ -75,7 +75,7 @@ export default async function migration() {
"firewallEnabled" boolean DEFAULT false NOT NULL,
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
"tpmAvailable" boolean DEFAULT false NOT NULL,
"windowsDefenderEnabled" boolean DEFAULT false NOT NULL,
"windowsAntivirusEnabled" boolean DEFAULT false NOT NULL,
"macosSipEnabled" boolean DEFAULT false NOT NULL,
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
"macosFirewallStealthMode" boolean DEFAULT false NOT NULL,
+2 -2
View File
@@ -53,7 +53,7 @@ CREATE TABLE 'currentFingerprint' (
'firewallEnabled' integer DEFAULT false NOT NULL,
'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
'tpmAvailable' integer DEFAULT false NOT NULL,
'windowsDefenderEnabled' integer DEFAULT false NOT NULL,
'windowsAntivirusEnabled' integer DEFAULT false NOT NULL,
'macosSipEnabled' integer DEFAULT false NOT NULL,
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
@@ -83,7 +83,7 @@ CREATE TABLE 'fingerprintSnapshots' (
'firewallEnabled' integer DEFAULT false NOT NULL,
'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
'tpmAvailable' integer DEFAULT false NOT NULL,
'windowsDefenderEnabled' integer DEFAULT false NOT NULL,
'windowsAntivirusEnabled' integer DEFAULT false NOT NULL,
'macosSipEnabled' integer DEFAULT false NOT NULL,
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
@@ -19,17 +19,6 @@ export interface ApprovalFeedPageProps {
export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
const params = await props.params;
let approvals: ApprovalItem[] = [];
const res = await internal
.get<
AxiosResponse<{ approvals: ApprovalItem[] }>
>(`/org/${params.orgId}/approvals`, await authCookieHeader())
.catch((e) => {});
if (res && res.status === 200) {
approvals = res.data.data.approvals;
}
let org: GetOrgResponse | null = null;
const orgRes = await getCachedOrg(params.orgId);
@@ -32,6 +32,7 @@ import CopyToClipboard from "@app/components/CopyToClipboard";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon } from "lucide-react";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { OlmInstallCommands } from "@app/components/olm-install-commands";
export default function CredentialsPage() {
const { env } = useEnvContext();
@@ -204,6 +205,12 @@ export default function CredentialsPage() {
</SettingsSectionFooter>
)}
</SettingsSection>
<OlmInstallCommands
id={displayOlmId ?? "********"}
endpoint={env.app.dashboardUrl}
secret={displaySecret ?? "********"}
/>
</SettingsContainer>
<ConfirmDeleteDialog
@@ -1,15 +1,22 @@
"use client";
import CopyToClipboard from "@app/components/CopyToClipboard";
import {
InfoSection,
InfoSectionContent,
InfoSections,
InfoSectionTitle
} from "@app/components/InfoSection";
import {
SettingsContainer,
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionForm,
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import { StrategySelect } from "@app/components/StrategySelect";
import HeaderTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
@@ -19,44 +26,24 @@ import {
FormLabel,
FormMessage
} from "@app/components/ui/form";
import HeaderTitle from "@app/components/SettingsSectionTitle";
import { z } from "zod";
import { createElement, useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@app/components/ui/input";
import { ChevronDown, ChevronUp, InfoIcon, Terminal } from "lucide-react";
import { Button } from "@app/components/ui/button";
import CopyTextBox from "@app/components/CopyTextBox";
import CopyToClipboard from "@app/components/CopyToClipboard";
import {
InfoSection,
InfoSectionContent,
InfoSections,
InfoSectionTitle
} from "@app/components/InfoSection";
import {
FaApple,
FaCubes,
FaDocker,
FaFreebsd,
FaWindows
} from "react-icons/fa";
import { SiNixos, SiKubernetes } from "react-icons/si";
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { zodResolver } from "@hookform/resolvers/zod";
import {
CreateClientBody,
CreateClientResponse,
PickClientDefaultsResponse
} from "@server/routers/client";
import { ListSitesResponse } from "@server/routers/site";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { ChevronDown, ChevronUp } from "lucide-react";
import { useParams, useRouter } from "next/navigation";
import { Tag, TagInput } from "@app/components/tags/tag-input";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { OlmInstallCommands } from "@app/components/olm-install-commands";
import { useTranslations } from "next-intl";
type ClientType = "olm";
@@ -68,18 +55,6 @@ interface TunnelTypeOption {
disabled?: boolean;
}
type CommandItem = string | { title: string; command: string };
type Commands = {
unix: Record<string, CommandItem[]>;
windows: Record<string, CommandItem[]>;
docker: Record<string, CommandItem[]>;
};
const platforms = ["unix", "docker", "windows"] as const;
type Platform = (typeof platforms)[number];
export default function Page() {
const { env } = useEnvContext();
const api = createApiClient({ env });
@@ -113,13 +88,9 @@ export default function Page() {
const [loadingPage, setLoadingPage] = useState(true);
const [platform, setPlatform] = useState<Platform>("unix");
const [architecture, setArchitecture] = useState("All");
const [commands, setCommands] = useState<Commands | null>(null);
const [olmId, setOlmId] = useState("");
const [olmSecret, setOlmSecret] = useState("");
const [olmCommand, setOlmCommand] = useState("");
const [olmVersion, setOlmVersion] = useState("latest");
const [createLoading, setCreateLoading] = useState(false);
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
@@ -127,136 +98,6 @@ export default function Page() {
const [clientDefaults, setClientDefaults] =
useState<PickClientDefaultsResponse | null>(null);
const hydrateCommands = (
id: string,
secret: string,
endpoint: string,
version: string
) => {
const commands = {
unix: {
All: [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash`
},
{
title: t("run"),
command: `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
}
]
},
windows: {
x64: [
{
title: t("install"),
command: `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`
},
{
title: t("run"),
command: `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}`
}
]
},
docker: {
"Docker Compose": [
`services:
olm:
image: fosrl/olm
container_name: olm
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- PANGOLIN_ENDPOINT=${endpoint}
- OLM_ID=${id}
- OLM_SECRET=${secret}`
],
"Docker Run": [
`docker run -dit --network host --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun fosrl/olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
]
}
};
setCommands(commands);
};
const getArchitectures = () => {
switch (platform) {
case "unix":
return ["All"];
case "windows":
return ["x64"];
case "docker":
return ["Docker Compose", "Docker Run"];
default:
return ["x64"];
}
};
const getPlatformName = (platformName: string) => {
switch (platformName) {
case "windows":
return "Windows";
case "unix":
return "Unix & macOS";
case "docker":
return "Docker";
default:
return "Unix & macOS";
}
};
const getCommand = (): CommandItem[] => {
const placeholder: CommandItem[] = [t("unknownCommand")];
if (!commands) {
return placeholder;
}
let platformCommands = commands[platform as keyof Commands];
if (!platformCommands) {
// get first key
const firstPlatform = Object.keys(commands)[0] as Platform;
platformCommands = commands[firstPlatform as keyof Commands];
setPlatform(firstPlatform);
}
let architectureCommands = platformCommands[architecture];
if (!architectureCommands) {
// get first key
const firstArchitecture = Object.keys(platformCommands)[0];
architectureCommands = platformCommands[firstArchitecture];
setArchitecture(firstArchitecture);
}
return architectureCommands || placeholder;
};
const getPlatformIcon = (platformName: string) => {
switch (platformName) {
case "windows":
return <FaWindows className="h-4 w-4 mr-2" />;
case "unix":
return <Terminal className="h-4 w-4 mr-2" />;
case "docker":
return <FaDocker className="h-4 w-4 mr-2" />;
case "kubernetes":
return <SiKubernetes className="h-4 w-4 mr-2" />;
case "podman":
return <FaCubes className="h-4 w-4 mr-2" />;
case "freebsd":
return <FaFreebsd className="h-4 w-4 mr-2" />;
case "nixos":
return <SiNixos className="h-4 w-4 mr-2" />;
default:
return <Terminal className="h-4 w-4 mr-2" />;
}
};
const form = useForm<CreateClientFormValues>({
resolver: zodResolver(createClientFormSchema),
defaultValues: {
@@ -311,23 +152,6 @@ export default function Page() {
const load = async () => {
setLoadingPage(true);
// Fetch available sites
// const res = await api.get<AxiosResponse<ListSitesResponse>>(
// `/org/${orgId}/sites/`
// );
// const sites = res.data.data.sites.filter(
// (s) => s.type === "newt" && s.subnet
// );
// setSites(
// sites.map((site) => ({
// id: site.siteId.toString(),
// text: site.name
// }))
// );
let olmVersion = "latest";
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
@@ -348,7 +172,7 @@ export default function Page() {
}
const data = await response.json();
const latestVersion = data.tag_name;
olmVersion = latestVersion;
setOlmVersion(latestVersion);
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
console.error(t("olmErrorFetchTimeout"));
@@ -377,18 +201,9 @@ export default function Page() {
const olmId = data.olmId;
const olmSecret = data.olmSecret;
const olmCommand = `olm --id ${olmId} --secret ${olmSecret} --endpoint ${env.app.dashboardUrl}`;
setOlmId(olmId);
setOlmSecret(olmSecret);
setOlmCommand(olmCommand);
hydrateCommands(
olmId,
olmSecret,
env.app.dashboardUrl,
olmVersion
);
if (data.subnet) {
form.setValue("subnet", data.subnet);
@@ -571,118 +386,12 @@ export default function Page() {
</InfoSections>
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("clientInstallOlm")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("clientInstallOlmDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<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-[120px] ${platform === os ? "bg-primary/10" : ""} shadow-none`}
onClick={() => {
setPlatform(os);
}}
>
{getPlatformIcon(os)}
{getPlatformName(os)}
</Button>
))}
</div>
</div>
<div>
<p className="font-bold mb-3">
{["docker", "podman"].includes(
platform
)
? t("method")
: t("architecture")}
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{getArchitectures().map(
(arch) => (
<Button
key={arch}
variant={
architecture ===
arch
? "squareOutlinePrimary"
: "squareOutline"
}
className={`flex-1 min-w-[120px] ${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">
{getCommand().map(
(item, index) => {
const commandText =
typeof item ===
"string"
? item
: item.command;
const title =
typeof item ===
"string"
? undefined
: item.title;
return (
<div
key={index}
>
{title && (
<p className="text-sm font-medium mb-1.5">
{
title
}
</p>
)}
<CopyTextBox
text={
commandText
}
outline={
true
}
/>
</div>
);
}
)}
</div>
</div>
</div>
</SettingsSectionBody>
</SettingsSection>
<OlmInstallCommands
id={olmId}
endpoint={env.app.dashboardUrl}
secret={olmSecret}
version={olmVersion}
/>
</>
)}
</SettingsContainer>
@@ -656,17 +656,17 @@ export default function GeneralPage() {
</InfoSection>
)}
{client.posture.windowsDefenderEnabled !== null &&
client.posture.windowsDefenderEnabled !== undefined && (
{client.posture.windowsAntivirusEnabled !== null &&
client.posture.windowsAntivirusEnabled !== undefined && (
<InfoSection>
<InfoSectionTitle>
{t("windowsDefenderEnabled")}
{t("windowsAntivirusEnabled")}
</InfoSectionTitle>
<InfoSectionContent>
{isPaidUser
? formatPostureValue(
client.posture
.windowsDefenderEnabled
.windowsAntivirusEnabled
)
: "-"}
</InfoSectionContent>
@@ -265,4 +265,3 @@ function GeneralSectionForm({ org }: SectionFormProps) {
</SettingsSection>
);
}
@@ -67,6 +67,7 @@ export default async function ClientResourcesPage(
destination: siteResource.destination,
// destinationPort: siteResource.destinationPort,
alias: siteResource.alias || null,
aliasAddress: siteResource.aliasAddress || null,
siteNiceId: siteResource.siteNiceId,
niceId: siteResource.niceId,
tcpPortRangeString: siteResource.tcpPortRangeString || null,
@@ -11,7 +11,6 @@ import {
SelectValue
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { ContainersSelector } from "@app/components/ContainersSelector";
import { HeadersInput } from "@app/components/HeadersInput";
import {
PathMatchDisplay,
@@ -19,6 +18,7 @@ import {
PathRewriteDisplay,
PathRewriteModal
} from "@app/components/PathMatchRenameModal";
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
import {
SettingsContainer,
SettingsSection,
@@ -30,15 +30,6 @@ import {
} from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription } from "@app/components/ui/alert";
import { Badge } from "@app/components/ui/badge";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@app/components/ui/command";
import {
Form,
FormControl,
@@ -48,11 +39,6 @@ import {
FormLabel,
FormMessage
} from "@app/components/ui/form";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import {
Table,
TableBody,
@@ -73,12 +59,9 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api";
import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { cn } from "@app/lib/cn";
import { DockerManager, DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget";
import { orgQueries, resourceQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { tlsNameSchema } from "@server/lib/schemas";
import { type GetResourceResponse } from "@server/routers/resource";
import type { ListSitesResponse } from "@server/routers/site";
@@ -98,7 +81,6 @@ import {
import { AxiosResponse } from "axios";
import {
AlertTriangle,
CheckIcon,
CircleCheck,
CircleX,
Info,
@@ -107,7 +89,7 @@ import {
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation";
import { use, useActionState, useEffect, useState } from "react";
import { use, useActionState, useCallback, useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -202,7 +184,7 @@ function ProxyResourceTargetsForm({
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
};
const refreshContainersForSite = async (siteId: number) => {
const refreshContainersForSite = useCallback(async (siteId: number) => {
const dockerManager = new DockerManager(api, siteId);
const containers = await dockerManager.fetchContainers();
@@ -214,9 +196,9 @@ function ProxyResourceTargetsForm({
}
return newMap;
});
};
}, [api]);
const getDockerStateForSite = (siteId: number): DockerState => {
const getDockerStateForSite = useCallback((siteId: number): DockerState => {
return (
dockerStates.get(siteId) || {
isEnabled: false,
@@ -224,7 +206,7 @@ function ProxyResourceTargetsForm({
containers: []
}
);
};
}, [dockerStates]);
const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
if (typeof window !== "undefined") {
@@ -234,8 +216,40 @@ function ProxyResourceTargetsForm({
return false;
});
const getColumns = (): ColumnDef<LocalTarget>[] => {
const isHttp = resource.http;
const isHttp = resource.http;
const removeTarget = useCallback((targetId: number) => {
setTargets((prevTargets) => {
const targetToRemove = prevTargets.find((target) => target.targetId === targetId);
if (targetToRemove && !targetToRemove.new) {
setTargetsToRemove((prev) => [...prev, targetId]);
}
return prevTargets.filter((target) => target.targetId !== targetId);
});
}, []);
const updateTarget = useCallback((targetId: number, data: Partial<LocalTarget>) => {
setTargets((prevTargets) => {
const site = sites.find((site) => site.siteId === data.siteId);
return prevTargets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
: target
);
});
}, [sites]);
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
setSelectedTargetForHealthCheck(target);
setHealthCheckDialogOpen(true);
}, []);
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
const priorityColumn: ColumnDef<LocalTarget> = {
id: "priority",
@@ -419,213 +433,15 @@ function ProxyResourceTargetsForm({
accessorKey: "address",
header: () => <span className="p-3">{t("address")}</span>,
cell: ({ row }) => {
const selectedSite = sites.find(
(site) => site.siteId === row.original.siteId
);
const handleContainerSelectForTarget = (
hostname: string,
port?: number
) => {
updateTarget(row.original.targetId, {
...row.original,
ip: hostname,
...(port && { port: port })
});
};
return (
<div className="flex items-center w-full">
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
{selectedSite &&
selectedSite.type === "newt" &&
(() => {
const dockerState = getDockerStateForSite(
selectedSite.siteId
);
return (
<ContainersSelector
site={selectedSite}
containers={dockerState.containers}
isAvailable={
dockerState.isAvailable
}
onContainerSelect={
handleContainerSelectForTarget
}
onRefresh={() =>
refreshContainersForSite(
selectedSite.siteId
)
}
/>
);
})()}
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
role="combobox"
className={cn(
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
!row.original.siteId &&
"text-muted-foreground"
)}
>
<span className="truncate max-w-[150px]">
{row.original.siteId
? selectedSite?.name
: t("siteSelect")}
</span>
<CaretSortIcon className="ml-2h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[180px]">
<Command>
<CommandInput
placeholder={t("siteSearch")}
/>
<CommandList>
<CommandEmpty>
{t("siteNotFound")}
</CommandEmpty>
<CommandGroup>
{sites.map((site) => (
<CommandItem
key={site.siteId}
value={`${site.siteId}:${site.name}`}
onSelect={() =>
updateTarget(
row.original
.targetId,
{
siteId: site.siteId
}
)
}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
site.siteId ===
row.original
.siteId
? "opacity-100"
: "opacity-0"
)}
/>
{site.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{resource.http && (
<Select
defaultValue={row.original.method ?? "http"}
onValueChange={(value) =>
updateTarget(row.original.targetId, {
...row.original,
method: value
})
}
>
<SelectTrigger className="h-8 px-2 w-[70px] text-sm font-normal border-none bg-transparent shadow-none focus:ring-0 focus:outline-none focus-visible:ring-0 data-[state=open]:bg-transparent">
{row.original.method || "http"}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="https">
https
</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
)}
{resource.http && (
<div className="flex items-center justify-center px-2 h-9">
{"://"}
</div>
)}
<Input
defaultValue={row.original.ip}
placeholder="Host"
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
onBlur={(e) => {
const input = e.target.value.trim();
const hasProtocol =
/^(https?|h2c):\/\//.test(input);
const hasPort = /:\d+(?:\/|$)/.test(input);
if (hasProtocol || hasPort) {
const parsed = parseHostTarget(input);
if (parsed) {
updateTarget(
row.original.targetId,
{
...row.original,
method: hasProtocol
? parsed.protocol
: row.original.method,
ip: parsed.host,
port: hasPort
? parsed.port
: row.original.port
}
);
} else {
updateTarget(
row.original.targetId,
{
...row.original,
ip: input
}
);
}
} else {
updateTarget(row.original.targetId, {
...row.original,
ip: input
});
}
}}
/>
<div className="flex items-center justify-center px-2 h-9">
{":"}
</div>
<Input
placeholder="Port"
defaultValue={
row.original.port === 0
? ""
: row.original.port
}
className="w-[75px] pl-0 border-none placeholder-gray-400"
onBlur={(e) => {
const value = parseInt(e.target.value, 10);
if (!isNaN(value) && value > 0) {
updateTarget(row.original.targetId, {
...row.original,
port: value
});
} else {
updateTarget(row.original.targetId, {
...row.original,
port: 0
});
}
}}
/>
</div>
</div>
<ResourceTargetAddressItem
isHttp={isHttp}
sites={sites}
getDockerStateForSite={getDockerStateForSite}
proxyTarget={row.original}
refreshContainersForSite={refreshContainersForSite}
updateTarget={updateTarget}
/>
);
},
size: 400,
@@ -765,7 +581,7 @@ function ProxyResourceTargetsForm({
actionsColumn
];
}
};
}, [isAdvancedMode, isHttp, sites, updateTarget, getDockerStateForSite, refreshContainersForSite, openHealthCheckDialog, removeTarget, t]);
function addNewTarget() {
const isHttp = resource.http;
@@ -806,32 +622,6 @@ function ProxyResourceTargetsForm({
setTargets((prev) => [...prev, newTarget]);
}
const removeTarget = (targetId: number) => {
setTargets([
...targets.filter((target) => target.targetId !== targetId)
]);
if (!targets.find((target) => target.targetId === targetId)?.new) {
setTargetsToRemove([...targetsToRemove, targetId]);
}
};
async function updateTarget(targetId: number, data: Partial<LocalTarget>) {
const site = sites.find((site) => site.siteId === data.siteId);
setTargets(
targets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
: target
)
);
}
function updateTargetHealthCheck(targetId: number, config: any) {
setTargets(
targets.map((target) =>
@@ -846,14 +636,6 @@ function ProxyResourceTargetsForm({
);
}
const openHealthCheckDialog = (target: LocalTarget) => {
console.log(target);
setSelectedTargetForHealthCheck(target);
setHealthCheckDialogOpen(true);
};
const columns = getColumns();
const table = useReactTable({
data: targets,
columns,
@@ -1,5 +1,14 @@
"use client";
import CopyTextBox from "@app/components/CopyTextBox";
import DomainPicker from "@app/components/DomainPicker";
import HealthCheckDialog from "@app/components/HealthCheckDialog";
import {
PathMatchDisplay,
PathMatchModal,
PathRewriteDisplay,
PathRewriteModal
} from "@app/components/PathMatchRenameModal";
import {
SettingsContainer,
SettingsSection,
@@ -9,6 +18,10 @@ import {
SettingsSectionHeader,
SettingsSectionTitle
} from "@app/components/Settings";
import HeaderTitle from "@app/components/SettingsSectionTitle";
import { StrategySelect } from "@app/components/StrategySelect";
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
import { Button } from "@app/components/ui/button";
import {
Form,
FormControl,
@@ -18,22 +31,7 @@ import {
FormLabel,
FormMessage
} from "@app/components/ui/form";
import HeaderTitle from "@app/components/SettingsSectionTitle";
import { z } from "zod";
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Input } from "@app/components/ui/input";
import { Button } from "@app/components/ui/button";
import { useParams, useRouter } from "next/navigation";
import { ListSitesResponse } from "@server/routers/site";
import { formatAxiosError } from "@app/lib/api";
import { createApiClient } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { Resource } from "@server/db";
import { StrategySelect } from "@app/components/StrategySelect";
import {
Select,
SelectContent,
@@ -41,48 +39,7 @@ import {
SelectTrigger,
SelectValue
} from "@app/components/ui/select";
import { ListDomainsResponse } from "@server/routers/domain";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "@app/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
import { cn } from "@app/lib/cn";
import {
ArrowRight,
CircleCheck,
CircleX,
Info,
MoveRight,
Plus,
Settings,
SquareArrowOutUpRight
} from "lucide-react";
import CopyTextBox from "@app/components/CopyTextBox";
import Link from "next/link";
import { useTranslations } from "next-intl";
import DomainPicker from "@app/components/DomainPicker";
import { build } from "@server/build";
import { ContainersSelector } from "@app/components/ContainersSelector";
import {
ColumnDef,
getFilteredRowModel,
getSortedRowModel,
getPaginationRowModel,
getCoreRowModel,
useReactTable,
flexRender,
Row
} from "@tanstack/react-table";
import { Switch } from "@app/components/ui/switch";
import {
Table,
TableBody,
@@ -91,30 +48,49 @@ import {
TableHeader,
TableRow
} from "@app/components/ui/table";
import { Switch } from "@app/components/ui/switch";
import { ArrayElement } from "@server/types/ArrayElement";
import { isTargetValid } from "@server/lib/validators";
import { ListTargetsResponse } from "@server/routers/target";
import { DockerManager, DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget";
import { toASCII, toUnicode } from "punycode";
import { DomainRow } from "@app/components/DomainsTable";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger
} from "@app/components/ui/tooltip";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { DockerManager, DockerState } from "@app/lib/docker";
import { orgQueries } from "@app/lib/queries";
import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils";
import { zodResolver } from "@hookform/resolvers/zod";
import { Resource } from "@server/db";
import { isTargetValid } from "@server/lib/validators";
import { ListTargetsResponse } from "@server/routers/target";
import { ArrayElement } from "@server/types/ArrayElement";
import { useQuery } from "@tanstack/react-query";
import {
PathMatchDisplay,
PathMatchModal,
PathRewriteDisplay,
PathRewriteModal
} from "@app/components/PathMatchRenameModal";
import { Badge } from "@app/components/ui/badge";
import HealthCheckDialog from "@app/components/HealthCheckDialog";
import { SwitchInput } from "@app/components/SwitchInput";
ColumnDef,
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from "@tanstack/react-table";
import { AxiosResponse } from "axios";
import {
CircleCheck,
CircleX,
Info,
Plus,
Settings,
SquareArrowOutUpRight
} from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useParams, useRouter } from "next/navigation";
import { toASCII } from "punycode";
import { useEffect, useMemo, useState, useCallback } from "react";
import { Controller, useForm } from "react-hook-form";
import { z } from "zod";
const baseResourceFormSchema = z.object({
name: z.string().min(1).max(255),
@@ -204,10 +180,6 @@ const addTargetSchema = z
}
);
type BaseResourceFormValues = z.infer<typeof baseResourceFormSchema>;
type HttpResourceFormValues = z.infer<typeof httpResourceFormSchema>;
type TcpUdpResourceFormValues = z.infer<typeof tcpUdpResourceFormSchema>;
type ResourceType = "http" | "raw";
interface ResourceTypeOption {
@@ -217,7 +189,7 @@ interface ResourceTypeOption {
disabled?: boolean;
}
type LocalTarget = Omit<
export type LocalTarget = Omit<
ArrayElement<ListTargetsResponse["targets"]> & {
new?: boolean;
updated?: boolean;
@@ -233,18 +205,16 @@ export default function Page() {
const router = useRouter();
const t = useTranslations();
const [loadingPage, setLoadingPage] = useState(true);
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
const [baseDomains, setBaseDomains] = useState<
{ domainId: string; baseDomain: string }[]
>([]);
const { data: sites = [], isLoading: loadingPage } = useQuery(
orgQueries.sites({ orgId: orgId as string })
);
const [createLoading, setCreateLoading] = useState(false);
const [showSnippets, setShowSnippets] = useState(false);
const [niceId, setNiceId] = useState<string>("");
// Target management state
const [targets, setTargets] = useState<LocalTarget[]>([]);
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
new Map()
);
@@ -405,102 +375,60 @@ export default function Page() {
setDockerStates((prev) => new Map(prev.set(siteId, dockerState)));
};
const refreshContainersForSite = async (siteId: number) => {
const dockerManager = new DockerManager(api, siteId);
const containers = await dockerManager.fetchContainers();
const refreshContainersForSite = useCallback(
async (siteId: number) => {
const dockerManager = new DockerManager(api, siteId);
const containers = await dockerManager.fetchContainers();
setDockerStates((prev) => {
const newMap = new Map(prev);
const existingState = newMap.get(siteId);
if (existingState) {
newMap.set(siteId, { ...existingState, containers });
}
return newMap;
setDockerStates((prev) => {
const newMap = new Map(prev);
const existingState = newMap.get(siteId);
if (existingState) {
newMap.set(siteId, { ...existingState, containers });
}
return newMap;
});
},
[api]
);
const getDockerStateForSite = useCallback(
(siteId: number): DockerState => {
return (
dockerStates.get(siteId) || {
isEnabled: false,
isAvailable: false,
containers: []
}
);
},
[dockerStates]
);
const removeTarget = useCallback((targetId: number) => {
setTargets((prevTargets) => {
return prevTargets.filter((target) => target.targetId !== targetId);
});
};
}, []);
const getDockerStateForSite = (siteId: number): DockerState => {
return (
dockerStates.get(siteId) || {
isEnabled: false,
isAvailable: false,
containers: []
}
);
};
async function addTarget(data: z.infer<typeof addTargetSchema>) {
const site = sites.find((site) => site.siteId === data.siteId);
const isHttp = baseForm.watch("http");
const newTarget: LocalTarget = {
...data,
path: isHttp ? data.path || null : null,
pathMatchType: isHttp ? data.pathMatchType || null : null,
rewritePath: isHttp ? data.rewritePath || null : null,
rewritePathType: isHttp ? data.rewritePathType || null : null,
siteType: site?.type || null,
enabled: true,
targetId: new Date().getTime(),
new: true,
resourceId: 0, // Will be set when resource is created
priority: isHttp ? data.priority || 100 : 100, // Default priority
hcEnabled: false,
hcPath: null,
hcMethod: null,
hcInterval: null,
hcTimeout: null,
hcHeaders: null,
hcScheme: null,
hcHostname: null,
hcPort: null,
hcFollowRedirects: null,
hcHealth: "unknown",
hcStatus: null,
hcMode: null,
hcUnhealthyInterval: null,
hcTlsServerName: null
};
setTargets([...targets, newTarget]);
addTargetForm.reset({
ip: "",
method: baseForm.watch("http") ? "http" : null,
port: "" as any as number,
path: null,
pathMatchType: null,
rewritePath: null,
rewritePathType: null,
priority: isHttp ? 100 : undefined
});
}
const removeTarget = (targetId: number) => {
setTargets([
...targets.filter((target) => target.targetId !== targetId)
]);
if (!targets.find((target) => target.targetId === targetId)?.new) {
setTargetsToRemove([...targetsToRemove, targetId]);
}
};
async function updateTarget(targetId: number, data: Partial<LocalTarget>) {
const site = sites.find((site) => site.siteId === data.siteId);
setTargets(
targets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
: target
)
);
}
const updateTarget = useCallback(
(targetId: number, data: Partial<LocalTarget>) => {
setTargets((prevTargets) => {
const site = sites.find((site) => site.siteId === data.siteId);
return prevTargets.map((target) =>
target.targetId === targetId
? {
...target,
...data,
updated: true,
siteType: site ? site.type : target.siteType
}
: target
);
});
},
[sites]
);
async function onSubmit() {
setCreateLoading(true);
@@ -638,82 +566,18 @@ export default function Page() {
}
useEffect(() => {
const load = async () => {
setLoadingPage(true);
// Initialize Docker for newt sites
for (const site of sites) {
if (site.type === "newt") {
initializeDockerForSite(site.siteId);
}
}
const fetchSites = async () => {
const res = await api
.get<
AxiosResponse<ListSitesResponse>
>(`/org/${orgId}/sites/`)
.catch((e) => {
toast({
variant: "destructive",
title: t("sitesErrorFetch"),
description: formatAxiosError(
e,
t("sitesErrorFetchDescription")
)
});
});
if (res?.status === 200) {
setSites(res.data.data.sites);
// Initialize Docker for newt sites
for (const site of res.data.data.sites) {
if (site.type === "newt") {
initializeDockerForSite(site.siteId);
}
}
// If there's only one site, set it as the default in the form
if (res.data.data.sites.length) {
addTargetForm.setValue(
"siteId",
res.data.data.sites[0].siteId
);
}
}
};
const fetchDomains = async () => {
const res = await api
.get<
AxiosResponse<ListDomainsResponse>
>(`/org/${orgId}/domains/`)
.catch((e) => {
toast({
variant: "destructive",
title: t("domainsErrorFetch"),
description: formatAxiosError(
e,
t("domainsErrorFetchDescription")
)
});
});
if (res?.status === 200) {
const rawDomains = res.data.data.domains as DomainRow[];
const domains = rawDomains.map((domain) => ({
...domain,
baseDomain: toUnicode(domain.baseDomain)
}));
setBaseDomains(domains);
// if (domains.length) {
// httpForm.setValue("domainId", domains[0].domainId);
// }
}
};
await fetchSites();
await fetchDomains();
setLoadingPage(false);
};
load();
}, []);
// If there's at least one site, set it as the default in the form
if (sites.length > 0) {
addTargetForm.setValue("siteId", sites[0].siteId);
}
}, [sites]);
function TargetHealthCheck(targetId: number, config: any) {
setTargets(
@@ -729,16 +593,15 @@ export default function Page() {
);
}
const openHealthCheckDialog = (target: LocalTarget) => {
const openHealthCheckDialog = useCallback((target: LocalTarget) => {
console.log(target);
setSelectedTargetForHealthCheck(target);
setHealthCheckDialogOpen(true);
};
}, []);
const getColumns = (): ColumnDef<LocalTarget>[] => {
const baseColumns: ColumnDef<LocalTarget>[] = [];
const isHttp = baseForm.watch("http");
const isHttp = baseForm.watch("http");
const columns = useMemo((): ColumnDef<LocalTarget>[] => {
const priorityColumn: ColumnDef<LocalTarget> = {
id: "priority",
header: () => (
@@ -875,7 +738,7 @@ export default function Page() {
trigger={
<Button
variant="outline"
className="flex items-center gap-2 p-2 w-full text-left cursor-pointer max-w-[200px]"
className="flex items-center gap-2 p-2 w-full text-left cursor-pointer max-w-50"
>
<PathMatchDisplay
value={{
@@ -899,7 +762,7 @@ export default function Page() {
trigger={
<Button
variant="outline"
className="w-full max-w-[200px]"
className="w-full max-w-50"
>
<Plus className="h-4 w-4 mr-2" />
{t("matchPath")}
@@ -918,216 +781,16 @@ export default function Page() {
const addressColumn: ColumnDef<LocalTarget> = {
accessorKey: "address",
header: () => <span className="p-3">{t("address")}</span>,
cell: ({ row }) => {
const selectedSite = sites.find(
(site) => site.siteId === row.original.siteId
);
const handleContainerSelectForTarget = (
hostname: string,
port?: number
) => {
updateTarget(row.original.targetId, {
...row.original,
ip: hostname,
...(port && { port: port })
});
};
return (
<div className="flex items-center w-full">
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
{selectedSite &&
selectedSite.type === "newt" &&
(() => {
const dockerState = getDockerStateForSite(
selectedSite.siteId
);
return (
<ContainersSelector
site={selectedSite}
containers={dockerState.containers}
isAvailable={
dockerState.isAvailable
}
onContainerSelect={
handleContainerSelectForTarget
}
onRefresh={() =>
refreshContainersForSite(
selectedSite.siteId
)
}
/>
);
})()}
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
role="combobox"
className={cn(
"w-[180px] justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
!row.original.siteId &&
"text-muted-foreground"
)}
>
<span className="truncate max-w-[150px]">
{row.original.siteId
? selectedSite?.name
: t("siteSelect")}
</span>
<CaretSortIcon className="ml-2h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-[180px]">
<Command>
<CommandInput
placeholder={t("siteSearch")}
/>
<CommandList>
<CommandEmpty>
{t("siteNotFound")}
</CommandEmpty>
<CommandGroup>
{sites.map((site) => (
<CommandItem
key={site.siteId}
value={`${site.siteId}:${site.name}`}
onSelect={() =>
updateTarget(
row.original
.targetId,
{
siteId: site.siteId
}
)
}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
site.siteId ===
row.original
.siteId
? "opacity-100"
: "opacity-0"
)}
/>
{site.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{isHttp && (
<Select
defaultValue={row.original.method ?? "http"}
onValueChange={(value) =>
updateTarget(row.original.targetId, {
...row.original,
method: value
})
}
>
<SelectTrigger className="h-8 px-2 w-[70px] border-none bg-transparent shadow-none focus:ring-0 focus:outline-none focus-visible:ring-0 data-[state=open]:bg-transparent">
{row.original.method || "http"}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">
http
</SelectItem>
<SelectItem value="https">
https
</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
)}
{isHttp && (
<div className="flex items-center justify-center px-2 h-9">
{"://"}
</div>
)}
<Input
defaultValue={row.original.ip}
placeholder="Host"
className="flex-1 min-w-[120px] pl-0 border-none placeholder-gray-400"
onBlur={(e) => {
const input = e.target.value.trim();
const hasProtocol =
/^(https?|h2c):\/\//.test(input);
const hasPort = /:\d+(?:\/|$)/.test(input);
if (hasProtocol || hasPort) {
const parsed = parseHostTarget(input);
if (parsed) {
updateTarget(
row.original.targetId,
{
...row.original,
method: hasProtocol
? parsed.protocol
: row.original.method,
ip: parsed.host,
port: hasPort
? parsed.port
: row.original.port
}
);
} else {
updateTarget(
row.original.targetId,
{
...row.original,
ip: input
}
);
}
} else {
updateTarget(row.original.targetId, {
...row.original,
ip: input
});
}
}}
/>
<div className="flex items-center justify-center px-2 h-9">
{":"}
</div>
<Input
placeholder="Port"
defaultValue={
row.original.port === 0
? ""
: row.original.port
}
className="w-[75px] pl-0 border-none placeholder-gray-400"
onBlur={(e) => {
const value = parseInt(e.target.value, 10);
if (!isNaN(value) && value > 0) {
updateTarget(row.original.targetId, {
...row.original,
port: value
});
} else {
updateTarget(row.original.targetId, {
...row.original,
port: 0
});
}
}}
/>
</div>
</div>
);
},
cell: ({ row }) => (
<ResourceTargetAddressItem
isHttp={isHttp}
sites={sites}
getDockerStateForSite={getDockerStateForSite}
proxyTarget={row.original}
refreshContainersForSite={refreshContainersForSite}
updateTarget={updateTarget}
/>
),
size: 400,
minSize: 350,
maxSize: 500
@@ -1186,7 +849,7 @@ export default function Page() {
<Button
variant="outline"
disabled={noPathMatch}
className="w-full max-w-[200px]"
className="w-full max-w-50"
>
<Plus className="h-4 w-4 mr-2" />
{t("rewritePath")}
@@ -1265,9 +928,17 @@ export default function Page() {
actionsColumn
];
}
};
const columns = getColumns();
}, [
isAdvancedMode,
isHttp,
sites,
updateTarget,
getDockerStateForSite,
refreshContainersForSite,
openHealthCheckDialog,
removeTarget,
t
]);
const table = useReactTable({
data: targets,
@@ -1649,9 +1320,6 @@ export default function Page() {
</TableRow>
)}
</TableBody>
{/* <TableCaption> */}
{/* {t('targetNoOneDescription')} */}
{/* </TableCaption> */}
</Table>
</div>
<div className="flex items-center justify-between mb-4">
@@ -39,6 +39,7 @@ import {
} from "@app/lib/wireguard";
import { QRCodeCanvas } from "qrcode.react";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
export default function CredentialsPage() {
const { env } = useEnvContext();
@@ -186,107 +187,119 @@ export default function CredentialsPage() {
return site?.name || site?.niceId || "My site";
};
const displayNewtId = currentNewtId || siteDefaults?.newtId || null;
const displaySecret = regeneratedSecret || null;
const displayNewtId = currentNewtId ?? siteDefaults?.newtId ?? null;
const displaySecret = regeneratedSecret ?? null;
return (
<>
<SettingsContainer>
{site?.type === "newt" && (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("siteNewtCredentials")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("siteNewtCredentialsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("siteNewtCredentials")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("siteNewtCredentialsDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<PaidFeaturesAlert />
<PaidFeaturesAlert />
<SettingsSectionBody>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
{t("newtEndpoint")}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
text={env.app.dashboardUrl}
/>
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("newtId")}
</InfoSectionTitle>
<InfoSectionContent>
{displayNewtId ? (
<SettingsSectionBody>
<InfoSections cols={3}>
<InfoSection>
<InfoSectionTitle>
{t("newtEndpoint")}
</InfoSectionTitle>
<InfoSectionContent>
<CopyToClipboard
text={displayNewtId}
text={env.app.dashboardUrl}
/>
) : (
<span>{"••••••••••••••••"}</span>
)}
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("newtSecretKey")}
</InfoSectionTitle>
<InfoSectionContent>
{displaySecret ? (
<CopyToClipboard
text={displaySecret}
/>
) : (
<span>
{
"••••••••••••••••••••••••••••••••"
}
</span>
)}
</InfoSectionContent>
</InfoSection>
</InfoSections>
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("newtId")}
</InfoSectionTitle>
<InfoSectionContent>
{displayNewtId ? (
<CopyToClipboard
text={displayNewtId}
/>
) : (
<span>
{"••••••••••••••••"}
</span>
)}
</InfoSectionContent>
</InfoSection>
<InfoSection>
<InfoSectionTitle>
{t("newtSecretKey")}
</InfoSectionTitle>
<InfoSectionContent>
{displaySecret ? (
<CopyToClipboard
text={displaySecret}
/>
) : (
<span>
{
"••••••••••••••••••••••••••••••••"
}
</span>
)}
</InfoSectionContent>
</InfoSection>
</InfoSections>
{showCredentialsAlert && displaySecret && (
<Alert variant="neutral" className="mt-4">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t("siteCredentialsSave")}
</AlertTitle>
<AlertDescription>
{t("siteCredentialsSaveDescription")}
</AlertDescription>
</Alert>
{showCredentialsAlert && displaySecret && (
<Alert variant="neutral" className="mt-4">
<InfoIcon className="h-4 w-4" />
<AlertTitle className="font-semibold">
{t("siteCredentialsSave")}
</AlertTitle>
<AlertDescription>
{t(
"siteCredentialsSaveDescription"
)}
</AlertDescription>
</Alert>
)}
</SettingsSectionBody>
{build !== "oss" && (
<SettingsSectionFooter>
<Button
variant="outline"
onClick={() => {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("regenerateCredentialsButton")}
</Button>
<Button
onClick={() => {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("siteRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
)}
</SettingsSectionBody>
{build !== "oss" && (
<SettingsSectionFooter>
<Button
variant="outline"
onClick={() => {
setShouldDisconnect(false);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("regenerateCredentialsButton")}
</Button>
<Button
onClick={() => {
setShouldDisconnect(true);
setModalOpen(true);
}}
disabled={isSecurityFeatureDisabled()}
>
{t("siteRegenerateAndDisconnect")}
</Button>
</SettingsSectionFooter>
)}
</SettingsSection>
</SettingsSection>
<NewtSiteInstallCommands
id={displayNewtId ?? "**********"}
secret={displaySecret ?? "**************"}
endpoint={env.app.dashboardUrl}
/>
</>
)}
{site?.type === "wireguard" && (
@@ -306,7 +319,7 @@ export default function CredentialsPage() {
{!loadingDefaults && (
<>
{wgConfig ? (
<div className="flex items-center gap-4">
<div className="flex flex-col sm:flex-row items-center gap-4">
<CopyTextBox
text={wgConfig}
outline={true}
+11 -407
View File
@@ -62,6 +62,8 @@ import { QRCodeCanvas } from "qrcode.react";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
import { id } from "date-fns/locale";
type SiteType = "newt" | "wireguard" | "local";
@@ -189,10 +191,6 @@ export default function Page() {
const [loadingPage, setLoadingPage] = useState(true);
const [platform, setPlatform] = useState<Platform>("unix");
const [architecture, setArchitecture] = useState("amd64");
const [commands, setCommands] = useState<Commands | null>(null);
const [newtId, setNewtId] = useState("");
const [newtSecret, setNewtSecret] = useState("");
const [newtEndpoint, setNewtEndpoint] = useState("");
@@ -202,7 +200,6 @@ export default function Page() {
const [wgConfig, setWgConfig] = useState("");
const [createLoading, setCreateLoading] = useState(false);
const [acceptClients, setAcceptClients] = useState(true);
const [newtVersion, setNewtVersion] = useState("latest");
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
@@ -216,187 +213,6 @@ export default function Page() {
string | undefined
>();
const hydrateCommands = (
id: string,
secret: string,
endpoint: string,
version: string,
acceptClients: boolean = true
) => {
const acceptClientsFlag = !acceptClients ? " --disable-clients" : "";
const acceptClientsEnv = !acceptClients
? "\n - DISABLE_CLIENTS=true"
: "";
const commands = {
unix: {
All: [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
},
{
title: t("run"),
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
}
]
},
windows: {
x64: [
{
title: t("install"),
command: `curl -o newt.exe -L "https://github.com/fosrl/newt/releases/download/${version}/newt_windows_amd64.exe"`
},
{
title: t("run"),
command: `newt.exe --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
}
]
},
docker: {
"Docker Compose": [
`services:
newt:
image: fosrl/newt
container_name: newt
restart: unless-stopped
environment:
- PANGOLIN_ENDPOINT=${endpoint}
- NEWT_ID=${id}
- NEWT_SECRET=${secret}${acceptClientsEnv}`
],
"Docker Run": [
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
},
kubernetes: {
"Helm Chart": [
`helm repo add fossorial https://charts.fossorial.io`,
`helm repo update fossorial`,
`helm install newt fossorial/newt \\
--create-namespace \\
--set newtInstances[0].name="main-tunnel" \\
--set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\
--set-string newtInstances[0].auth.keys.idKey="${id}" \\
--set-string newtInstances[0].auth.keys.secretKey="${secret}"`
]
},
podman: {
"Podman Quadlet": [
`[Unit]
Description=Newt container
[Container]
ContainerName=newt
Image=docker.io/fosrl/newt
Environment=PANGOLIN_ENDPOINT=${endpoint}
Environment=NEWT_ID=${id}
Environment=NEWT_SECRET=${secret}${!acceptClients ? "\nEnvironment=DISABLE_CLIENTS=true" : ""}
# Secret=newt-secret,type=env,target=NEWT_SECRET
[Service]
Restart=always
[Install]
WantedBy=default.target`
],
"Podman Run": [
`podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
},
nixos: {
All: [
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
}
};
setCommands(commands);
};
const getArchitectures = () => {
switch (platform) {
case "unix":
return ["All"];
case "windows":
return ["x64"];
case "docker":
return ["Docker Compose", "Docker Run"];
case "kubernetes":
return ["Helm Chart"];
case "podman":
return ["Podman Quadlet", "Podman Run"];
case "nixos":
return ["All"];
default:
return ["x64"];
}
};
const getPlatformName = (platformName: string) => {
switch (platformName) {
case "windows":
return "Windows";
case "unix":
return "Unix & macOS";
case "docker":
return "Docker";
case "kubernetes":
return "Kubernetes";
case "podman":
return "Podman";
case "nixos":
return "NixOS";
default:
return "Unix / macOS";
}
};
const getCommand = (): CommandItem[] => {
const placeholder: CommandItem[] = [t("unknownCommand")];
if (!commands) {
return placeholder;
}
let platformCommands = commands[platform as keyof Commands];
if (!platformCommands) {
// get first key
const firstPlatform = Object.keys(commands)[0] as Platform;
platformCommands = commands[firstPlatform as keyof Commands];
setPlatform(firstPlatform);
}
let architectureCommands = platformCommands[architecture];
if (!architectureCommands) {
// get first key
const firstArchitecture = Object.keys(platformCommands)[0];
architectureCommands = platformCommands[firstArchitecture];
setArchitecture(firstArchitecture);
}
return architectureCommands || placeholder;
};
const getPlatformIcon = (platformName: string) => {
switch (platformName) {
case "windows":
return <FaWindows className="h-4 w-4 mr-2" />;
case "unix":
return <Terminal className="h-4 w-4 mr-2" />;
case "docker":
return <FaDocker className="h-4 w-4 mr-2" />;
case "kubernetes":
return <SiKubernetes className="h-4 w-4 mr-2" />;
case "podman":
return <FaCubes className="h-4 w-4 mr-2" />;
case "nixos":
return <SiNixos className="h-4 w-4 mr-2" />;
default:
return <Terminal className="h-4 w-4 mr-2" />;
}
};
const form = useForm({
resolver: zodResolver(createSiteFormSchema),
defaultValues: {
@@ -414,7 +230,7 @@ WantedBy=default.target`
let payload: CreateSiteBody = {
name: data.name,
type: data.method as "newt" | "wireguard" | "local"
type: data.method
};
if (data.method == "wireguard") {
@@ -568,14 +384,6 @@ WantedBy=default.target`
setNewtEndpoint(newtEndpoint);
setClientAddress(clientAddress);
hydrateCommands(
newtId,
newtSecret,
env.app.dashboardUrl,
currentNewtVersion,
acceptClients
);
const wgConfig = generateWireGuardConfig(
privateKey,
data.publicKey,
@@ -631,11 +439,6 @@ WantedBy=default.target`
load();
}, []);
// Sync form acceptClients value with local state
useEffect(() => {
form.setValue("acceptClients", acceptClients);
}, [acceptClients, form]);
// Sync form exitNodeId value with local state
useEffect(() => {
if (build !== "saas") {
@@ -847,214 +650,15 @@ WantedBy=default.target`
</InfoSectionContent>
</InfoSection>
</InfoSections>
{/* <Form {...form}> */}
{/* <form */}
{/* className="space-y-4" */}
{/* id="create-site-form" */}
{/* > */}
{/* <FormField */}
{/* control={form.control} */}
{/* name="copied" */}
{/* render={({ field }) => ( */}
{/* <FormItem> */}
{/* <div className="flex items-center space-x-2"> */}
{/* <Checkbox */}
{/* id="terms" */}
{/* defaultChecked={ */}
{/* form.getValues( */}
{/* "copied" */}
{/* ) as boolean */}
{/* } */}
{/* onCheckedChange={( */}
{/* e */}
{/* ) => { */}
{/* form.setValue( */}
{/* "copied", */}
{/* e as boolean */}
{/* ); */}
{/* }} */}
{/* /> */}
{/* <label */}
{/* htmlFor="terms" */}
{/* className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" */}
{/* > */}
{/* {t('siteConfirmCopy')} */}
{/* </label> */}
{/* </div> */}
{/* <FormMessage /> */}
{/* </FormItem> */}
{/* )} */}
{/* /> */}
{/* </form> */}
{/* </Form> */}
</SettingsSectionBody>
</SettingsSection>
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("siteInstallNewt")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("siteInstallNewtDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<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);
}}
>
{getPlatformIcon(os)}
{getPlatformName(os)}
</Button>
))}
</div>
</div>
<div>
<p className="font-bold mb-3">
{["docker", "podman"].includes(
platform
)
? t("method")
: t("architecture")}
</p>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2">
{getArchitectures().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("siteConfiguration")}
</p>
<div className="flex items-center space-x-2 mb-2">
<CheckboxWithLabel
id="acceptClients"
aria-describedby="acceptClients-desc"
checked={acceptClients}
onCheckedChange={(
checked
) => {
const value =
checked as boolean;
setAcceptClients(
value
);
form.setValue(
"acceptClients",
value
);
// Re-hydrate commands with new acceptClients value
if (
newtId &&
newtSecret &&
newtVersion
) {
hydrateCommands(
newtId,
newtSecret,
env.app
.dashboardUrl,
newtVersion,
value
);
}
}}
label={t(
"siteAcceptClientConnections"
)}
/>
</div>
<p
id="acceptClients-desc"
className="text-sm text-muted-foreground"
>
{t(
"siteAcceptClientConnectionsDescription"
)}
</p>
</div>
<div className="pt-4">
<p className="font-bold mb-3">
{t("commands")}
</p>
<div className="mt-2 space-y-3">
{getCommand().map(
(item, index) => {
const commandText =
typeof item ===
"string"
? item
: item.command;
const title =
typeof item ===
"string"
? undefined
: item.title;
return (
<div
key={index}
>
{title && (
<p className="text-sm font-medium mb-1.5">
{
title
}
</p>
)}
<CopyTextBox
text={
commandText
}
outline={
true
}
/>
</div>
);
}
)}
</div>
</div>
</div>
</SettingsSectionBody>
</SettingsSection>
<NewtSiteInstallCommands
id={newtId}
secret={newtSecret}
endpoint={env.app.dashboardUrl}
version={newtVersion}
/>
</>
)}
@@ -1069,7 +673,7 @@ WantedBy=default.target`
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<div className="flex items-center gap-4">
<div className="flex flex-col sm:flex-row items-center gap-4">
<CopyTextBox text={wgConfig} />
<div
className={`relative w-fit border rounded-md`}
@@ -1087,7 +691,7 @@ WantedBy=default.target`
</SettingsSection>
)}
{build == "saas" &&
{build === "saas" &&
form.watch("method") === "local" && (
<SettingsSection>
<SettingsSectionHeader>
+1 -1
View File
@@ -19,7 +19,7 @@ export const ApprovalsBanner = () => {
description={t("approvalsBannerDescription")}
>
<Link
href="https://docs.pangolin.net/manage/access/approvals"
href="https://docs.pangolin.net/manage/access-control/approvals"
target="_blank"
rel="noopener noreferrer"
>
+9
View File
@@ -110,6 +110,15 @@ export default function BlueprintDetailsForm({
Dashboard
</Badge>
)}{" "}
{blueprint.source === "CLI" && (
<Badge
variant="secondary"
className="inline-flex items-center gap-1 "
>
<Terminal className="w-3 h-3 flex-none" />
CLI
</Badge>
)}{" "}
</InfoSectionContent>
</InfoSection>
<InfoSection>
+13
View File
@@ -128,6 +128,19 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
</Badge>
);
}
case "CLI": {
return (
<Badge
variant="secondary"
className="inline-flex items-center gap-1"
>
<span className="inline-flex items-center gap-1 ">
<Terminal className="w-3 h-3" />
CLI
</span>
</Badge>
);
}
}
}
},
+29
View File
@@ -4,6 +4,7 @@ import React from "react";
import { Button } from "@app/components/ui/button";
import { Download } from "lucide-react";
import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
import { SiAndroid } from "react-icons/si";
import { useTranslations } from "next-intl";
import Link from "next/link";
import DismissableBanner from "./DismissableBanner";
@@ -61,6 +62,34 @@ export const ClientDownloadBanner = () => {
Linux
</Button>
</Link>
<Link
href="https://pangolin.net/downloads/ios"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant="outline"
size="sm"
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
>
<FaApple className="w-4 h-4" />
iOS
</Button>
</Link>
<Link
href="https://pangolin.net/downloads/android"
target="_blank"
rel="noopener noreferrer"
>
<Button
variant="outline"
size="sm"
className="gap-2 hover:bg-primary/10 hover:border-primary/50 transition-colors"
>
<SiAndroid className="w-4 h-4" />
Android
</Button>
</Link>
</DismissableBanner>
);
};
+27 -1
View File
@@ -11,6 +11,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu";
import { InfoPopup } from "@app/components/ui/info-popup";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api";
@@ -40,6 +41,7 @@ export type InternalResourceRow = {
destination: string;
// destinationPort: number | null;
alias: string | null;
aliasAddress: string | null;
niceId: string;
tcpPortRangeString: string | null;
udpPortRangeString: string | null;
@@ -228,6 +230,29 @@ export default function ClientResourcesTable({
);
}
},
{
accessorKey: "aliasAddress",
friendlyName: t("resourcesTableAliasAddress"),
enableHiding: true,
header: () => (
<div className="flex items-center gap-2 p-3">
<span>{t("resourcesTableAliasAddress")}</span>
<InfoPopup info={t("resourcesTableAliasAddressInfo")} />
</div>
),
cell: ({ row }) => {
const resourceRow = row.original;
return resourceRow.aliasAddress ? (
<CopyToClipboard
text={resourceRow.aliasAddress}
isLink={false}
displayText={resourceRow.aliasAddress}
/>
) : (
<span>-</span>
);
}
},
{
id: "actions",
enableHiding: false,
@@ -316,7 +341,8 @@ export default function ClientResourcesTable({
enableColumnVisibility={true}
persistColumnVisibility="internal-resources"
columnVisibility={{
niceId: false
niceId: false,
aliasAddress: false
}}
stickyLeftColumn="name"
stickyRightColumn="actions"
-12
View File
@@ -94,12 +94,6 @@ export default function DomainPicker({
const api = createApiClient({ env });
const t = useTranslations();
console.log({
defaultFullDomain,
defaultSubdomain,
defaultDomainId
});
const { data = [], isLoading: loadingDomains } = useQuery(
orgQueries.domains({ orgId })
);
@@ -369,9 +363,6 @@ export default function DomainPicker({
setSelectedProvidedDomain(null);
}
console.log({
setSelectedBaseDomain: option
});
setSelectedBaseDomain(option);
setOpen(false);
@@ -442,9 +433,6 @@ export default function DomainPicker({
0,
providedDomainsShown
);
console.log({
displayedProvidedOptions
});
const selectedDomainNamespaceId =
selectedProvidedDomain?.domainNamespaceId ??
+12
View File
@@ -60,6 +60,18 @@ export default function LocaleSwitcher() {
{
value: "zh-TW",
label: "繁體中文"
},
{
value: "ru-RU",
label: "Русский"
},
{
value: "bg-BG",
label: "Български"
},
{
value: "cs-CZ",
label: "Čeština"
}
]}
/>
-1
View File
@@ -68,7 +68,6 @@ export function LogAnalyticsData(props: AnalyticsContentProps) {
const router = useRouter();
console.log({ filters });
const dateRange = {
startDate: filters.timeStart
? new Date(filters.timeStart)
-1
View File
@@ -143,7 +143,6 @@ export default function LoginOrgSelector({
<IdpLoginButtons
idps={idps}
redirect={redirect}
orgId={org.orgId}
/>
</div>
</div>
+2 -2
View File
@@ -548,7 +548,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{clientRow.approvalState === "pending" && (
{clientRow.approvalState === "pending" && build !== "oss" && (
<>
<DropdownMenuItem
onClick={() => approveDevice(clientRow)}
@@ -653,7 +653,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
];
if (build === "oss") {
return allOptions.filter((option) => option.value !== "pending");
return allOptions.filter((option) => option.value !== "pending" && option.value !== "denied");
}
return allOptions;
+314
View File
@@ -0,0 +1,314 @@
import { useTranslations } from "next-intl";
import CopyTextBox from "./CopyTextBox";
import {
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionHeader,
SettingsSectionTitle
} from "./Settings";
import { CheckboxWithLabel } from "./ui/checkbox";
import { Button } from "./ui/button";
import { useState } from "react";
import { FaCubes, FaDocker, FaWindows } from "react-icons/fa";
import { Terminal } from "lucide-react";
import { SiKubernetes, SiNixos } from "react-icons/si";
export type CommandItem = string | { title: string; command: string };
const PLATFORMS = [
"unix",
"windows",
"docker",
"kubernetes",
"podman",
"nixos"
] as const;
type Platform = (typeof PLATFORMS)[number];
export type NewtSiteInstallCommandsProps = {
id: string;
secret: string;
endpoint: string;
version?: string;
};
export function NewtSiteInstallCommands({
id,
secret,
endpoint,
version = "latest"
}: NewtSiteInstallCommandsProps) {
const t = useTranslations();
const [acceptClients, setAcceptClients] = useState(true);
const [platform, setPlatform] = useState<Platform>("unix");
const [architecture, setArchitecture] = useState(
() => getArchitectures(platform)[0]
);
const acceptClientsFlag = !acceptClients ? " --disable-clients" : "";
const acceptClientsEnv = !acceptClients
? "\n - DISABLE_CLIENTS=true"
: "";
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
unix: {
All: [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-newt.sh | bash`
},
{
title: t("run"),
command: `newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
}
]
},
windows: {
x64: [
{
title: t("install"),
command: `curl -o newt.exe -L "https://github.com/fosrl/newt/releases/download/${version}/newt_windows_amd64.exe"`
},
{
title: t("run"),
command: `newt.exe --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
}
]
},
docker: {
"Docker Compose": [
`services:
newt:
image: fosrl/newt
container_name: newt
restart: unless-stopped
environment:
- PANGOLIN_ENDPOINT=${endpoint}
- NEWT_ID=${id}
- NEWT_SECRET=${secret}${acceptClientsEnv}`
],
"Docker Run": [
`docker run -dit fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
},
kubernetes: {
"Helm Chart": [
`helm repo add fossorial https://charts.fossorial.io`,
`helm repo update fossorial`,
`helm install newt fossorial/newt \\
--create-namespace \\
--set newtInstances[0].name="main-tunnel" \\
--set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\
--set-string newtInstances[0].auth.keys.idKey="${id}" \\
--set-string newtInstances[0].auth.keys.secretKey="${secret}"`
]
},
podman: {
"Podman Quadlet": [
`[Unit]
Description=Newt container
[Container]
ContainerName=newt
Image=docker.io/fosrl/newt
Environment=PANGOLIN_ENDPOINT=${endpoint}
Environment=NEWT_ID=${id}
Environment=NEWT_SECRET=${secret}${!acceptClients ? "\nEnvironment=DISABLE_CLIENTS=true" : ""}
# Secret=newt-secret,type=env,target=NEWT_SECRET
[Service]
Restart=always
[Install]
WantedBy=default.target`
],
"Podman Run": [
`podman run -dit docker.io/fosrl/newt --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
},
nixos: {
All: [
`nix run 'nixpkgs#fosrl-newt' -- --id ${id} --secret ${secret} --endpoint ${endpoint}${acceptClientsFlag}`
]
}
};
const commands = commandList[platform][architecture];
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("siteInstallNewt")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("siteInstallNewtDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<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>
<div>
<p className="font-bold mb-3">
{["docker", "podman"].includes(platform)
? t("method")
: 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("siteConfiguration")}
</p>
<div className="flex items-center space-x-2 mb-2">
<CheckboxWithLabel
id="acceptClients"
aria-describedby="acceptClients-desc"
checked={acceptClients}
onCheckedChange={(checked) => {
const value = checked as boolean;
setAcceptClients(value);
}}
label={t("siteAcceptClientConnections")}
/>
</div>
<p
id="acceptClients-desc"
className="text-sm text-muted-foreground"
>
{t("siteAcceptClientConnectionsDescription")}
</p>
</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) => {
const commandText =
typeof item === "string"
? item
: item.command;
const title =
typeof item === "string"
? undefined
: item.title;
return (
<div key={index}>
{title && (
<p className="text-sm font-medium mb-1.5">
{title}
</p>
)}
<CopyTextBox
text={commandText}
outline={true}
/>
</div>
);
})}
</div>
</div>
</div>
</SettingsSectionBody>
</SettingsSection>
);
}
function getPlatformIcon(platformName: Platform) {
switch (platformName) {
case "windows":
return <FaWindows className="h-4 w-4 mr-2" />;
case "unix":
return <Terminal className="h-4 w-4 mr-2" />;
case "docker":
return <FaDocker className="h-4 w-4 mr-2" />;
case "kubernetes":
return <SiKubernetes className="h-4 w-4 mr-2" />;
case "podman":
return <FaCubes className="h-4 w-4 mr-2" />;
case "nixos":
return <SiNixos className="h-4 w-4 mr-2" />;
default:
return <Terminal className="h-4 w-4 mr-2" />;
}
}
function getPlatformName(platformName: Platform) {
switch (platformName) {
case "windows":
return "Windows";
case "unix":
return "Unix & macOS";
case "docker":
return "Docker";
case "kubernetes":
return "Kubernetes";
case "podman":
return "Podman";
case "nixos":
return "NixOS";
default:
return "Unix / macOS";
}
}
function getArchitectures(platform: Platform) {
switch (platform) {
case "unix":
return ["All"];
case "windows":
return ["x64"];
case "docker":
return ["Docker Compose", "Docker Run"];
case "kubernetes":
return ["Helm Chart"];
case "podman":
return ["Podman Quadlet", "Podman Run"];
case "nixos":
return ["All"];
default:
return ["x64"];
}
}
+220
View File
@@ -0,0 +1,220 @@
import { Terminal } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { FaDocker, FaWindows } from "react-icons/fa";
import CopyTextBox from "./CopyTextBox";
import {
SettingsSection,
SettingsSectionBody,
SettingsSectionDescription,
SettingsSectionHeader,
SettingsSectionTitle
} from "./Settings";
import { Button } from "./ui/button";
export type CommandItem = string | { title: string; command: string };
const PLATFORMS = ["unix", "windows", "docker"] as const;
type Platform = (typeof PLATFORMS)[number];
export type OlmInstallCommandsProps = {
id: string;
secret: string;
endpoint: string;
version?: string;
};
export function OlmInstallCommands({
id,
secret,
endpoint,
version = "latest"
}: OlmInstallCommandsProps) {
const t = useTranslations();
const [platform, setPlatform] = useState<Platform>("unix");
const [architecture, setArchitecture] = useState(
() => getArchitectures(platform)[0]
);
const commandList: Record<Platform, Record<string, CommandItem[]>> = {
unix: {
All: [
{
title: t("install"),
command: `curl -fsSL https://static.pangolin.net/get-olm.sh | bash`
},
{
title: t("run"),
command: `sudo olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
}
]
},
windows: {
x64: [
{
title: t("install"),
command: `curl -o olm.exe -L "https://github.com/fosrl/olm/releases/download/${version}/olm_windows_installer.exe"`
},
{
title: t("run"),
command: `olm.exe --id ${id} --secret ${secret} --endpoint ${endpoint}`
}
]
},
docker: {
"Docker Compose": [
`services:
olm:
image: fosrl/olm
container_name: olm
restart: unless-stopped
network_mode: host
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- PANGOLIN_ENDPOINT=${endpoint}
- OLM_ID=${id}
- OLM_SECRET=${secret}`
],
"Docker Run": [
`docker run -dit --network host --cap-add NET_ADMIN --device /dev/net/tun:/dev/net/tun fosrl/olm --id ${id} --secret ${secret} --endpoint ${endpoint}`
]
}
};
const commands = commandList[platform][architecture];
return (
<SettingsSection>
<SettingsSectionHeader>
<SettingsSectionTitle>
{t("clientInstallOlm")}
</SettingsSectionTitle>
<SettingsSectionDescription>
{t("clientInstallOlmDescription")}
</SettingsSectionDescription>
</SettingsSectionHeader>
<SettingsSectionBody>
<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>
<div>
<p className="font-bold mb-3">
{["docker", "podman"].includes(platform)
? t("method")
: 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) => {
const commandText =
typeof item === "string"
? item
: item.command;
const title =
typeof item === "string"
? undefined
: item.title;
return (
<div key={index}>
{title && (
<p className="text-sm font-medium mb-1.5">
{title}
</p>
)}
<CopyTextBox
text={commandText}
outline={true}
/>
</div>
);
})}
</div>
</div>
</div>
</SettingsSectionBody>
</SettingsSection>
);
}
function getArchitectures(platform: Platform) {
switch (platform) {
case "unix":
return ["All"];
case "windows":
return ["x64"];
case "docker":
return ["Docker Compose", "Docker Run"];
default:
return ["x64"];
}
}
function getPlatformName(platformName: Platform) {
switch (platformName) {
case "windows":
return "Windows";
case "unix":
return "Unix & macOS";
case "docker":
return "Docker";
default:
return "Unix & macOS";
}
}
function getPlatformIcon(platformName: Platform) {
switch (platformName) {
case "windows":
return <FaWindows className="h-4 w-4 mr-2" />;
case "unix":
return <Terminal className="h-4 w-4 mr-2" />;
case "docker":
return <FaDocker className="h-4 w-4 mr-2" />;
default:
return <Terminal className="h-4 w-4 mr-2" />;
}
}
@@ -0,0 +1,241 @@
import { cn } from "@app/lib/cn";
import type { DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget";
import { CaretSortIcon } from "@radix-ui/react-icons";
import type { ListSitesResponse } from "@server/routers/site";
import { type ListTargetsResponse } from "@server/routers/target";
import type { ArrayElement } from "@server/types/ArrayElement";
import { CheckIcon } from "lucide-react";
import { useTranslations } from "next-intl";
import { ContainersSelector } from "./ContainersSelector";
import { Button } from "./ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList
} from "./ui/command";
import { Input } from "./ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
type SiteWithUpdateAvailable = ListSitesResponse["sites"][number];
export type LocalTarget = Omit<
ArrayElement<ListTargetsResponse["targets"]> & {
new?: boolean;
updated?: boolean;
siteType: string | null;
},
"protocol"
>;
export type ResourceTargetAddressItemProps = {
getDockerStateForSite: (siteId: number) => DockerState;
updateTarget: (targetId: number, data: Partial<LocalTarget>) => void;
sites: SiteWithUpdateAvailable[];
proxyTarget: LocalTarget;
isHttp: boolean;
refreshContainersForSite: (siteId: number) => void;
};
export function ResourceTargetAddressItem({
sites,
getDockerStateForSite,
updateTarget,
proxyTarget,
isHttp,
refreshContainersForSite
}: ResourceTargetAddressItemProps) {
const t = useTranslations();
const selectedSite = sites.find(
(site) => site.siteId === proxyTarget.siteId
);
const handleContainerSelectForTarget = (
hostname: string,
port?: number
) => {
updateTarget(proxyTarget.targetId, {
...proxyTarget,
ip: hostname,
...(port && { port: port })
});
};
return (
<div className="flex items-center w-full" key={proxyTarget.targetId}>
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md">
{selectedSite &&
selectedSite.type === "newt" &&
(() => {
const dockerState = getDockerStateForSite(
selectedSite.siteId
);
return (
<ContainersSelector
site={selectedSite}
containers={dockerState.containers}
isAvailable={dockerState.isAvailable}
onContainerSelect={
handleContainerSelectForTarget
}
onRefresh={() =>
refreshContainersForSite(
selectedSite.siteId
)
}
/>
);
})()}
<Popover>
<PopoverTrigger asChild>
<Button
variant="ghost"
role="combobox"
className={cn(
"w-45 justify-between text-sm border-r pr-4 rounded-none h-8 hover:bg-transparent",
"rounded-l-md rounded-r-xs",
!proxyTarget.siteId && "text-muted-foreground"
)}
>
<span className="truncate max-w-37.5">
{proxyTarget.siteId
? selectedSite?.name
: t("siteSelect")}
</span>
<CaretSortIcon className="ml-2h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="p-0 w-45">
<Command>
<CommandInput placeholder={t("siteSearch")} />
<CommandList>
<CommandEmpty>{t("siteNotFound")}</CommandEmpty>
<CommandGroup>
{sites.map((site) => (
<CommandItem
key={site.siteId}
value={`${site.siteId}:${site.name}`}
onSelect={() =>
updateTarget(
proxyTarget.targetId,
{
siteId: site.siteId
}
)
}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
site.siteId ===
proxyTarget.siteId
? "opacity-100"
: "opacity-0"
)}
/>
{site.name}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{isHttp && (
<Select
defaultValue={proxyTarget.method ?? "http"}
onValueChange={(value) =>
updateTarget(proxyTarget.targetId, {
...proxyTarget,
method: value
})
}
>
<SelectTrigger className="h-8 px-2 w-17.5 border-none bg-transparent shadow-none data-[state=open]:bg-transparent rounded-xs">
{proxyTarget.method || "http"}
</SelectTrigger>
<SelectContent>
<SelectItem value="http">http</SelectItem>
<SelectItem value="https">https</SelectItem>
<SelectItem value="h2c">h2c</SelectItem>
</SelectContent>
</Select>
)}
{isHttp && (
<div className="flex items-center justify-center px-2 h-9">
{"://"}
</div>
)}
<Input
defaultValue={proxyTarget.ip}
placeholder="Host"
className="flex-1 min-w-30 px-2 border-none placeholder-gray-400 rounded-xs"
onBlur={(e) => {
const input = e.target.value.trim();
const hasProtocol = /^(https?|h2c):\/\//.test(input);
const hasPort = /:\d+(?:\/|$)/.test(input);
if (hasProtocol || hasPort) {
const parsed = parseHostTarget(input);
if (parsed) {
updateTarget(proxyTarget.targetId, {
...proxyTarget,
method: hasProtocol
? parsed.protocol
: proxyTarget.method,
ip: parsed.host,
port: hasPort
? parsed.port
: proxyTarget.port
});
} else {
updateTarget(proxyTarget.targetId, {
...proxyTarget,
ip: input
});
}
} else {
updateTarget(proxyTarget.targetId, {
...proxyTarget,
ip: input
});
}
}}
/>
<div className="flex items-center justify-center px-2 h-9">
{":"}
</div>
<Input
placeholder="Port"
defaultValue={
proxyTarget.port === 0 ? "" : proxyTarget.port
}
className="w-18.75 px-2 border-none placeholder-gray-400 rounded-l-xs"
onBlur={(e) => {
const value = parseInt(e.target.value, 10);
if (!isNaN(value) && value > 0) {
updateTarget(proxyTarget.targetId, {
...proxyTarget,
port: value
});
} else {
updateTarget(proxyTarget.targetId, {
...proxyTarget,
port: 0
});
}
}}
/>
</div>
</div>
);
}
+1 -1
View File
@@ -44,8 +44,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
data-slot="input"
className={cn(
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
"focus-visible:border-ring focus-visible:ring-ring/50",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
"focus-visible:outline-none focus-visible:border-ring focus-visible:ring-offset-0",
className
)}
ref={ref}
+5 -3
View File
@@ -36,7 +36,9 @@ function SelectTrigger({
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full",
"border-input data-placeholder:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive cursor-pointer flex items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 w-full",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-0",
// "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-0",
className
)}
{...props}
@@ -60,7 +62,7 @@ function SelectContent({
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-sm",
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 relative z-50 max-h-(--radix-select-content-available-height) min-w-32 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-sm",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
@@ -73,7 +75,7 @@ function SelectContent({
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
"h-(--radix-select-trigger-height) w-full min-w-(--radix-select-trigger-width) scroll-my-1"
)}
>
{children}