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
+5 -5
View File
@@ -29,7 +29,7 @@ jobs:
permissions: write-all permissions: write-all
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
@@ -264,7 +264,7 @@ jobs:
shell: bash shell: bash
- name: Install Go - name: Install Go
uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with: with:
go-version: 1.24 go-version: 1.24
@@ -345,7 +345,7 @@ jobs:
# Determine if this is an RC release # Determine if this is an RC release
IS_RC="false" IS_RC="false"
if echo "$TAG" | grep -qE "rc[0-9]+$"; then if [[ "$TAG" == *"-rc."* ]]; then
IS_RC="true" IS_RC="true"
fi fi
@@ -442,7 +442,7 @@ jobs:
# Determine if this is an RC release # Determine if this is an RC release
IS_RC="false" IS_RC="false"
if echo "$TAG" | grep -qE "rc[0-9]+$"; then if [[ "$TAG" == *"-rc."* ]]; then
IS_RC="true" IS_RC="true"
fi fi
@@ -513,7 +513,7 @@ jobs:
permissions: write-all permissions: write-all
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: '22' node-version: '22'
+3 -3
View File
@@ -23,7 +23,7 @@ jobs:
permissions: write-all permissions: write-all
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
@@ -69,7 +69,7 @@ jobs:
fi fi
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
@@ -110,7 +110,7 @@ jobs:
permissions: write-all permissions: write-all
steps: steps:
- name: Configure AWS credentials - name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2 uses: aws-actions/configure-aws-credentials@v5
with: with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
role-duration-seconds: 3600 role-duration-seconds: 3600
+1 -1
View File
@@ -17,7 +17,7 @@ jobs:
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install Node - name: Install Node
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with: with:
node-version: '22' node-version: '22'
+2 -2
View File
@@ -4,13 +4,13 @@
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"[jsonc]": { "[jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "vscode.json-language-features"
}, },
"[javascript]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"[typescript]": { "[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "vscode.typescript-language-features"
}, },
"[typescriptreact]": { "[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
+8
View File
@@ -35,6 +35,12 @@
</div> </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"> <p align="center">
<strong> <strong>
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a> 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) - [Mac](https://pangolin.net/downloads/mac)
- [Windows](https://pangolin.net/downloads/windows) - [Windows](https://pangolin.net/downloads/windows)
- [Linux](https://pangolin.net/downloads/linux) - [Linux](https://pangolin.net/downloads/linux)
- [iOS](https://pangolin.net/downloads/ios)
- [Android](https://pangolin.net/downloads/android)
## Get Started ## 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 { clearExitNodes } from "./commands/clearExitNodes";
import { rotateServerSecret } from "./commands/rotateServerSecret"; import { rotateServerSecret } from "./commands/rotateServerSecret";
import { clearLicenseKeys } from "./commands/clearLicenseKeys"; import { clearLicenseKeys } from "./commands/clearLicenseKeys";
import { deleteClient } from "./commands/deleteClient";
yargs(hideBin(process.argv)) yargs(hideBin(process.argv))
.scriptName("pangctl") .scriptName("pangctl")
@@ -15,5 +16,6 @@ yargs(hideBin(process.argv))
.command(clearExitNodes) .command(clearExitNodes)
.command(rotateServerSecret) .command(rotateServerSecret)
.command(clearLicenseKeys) .command(clearLicenseKeys)
.command(deleteClient)
.demandCommand() .demandCommand()
.help().argv; .help().argv;
+17 -14
View File
@@ -1,27 +1,30 @@
# To see all available options, please visit the docs: # To see all available options, please visit the docs:
# https://docs.pangolin.net/self-host/advanced/config-file # https://docs.pangolin.net/
gerbil:
start_port: 51820
base_endpoint: "{{.DashboardDomain}}"
app: app:
dashboard_url: http://localhost:3002 dashboard_url: "https://{{.DashboardDomain}}"
log_level: debug log_level: "info"
telemetry:
anonymous_usage: true
domains: domains:
domain1: domain1:
base_domain: example.com base_domain: "{{.BaseDomain}}"
server: server:
secret: my_secret_key secret: "{{.Secret}}"
cors:
gerbil: origins: ["https://{{.DashboardDomain}}"]
base_endpoint: example.com methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"]
orgs: credentials: false
block_size: 24
subnet_group: 100.90.137.0/20
flags: flags:
require_email_verification: false require_email_verification: false
disable_signup_without_invite: true disable_signup_without_invite: true
disable_user_create_org: true disable_user_create_org: false
allow_raw_resources: true allow_raw_resources: true
enable_integration_api: true
+10 -3
View File
@@ -21,9 +21,8 @@ http:
# Next.js router (handles everything except API and WebSocket paths) # Next.js router (handles everything except API and WebSocket paths)
next-router: next-router:
rule: "Host(`{{.DashboardDomain}}`)" rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
service: next-service service: next-service
priority: 10
entryPoints: entryPoints:
- websecure - websecure
middlewares: middlewares:
@@ -35,7 +34,6 @@ http:
api-router: api-router:
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)" rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
service: api-service service: api-service
priority: 100
entryPoints: entryPoints:
- websecure - websecure
middlewares: middlewares:
@@ -53,3 +51,12 @@ http:
loadBalancer: loadBalancer:
servers: servers:
- url: "http://pangolin:3000" # API/WebSocket server - 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 dashboard: true
providers: providers:
http:
endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s"
file: file:
directory: "/var/dynamic" filename: "/etc/traefik/dynamic_config.yml"
watch: true
experimental: experimental:
plugins: plugins:
badger: badger:
moduleName: "github.com/fosrl/badger" moduleName: "github.com/fosrl/badger"
version: "v1.3.0" version: "{{.BadgerVersion}}"
log: log:
level: "DEBUG" level: "INFO"
format: "common" format: "common"
maxSize: 100 maxSize: 100
maxBackups: 3 maxBackups: 3
maxAge: 3 maxAge: 3
compress: true compress: true
certificatesResolvers:
letsencrypt:
acme:
httpChallenge:
entryPoint: web
email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
entryPoints: entryPoints:
web: web:
address: ":80" address: ":80"
websecure: websecure:
address: ":9443" address: ":443"
transport: transport:
respondingTimeouts: respondingTimeouts:
readTimeout: "30m" readTimeout: "30m"
http:
tls:
certResolver: "letsencrypt"
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true
serversTransport: serversTransport:
insecureSkipVerify: true insecureSkipVerify: true
ping:
entryPoint: "web"
+2 -2
View File
@@ -3,8 +3,8 @@ module installer
go 1.24.0 go 1.24.0
require ( require (
golang.org/x/term v0.38.0 golang.org/x/term v0.39.0
gopkg.in/yaml.v3 v3.0.1 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.", "resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.",
"resourcesTableDestination": "Дестинация", "resourcesTableDestination": "Дестинация",
"resourcesTableAlias": "Псевдоним", "resourcesTableAlias": "Псевдоним",
"resourcesTableAliasAddress": "Адрес на псевдоним.",
"resourcesTableAliasAddressInfo": "Този адрес е част от подсистемата на организацията. Използва се за разрешаване на псевдонимни записи чрез вътрешно DNS разрешаване.",
"resourcesTableClients": "Клиенти", "resourcesTableClients": "Клиенти",
"resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.", "resourcesTableAndOnlyAccessibleInternally": "и са достъпни само вътрешно при свързване с клиент.",
"resourcesTableNoTargets": "Без цели", "resourcesTableNoTargets": "Без цели",
@@ -2489,6 +2491,8 @@
"logIn": "Вход", "logIn": "Вход",
"deviceInformation": "Информация за устройството", "deviceInformation": "Информация за устройството",
"deviceInformationDescription": "Информация за устройството и агента", "deviceInformationDescription": "Информация за устройството и агента",
"deviceSecurity": "Защита на устройството.",
"deviceSecurityDescription": "Информация за състоянието на защитата на устройството.",
"platform": "Платформа", "platform": "Платформа",
"macosVersion": "Версия на macOS", "macosVersion": "Версия на macOS",
"windowsVersion": "Версия на Windows", "windowsVersion": "Версия на Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Име на хост", "hostname": "Име на хост",
"firstSeen": "Видян за първи път", "firstSeen": "Видян за първи път",
"lastSeen": "Последно видян", "lastSeen": "Последно видян",
"biometricsEnabled": "Активирани биометрични данни.",
"diskEncrypted": "Криптиран диск.",
"firewallEnabled": "Активирана защитна стена.",
"autoUpdatesEnabled": "Активирани автоматични актуализации.",
"tpmAvailable": "TPM е на разположение.",
"macosSipEnabled": "Protection на системната цялост (SIP).",
"macosGatekeeperEnabled": "Gatekeeper.",
"macosFirewallStealthMode": "Скрит режим на защитната стена.",
"linuxAppArmorEnabled": "AppArmor.",
"linuxSELinuxEnabled": "SELinux.",
"deviceSettingsDescription": "Разгледайте информация и настройки на устройството", "deviceSettingsDescription": "Разгледайте информация и настройки на устройството",
"devicePendingApprovalDescription": "Това устройство чака одобрение", "devicePendingApprovalDescription": "Това устройство чака одобрение",
"deviceBlockedDescription": "Това устройство е в момента блокирано. Няма да може да се свърже с никакви ресурси, освен ако не бъде деблокирано.", "deviceBlockedDescription": "Това устройство е в момента блокирано. Няма да може да се свърже с никакви ресурси, освен ако не бъде деблокирано.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné vnitřní zdroje.", "resourcesTableNoInternalResourcesFound": "Nebyly nalezeny žádné vnitřní zdroje.",
"resourcesTableDestination": "Místo určení", "resourcesTableDestination": "Místo určení",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Klienti",
"resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.", "resourcesTableAndOnlyAccessibleInternally": "a jsou interně přístupné pouze v případě, že jsou propojeni s klientem.",
"resourcesTableNoTargets": "Žádné cíle", "resourcesTableNoTargets": "Žádné cíle",
@@ -2489,6 +2491,8 @@
"logIn": "Přihlásit se", "logIn": "Přihlásit se",
"deviceInformation": "Informace o zařízení", "deviceInformation": "Informace o zařízení",
"deviceInformationDescription": "Informace o zařízení a agentovi", "deviceInformationDescription": "Informace o zařízení a agentovi",
"deviceSecurity": "Zabezpečení zařízení",
"deviceSecurityDescription": "Informace o bezpečnostní pozici zařízení",
"platform": "Platforma", "platform": "Platforma",
"macosVersion": "macOS verze", "macosVersion": "macOS verze",
"windowsVersion": "Verze Windows", "windowsVersion": "Verze Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "První vidění", "firstSeen": "První vidění",
"lastSeen": "Naposledy viděno", "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í", "deviceSettingsDescription": "Zobrazit informace o zařízení a nastavení",
"devicePendingApprovalDescription": "Toto zařízení čeká na schválení", "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.", "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.", "resourcesTableNoInternalResourcesFound": "Keine internen Ressourcen gefunden.",
"resourcesTableDestination": "Ziel", "resourcesTableDestination": "Ziel",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.", "resourcesTableAndOnlyAccessibleInternally": "und sind nur intern zugänglich, wenn mit einem Client verbunden.",
"resourcesTableNoTargets": "Keine Ziele", "resourcesTableNoTargets": "Keine Ziele",
@@ -2489,6 +2491,8 @@
"logIn": "Anmelden", "logIn": "Anmelden",
"deviceInformation": "Geräteinformationen", "deviceInformation": "Geräteinformationen",
"deviceInformationDescription": "Informationen über das Gerät und den Agent", "deviceInformationDescription": "Informationen über das Gerät und den Agent",
"deviceSecurity": "Gerätesicherheit",
"deviceSecurityDescription": "Informationen zur Gerätesicherheit",
"platform": "Plattform", "platform": "Plattform",
"macosVersion": "macOS-Version", "macosVersion": "macOS-Version",
"windowsVersion": "Windows-Version", "windowsVersion": "Windows-Version",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "Erster Blick", "firstSeen": "Erster Blick",
"lastSeen": "Zuletzt gesehen", "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", "deviceSettingsDescription": "Geräteinformationen und -einstellungen anzeigen",
"devicePendingApprovalDescription": "Dieses Gerät wartet auf Freigabe", "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.", "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.", "resourcesTableNoInternalResourcesFound": "No internal resources found.",
"resourcesTableDestination": "Destination", "resourcesTableDestination": "Destination",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.", "resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
"resourcesTableNoTargets": "No targets", "resourcesTableNoTargets": "No targets",
@@ -2508,7 +2510,7 @@
"firewallEnabled": "Firewall Enabled", "firewallEnabled": "Firewall Enabled",
"autoUpdatesEnabled": "Auto Updates Enabled", "autoUpdatesEnabled": "Auto Updates Enabled",
"tpmAvailable": "TPM Available", "tpmAvailable": "TPM Available",
"windowsDefenderEnabled": "Windows Defender Enabled", "windowsAntivirusEnabled": "Antivirus Enabled",
"macosSipEnabled": "System Integrity Protection (SIP)", "macosSipEnabled": "System Integrity Protection (SIP)",
"macosGatekeeperEnabled": "Gatekeeper", "macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Firewall Stealth Mode", "macosFirewallStealthMode": "Firewall Stealth Mode",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.", "resourcesTableNoInternalResourcesFound": "No se encontraron recursos internos.",
"resourcesTableDestination": "Destino", "resourcesTableDestination": "Destino",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Clientes",
"resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.", "resourcesTableAndOnlyAccessibleInternally": "y solo son accesibles internamente cuando se conectan con un cliente.",
"resourcesTableNoTargets": "Sin objetivos", "resourcesTableNoTargets": "Sin objetivos",
@@ -2489,6 +2491,8 @@
"logIn": "Iniciar sesión", "logIn": "Iniciar sesión",
"deviceInformation": "Información del dispositivo", "deviceInformation": "Información del dispositivo",
"deviceInformationDescription": "Información sobre el dispositivo y el agente", "deviceInformationDescription": "Información sobre el dispositivo y el agente",
"deviceSecurity": "Seguridad del dispositivo",
"deviceSecurityDescription": "Información de postura de seguridad del dispositivo",
"platform": "Plataforma", "platform": "Plataforma",
"macosVersion": "versión macOS", "macosVersion": "versión macOS",
"windowsVersion": "Versión de Windows", "windowsVersion": "Versión de Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "Primer detectado", "firstSeen": "Primer detectado",
"lastSeen": "Último Visto", "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", "deviceSettingsDescription": "Ver información y ajustes del dispositivo",
"devicePendingApprovalDescription": "Este dispositivo está esperando su aprobación", "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.", "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.", "resourcesTableNoInternalResourcesFound": "Aucune ressource interne trouvée.",
"resourcesTableDestination": "Destination", "resourcesTableDestination": "Destination",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Clients",
"resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.", "resourcesTableAndOnlyAccessibleInternally": "et sont uniquement accessibles en interne lorsqu'elles sont connectées avec un client.",
"resourcesTableNoTargets": "Aucune cible", "resourcesTableNoTargets": "Aucune cible",
@@ -2489,6 +2491,8 @@
"logIn": "Se connecter", "logIn": "Se connecter",
"deviceInformation": "Informations sur l'appareil", "deviceInformation": "Informations sur l'appareil",
"deviceInformationDescription": "Informations sur l'appareil et l'agent", "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", "platform": "Plateforme",
"macosVersion": "Version macOS", "macosVersion": "Version macOS",
"windowsVersion": "Version de Windows", "windowsVersion": "Version de Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "Première vue", "firstSeen": "Première vue",
"lastSeen": "Derniè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", "deviceSettingsDescription": "Afficher les informations et les paramètres de l'appareil",
"devicePendingApprovalDescription": "Cet appareil est en attente d'approbation", "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é.", "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.", "resourcesTableNoInternalResourcesFound": "Nessuna risorsa interna trovata.",
"resourcesTableDestination": "Destinazione", "resourcesTableDestination": "Destinazione",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Client",
"resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.", "resourcesTableAndOnlyAccessibleInternally": "e sono accessibili solo internamente quando connessi con un client.",
"resourcesTableNoTargets": "Nessun obiettivo", "resourcesTableNoTargets": "Nessun obiettivo",
@@ -2489,6 +2491,8 @@
"logIn": "Log In", "logIn": "Log In",
"deviceInformation": "Informazioni Sul Dispositivo", "deviceInformation": "Informazioni Sul Dispositivo",
"deviceInformationDescription": "Informazioni sul dispositivo e sull'agente", "deviceInformationDescription": "Informazioni sul dispositivo e sull'agente",
"deviceSecurity": "Sicurezza Del Dispositivo",
"deviceSecurityDescription": "Informazioni postura sicurezza dispositivo",
"platform": "Piattaforma", "platform": "Piattaforma",
"macosVersion": "versione macOS", "macosVersion": "versione macOS",
"windowsVersion": "Versione Windows", "windowsVersion": "Versione Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "Prima Visto", "firstSeen": "Prima Visto",
"lastSeen": "Visto L'Ultima", "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", "deviceSettingsDescription": "Visualizza informazioni e impostazioni del dispositivo",
"devicePendingApprovalDescription": "Questo dispositivo è in attesa di approvazione", "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.", "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": "내부 리소스를 찾을 수 없습니다.", "resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.",
"resourcesTableDestination": "대상지", "resourcesTableDestination": "대상지",
"resourcesTableAlias": "별칭", "resourcesTableAlias": "별칭",
"resourcesTableAliasAddress": "별칭 주소",
"resourcesTableAliasAddressInfo": "이 주소는 조직의 유틸리티 서브넷의 일부로, 내부 DNS 해석을 사용하여 별칭 레코드를 해석하는 데 사용됩니다.",
"resourcesTableClients": "클라이언트", "resourcesTableClients": "클라이언트",
"resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.", "resourcesTableAndOnlyAccessibleInternally": "클라이언트와 연결되었을 때만 내부적으로 접근 가능합니다.",
"resourcesTableNoTargets": "대상 없음", "resourcesTableNoTargets": "대상 없음",
@@ -2489,6 +2491,8 @@
"logIn": "로그인", "logIn": "로그인",
"deviceInformation": "장치 정보", "deviceInformation": "장치 정보",
"deviceInformationDescription": "장치와 에이전트 정보", "deviceInformationDescription": "장치와 에이전트 정보",
"deviceSecurity": "디바이스 보안",
"deviceSecurityDescription": "디바이스 보안 상태 정보",
"platform": "플랫폼", "platform": "플랫폼",
"macosVersion": "macOS 버전", "macosVersion": "macOS 버전",
"windowsVersion": "Windows 버전", "windowsVersion": "Windows 버전",
@@ -2501,6 +2505,16 @@
"hostname": "호스트 이름", "hostname": "호스트 이름",
"firstSeen": "처음 발견됨", "firstSeen": "처음 발견됨",
"lastSeen": "마지막으로 발견됨", "lastSeen": "마지막으로 발견됨",
"biometricsEnabled": "생체 인식 활성화",
"diskEncrypted": "디스크 암호화됨",
"firewallEnabled": "방화벽 활성화",
"autoUpdatesEnabled": "자동 업데이트 활성화",
"tpmAvailable": "TPM 사용 가능",
"macosSipEnabled": "시스템 무결성 보호 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "방화벽 스텔스 모드",
"linuxAppArmorEnabled": "AppArmor",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "장치 정보 및 설정 보기", "deviceSettingsDescription": "장치 정보 및 설정 보기",
"devicePendingApprovalDescription": "이 장치는 승인을 기다리고 있습니다.", "devicePendingApprovalDescription": "이 장치는 승인을 기다리고 있습니다.",
"deviceBlockedDescription": "이 장치는 현재 차단되었습니다. 차단이 해제되지 않으면 리소스에 연결할 수 없습니다.", "deviceBlockedDescription": "이 장치는 현재 차단되었습니다. 차단이 해제되지 않으면 리소스에 연결할 수 없습니다.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.", "resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.",
"resourcesTableDestination": "Destinasjon", "resourcesTableDestination": "Destinasjon",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Klienter",
"resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.", "resourcesTableAndOnlyAccessibleInternally": "og er kun tilgjengelig internt når de er koblet til med en klient.",
"resourcesTableNoTargets": "Ingen mål", "resourcesTableNoTargets": "Ingen mål",
@@ -2489,6 +2491,8 @@
"logIn": "Logg inn", "logIn": "Logg inn",
"deviceInformation": "Enhetens informasjon", "deviceInformation": "Enhetens informasjon",
"deviceInformationDescription": "Informasjon om enheten og agenten", "deviceInformationDescription": "Informasjon om enheten og agenten",
"deviceSecurity": "Enhetens sikkerhet",
"deviceSecurityDescription": "Sikkerhetsstillings informasjon om utstyr",
"platform": "Plattform", "platform": "Plattform",
"macosVersion": "macOS versjon", "macosVersion": "macOS versjon",
"windowsVersion": "Windows versjon", "windowsVersion": "Windows versjon",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "Først sett", "firstSeen": "Først sett",
"lastSeen": "Sist 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", "deviceSettingsDescription": "Vis enhetsinformasjon og innstillinger",
"devicePendingApprovalDescription": "Denne enheten venter på godkjenning", "devicePendingApprovalDescription": "Denne enheten venter på godkjenning",
"deviceBlockedDescription": "Denne enheten er blokkert. Det kan ikke kobles til noen ressurser med mindre de ikke blir blokkert.", "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.", "resourcesTableNoInternalResourcesFound": "Geen interne bronnen gevonden.",
"resourcesTableDestination": "Bestemming", "resourcesTableDestination": "Bestemming",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Clienten",
"resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.", "resourcesTableAndOnlyAccessibleInternally": "en zijn alleen intern toegankelijk wanneer verbonden met een client.",
"resourcesTableNoTargets": "Geen doelen", "resourcesTableNoTargets": "Geen doelen",
@@ -2489,6 +2491,8 @@
"logIn": "Log in", "logIn": "Log in",
"deviceInformation": "Apparaat informatie", "deviceInformation": "Apparaat informatie",
"deviceInformationDescription": "Informatie over het apparaat en de agent", "deviceInformationDescription": "Informatie over het apparaat en de agent",
"deviceSecurity": "Apparaat beveiliging",
"deviceSecurityDescription": "Apparaat beveiligingsinformatie",
"platform": "Platform", "platform": "Platform",
"macosVersion": "macOS versie", "macosVersion": "macOS versie",
"windowsVersion": "Windows versie", "windowsVersion": "Windows versie",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "Eerst gezien", "firstSeen": "Eerst gezien",
"lastSeen": "Laatst gezien op", "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", "deviceSettingsDescription": "Apparaatinformatie en -instellingen bekijken",
"devicePendingApprovalDescription": "Dit apparaat wacht op goedkeuring", "devicePendingApprovalDescription": "Dit apparaat wacht op goedkeuring",
"deviceBlockedDescription": "Dit apparaat is momenteel geblokkeerd. Het kan geen verbinding maken met bronnen tenzij het wordt gedeblokkeerd.", "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.", "resourcesTableNoInternalResourcesFound": "Nie znaleziono wewnętrznych zasobów.",
"resourcesTableDestination": "Miejsce docelowe", "resourcesTableDestination": "Miejsce docelowe",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Klientami",
"resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.", "resourcesTableAndOnlyAccessibleInternally": "i są dostępne tylko wewnętrznie po połączeniu z klientem.",
"resourcesTableNoTargets": "Brak celów", "resourcesTableNoTargets": "Brak celów",
@@ -2489,6 +2491,8 @@
"logIn": "Zaloguj się", "logIn": "Zaloguj się",
"deviceInformation": "Informacje o urządzeniu", "deviceInformation": "Informacje o urządzeniu",
"deviceInformationDescription": "Informacje o urządzeniu i agentach", "deviceInformationDescription": "Informacje o urządzeniu i agentach",
"deviceSecurity": "Bezpieczeństwo urządzenia",
"deviceSecurityDescription": "Informacje o bezpieczeństwie urządzenia",
"platform": "Platforma", "platform": "Platforma",
"macosVersion": "Wersja macOS", "macosVersion": "Wersja macOS",
"windowsVersion": "Wersja Windows", "windowsVersion": "Wersja Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "Widziany po raz pierwszy", "firstSeen": "Widziany po raz pierwszy",
"lastSeen": "Ostatnio widziane", "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", "deviceSettingsDescription": "Wyświetl informacje o urządzeniu i ustawienia",
"devicePendingApprovalDescription": "To urządzenie czeka na zatwierdzenie", "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.", "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.", "resourcesTableNoInternalResourcesFound": "Nenhum recurso interno encontrado.",
"resourcesTableDestination": "Destino", "resourcesTableDestination": "Destino",
"resourcesTableAlias": "Alias", "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", "resourcesTableClients": "Clientes",
"resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.", "resourcesTableAndOnlyAccessibleInternally": "e são acessíveis apenas internamente quando conectados com um cliente.",
"resourcesTableNoTargets": "Nenhum alvo", "resourcesTableNoTargets": "Nenhum alvo",
@@ -2489,6 +2491,8 @@
"logIn": "Iniciar sessão", "logIn": "Iniciar sessão",
"deviceInformation": "Informações do dispositivo", "deviceInformation": "Informações do dispositivo",
"deviceInformationDescription": "Informações sobre o dispositivo e o agente", "deviceInformationDescription": "Informações sobre o dispositivo e o agente",
"deviceSecurity": "Segurança do dispositivo",
"deviceSecurityDescription": "Informações sobre postagem de segurança",
"platform": "Plataforma", "platform": "Plataforma",
"macosVersion": "Versão do macOS", "macosVersion": "Versão do macOS",
"windowsVersion": "Versão do Windows", "windowsVersion": "Versão do Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "Visto primeiro", "firstSeen": "Visto primeiro",
"lastSeen": "Visto por último", "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", "deviceSettingsDescription": "Ver informações e configurações do dispositivo",
"devicePendingApprovalDescription": "Este dispositivo está aguardando aprovação", "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.", "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": "Внутренних ресурсов не найдено.", "resourcesTableNoInternalResourcesFound": "Внутренних ресурсов не найдено.",
"resourcesTableDestination": "Пункт назначения", "resourcesTableDestination": "Пункт назначения",
"resourcesTableAlias": "Alias", "resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Псевдоним адреса",
"resourcesTableAliasAddressInfo": "Этот адрес является частью вспомогательной подсети организации. Он используется для разрешения псевдонимов с использованием внутреннего разрешения DNS.",
"resourcesTableClients": "Клиенты", "resourcesTableClients": "Клиенты",
"resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.", "resourcesTableAndOnlyAccessibleInternally": "и доступны только внутренне при подключении с клиентом.",
"resourcesTableNoTargets": "Нет ярлыков", "resourcesTableNoTargets": "Нет ярлыков",
@@ -2489,6 +2491,8 @@
"logIn": "Войти", "logIn": "Войти",
"deviceInformation": "Информация об устройстве", "deviceInformation": "Информация об устройстве",
"deviceInformationDescription": "Информация о устройстве и агенте", "deviceInformationDescription": "Информация о устройстве и агенте",
"deviceSecurity": "Безопасность устройства",
"deviceSecurityDescription": "Информация о позе безопасности устройства",
"platform": "Платформа", "platform": "Платформа",
"macosVersion": "Версия macOS", "macosVersion": "Версия macOS",
"windowsVersion": "Версия Windows", "windowsVersion": "Версия Windows",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "Первый раз виден", "firstSeen": "Первый раз виден",
"lastSeen": "Последнее посещение", "lastSeen": "Последнее посещение",
"biometricsEnabled": "Включены биометрические данные",
"diskEncrypted": "Диск зашифрован",
"firewallEnabled": "Брандмауэр включен",
"autoUpdatesEnabled": "Автоматические обновления включены",
"tpmAvailable": "Доступно TPM",
"macosSipEnabled": "Защита целостности системы (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "Стилс-режим брандмауэра",
"linuxAppArmorEnabled": "Броня",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "Просмотр информации и настроек устройства", "deviceSettingsDescription": "Просмотр информации и настроек устройства",
"devicePendingApprovalDescription": "Это устройство ожидает одобрения", "devicePendingApprovalDescription": "Это устройство ожидает одобрения",
"deviceBlockedDescription": "Это устройство заблокировано. Оно не сможет подключаться к ресурсам, если не разблокировано.", "deviceBlockedDescription": "Это устройство заблокировано. Оно не сможет подключаться к ресурсам, если не разблокировано.",
+14
View File
@@ -1625,6 +1625,8 @@
"resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.", "resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.",
"resourcesTableDestination": "Hedef", "resourcesTableDestination": "Hedef",
"resourcesTableAlias": "Takma Ad", "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", "resourcesTableClients": "İstemciler",
"resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.", "resourcesTableAndOnlyAccessibleInternally": "veyalnızca bir istemci ile bağlandığında dahili olarak erişilebilir.",
"resourcesTableNoTargets": "Hedef yok", "resourcesTableNoTargets": "Hedef yok",
@@ -2489,6 +2491,8 @@
"logIn": "Giriş Yap", "logIn": "Giriş Yap",
"deviceInformation": "Cihaz Bilgisi", "deviceInformation": "Cihaz Bilgisi",
"deviceInformationDescription": "Cihaz ve temsilci hakkında bilgi", "deviceInformationDescription": "Cihaz ve temsilci hakkında bilgi",
"deviceSecurity": "Cihaz Güvenliği",
"deviceSecurityDescription": "Cihaz güvenliği durumu bilgisi",
"platform": "Platform", "platform": "Platform",
"macosVersion": "macOS Sürümü", "macosVersion": "macOS Sürümü",
"windowsVersion": "Windows Sürümü", "windowsVersion": "Windows Sürümü",
@@ -2501,6 +2505,16 @@
"hostname": "Ana Makine Adı", "hostname": "Ana Makine Adı",
"firstSeen": "İlk Görüldü", "firstSeen": "İlk Görüldü",
"lastSeen": "Son 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", "deviceSettingsDescription": "Cihaz bilgilerini ve ayarlarını görüntüleyin",
"devicePendingApprovalDescription": "Bu cihaz onay bekliyor", "devicePendingApprovalDescription": "Bu cihaz onay bekliyor",
"deviceBlockedDescription": "Bu cihaz şu anda engellidir. Engeli kaldırılmadığı sürece hiçbir kaynağa bağlanamayacaktır.", "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": "未找到内部资源。", "resourcesTableNoInternalResourcesFound": "未找到内部资源。",
"resourcesTableDestination": "目标", "resourcesTableDestination": "目标",
"resourcesTableAlias": "Alias", "resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "别名地址",
"resourcesTableAliasAddressInfo": "此地址是组织实用子网的一部分。它用来使用内部DNS解析来解析别名记录。",
"resourcesTableClients": "客户端", "resourcesTableClients": "客户端",
"resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。", "resourcesTableAndOnlyAccessibleInternally": "且仅在与客户端连接时可内部访问。",
"resourcesTableNoTargets": "没有目标", "resourcesTableNoTargets": "没有目标",
@@ -2489,6 +2491,8 @@
"logIn": "登录", "logIn": "登录",
"deviceInformation": "设备信息", "deviceInformation": "设备信息",
"deviceInformationDescription": "关于设备和代理的信息", "deviceInformationDescription": "关于设备和代理的信息",
"deviceSecurity": "设备安全",
"deviceSecurityDescription": "设备安全态势信息",
"platform": "平台", "platform": "平台",
"macosVersion": "macOS 版本", "macosVersion": "macOS 版本",
"windowsVersion": "Windows 版本", "windowsVersion": "Windows 版本",
@@ -2501,6 +2505,16 @@
"hostname": "Hostname", "hostname": "Hostname",
"firstSeen": "第一次查看", "firstSeen": "第一次查看",
"lastSeen": "上次查看时间", "lastSeen": "上次查看时间",
"biometricsEnabled": "生物计已启用",
"diskEncrypted": "磁盘加密",
"firewallEnabled": "防火墙已启用",
"autoUpdatesEnabled": "启用自动更新",
"tpmAvailable": "TPM 可用",
"macosSipEnabled": "系统完整性保护 (SIP)",
"macosGatekeeperEnabled": "Gatekeeper",
"macosFirewallStealthMode": "防火墙隐形模式",
"linuxAppArmorEnabled": "AppArmor",
"linuxSELinuxEnabled": "SELinux",
"deviceSettingsDescription": "查看设备信息和设置", "deviceSettingsDescription": "查看设备信息和设置",
"devicePendingApprovalDescription": "此设备正在等待批准", "devicePendingApprovalDescription": "此设备正在等待批准",
"deviceBlockedDescription": "此设备目前已被屏蔽。除非解除屏蔽,否则无法连接到任何资源。", "deviceBlockedDescription": "此设备目前已被屏蔽。除非解除屏蔽,否则无法连接到任何资源。",
+319 -22
View File
@@ -1,5 +1,7 @@
{ {
"setupCreate": "創建您的第一個組織、網站和資源", "setupCreate": "創建您的第一個組織、網站和資源",
"headerAuthCompatibilityInfo": "啟用此選項以在缺少驗證令牌時強制回傳 401 未授權回應。這對於不會在沒有伺服器挑戰的情況下發送憑證的瀏覽器或特定 HTTP 函式庫是必需的。",
"headerAuthCompatibility": "擴展相容性",
"setupNewOrg": "新建組織", "setupNewOrg": "新建組織",
"setupCreateOrg": "創建組織", "setupCreateOrg": "創建組織",
"setupCreateResources": "創建資源", "setupCreateResources": "創建資源",
@@ -13,7 +15,7 @@
"welcome": "歡迎使用 Pangolin", "welcome": "歡迎使用 Pangolin",
"welcomeTo": "歡迎來到", "welcomeTo": "歡迎來到",
"componentsCreateOrg": "創建組織", "componentsCreateOrg": "創建組織",
"componentsMember": "您屬於{count, plural, =0 {沒有組織} one {一個組織} other {# 個組織}}。", "componentsMember": "您屬於 {count, plural, =0 {沒有組織} one {一個組織} other {# 個組織}}。",
"componentsInvalidKey": "檢測到無效或過期的許可證金鑰。按照許可證條款操作以繼續使用所有功能。", "componentsInvalidKey": "檢測到無效或過期的許可證金鑰。按照許可證條款操作以繼續使用所有功能。",
"dismiss": "忽略", "dismiss": "忽略",
"componentsLicenseViolation": "許可證超限:該伺服器使用了 {usedSites} 個站點,已超過授權的 {maxSites} 個。請遵守許可證條款以繼續使用全部功能。", "componentsLicenseViolation": "許可證超限:該伺服器使用了 {usedSites} 個站點,已超過授權的 {maxSites} 個。請遵守許可證條款以繼續使用全部功能。",
@@ -51,6 +53,9 @@
"siteQuestionRemove": "您確定要從組織中刪除該站點嗎?", "siteQuestionRemove": "您確定要從組織中刪除該站點嗎?",
"siteManageSites": "管理站點", "siteManageSites": "管理站點",
"siteDescription": "允許通過安全隧道連接到您的網路", "siteDescription": "允許通過安全隧道連接到您的網路",
"sitesBannerTitle": "連接任何網路",
"sitesBannerDescription": "站點是與遠端網路的連接,使 Pangolin 能夠為任何地方的使用者提供對公共或私有資源的存取。在任何可以執行二進位檔案或容器的地方安裝站點網路連接器 (Newt) 以建立連接。",
"sitesBannerButtonText": "安裝站點",
"siteCreate": "創建站點", "siteCreate": "創建站點",
"siteCreateDescription2": "按照下面的步驟創建和連接一個新站點", "siteCreateDescription2": "按照下面的步驟創建和連接一個新站點",
"siteCreateDescription": "創建一個新站點開始連接您的資源", "siteCreateDescription": "創建一個新站點開始連接您的資源",
@@ -65,8 +70,8 @@
"siteLoadWGConfig": "正在載入 WireGuard 配置...", "siteLoadWGConfig": "正在載入 WireGuard 配置...",
"siteDocker": "擴展 Docker 部署詳細資訊", "siteDocker": "擴展 Docker 部署詳細資訊",
"toggle": "切換", "toggle": "切換",
"dockerCompose": "Docker 配置", "dockerCompose": "Docker Compose",
"dockerRun": "停靠欄", "dockerRun": "Docker Run",
"siteLearnLocal": "本地站點不需要隧道連接,點擊了解更多", "siteLearnLocal": "本地站點不需要隧道連接,點擊了解更多",
"siteConfirmCopy": "我已經複製了配置資訊", "siteConfirmCopy": "我已經複製了配置資訊",
"searchSitesProgress": "搜索站點...", "searchSitesProgress": "搜索站點...",
@@ -93,14 +98,15 @@
"siteNewtTunnelDescription": "最簡單的方式來連接到您的網路。不需要任何額外設置。", "siteNewtTunnelDescription": "最簡單的方式來連接到您的網路。不需要任何額外設置。",
"siteWg": "基本 WireGuard", "siteWg": "基本 WireGuard",
"siteWgDescription": "使用任何 WireGuard 用戶端來建立隧道。需要手動配置 NAT。", "siteWgDescription": "使用任何 WireGuard 用戶端來建立隧道。需要手動配置 NAT。",
"siteWgDescriptionSaas": "使用任何WireGuard用戶端建立隧道。需要手動配置NAT。僅適用於自託管節點。", "siteWgDescriptionSaas": "使用任何 WireGuard 用戶端建立隧道。需要手動配置 NAT。僅適用於自託管節點。",
"siteLocalDescription": "僅限本地資源。不需要隧道。", "siteLocalDescription": "僅限本地資源。不需要隧道。",
"siteLocalDescriptionSaas": "僅本地資源。沒有隧道。僅在遠程節點上可用。", "siteLocalDescriptionSaas": "僅本地資源。沒有隧道。僅在遠程節點上可用。",
"siteSeeAll": "查看所有站點", "siteSeeAll": "查看所有站點",
"siteTunnelDescription": "確定如何連接到您的網站", "siteTunnelDescription": "確定如何連接到您的網站",
"siteNewtCredentials": "Newt 憑", "siteNewtCredentials": "Newt 憑",
"siteNewtCredentialsDescription": "這是 Newt 伺服器的身份驗證憑", "siteNewtCredentialsDescription": "這是 Newt 伺服器的身份驗證憑",
"siteCredentialsSave": "保存您的憑據", "remoteNodeCredentialsDescription": "這是遠端節點與伺服器進行驗證的方式",
"siteCredentialsSave": "保存您的憑證",
"siteCredentialsSaveDescription": "您只能看到一次。請確保將其複製並保存到一個安全的地方。", "siteCredentialsSaveDescription": "您只能看到一次。請確保將其複製並保存到一個安全的地方。",
"siteInfo": "站點資訊", "siteInfo": "站點資訊",
"status": "狀態", "status": "狀態",
@@ -144,8 +150,14 @@
"expires": "過期時間", "expires": "過期時間",
"never": "永不過期", "never": "永不過期",
"shareErrorSelectResource": "請選擇一個資源", "shareErrorSelectResource": "請選擇一個資源",
"resourceTitle": "管理資源", "proxyResourceTitle": "管理公開資源",
"resourceDescription": "為您的私人應用程式創建安全代理", "proxyResourceDescription": "建立和管理可透過網頁瀏覽器公開存取的資源",
"proxyResourcesBannerTitle": "基於網頁的公開存取",
"proxyResourcesBannerDescription": "公開資源是任何人都可以透過網頁瀏覽器存取的 HTTPS 或 TCP/UDP 代理。與私有資源不同,它們不需要客戶端軟體,並且可以包含基於身份和情境感知的存取策略。",
"clientResourceTitle": "管理私有資源",
"clientResourceDescription": "建立和管理只能透過已連接的客戶端存取的資源",
"privateResourcesBannerTitle": "零信任私有存取",
"privateResourcesBannerDescription": "私有資源使用零信任安全性,確保使用者和機器只能存取您明確授權的資源。連接使用者裝置或機器客戶端以透過安全的虛擬私人網路存取這些資源。",
"resourcesSearch": "搜索資源...", "resourcesSearch": "搜索資源...",
"resourceAdd": "添加資源", "resourceAdd": "添加資源",
"resourceErrorDelte": "刪除資源時出錯", "resourceErrorDelte": "刪除資源時出錯",
@@ -179,7 +191,7 @@
"baseDomain": "根域名", "baseDomain": "根域名",
"subdomnainDescription": "您的資源可以訪問的子域名。", "subdomnainDescription": "您的資源可以訪問的子域名。",
"resourceRawSettings": "TCP/UDP 設置", "resourceRawSettings": "TCP/UDP 設置",
"resourceRawSettingsDescription": "配置如何過 TCP/UDP 訪問您的資源。 您映射資源到主機Pangolin伺服器上的埠,這樣您就可以訪問伺服器-公共-ip:mapped埠的資源", "resourceRawSettingsDescription": "設定如何過 TCP/UDP 存取資源",
"protocol": "協議", "protocol": "協議",
"protocolSelect": "選擇協議", "protocolSelect": "選擇協議",
"resourcePortNumber": "埠號", "resourcePortNumber": "埠號",
@@ -436,6 +448,16 @@
"inviteEmailSent": "發送邀請郵件給用戶", "inviteEmailSent": "發送邀請郵件給用戶",
"inviteValid": "有效", "inviteValid": "有效",
"selectDuration": "選擇持續時間", "selectDuration": "選擇持續時間",
"selectResource": "選擇資源",
"filterByResource": "依資源篩選",
"resetFilters": "重設篩選條件",
"totalBlocked": "被 Pangolin 阻擋的請求",
"totalRequests": "總請求數",
"requestsByCountry": "依國家/地區的請求",
"requestsByDay": "依日期的請求",
"blocked": "已阻擋",
"allowed": "已允許",
"topCountries": "熱門國家/地區",
"accessRoleSelect": "選擇角色", "accessRoleSelect": "選擇角色",
"inviteEmailSentDescription": "一封電子郵件已經發送給用戶,帶有下面的訪問連結。他們必須訪問該連結才能接受邀請。", "inviteEmailSentDescription": "一封電子郵件已經發送給用戶,帶有下面的訪問連結。他們必須訪問該連結才能接受邀請。",
"inviteSentDescription": "用戶已被邀請。他們必須訪問下面的連結才能接受邀請。", "inviteSentDescription": "用戶已被邀請。他們必須訪問下面的連結才能接受邀請。",
@@ -465,7 +487,7 @@
"proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。", "proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。",
"proxyEnableSSL": "啟用 SSL", "proxyEnableSSL": "啟用 SSL",
"proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。", "proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。",
"target": "Target", "target": "目標",
"configureTarget": "配置目標", "configureTarget": "配置目標",
"targetErrorFetch": "獲取目標失敗", "targetErrorFetch": "獲取目標失敗",
"targetErrorFetchDescription": "獲取目標時出錯", "targetErrorFetchDescription": "獲取目標時出錯",
@@ -516,6 +538,8 @@
"targetCreatedDescription": "目標已成功創建", "targetCreatedDescription": "目標已成功創建",
"targetErrorCreate": "創建目標失敗", "targetErrorCreate": "創建目標失敗",
"targetErrorCreateDescription": "創建目標時出錯", "targetErrorCreateDescription": "創建目標時出錯",
"tlsServerName": "TLS 伺服器名稱",
"tlsServerNameDescription": "用於 SNI 的 TLS 伺服器名稱",
"save": "保存", "save": "保存",
"proxyAdditional": "附加代理設置", "proxyAdditional": "附加代理設置",
"proxyAdditionalDescription": "配置你的資源如何處理代理設置", "proxyAdditionalDescription": "配置你的資源如何處理代理設置",
@@ -702,6 +726,7 @@
"resourceTransferSubmit": "轉移資源", "resourceTransferSubmit": "轉移資源",
"siteDestination": "目標站點", "siteDestination": "目標站點",
"searchSites": "搜索站點", "searchSites": "搜索站點",
"countries": "國家/地區",
"accessRoleCreate": "創建角色", "accessRoleCreate": "創建角色",
"accessRoleCreateDescription": "創建一個新角色來分組用戶並管理他們的權限。", "accessRoleCreateDescription": "創建一個新角色來分組用戶並管理他們的權限。",
"accessRoleCreateSubmit": "創建角色", "accessRoleCreateSubmit": "創建角色",
@@ -825,6 +850,7 @@
"orgPolicyConfig": "配置組織訪問權限", "orgPolicyConfig": "配置組織訪問權限",
"idpUpdatedDescription": "身份提供商更新成功", "idpUpdatedDescription": "身份提供商更新成功",
"redirectUrl": "重定向網址", "redirectUrl": "重定向網址",
"orgIdpRedirectUrls": "重新導向網址",
"redirectUrlAbout": "關於重定向網址", "redirectUrlAbout": "關於重定向網址",
"redirectUrlAboutDescription": "這是用戶在驗證後將被重定向到的URL。您需要在身份提供商設置中配置此URL。", "redirectUrlAboutDescription": "這是用戶在驗證後將被重定向到的URL。您需要在身份提供商設置中配置此URL。",
"pangolinAuth": "認證 - Pangolin", "pangolinAuth": "認證 - Pangolin",
@@ -909,6 +935,10 @@
"passwordResetSent": "我們將發送一個驗證碼到這個電子郵件地址。", "passwordResetSent": "我們將發送一個驗證碼到這個電子郵件地址。",
"passwordResetCode": "驗證碼", "passwordResetCode": "驗證碼",
"passwordResetCodeDescription": "請檢查您的電子郵件以獲取驗證碼。", "passwordResetCodeDescription": "請檢查您的電子郵件以獲取驗證碼。",
"generatePasswordResetCode": "產生密碼重設代碼",
"passwordResetCodeGenerated": "密碼重設代碼已產生",
"passwordResetCodeGeneratedDescription": "請將此代碼分享給使用者。他們可以用它來重設密碼。",
"passwordResetUrl": "重設網址",
"passwordNew": "新密碼", "passwordNew": "新密碼",
"passwordNewConfirm": "確認新密碼", "passwordNewConfirm": "確認新密碼",
"changePassword": "更改密碼", "changePassword": "更改密碼",
@@ -926,6 +956,9 @@
"pincodeAuth": "驗證器代碼", "pincodeAuth": "驗證器代碼",
"pincodeSubmit2": "提交代碼", "pincodeSubmit2": "提交代碼",
"passwordResetSubmit": "請求重設", "passwordResetSubmit": "請求重設",
"passwordResetAlreadyHaveCode": "輸入代碼",
"passwordResetSmtpRequired": "請聯絡您的管理員",
"passwordResetSmtpRequiredDescription": "需要密碼重設代碼才能重設您的密碼。請聯絡您的管理員尋求協助。",
"passwordBack": "回到密碼", "passwordBack": "回到密碼",
"loginBack": "返回登錄", "loginBack": "返回登錄",
"signup": "註冊", "signup": "註冊",
@@ -1013,6 +1046,7 @@
"updateOrgUser": "更新組織用戶", "updateOrgUser": "更新組織用戶",
"createOrgUser": "創建組織用戶", "createOrgUser": "創建組織用戶",
"actionUpdateOrg": "更新組織", "actionUpdateOrg": "更新組織",
"actionRemoveInvitation": "移除邀請",
"actionUpdateUser": "更新用戶", "actionUpdateUser": "更新用戶",
"actionGetUser": "獲取用戶", "actionGetUser": "獲取用戶",
"actionGetOrgUser": "獲取組織用戶", "actionGetOrgUser": "獲取組織用戶",
@@ -1093,12 +1127,15 @@
"actionListSiteResources": "列出站點資源", "actionListSiteResources": "列出站點資源",
"actionUpdateSiteResource": "更新站點資源", "actionUpdateSiteResource": "更新站點資源",
"actionListInvitations": "邀請列表", "actionListInvitations": "邀請列表",
"actionExportLogs": "匯出日誌",
"actionViewLogs": "查看日誌",
"noneSelected": "未選擇", "noneSelected": "未選擇",
"orgNotFound2": "未找到組織。", "orgNotFound2": "未找到組織。",
"searchProgress": "搜索中...", "searchProgress": "搜索中...",
"create": "創建", "create": "創建",
"orgs": "組織", "orgs": "組織",
"loginError": "登錄時出錯", "loginError": "登錄時出錯",
"loginRequiredForDevice": "需要登入以驗證您的裝置。",
"passwordForgot": "忘記密碼?", "passwordForgot": "忘記密碼?",
"otpAuth": "兩步驗證", "otpAuth": "兩步驗證",
"otpAuthDescription": "從您的身份驗證程序中輸入代碼或您的單次備份代碼。", "otpAuthDescription": "從您的身份驗證程序中輸入代碼或您的單次備份代碼。",
@@ -1153,8 +1190,12 @@
"sidebarHome": "首頁", "sidebarHome": "首頁",
"sidebarSites": "站點", "sidebarSites": "站點",
"sidebarResources": "資源", "sidebarResources": "資源",
"sidebarProxyResources": "公開",
"sidebarClientResources": "私有",
"sidebarAccessControl": "訪問控制", "sidebarAccessControl": "訪問控制",
"sidebarLogsAndAnalytics": "日誌與分析",
"sidebarUsers": "用戶", "sidebarUsers": "用戶",
"sidebarAdmin": "管理員",
"sidebarInvitations": "邀請", "sidebarInvitations": "邀請",
"sidebarRoles": "角色", "sidebarRoles": "角色",
"sidebarShareableLinks": "分享連結", "sidebarShareableLinks": "分享連結",
@@ -1164,8 +1205,14 @@
"sidebarIdentityProviders": "身份提供商", "sidebarIdentityProviders": "身份提供商",
"sidebarLicense": "證書", "sidebarLicense": "證書",
"sidebarClients": "用戶端", "sidebarClients": "用戶端",
"sidebarUserDevices": "使用者",
"sidebarMachineClients": "機器",
"sidebarDomains": "域", "sidebarDomains": "域",
"sidebarGeneral": "管理",
"sidebarLogAndAnalytics": "日誌與分析",
"sidebarBluePrints": "藍圖", "sidebarBluePrints": "藍圖",
"sidebarOrganization": "組織",
"sidebarLogsAnalytics": "分析",
"blueprints": "藍圖", "blueprints": "藍圖",
"blueprintsDescription": "應用聲明配置並查看先前運行的", "blueprintsDescription": "應用聲明配置並查看先前運行的",
"blueprintAdd": "添加藍圖", "blueprintAdd": "添加藍圖",
@@ -1275,12 +1322,24 @@
"accountSetupSuccess": "帳號設定完成!歡迎來到 Pangolin", "accountSetupSuccess": "帳號設定完成!歡迎來到 Pangolin",
"documentation": "文件", "documentation": "文件",
"saveAllSettings": "保存所有設置", "saveAllSettings": "保存所有設置",
"saveResourceTargets": "儲存目標",
"saveResourceHttp": "儲存代理設定",
"saveProxyProtocol": "儲存代理協定設定",
"settingsUpdated": "設置已更新", "settingsUpdated": "設置已更新",
"settingsUpdatedDescription": "所有設置已成功更新", "settingsUpdatedDescription": "所有設置已成功更新",
"settingsErrorUpdate": "設置更新失敗", "settingsErrorUpdate": "設置更新失敗",
"settingsErrorUpdateDescription": "更新設置時發生錯誤", "settingsErrorUpdateDescription": "更新設置時發生錯誤",
"sidebarCollapse": "摺疊", "sidebarCollapse": "摺疊",
"sidebarExpand": "展開", "sidebarExpand": "展開",
"productUpdateMoreInfo": "還有 {noOfUpdates} 項更新",
"productUpdateInfo": "{noOfUpdates} 項更新",
"productUpdateWhatsNew": "新功能",
"productUpdateTitle": "產品更新",
"productUpdateEmpty": "沒有更新",
"dismissAll": "全部關閉",
"pangolinUpdateAvailable": "有可用更新",
"pangolinUpdateAvailableInfo": "版本 {version} 已準備好安裝",
"pangolinUpdateAvailableReleaseNotes": "查看發行說明",
"newtUpdateAvailable": "更新可用", "newtUpdateAvailable": "更新可用",
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。請更新到最新版本以獲得最佳體驗。", "newtUpdateAvailableInfo": "新版本的 Newt 已可用。請更新到最新版本以獲得最佳體驗。",
"domainPickerEnterDomain": "域名", "domainPickerEnterDomain": "域名",
@@ -1423,6 +1482,9 @@
"and": "和", "and": "和",
"privacyPolicy": "隱私政策" "privacyPolicy": "隱私政策"
}, },
"signUpMarketing": {
"keepMeInTheLoop": "透過電子郵件接收新聞、更新和新功能通知。"
},
"siteRequired": "需要站點。", "siteRequired": "需要站點。",
"olmTunnel": "Olm 隧道", "olmTunnel": "Olm 隧道",
"olmTunnelDescription": "使用 Olm 進行用戶端連接", "olmTunnelDescription": "使用 Olm 進行用戶端連接",
@@ -1456,15 +1518,14 @@
"sitesFetchError": "獲取站點時出錯。", "sitesFetchError": "獲取站點時出錯。",
"olmErrorFetchReleases": "獲取 Olm 發布版本時出錯。", "olmErrorFetchReleases": "獲取 Olm 發布版本時出錯。",
"olmErrorFetchLatest": "獲取最新 Olm 發布版本時出錯。", "olmErrorFetchLatest": "獲取最新 Olm 發布版本時出錯。",
"remoteSubnets": "遠程子網",
"enterCidrRange": "輸入 CIDR 範圍", "enterCidrRange": "輸入 CIDR 範圍",
"remoteSubnetsDescription": "添加可以通過用戶端遠端存取該站點的 CIDR 範圍。使用類似 10.0.0.0/24 的格式。這僅適用於 VPN 用戶端連接。",
"resourceEnableProxy": "啟用公共代理", "resourceEnableProxy": "啟用公共代理",
"resourceEnableProxyDescription": "啟用到此資源的公共代理。這允許外部網路通過開放埠訪問資源。需要 Traefik 配置。", "resourceEnableProxyDescription": "啟用到此資源的公共代理。這允許外部網路通過開放埠訪問資源。需要 Traefik 配置。",
"externalProxyEnabled": "外部代理已啟用", "externalProxyEnabled": "外部代理已啟用",
"addNewTarget": "添加新目標", "addNewTarget": "添加新目標",
"targetsList": "目標列表", "targetsList": "目標列表",
"advancedMode": "高級模式", "advancedMode": "高級模式",
"advancedSettings": "進階設定",
"targetErrorDuplicateTargetFound": "找到重複的目標", "targetErrorDuplicateTargetFound": "找到重複的目標",
"healthCheckHealthy": "正常", "healthCheckHealthy": "正常",
"healthCheckUnhealthy": "不正常", "healthCheckUnhealthy": "不正常",
@@ -1476,6 +1537,7 @@
"enableHealthChecksDescription": "監視此目標的健康狀況。如果需要,您可以監視一個不同的終點。", "enableHealthChecksDescription": "監視此目標的健康狀況。如果需要,您可以監視一個不同的終點。",
"healthScheme": "方法", "healthScheme": "方法",
"healthSelectScheme": "選擇方法", "healthSelectScheme": "選擇方法",
"healthCheckPortInvalid": "健康檢查連接埠必須介於 1 到 65535 之間",
"healthCheckPath": "路徑", "healthCheckPath": "路徑",
"healthHostname": "IP / 主機", "healthHostname": "IP / 主機",
"healthPort": "埠", "healthPort": "埠",
@@ -1524,9 +1586,15 @@
"resourcesTableNoProxyResourcesFound": "未找到代理資源。", "resourcesTableNoProxyResourcesFound": "未找到代理資源。",
"resourcesTableNoInternalResourcesFound": "未找到內部資源。", "resourcesTableNoInternalResourcesFound": "未找到內部資源。",
"resourcesTableDestination": "目標", "resourcesTableDestination": "目標",
"resourcesTableTheseResourcesForUseWith": "這些資源供...使用", "resourcesTableAlias": "別名",
"resourcesTableClients": "用戶端", "resourcesTableClients": "用戶端",
"resourcesTableAndOnlyAccessibleInternally": "且僅在與用戶端連接時可內部訪問。", "resourcesTableAndOnlyAccessibleInternally": "且僅在與用戶端連接時可內部訪問。",
"resourcesTableNoTargets": "無目標",
"resourcesTableHealthy": "健康",
"resourcesTableDegraded": "降級",
"resourcesTableOffline": "離線",
"resourcesTableUnknown": "未知",
"resourcesTableNotMonitored": "未監控",
"editInternalResourceDialogEditClientResource": "編輯用戶端資源", "editInternalResourceDialogEditClientResource": "編輯用戶端資源",
"editInternalResourceDialogUpdateResourceProperties": "更新 {resourceName} 的資源屬性和目標配置。", "editInternalResourceDialogUpdateResourceProperties": "更新 {resourceName} 的資源屬性和目標配置。",
"editInternalResourceDialogResourceProperties": "資源屬性", "editInternalResourceDialogResourceProperties": "資源屬性",
@@ -1547,6 +1615,17 @@
"editInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式", "editInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
"editInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1", "editInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
"editInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536", "editInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
"editInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠",
"editInternalResourceDialogMode": "模式",
"editInternalResourceDialogModePort": "連接埠",
"editInternalResourceDialogModeHost": "主機",
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogDestination": "目的地",
"editInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。",
"editInternalResourceDialogDestinationIPDescription": "站點網路上資源的 IP 或主機名稱位址。",
"editInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。",
"editInternalResourceDialogAlias": "別名",
"editInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。",
"createInternalResourceDialogNoSitesAvailable": "暫無可用站點", "createInternalResourceDialogNoSitesAvailable": "暫無可用站點",
"createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一個子網的 Newt 站點來創建內部資源。", "createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一個子網的 Newt 站點來創建內部資源。",
"createInternalResourceDialogClose": "關閉", "createInternalResourceDialogClose": "關閉",
@@ -1555,9 +1634,8 @@
"createInternalResourceDialogResourceProperties": "資源屬性", "createInternalResourceDialogResourceProperties": "資源屬性",
"createInternalResourceDialogName": "名稱", "createInternalResourceDialogName": "名稱",
"createInternalResourceDialogSite": "站點", "createInternalResourceDialogSite": "站點",
"createInternalResourceDialogSelectSite": "選擇站點...", "selectSite": "選擇站點...",
"createInternalResourceDialogSearchSites": "搜索站點...", "noSitesFound": "找不到站點。",
"createInternalResourceDialogNoSitesFound": "未找到站點。",
"createInternalResourceDialogProtocol": "協議", "createInternalResourceDialogProtocol": "協議",
"createInternalResourceDialogTcp": "TCP", "createInternalResourceDialogTcp": "TCP",
"createInternalResourceDialogUdp": "UDP", "createInternalResourceDialogUdp": "UDP",
@@ -1580,11 +1658,22 @@
"createInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式", "createInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
"createInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1", "createInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
"createInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536", "createInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
"createInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠",
"createInternalResourceDialogMode": "模式",
"createInternalResourceDialogModePort": "連接埠",
"createInternalResourceDialogModeHost": "主機",
"createInternalResourceDialogModeCidr": "CIDR",
"createInternalResourceDialogDestination": "目的地",
"createInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。",
"createInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。",
"createInternalResourceDialogAlias": "別名",
"createInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。",
"siteConfiguration": "配置", "siteConfiguration": "配置",
"siteAcceptClientConnections": "接受用戶端連接", "siteAcceptClientConnections": "接受用戶端連接",
"siteAcceptClientConnectionsDescription": "允許其他設備透過此 Newt 實例使用用戶端作為閘道器連接。", "siteAcceptClientConnectionsDescription": "允許其他設備透過此 Newt 實例使用用戶端作為閘道器連接。",
"siteAddress": "站點地址", "siteAddress": "站點地址",
"siteAddressDescription": "指定主機的 IP 位址以供用戶端連接。這是 Pangolin 網路中站點的內部地址,供用戶端訪問。必須在 Org 子網內。", "siteAddressDescription": "指定主機的 IP 位址以供用戶端連接。這是 Pangolin 網路中站點的內部地址,供用戶端訪問。必須在 Org 子網內。",
"siteNameDescription": "站點的顯示名稱,可以稍後更改。",
"autoLoginExternalIdp": "自動使用外部 IDP 登錄", "autoLoginExternalIdp": "自動使用外部 IDP 登錄",
"autoLoginExternalIdpDescription": "立即將用戶重定向到外部 IDP 進行身份驗證。", "autoLoginExternalIdpDescription": "立即將用戶重定向到外部 IDP 進行身份驗證。",
"selectIdp": "選擇 IDP", "selectIdp": "選擇 IDP",
@@ -1608,6 +1697,8 @@
"remoteExitNodeConfirmDelete": "確認刪除節點", "remoteExitNodeConfirmDelete": "確認刪除節點",
"remoteExitNodeDelete": "刪除節點", "remoteExitNodeDelete": "刪除節點",
"sidebarRemoteExitNodes": "遠程節點", "sidebarRemoteExitNodes": "遠程節點",
"remoteExitNodeId": "ID",
"remoteExitNodeSecretKey": "密鑰",
"remoteExitNodeCreate": { "remoteExitNodeCreate": {
"title": "創建節點", "title": "創建節點",
"description": "創建一個新節點來擴展您的網路連接", "description": "創建一個新節點來擴展您的網路連接",
@@ -1731,12 +1822,33 @@
"idpAzureClientIdDescription2": "您的 Azure 應用程式註冊用戶端 ID", "idpAzureClientIdDescription2": "您的 Azure 應用程式註冊用戶端 ID",
"idpAzureClientSecretDescription2": "您的 Azure 應用程式註冊用戶端金鑰", "idpAzureClientSecretDescription2": "您的 Azure 應用程式註冊用戶端金鑰",
"idpGoogleDescription": "Google OAuth2/OIDC 提供商", "idpGoogleDescription": "Google OAuth2/OIDC 提供商",
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider", "idpAzureDescription": "Microsoft Azure OAuth2/OIDC 提供者",
"subnet": "子網", "subnet": "子網",
"subnetDescription": "此組織網路配置的子網。", "subnetDescription": "此組織網路配置的子網。",
"customDomain": "自訂網域",
"authPage": "認證頁面", "authPage": "認證頁面",
"authPageDescription": "配置您的組織認證頁面", "authPageDescription": "配置您的組織認證頁面",
"authPageDomain": "認證頁面域", "authPageDomain": "認證頁面域",
"authPageBranding": "自訂品牌",
"authPageBrandingDescription": "設定此組織驗證頁面上顯示的品牌",
"authPageBrandingUpdated": "驗證頁面品牌更新成功",
"authPageBrandingRemoved": "驗證頁面品牌移除成功",
"authPageBrandingRemoveTitle": "移除驗證頁面品牌",
"authPageBrandingQuestionRemove": "您確定要移除驗證頁面的品牌嗎?",
"authPageBrandingDeleteConfirm": "確認刪除品牌",
"brandingLogoURL": "Logo 網址",
"brandingPrimaryColor": "主要顏色",
"brandingLogoWidth": "寬度 (px)",
"brandingLogoHeight": "高度 (px)",
"brandingOrgTitle": "組織驗證頁面標題",
"brandingOrgDescription": "{orgName} 將被替換為組織名稱",
"brandingOrgSubtitle": "組織驗證頁面副標題",
"brandingResourceTitle": "資源驗證頁面標題",
"brandingResourceSubtitle": "資源驗證頁面副標題",
"brandingResourceDescription": "{resourceName} 將被替換為組織名稱",
"saveAuthPageDomain": "儲存網域",
"saveAuthPageBranding": "儲存品牌",
"removeAuthPageBranding": "移除品牌",
"noDomainSet": "沒有域設置", "noDomainSet": "沒有域設置",
"changeDomain": "更改域", "changeDomain": "更改域",
"selectDomain": "選擇域", "selectDomain": "選擇域",
@@ -1764,6 +1876,15 @@
"orgAuthChooseIdpDescription": "選擇您的身份提供商以繼續", "orgAuthChooseIdpDescription": "選擇您的身份提供商以繼續",
"orgAuthNoIdpConfigured": "此機構沒有配置任何身份提供者。您可以使用您的 Pangolin 身份登錄。", "orgAuthNoIdpConfigured": "此機構沒有配置任何身份提供者。您可以使用您的 Pangolin 身份登錄。",
"orgAuthSignInWithPangolin": "使用 Pangolin 登錄", "orgAuthSignInWithPangolin": "使用 Pangolin 登錄",
"orgAuthSignInToOrg": "登入組織",
"orgAuthSelectOrgTitle": "組織登入",
"orgAuthSelectOrgDescription": "輸入您的組織 ID 以繼續",
"orgAuthOrgIdPlaceholder": "your-organization",
"orgAuthOrgIdHelp": "輸入您組織的唯一識別碼",
"orgAuthSelectOrgHelp": "輸入組織 ID 後,您將被導向到組織的登入頁面,在那裡您可以使用 SSO 或組織憑證。",
"orgAuthRememberOrgId": "記住此組織 ID",
"orgAuthBackToSignIn": "返回標準登入",
"orgAuthNoAccount": "沒有帳戶?",
"subscriptionRequiredToUse": "需要訂閱才能使用此功能。", "subscriptionRequiredToUse": "需要訂閱才能使用此功能。",
"idpDisabled": "身份提供者已禁用。", "idpDisabled": "身份提供者已禁用。",
"orgAuthPageDisabled": "組織認證頁面已禁用。", "orgAuthPageDisabled": "組織認證頁面已禁用。",
@@ -1778,6 +1899,8 @@
"enableTwoFactorAuthentication": "啟用兩步驗證", "enableTwoFactorAuthentication": "啟用兩步驗證",
"completeSecuritySteps": "完成安全步驟", "completeSecuritySteps": "完成安全步驟",
"securitySettings": "安全設定", "securitySettings": "安全設定",
"dangerSection": "危險區域",
"dangerSectionDescription": "永久刪除與此組織相關的所有資料",
"securitySettingsDescription": "配置您組織的安全策略", "securitySettingsDescription": "配置您組織的安全策略",
"requireTwoFactorForAllUsers": "所有用戶需要兩步驗證", "requireTwoFactorForAllUsers": "所有用戶需要兩步驗證",
"requireTwoFactorDescription": "如果啟用,此組織的所有內部用戶必須啟用雙重身份驗證才能訪問組織。", "requireTwoFactorDescription": "如果啟用,此組織的所有內部用戶必須啟用雙重身份驗證才能訪問組織。",
@@ -1815,7 +1938,7 @@
"securityPolicyChangeWarningText": "這將影響組織中的所有用戶", "securityPolicyChangeWarningText": "這將影響組織中的所有用戶",
"authPageErrorUpdateMessage": "更新身份驗證頁面設置時出錯", "authPageErrorUpdateMessage": "更新身份驗證頁面設置時出錯",
"authPageErrorUpdate": "無法更新認證頁面", "authPageErrorUpdate": "無法更新認證頁面",
"authPageUpdated": "身份驗證頁面更新成功", "authPageDomainUpdated": "驗證頁面網域更新成功",
"healthCheckNotAvailable": "本地的", "healthCheckNotAvailable": "本地的",
"rewritePath": "重寫路徑", "rewritePath": "重寫路徑",
"rewritePathDescription": "在轉發到目標之前,可以選擇重寫路徑。", "rewritePathDescription": "在轉發到目標之前,可以選擇重寫路徑。",
@@ -1841,8 +1964,19 @@
"enterpriseEdition": "企業版", "enterpriseEdition": "企業版",
"unlicensed": "未授權", "unlicensed": "未授權",
"beta": "測試版", "beta": "測試版",
"manageClients": "管理用戶端", "manageUserDevices": "使用者裝置",
"manageClientsDescription": "用戶端是可以連接到您的站點的設備", "manageUserDevicesDescription": "查看和管理使用者用於私密連接資源的裝置",
"downloadClientBannerTitle": "下載 Pangolin 客戶端",
"downloadClientBannerDescription": "下載適用於您系統的 Pangolin 客戶端,以連接到 Pangolin 網路並私密存取資源。",
"manageMachineClients": "管理機器客戶端",
"manageMachineClientsDescription": "建立和管理伺服器和系統用於私密連接資源的客戶端",
"machineClientsBannerTitle": "伺服器與自動化系統",
"machineClientsBannerDescription": "機器客戶端適用於與特定使用者無關的伺服器和自動化系統。它們使用 ID 和密鑰進行驗證,可以透過 Pangolin CLI、Olm CLI 或 Olm 容器執行。",
"machineClientsBannerPangolinCLI": "Pangolin CLI",
"machineClientsBannerOlmCLI": "Olm CLI",
"machineClientsBannerOlmContainer": "Olm 容器",
"clientsTableUserClients": "使用者",
"clientsTableMachineClients": "機器",
"licenseTableValidUntil": "有效期至", "licenseTableValidUntil": "有效期至",
"saasLicenseKeysSettingsTitle": "企業許可證", "saasLicenseKeysSettingsTitle": "企業許可證",
"saasLicenseKeysSettingsDescription": "為自我託管的 Pangolin 實例生成和管理企業許可證金鑰", "saasLicenseKeysSettingsDescription": "為自我託管的 Pangolin 實例生成和管理企業許可證金鑰",
@@ -1982,6 +2116,7 @@
"clientMessageRemove": "一旦刪除,用戶端將無法連接到站點。", "clientMessageRemove": "一旦刪除,用戶端將無法連接到站點。",
"sidebarLogs": "日誌", "sidebarLogs": "日誌",
"request": "請求", "request": "請求",
"requests": "請求",
"logs": "日誌", "logs": "日誌",
"logsSettingsDescription": "監視從此 orginization 中收集的日誌", "logsSettingsDescription": "監視從此 orginization 中收集的日誌",
"searchLogs": "搜索日誌...", "searchLogs": "搜索日誌...",
@@ -1990,6 +2125,8 @@
"timestamp": "時間戳", "timestamp": "時間戳",
"accessLogs": "訪問日誌", "accessLogs": "訪問日誌",
"exportCsv": "導出 CSV", "exportCsv": "導出 CSV",
"exportError": "匯出 CSV 時發生未知錯誤",
"exportCsvTooltip": "在時間範圍內",
"actorId": "執行者 ID", "actorId": "執行者 ID",
"allowedByRule": "根據規則允許", "allowedByRule": "根據規則允許",
"allowedNoAuth": "無認證", "allowedNoAuth": "無認證",
@@ -2007,6 +2144,7 @@
"ip": "IP", "ip": "IP",
"reason": "原因", "reason": "原因",
"requestLogs": "請求日誌", "requestLogs": "請求日誌",
"requestAnalytics": "請求分析",
"host": "主機", "host": "主機",
"location": "地點", "location": "地點",
"actionLogs": "操作日誌", "actionLogs": "操作日誌",
@@ -2016,6 +2154,7 @@
"logRetention": "日誌保留", "logRetention": "日誌保留",
"logRetentionDescription": "管理不同類型的日誌為這個機構保留多長時間或禁用這些日誌", "logRetentionDescription": "管理不同類型的日誌為這個機構保留多長時間或禁用這些日誌",
"requestLogsDescription": "查看此機構資源的詳細請求日誌", "requestLogsDescription": "查看此機構資源的詳細請求日誌",
"requestAnalyticsDescription": "查看此組織資源的詳細請求分析",
"logRetentionRequestLabel": "請求日誌保留", "logRetentionRequestLabel": "請求日誌保留",
"logRetentionRequestDescription": "保留請求日誌的時間", "logRetentionRequestDescription": "保留請求日誌的時間",
"logRetentionAccessLabel": "訪問日誌保留", "logRetentionAccessLabel": "訪問日誌保留",
@@ -2029,6 +2168,7 @@
"logRetention30Days": "30 天", "logRetention30Days": "30 天",
"logRetention90Days": "90 天", "logRetention90Days": "90 天",
"logRetentionForever": "永遠的", "logRetentionForever": "永遠的",
"logRetentionEndOfFollowingYear": "次年年底",
"actionLogsDescription": "查看此機構執行的操作歷史", "actionLogsDescription": "查看此機構執行的操作歷史",
"accessLogsDescription": "查看此機構資源的訪問認證請求", "accessLogsDescription": "查看此機構資源的訪問認證請求",
"licenseRequiredToUse": "需要企業許可證才能使用此功能。", "licenseRequiredToUse": "需要企業許可證才能使用此功能。",
@@ -2084,6 +2224,43 @@
"supportMessageSent": "消息已發送!", "supportMessageSent": "消息已發送!",
"supportWillContact": "我們很快就會聯繫起來!", "supportWillContact": "我們很快就會聯繫起來!",
"selectLogRetention": "選擇保留日誌", "selectLogRetention": "選擇保留日誌",
"terms": "條款",
"privacy": "隱私權",
"security": "安全性",
"docs": "文件",
"deviceActivation": "裝置啟用",
"deviceCodeInvalidFormat": "代碼必須為 9 個字元(例如:A1AJ-N5JD",
"deviceCodeInvalidOrExpired": "代碼無效或已過期",
"deviceCodeVerifyFailed": "驗證裝置代碼失敗",
"signedInAs": "已登入為",
"deviceCodeEnterPrompt": "輸入裝置上顯示的代碼",
"continue": "繼續",
"deviceUnknownLocation": "未知位置",
"deviceAuthorizationRequested": "此授權請求來自 {location},時間為 {date}。請確保您信任此裝置,因為它將獲得帳戶存取權限。",
"deviceLabel": "裝置:{deviceName}",
"deviceWantsAccess": "想要存取您的帳戶",
"deviceExistingAccess": "現有存取權限:",
"deviceFullAccess": "完整帳戶存取權限",
"deviceOrganizationsAccess": "存取您帳戶有權限的所有組織",
"deviceAuthorize": "授權 {applicationName}",
"deviceConnected": "裝置已連接!",
"deviceAuthorizedMessage": "裝置已獲授權存取您的帳戶。請返回客戶端應用程式。",
"pangolinCloud": "Pangolin 雲端",
"viewDevices": "查看裝置",
"viewDevicesDescription": "管理您已連接的裝置",
"noDevices": "找不到裝置",
"dateCreated": "建立日期",
"unnamedDevice": "未命名裝置",
"deviceQuestionRemove": "您確定要刪除此裝置嗎?",
"deviceMessageRemove": "此操作無法復原。",
"deviceDeleteConfirm": "刪除裝置",
"deleteDevice": "刪除裝置",
"errorLoadingDevices": "載入裝置時發生錯誤",
"failedToLoadDevices": "載入裝置失敗",
"deviceDeleted": "裝置已刪除",
"deviceDeletedDescription": "裝置已成功刪除。",
"errorDeletingDevice": "刪除裝置時發生錯誤",
"failedToDeleteDevice": "刪除裝置失敗",
"showColumns": "顯示列", "showColumns": "顯示列",
"hideColumns": "隱藏列", "hideColumns": "隱藏列",
"columnVisibility": "列可見性", "columnVisibility": "列可見性",
@@ -2097,5 +2274,125 @@
"selectedResources": "選定的資源", "selectedResources": "選定的資源",
"enableSelected": "啟用選中的", "enableSelected": "啟用選中的",
"disableSelected": "禁用選中的", "disableSelected": "禁用選中的",
"checkSelectedStatus": "檢查選中的狀態" "checkSelectedStatus": "檢查選中的狀態",
"clients": "客戶端",
"accessClientSelect": "選擇機器客戶端",
"resourceClientDescription": "可以存取此資源的機器客戶端",
"regenerate": "重新產生",
"credentials": "憑證",
"savecredentials": "儲存憑證",
"regenerateCredentialsButton": "重新產生憑證",
"regenerateCredentials": "重新產生憑證",
"generatedcredentials": "已產生的憑證",
"copyandsavethesecredentials": "複製並儲存這些憑證",
"copyandsavethesecredentialsdescription": "離開此頁面後將不會再顯示這些憑證。請立即安全儲存。",
"credentialsSaved": "憑證已儲存",
"credentialsSavedDescription": "憑證已成功重新產生並儲存。",
"credentialsSaveError": "憑證儲存錯誤",
"credentialsSaveErrorDescription": "重新產生和儲存憑證時發生錯誤。",
"regenerateCredentialsWarning": "重新產生憑證將使先前的憑證失效並導致斷線。請確保更新任何使用這些憑證的設定。",
"confirm": "確認",
"regenerateCredentialsConfirmation": "您確定要重新產生憑證嗎?",
"endpoint": "端點",
"Id": "ID",
"SecretKey": "密鑰",
"niceId": "友善 ID",
"niceIdUpdated": "友善 ID 已更新",
"niceIdUpdatedSuccessfully": "友善 ID 更新成功",
"niceIdUpdateError": "更新友善 ID 時發生錯誤",
"niceIdUpdateErrorDescription": "更新友善 ID 時發生錯誤。",
"niceIdCannotBeEmpty": "友善 ID 不能為空",
"enterIdentifier": "輸入識別碼",
"identifier": "識別碼",
"deviceLoginUseDifferentAccount": "不是您嗎?使用其他帳戶。",
"deviceLoginDeviceRequestingAccessToAccount": "有裝置正在請求存取此帳戶。",
"noData": "無資料",
"machineClients": "機器客戶端",
"install": "安裝",
"run": "執行",
"clientNameDescription": "客戶端的顯示名稱,可以稍後更改。",
"clientAddress": "客戶端位址(進階)",
"setupFailedToFetchSubnet": "取得預設子網路失敗",
"setupSubnetAdvanced": "子網路(進階)",
"setupSubnetDescription": "此組織內部網路的子網路。",
"setupUtilitySubnet": "工具子網路(進階)",
"setupUtilitySubnetDescription": "此組織別名位址和 DNS 伺服器的子網路。",
"siteRegenerateAndDisconnect": "重新產生並斷開連接",
"siteRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此站點的連接嗎?",
"siteRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開站點連接。站點需要使用新憑證重新啟動。",
"siteRegenerateCredentialsConfirmation": "您確定要重新產生此站點的憑證嗎?",
"siteRegenerateCredentialsWarning": "這將重新產生憑證。站點將保持連接,直到您手動重新啟動並使用新憑證。",
"clientRegenerateAndDisconnect": "重新產生並斷開連接",
"clientRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此客戶端的連接嗎?",
"clientRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開客戶端連接。客戶端需要使用新憑證重新啟動。",
"clientRegenerateCredentialsConfirmation": "您確定要重新產生此客戶端的憑證嗎?",
"clientRegenerateCredentialsWarning": "這將重新產生憑證。客戶端將保持連接,直到您手動重新啟動並使用新憑證。",
"remoteExitNodeRegenerateAndDisconnect": "重新產生並斷開連接",
"remoteExitNodeRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此遠端出口節點的連接嗎?",
"remoteExitNodeRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開遠端出口節點連接。遠端出口節點需要使用新憑證重新啟動。",
"remoteExitNodeRegenerateCredentialsConfirmation": "您確定要重新產生此遠端出口節點的憑證嗎?",
"remoteExitNodeRegenerateCredentialsWarning": "這將重新產生憑證。遠端出口節點將保持連接,直到您手動重新啟動並使用新憑證。",
"agent": "代理",
"personalUseOnly": "僅限個人使用",
"loginPageLicenseWatermark": "此實例僅授權個人使用。",
"instanceIsUnlicensed": "此實例未授權。",
"portRestrictions": "連接埠限制",
"allPorts": "全部",
"custom": "自訂",
"allPortsAllowed": "允許所有連接埠",
"allPortsBlocked": "阻擋所有連接埠",
"tcpPortsDescription": "指定此資源允許的 TCP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:80,443,8000-9000)。",
"udpPortsDescription": "指定此資源允許的 UDP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:53,123,500-600)。",
"organizationLoginPageTitle": "組織登入頁面",
"organizationLoginPageDescription": "自訂此組織的登入頁面",
"resourceLoginPageTitle": "資源登入頁面",
"resourceLoginPageDescription": "自訂個別資源的登入頁面",
"enterConfirmation": "輸入確認",
"blueprintViewDetails": "詳細資訊",
"defaultIdentityProvider": "預設身份提供者",
"defaultIdentityProviderDescription": "當選擇預設身份提供者時,使用者將自動被重新導向到該提供者進行驗證。",
"editInternalResourceDialogNetworkSettings": "網路設定",
"editInternalResourceDialogAccessPolicy": "存取策略",
"editInternalResourceDialogAddRoles": "新增角色",
"editInternalResourceDialogAddUsers": "新增使用者",
"editInternalResourceDialogAddClients": "新增客戶端",
"editInternalResourceDialogDestinationLabel": "目的地",
"editInternalResourceDialogDestinationDescription": "指定內部資源的目的地位址。根據所選模式,這可以是主機名稱、IP 位址或 CIDR 範圍。可選擇設定內部 DNS 別名以便識別。",
"editInternalResourceDialogPortRestrictionsDescription": "限制對特定 TCP/UDP 連接埠的存取,或允許/阻擋所有連接埠。",
"editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP",
"editInternalResourceDialogIcmp": "ICMP",
"editInternalResourceDialogAccessControl": "存取控制",
"editInternalResourceDialogAccessControlDescription": "控制哪些角色、使用者和機器客戶端在連接時可以存取此資源。管理員始終擁有存取權限。",
"editInternalResourceDialogPortRangeValidationError": "連接埠範圍必須是「*」表示所有連接埠,或以逗號分隔的連接埠和範圍列表(例如:「80,443,8000-9000」)。連接埠必須介於 1 到 65535 之間。",
"orgAuthWhatsThis": "我在哪裡可以找到我的組織 ID?",
"learnMore": "了解更多",
"backToHome": "返回首頁",
"needToSignInToOrg": "需要使用您組織的身份提供者嗎?",
"maintenanceMode": "維護模式",
"maintenanceModeDescription": "向訪客顯示維護頁面",
"maintenanceModeType": "維護模式類型",
"showMaintenancePage": "向訪客顯示維護頁面",
"enableMaintenanceMode": "啟用維護模式",
"automatic": "自動",
"automaticModeDescription": "僅在所有後端目標都關閉或不健康時顯示維護頁面。只要至少有一個目標健康,您的資源就會正常運作。",
"forced": "強制",
"forcedModeDescription": "無論後端健康狀況如何,始終顯示維護頁面。當您想要阻止所有存取時,用於計劃維護。",
"warning:": "警告:",
"forcedeModeWarning": "所有流量將被導向維護頁面。您的後端資源將不會收到任何請求。",
"pageTitle": "頁面標題",
"pageTitleDescription": "維護頁面上顯示的主標題",
"maintenancePageMessage": "維護訊息",
"maintenancePageMessagePlaceholder": "我們很快就會回來!我們的網站目前正在進行預定維護。",
"maintenancePageMessageDescription": "說明維護的詳細訊息",
"maintenancePageTimeTitle": "預計完成時間(可選)",
"maintenanceTime": "例如:2 小時、11 月 1 日下午 5:00",
"maintenanceEstimatedTimeDescription": "您預計何時完成維護",
"editDomain": "編輯網域",
"editDomainDescription": "為您的資源選擇網域",
"maintenanceModeDisabledTooltip": "此功能需要有效的授權才能啟用。",
"maintenanceScreenTitle": "服務暫時無法使用",
"maintenanceScreenMessage": "我們目前遇到技術問題。請稍後再試。",
"maintenanceScreenEstimatedCompletion": "預計完成時間:",
"createInternalResourceDialogDestinationRequired": "目的地為必填欄位"
} }
+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 ." "format": "prettier --write ."
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "8.2.0", "@asteasolutions/zod-to-openapi": "8.4.0",
"@aws-sdk/client-s3": "3.955.0", "@aws-sdk/client-s3": "3.971.0",
"@faker-js/faker": "10.1.0", "@faker-js/faker": "10.2.0",
"@headlessui/react": "2.2.9", "@headlessui/react": "2.2.9",
"@hookform/resolvers": "5.2.2", "@hookform/resolvers": "5.2.2",
"@monaco-editor/react": "4.7.0", "@monaco-editor/react": "4.7.0",
@@ -92,7 +92,7 @@
"http-errors": "2.0.1", "http-errors": "2.0.1",
"i": "0.3.7", "i": "0.3.7",
"input-otp": "1.4.2", "input-otp": "1.4.2",
"ioredis": "5.8.2", "ioredis": "5.9.2",
"jmespath": "0.16.0", "jmespath": "0.16.0",
"js-yaml": "4.1.1", "js-yaml": "4.1.1",
"jsonwebtoken": "9.0.3", "jsonwebtoken": "9.0.3",
@@ -100,7 +100,7 @@
"maxmind": "5.0.1", "maxmind": "5.0.1",
"moment": "2.30.1", "moment": "2.30.1",
"next": "15.5.9", "next": "15.5.9",
"next-intl": "4.6.1", "next-intl": "4.7.0",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"nextjs-toploader": "3.9.17", "nextjs-toploader": "3.9.17",
"node-cache": "5.1.2", "node-cache": "5.1.2",
@@ -109,21 +109,21 @@
"npm": "11.7.0", "npm": "11.7.0",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "8.16.3", "pg": "8.17.1",
"posthog-node": "5.17.4", "posthog-node": "5.23.0",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.3", "react": "19.2.3",
"react-day-picker": "9.13.0", "react-day-picker": "9.13.0",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"react-easy-sort": "1.8.0", "react-easy-sort": "1.8.0",
"react-hook-form": "7.68.0", "react-hook-form": "7.71.1",
"react-icons": "5.5.0", "react-icons": "5.5.0",
"rebuild": "0.1.2", "rebuild": "0.1.2",
"recharts": "2.15.4", "recharts": "2.15.4",
"reodotdev": "1.0.0", "reodotdev": "1.0.0",
"resend": "6.6.0", "resend": "6.8.0",
"semver": "7.7.3", "semver": "7.7.3",
"stripe": "20.1.0", "stripe": "20.2.0",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"tailwind-merge": "3.4.0", "tailwind-merge": "3.4.0",
"topojson-client": "3.1.0", "topojson-client": "3.1.0",
@@ -133,10 +133,10 @@
"visionscarto-world-atlas": "1.0.0", "visionscarto-world-atlas": "1.0.0",
"winston": "3.19.0", "winston": "3.19.0",
"winston-daily-rotate-file": "5.0.0", "winston-daily-rotate-file": "5.0.0",
"ws": "8.18.3", "ws": "8.19.0",
"yaml": "2.8.2", "yaml": "2.8.2",
"yargs": "18.0.0", "yargs": "18.0.0",
"zod": "4.2.1", "zod": "4.3.5",
"zod-validation-error": "5.0.0" "zod-validation-error": "5.0.0"
}, },
"devDependencies": { "devDependencies": {
@@ -170,12 +170,12 @@
"esbuild": "0.27.2", "esbuild": "0.27.2",
"esbuild-node-externals": "1.20.1", "esbuild-node-externals": "1.20.1",
"postcss": "8.5.6", "postcss": "8.5.6",
"prettier": "3.7.4", "prettier": "3.8.0",
"react-email": "5.0.7", "react-email": "5.2.5",
"tailwindcss": "4.1.18", "tailwindcss": "4.1.18",
"tsc-alias": "1.8.16", "tsc-alias": "1.8.16",
"tsx": "4.21.0", "tsx": "4.21.0",
"typescript": "5.9.3", "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 // Windows-specific posture check information
windowsDefenderEnabled: boolean("windowsDefenderEnabled") windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
.notNull() .notNull()
.default(false), .default(false),
@@ -830,7 +830,7 @@ export const fingerprintSnapshots = pgTable("fingerprintSnapshots", {
// Windows-specific posture check information // Windows-specific posture check information
windowsDefenderEnabled: boolean("windowsDefenderEnabled") windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
.notNull() .notNull()
.default(false), .default(false),
+2 -2
View File
@@ -475,7 +475,7 @@ export const currentFingerprint = sqliteTable("currentFingerprint", {
// Windows-specific posture check information // Windows-specific posture check information
windowsDefenderEnabled: integer("windowsDefenderEnabled", { windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
mode: "boolean" mode: "boolean"
}) })
.notNull() .notNull()
@@ -549,7 +549,7 @@ export const fingerprintSnapshots = sqliteTable("fingerprintSnapshots", {
// Windows-specific posture check information // Windows-specific posture check information
windowsDefenderEnabled: integer("windowsDefenderEnabled", { windowsAntivirusEnabled: integer("windowsAntivirusEnabled", {
mode: "boolean" mode: "boolean"
}) })
.notNull() .notNull()
+3 -11
View File
@@ -31,7 +31,7 @@ import { pickPort } from "@server/routers/target/helpers";
import { resourcePassword } from "@server/db"; import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password"; import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators"; import { isValidCIDR, isValidIP, isValidUrlGlobPattern } from "../validators";
import { isLicensedOrSubscribed } from "../isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { build } from "@server/build"; import { build } from "@server/build";
export type ProxyResourcesResults = { export type ProxyResourcesResults = {
@@ -213,11 +213,7 @@ export async function updateProxyResources(
// Update existing resource // Update existing resource
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) { if (!isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
resourceData.maintenance = undefined; resourceData.maintenance = undefined;
} }
@@ -653,11 +649,7 @@ export async function updateProxyResources(
} }
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) { if (!isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
resourceData.maintenance = undefined; resourceData.maintenance = undefined;
} }
+1 -1
View File
@@ -14,7 +14,7 @@ import {
} from "@server/db"; } from "@server/db";
import { getUniqueClientName } from "@server/db/names"; import { getUniqueClientName } from "@server/db/names";
import { getNextAvailableClientSubnet } from "@server/lib/ip"; import { getNextAvailableClientSubnet } from "@server/lib/ip";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import logger from "@server/logger"; import logger from "@server/logger";
import { sendTerminateClient } from "@server/routers/client/terminate"; import { sendTerminateClient } from "@server/routers/client/terminate";
import { and, eq, notInArray, type InferInsertModel } from "drizzle-orm"; 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"; import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process // 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 __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME); export const __DIRNAME = path.dirname(__FILENAME);
+1 -15
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> { export async function isLicensedOrSubscribed(orgId: string): Promise<boolean> {
if (build === "enterprise") { return false;
return await license.isUnlocked();
}
if (build === "saas") {
const { tier } = await getOrgTierData(orgId);
return tier === TierId.STANDARD;
}
return true;
} }
@@ -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 type { Request, Response, NextFunction } from "express";
import { build } from "@server/build"; 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 { TierId } from "@server/lib/billing/tiers";
import { import {
approvals, approvals,
@@ -19,7 +19,7 @@ import { fromError } from "zod-validation-error";
import { build } from "@server/build"; import { build } from "@server/build";
import { approvals, clients, db, orgs, type Approval } from "@server/db"; 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 { TierId } from "@server/lib/billing/tiers";
import response from "@server/lib/response"; import response from "@server/lib/response";
import { and, eq, type InferInsertModel } from "drizzle-orm"; 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"}` message: `Invalid YAML: ${error instanceof Error ? error.message : "Unknown error"}`
}); });
} }
}) }),
source: z.enum(["API", "UI", "CLI"]).optional()
}) })
.strict(); .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); logger.debug(`Received blueprint:`, contents);
@@ -107,7 +108,7 @@ export async function applyYAMLBlueprint(
blueprint = await applyBlueprint({ blueprint = await applyBlueprint({
orgId, orgId,
name, name,
source: "UI", source,
configData: parsedConfig configData: parsedConfig
}); });
} catch (err) { } catch (err) {
+1 -1
View File
@@ -1,6 +1,6 @@
import type { Blueprint } from "@server/db"; 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"> & { export type BlueprintData = Omit<Blueprint, "source"> & {
source: BlueprintSource; source: BlueprintSource;
+83 -30
View File
@@ -12,6 +12,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { getUserDeviceName } from "@server/db/names"; import { getUserDeviceName } from "@server/db/names";
import { build } from "@server/build"; import { build } from "@server/build";
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
const getClientSchema = z.strictObject({ const getClientSchema = z.strictObject({
clientId: z clientId: z
@@ -58,7 +59,7 @@ type PostureData = {
firewallEnabled?: boolean | null; firewallEnabled?: boolean | null;
autoUpdatesEnabled?: boolean | null; autoUpdatesEnabled?: boolean | null;
tpmAvailable?: boolean | null; tpmAvailable?: boolean | null;
windowsDefenderEnabled?: boolean | null; windowsAntivirusEnabled?: boolean | null;
macosSipEnabled?: boolean | null; macosSipEnabled?: boolean | null;
macosGatekeeperEnabled?: boolean | null; macosGatekeeperEnabled?: boolean | null;
macosFirewallStealthMode?: boolean | null; macosFirewallStealthMode?: boolean | null;
@@ -75,75 +76,123 @@ function getPlatformPostureData(
const normalizedPlatform = platform?.toLowerCase() || "unknown"; const normalizedPlatform = platform?.toLowerCase() || "unknown";
const posture: PostureData = {}; 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 (normalizedPlatform === "windows") {
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { if (
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted; posture.diskEncrypted = fingerprint.diskEncrypted;
} }
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { if (
fingerprint.firewallEnabled !== null &&
fingerprint.firewallEnabled !== undefined
) {
posture.firewallEnabled = fingerprint.firewallEnabled; posture.firewallEnabled = fingerprint.firewallEnabled;
} }
if (fingerprint.autoUpdatesEnabled !== null && fingerprint.autoUpdatesEnabled !== undefined) { if (
posture.autoUpdatesEnabled = fingerprint.autoUpdatesEnabled; fingerprint.tpmAvailable !== null &&
} fingerprint.tpmAvailable !== undefined
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) { ) {
posture.tpmAvailable = fingerprint.tpmAvailable; posture.tpmAvailable = fingerprint.tpmAvailable;
} }
if (fingerprint.windowsDefenderEnabled !== null && fingerprint.windowsDefenderEnabled !== undefined) { if (
posture.windowsDefenderEnabled = fingerprint.windowsDefenderEnabled; 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 // macOS: Hard drive encryption, Biometric configuration, Firewall, System Integrity Protection (SIP), Gatekeeper, Firewall stealth mode
else if (normalizedPlatform === "macos") { else if (normalizedPlatform === "macos") {
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { if (
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted; posture.diskEncrypted = fingerprint.diskEncrypted;
} }
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) { if (
fingerprint.biometricsEnabled !== null &&
fingerprint.biometricsEnabled !== undefined
) {
posture.biometricsEnabled = fingerprint.biometricsEnabled; posture.biometricsEnabled = fingerprint.biometricsEnabled;
} }
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { if (
fingerprint.firewallEnabled !== null &&
fingerprint.firewallEnabled !== undefined
) {
posture.firewallEnabled = fingerprint.firewallEnabled; posture.firewallEnabled = fingerprint.firewallEnabled;
} }
if (fingerprint.macosSipEnabled !== null && fingerprint.macosSipEnabled !== undefined) { if (
fingerprint.macosSipEnabled !== null &&
fingerprint.macosSipEnabled !== undefined
) {
posture.macosSipEnabled = fingerprint.macosSipEnabled; posture.macosSipEnabled = fingerprint.macosSipEnabled;
} }
if (fingerprint.macosGatekeeperEnabled !== null && fingerprint.macosGatekeeperEnabled !== undefined) { if (
fingerprint.macosGatekeeperEnabled !== null &&
fingerprint.macosGatekeeperEnabled !== undefined
) {
posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled; posture.macosGatekeeperEnabled = fingerprint.macosGatekeeperEnabled;
} }
if (fingerprint.macosFirewallStealthMode !== null && fingerprint.macosFirewallStealthMode !== undefined) { if (
posture.macosFirewallStealthMode = fingerprint.macosFirewallStealthMode; 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 // Linux: Hard drive encryption, Firewall, AppArmor, SELinux, TPM availability
else if (normalizedPlatform === "linux") { else if (normalizedPlatform === "linux") {
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { if (
fingerprint.diskEncrypted !== null &&
fingerprint.diskEncrypted !== undefined
) {
posture.diskEncrypted = fingerprint.diskEncrypted; posture.diskEncrypted = fingerprint.diskEncrypted;
} }
if (fingerprint.firewallEnabled !== null && fingerprint.firewallEnabled !== undefined) { if (
fingerprint.firewallEnabled !== null &&
fingerprint.firewallEnabled !== undefined
) {
posture.firewallEnabled = fingerprint.firewallEnabled; posture.firewallEnabled = fingerprint.firewallEnabled;
} }
if (fingerprint.linuxAppArmorEnabled !== null && fingerprint.linuxAppArmorEnabled !== undefined) { if (
fingerprint.linuxAppArmorEnabled !== null &&
fingerprint.linuxAppArmorEnabled !== undefined
) {
posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled; posture.linuxAppArmorEnabled = fingerprint.linuxAppArmorEnabled;
} }
if (fingerprint.linuxSELinuxEnabled !== null && fingerprint.linuxSELinuxEnabled !== undefined) { if (
fingerprint.linuxSELinuxEnabled !== null &&
fingerprint.linuxSELinuxEnabled !== undefined
) {
posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled; posture.linuxSELinuxEnabled = fingerprint.linuxSELinuxEnabled;
} }
if (fingerprint.tpmAvailable !== null && fingerprint.tpmAvailable !== undefined) { if (
fingerprint.tpmAvailable !== null &&
fingerprint.tpmAvailable !== undefined
) {
posture.tpmAvailable = fingerprint.tpmAvailable; posture.tpmAvailable = fingerprint.tpmAvailable;
} }
} }
// iOS: Biometric configuration // iOS: Biometric configuration
else if (normalizedPlatform === "ios") { else if (normalizedPlatform === "ios") {
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) { // none supported yet
posture.biometricsEnabled = fingerprint.biometricsEnabled;
}
} }
// Android: Screen lock, Biometric configuration, Hard drive encryption // Android: Screen lock, Biometric configuration, Hard drive encryption
else if (normalizedPlatform === "android") { else if (normalizedPlatform === "android") {
if (fingerprint.biometricsEnabled !== null && fingerprint.biometricsEnabled !== undefined) { if (
posture.biometricsEnabled = fingerprint.biometricsEnabled; fingerprint.diskEncrypted !== null &&
} fingerprint.diskEncrypted !== undefined
if (fingerprint.diskEncrypted !== null && fingerprint.diskEncrypted !== undefined) { ) {
posture.diskEncrypted = fingerprint.diskEncrypted; posture.diskEncrypted = fingerprint.diskEncrypted;
} }
} }
@@ -255,8 +304,12 @@ export async function getClient(
: null; : null;
// Build posture data if available (platform-specific) // Build posture data if available (platform-specific)
// Only return posture data if org is licensed/subscribed
let postureData: PostureData | null = null; let postureData: PostureData | null = null;
if (build !== "oss") { const isOrgLicensed = await isLicensedOrSubscribed(
client.clients.orgId
);
if (isOrgLicensed) {
postureData = getPlatformPostureData( postureData = getPlatformPostureData(
client.currentFingerprint?.platform || null, client.currentFingerprint?.platform || null,
client.currentFingerprint client.currentFingerprint
+2 -6
View File
@@ -175,10 +175,7 @@ async function getSiteAssociations(clientIds: number[]) {
.where(inArray(clientSitesAssociationsCache.clientId, clientIds)); .where(inArray(clientSitesAssociationsCache.clientId, clientIds));
} }
type ClientWithSites = Omit< type ClientWithSites = Awaited<ReturnType<typeof queryClients>>[0] & {
Awaited<ReturnType<typeof queryClients>>[0],
"deviceModel"
> & {
sites: Array<{ sites: Array<{
siteId: number; siteId: number;
siteName: string | null; siteName: string | null;
@@ -324,9 +321,8 @@ export async function listClients(
const clientsWithSites = clientsList.map((client) => { const clientsWithSites = clientsList.map((client) => {
const model = client.deviceModel || null; const model = client.deviceModel || null;
const newName = getUserDeviceName(model, client.name); const newName = getUserDeviceName(model, client.name);
const { deviceModel, ...clientWithoutDeviceModel } = client;
return { return {
...clientWithoutDeviceModel, ...client,
name: newName, name: newName,
sites: sitesByClient[client.clientId] || [] sites: sitesByClient[client.clientId] || []
}; };
+5 -5
View File
@@ -22,7 +22,7 @@ function fingerprintSnapshotHash(fingerprint: any, postures: any): string {
autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false, autoUpdatesEnabled: postures.autoUpdatesEnabled ?? false,
tpmAvailable: postures.tpmAvailable ?? false, tpmAvailable: postures.tpmAvailable ?? false,
windowsDefenderEnabled: postures.windowsDefenderEnabled ?? false, windowsAntivirusEnabled: postures.windowsAntivirusEnabled ?? false,
macosSipEnabled: postures.macosSipEnabled ?? false, macosSipEnabled: postures.macosSipEnabled ?? false,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false, macosGatekeeperEnabled: postures.macosGatekeeperEnabled ?? false,
@@ -87,7 +87,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled, autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable, tpmAvailable: postures.tpmAvailable,
windowsDefenderEnabled: postures.windowsDefenderEnabled, windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
macosSipEnabled: postures.macosSipEnabled, macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -117,7 +117,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled, autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable, tpmAvailable: postures.tpmAvailable,
windowsDefenderEnabled: postures.windowsDefenderEnabled, windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
macosSipEnabled: postures.macosSipEnabled, macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -162,7 +162,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled, autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable, tpmAvailable: postures.tpmAvailable,
windowsDefenderEnabled: postures.windowsDefenderEnabled, windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
macosSipEnabled: postures.macosSipEnabled, macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -197,7 +197,7 @@ export async function handleFingerprintInsertion(
autoUpdatesEnabled: postures.autoUpdatesEnabled, autoUpdatesEnabled: postures.autoUpdatesEnabled,
tpmAvailable: postures.tpmAvailable, tpmAvailable: postures.tpmAvailable,
windowsDefenderEnabled: postures.windowsDefenderEnabled, windowsAntivirusEnabled: postures.windowsAntivirusEnabled,
macosSipEnabled: postures.macosSipEnabled, macosSipEnabled: postures.macosSipEnabled,
macosGatekeeperEnabled: postures.macosGatekeeperEnabled, macosGatekeeperEnabled: postures.macosGatekeeperEnabled,
@@ -46,6 +46,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return; return;
} }
logger.debug("Handling fingerprint insertion for olm register...", {
olmId: olm.olmId,
fingerprint,
postures
});
await handleFingerprintInsertion(olm, fingerprint, postures); await handleFingerprintInsertion(olm, fingerprint, postures);
if ( if (
@@ -143,7 +149,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
return; return;
} }
if (!policyCheck.policies?.passwordAge?.compliant === false) { if (policyCheck.policies?.passwordAge?.compliant === false) {
logger.warn( logger.warn(
`Olm user ${olm.userId} has non-compliant password age for org ${orgId}` `Olm user ${olm.userId} has non-compliant password age for org ${orgId}`
); );
@@ -153,7 +159,7 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
); );
return; return;
} else if ( } else if (
!policyCheck.policies?.maxSessionLength?.compliant === false policyCheck.policies?.maxSessionLength?.compliant === false
) { ) {
logger.warn( logger.warn(
`Olm user ${olm.userId} has non-compliant session length for org ${orgId}` `Olm user ${olm.userId} has non-compliant session length for org ${orgId}`
@@ -65,7 +65,6 @@ export async function recoverOlmWithFingerprint(
.where( .where(
and( and(
eq(olms.userId, userId), eq(olms.userId, userId),
eq(olms.archived, false),
eq( eq(
currentFingerprint.platformFingerprint, currentFingerprint.platformFingerprint,
platformFingerprint platformFingerprint
+2 -2
View File
@@ -13,7 +13,7 @@ import { build } from "@server/build";
import { getOrgTierData } from "#dynamic/lib/billing"; import { getOrgTierData } from "#dynamic/lib/billing";
import { TierId } from "@server/lib/billing/tiers"; import { TierId } from "@server/lib/billing/tiers";
import { cache } from "@server/lib/cache"; import { cache } from "@server/lib/cache";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
const updateOrgParamsSchema = z.strictObject({ const updateOrgParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -89,7 +89,7 @@ export async function updateOrg(
const { orgId } = parsedParams.data; const { orgId } = parsedParams.data;
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (build == "enterprise" && !isLicensed) { if (!isLicensed) {
parsedBody.data.requireTwoFactor = undefined; parsedBody.data.requireTwoFactor = undefined;
parsedBody.data.maxSessionLengthHours = undefined; parsedBody.data.maxSessionLengthHours = undefined;
parsedBody.data.passwordExpiryDays = 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 { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { validateAndConstructDomain } from "@server/lib/domainUtils"; import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { build } from "@server/build"; import { build } from "@server/build";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
const updateResourceParamsSchema = z.strictObject({ const updateResourceParamsSchema = z.strictObject({
resourceId: z.string().transform(Number).pipe(z.int().positive()) resourceId: z.string().transform(Number).pipe(z.int().positive())
@@ -342,11 +342,7 @@ async function updateHttpResource(
} }
const isLicensed = await isLicensedOrSubscribed(resource.orgId); const isLicensed = await isLicensedOrSubscribed(resource.orgId);
if (build == "enterprise" && !isLicensed) { if (!isLicensed) {
logger.warn(
"Server is not licensed! Clearing set maintenance screen values"
);
// null the maintenance mode fields if not licensed
updateData.maintenanceModeEnabled = undefined; updateData.maintenanceModeEnabled = undefined;
updateData.maintenanceModeType = undefined; updateData.maintenanceModeType = undefined;
updateData.maintenanceTitle = 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 { eq, and } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build"; import { build } from "@server/build";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
const createRoleParamsSchema = z.strictObject({ const createRoleParamsSchema = z.strictObject({
orgId: z.string() orgId: z.string()
@@ -101,7 +101,7 @@ export async function createRole(
} }
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (build === "oss" || !isLicensed) { if (!isLicensed) {
roleData.requireDeviceApproval = undefined; roleData.requireDeviceApproval = undefined;
} }
+2 -3
View File
@@ -8,8 +8,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors"; import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { build } from "@server/build"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
import { isLicensedOrSubscribed } from "@server/lib/isLicencedOrSubscribed";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
const updateRoleParamsSchema = z.strictObject({ const updateRoleParamsSchema = z.strictObject({
@@ -112,7 +111,7 @@ export async function updateRole(
} }
const isLicensed = await isLicensedOrSubscribed(orgId); const isLicensed = await isLicensedOrSubscribed(orgId);
if (build === "oss" || !isLicensed) { if (!isLicensed) {
updateData.requireDeviceApproval = undefined; updateData.requireDeviceApproval = undefined;
} }
@@ -97,6 +97,7 @@ export async function listAllSiteResourcesByOrg(
destination: siteResources.destination, destination: siteResources.destination,
enabled: siteResources.enabled, enabled: siteResources.enabled,
alias: siteResources.alias, alias: siteResources.alias,
aliasAddress: siteResources.aliasAddress,
tcpPortRangeString: siteResources.tcpPortRangeString, tcpPortRangeString: siteResources.tcpPortRangeString,
udpPortRangeString: siteResources.udpPortRangeString, udpPortRangeString: siteResources.udpPortRangeString,
disableIcmp: siteResources.disableIcmp, disableIcmp: siteResources.disableIcmp,
+2 -2
View File
@@ -49,7 +49,7 @@ export default async function migration() {
"firewallEnabled" boolean DEFAULT false NOT NULL, "firewallEnabled" boolean DEFAULT false NOT NULL,
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL, "autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
"tpmAvailable" 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, "macosSipEnabled" boolean DEFAULT false NOT NULL,
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL, "macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
"macosFirewallStealthMode" 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, "firewallEnabled" boolean DEFAULT false NOT NULL,
"autoUpdatesEnabled" boolean DEFAULT false NOT NULL, "autoUpdatesEnabled" boolean DEFAULT false NOT NULL,
"tpmAvailable" 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, "macosSipEnabled" boolean DEFAULT false NOT NULL,
"macosGatekeeperEnabled" boolean DEFAULT false NOT NULL, "macosGatekeeperEnabled" boolean DEFAULT false NOT NULL,
"macosFirewallStealthMode" 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, 'firewallEnabled' integer DEFAULT false NOT NULL,
'autoUpdatesEnabled' integer DEFAULT false NOT NULL, 'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
'tpmAvailable' 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, 'macosSipEnabled' integer DEFAULT false NOT NULL,
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL, 'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
'macosFirewallStealthMode' integer DEFAULT false NOT NULL, 'macosFirewallStealthMode' integer DEFAULT false NOT NULL,
@@ -83,7 +83,7 @@ CREATE TABLE 'fingerprintSnapshots' (
'firewallEnabled' integer DEFAULT false NOT NULL, 'firewallEnabled' integer DEFAULT false NOT NULL,
'autoUpdatesEnabled' integer DEFAULT false NOT NULL, 'autoUpdatesEnabled' integer DEFAULT false NOT NULL,
'tpmAvailable' 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, 'macosSipEnabled' integer DEFAULT false NOT NULL,
'macosGatekeeperEnabled' integer DEFAULT false NOT NULL, 'macosGatekeeperEnabled' integer DEFAULT false NOT NULL,
'macosFirewallStealthMode' 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) { export default async function ApprovalFeedPage(props: ApprovalFeedPageProps) {
const params = await props.params; 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; let org: GetOrgResponse | null = null;
const orgRes = await getCachedOrg(params.orgId); 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 { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
import { InfoIcon } from "lucide-react"; import { InfoIcon } from "lucide-react";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { OlmInstallCommands } from "@app/components/olm-install-commands";
export default function CredentialsPage() { export default function CredentialsPage() {
const { env } = useEnvContext(); const { env } = useEnvContext();
@@ -204,6 +205,12 @@ export default function CredentialsPage() {
</SettingsSectionFooter> </SettingsSectionFooter>
)} )}
</SettingsSection> </SettingsSection>
<OlmInstallCommands
id={displayOlmId ?? "********"}
endpoint={env.app.dashboardUrl}
secret={displaySecret ?? "********"}
/>
</SettingsContainer> </SettingsContainer>
<ConfirmDeleteDialog <ConfirmDeleteDialog
@@ -1,15 +1,22 @@
"use client"; "use client";
import CopyToClipboard from "@app/components/CopyToClipboard";
import {
InfoSection,
InfoSectionContent,
InfoSections,
InfoSectionTitle
} from "@app/components/InfoSection";
import { import {
SettingsContainer, SettingsContainer,
SettingsSection, SettingsSection,
SettingsSectionBody, SettingsSectionBody,
SettingsSectionDescription, SettingsSectionDescription,
SettingsSectionForm,
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle
} from "@app/components/Settings"; } from "@app/components/Settings";
import { StrategySelect } from "@app/components/StrategySelect"; import HeaderTitle from "@app/components/SettingsSectionTitle";
import { Button } from "@app/components/ui/button";
import { import {
Form, Form,
FormControl, FormControl,
@@ -19,44 +26,24 @@ import {
FormLabel, FormLabel,
FormMessage FormMessage
} from "@app/components/ui/form"; } 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 { 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 { 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 { import {
CreateClientBody, CreateClientBody,
CreateClientResponse, CreateClientResponse,
PickClientDefaultsResponse PickClientDefaultsResponse
} from "@server/routers/client"; } from "@server/routers/client";
import { ListSitesResponse } from "@server/routers/site";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { ChevronDown, ChevronUp } from "lucide-react";
import { useParams, useRouter } from "next/navigation"; 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"; import { useTranslations } from "next-intl";
type ClientType = "olm"; type ClientType = "olm";
@@ -68,18 +55,6 @@ interface TunnelTypeOption {
disabled?: boolean; 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() { export default function Page() {
const { env } = useEnvContext(); const { env } = useEnvContext();
const api = createApiClient({ env }); const api = createApiClient({ env });
@@ -113,13 +88,9 @@ export default function Page() {
const [loadingPage, setLoadingPage] = useState(true); 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 [olmId, setOlmId] = useState("");
const [olmSecret, setOlmSecret] = useState(""); const [olmSecret, setOlmSecret] = useState("");
const [olmCommand, setOlmCommand] = useState(""); const [olmVersion, setOlmVersion] = useState("latest");
const [createLoading, setCreateLoading] = useState(false); const [createLoading, setCreateLoading] = useState(false);
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
@@ -127,136 +98,6 @@ export default function Page() {
const [clientDefaults, setClientDefaults] = const [clientDefaults, setClientDefaults] =
useState<PickClientDefaultsResponse | null>(null); 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>({ const form = useForm<CreateClientFormValues>({
resolver: zodResolver(createClientFormSchema), resolver: zodResolver(createClientFormSchema),
defaultValues: { defaultValues: {
@@ -311,23 +152,6 @@ export default function Page() {
const load = async () => { const load = async () => {
setLoadingPage(true); 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 { try {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000); const timeoutId = setTimeout(() => controller.abort(), 3000);
@@ -348,7 +172,7 @@ export default function Page() {
} }
const data = await response.json(); const data = await response.json();
const latestVersion = data.tag_name; const latestVersion = data.tag_name;
olmVersion = latestVersion; setOlmVersion(latestVersion);
} catch (error) { } catch (error) {
if (error instanceof Error && error.name === "AbortError") { if (error instanceof Error && error.name === "AbortError") {
console.error(t("olmErrorFetchTimeout")); console.error(t("olmErrorFetchTimeout"));
@@ -377,18 +201,9 @@ export default function Page() {
const olmId = data.olmId; const olmId = data.olmId;
const olmSecret = data.olmSecret; const olmSecret = data.olmSecret;
const olmCommand = `olm --id ${olmId} --secret ${olmSecret} --endpoint ${env.app.dashboardUrl}`;
setOlmId(olmId); setOlmId(olmId);
setOlmSecret(olmSecret); setOlmSecret(olmSecret);
setOlmCommand(olmCommand);
hydrateCommands(
olmId,
olmSecret,
env.app.dashboardUrl,
olmVersion
);
if (data.subnet) { if (data.subnet) {
form.setValue("subnet", data.subnet); form.setValue("subnet", data.subnet);
@@ -571,118 +386,12 @@ export default function Page() {
</InfoSections> </InfoSections>
</SettingsSectionBody> </SettingsSectionBody>
</SettingsSection> </SettingsSection>
<SettingsSection> <OlmInstallCommands
<SettingsSectionHeader> id={olmId}
<SettingsSectionTitle> endpoint={env.app.dashboardUrl}
{t("clientInstallOlm")} secret={olmSecret}
</SettingsSectionTitle> version={olmVersion}
<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>
</> </>
)} )}
</SettingsContainer> </SettingsContainer>
@@ -656,17 +656,17 @@ export default function GeneralPage() {
</InfoSection> </InfoSection>
)} )}
{client.posture.windowsDefenderEnabled !== null && {client.posture.windowsAntivirusEnabled !== null &&
client.posture.windowsDefenderEnabled !== undefined && ( client.posture.windowsAntivirusEnabled !== undefined && (
<InfoSection> <InfoSection>
<InfoSectionTitle> <InfoSectionTitle>
{t("windowsDefenderEnabled")} {t("windowsAntivirusEnabled")}
</InfoSectionTitle> </InfoSectionTitle>
<InfoSectionContent> <InfoSectionContent>
{isPaidUser {isPaidUser
? formatPostureValue( ? formatPostureValue(
client.posture client.posture
.windowsDefenderEnabled .windowsAntivirusEnabled
) )
: "-"} : "-"}
</InfoSectionContent> </InfoSectionContent>
@@ -265,4 +265,3 @@ function GeneralSectionForm({ org }: SectionFormProps) {
</SettingsSection> </SettingsSection>
); );
} }
@@ -67,6 +67,7 @@ export default async function ClientResourcesPage(
destination: siteResource.destination, destination: siteResource.destination,
// destinationPort: siteResource.destinationPort, // destinationPort: siteResource.destinationPort,
alias: siteResource.alias || null, alias: siteResource.alias || null,
aliasAddress: siteResource.aliasAddress || null,
siteNiceId: siteResource.siteNiceId, siteNiceId: siteResource.siteNiceId,
niceId: siteResource.niceId, niceId: siteResource.niceId,
tcpPortRangeString: siteResource.tcpPortRangeString || null, tcpPortRangeString: siteResource.tcpPortRangeString || null,
@@ -11,7 +11,6 @@ import {
SelectValue SelectValue
} from "@/components/ui/select"; } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch"; import { Switch } from "@/components/ui/switch";
import { ContainersSelector } from "@app/components/ContainersSelector";
import { HeadersInput } from "@app/components/HeadersInput"; import { HeadersInput } from "@app/components/HeadersInput";
import { import {
PathMatchDisplay, PathMatchDisplay,
@@ -19,6 +18,7 @@ import {
PathRewriteDisplay, PathRewriteDisplay,
PathRewriteModal PathRewriteModal
} from "@app/components/PathMatchRenameModal"; } from "@app/components/PathMatchRenameModal";
import { ResourceTargetAddressItem } from "@app/components/resource-target-address-item";
import { import {
SettingsContainer, SettingsContainer,
SettingsSection, SettingsSection,
@@ -30,15 +30,6 @@ import {
} from "@app/components/Settings"; } from "@app/components/Settings";
import { SwitchInput } from "@app/components/SwitchInput"; import { SwitchInput } from "@app/components/SwitchInput";
import { Alert, AlertDescription } from "@app/components/ui/alert"; 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 { import {
Form, Form,
FormControl, FormControl,
@@ -48,11 +39,6 @@ import {
FormLabel, FormLabel,
FormMessage FormMessage
} from "@app/components/ui/form"; } from "@app/components/ui/form";
import {
Popover,
PopoverContent,
PopoverTrigger
} from "@app/components/ui/popover";
import { import {
Table, Table,
TableBody, TableBody,
@@ -73,12 +59,9 @@ import { useResourceContext } from "@app/hooks/useResourceContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient } from "@app/lib/api"; import { createApiClient } from "@app/lib/api";
import { formatAxiosError } from "@app/lib/api/formatAxiosError"; import { formatAxiosError } from "@app/lib/api/formatAxiosError";
import { cn } from "@app/lib/cn";
import { DockerManager, DockerState } from "@app/lib/docker"; import { DockerManager, DockerState } from "@app/lib/docker";
import { parseHostTarget } from "@app/lib/parseHostTarget";
import { orgQueries, resourceQueries } from "@app/lib/queries"; import { orgQueries, resourceQueries } from "@app/lib/queries";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { tlsNameSchema } from "@server/lib/schemas"; import { tlsNameSchema } from "@server/lib/schemas";
import { type GetResourceResponse } from "@server/routers/resource"; import { type GetResourceResponse } from "@server/routers/resource";
import type { ListSitesResponse } from "@server/routers/site"; import type { ListSitesResponse } from "@server/routers/site";
@@ -98,7 +81,6 @@ import {
import { AxiosResponse } from "axios"; import { AxiosResponse } from "axios";
import { import {
AlertTriangle, AlertTriangle,
CheckIcon,
CircleCheck, CircleCheck,
CircleX, CircleX,
Info, Info,
@@ -107,7 +89,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; 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 { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
@@ -202,7 +184,7 @@ function ProxyResourceTargetsForm({
setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); 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 dockerManager = new DockerManager(api, siteId);
const containers = await dockerManager.fetchContainers(); const containers = await dockerManager.fetchContainers();
@@ -214,9 +196,9 @@ function ProxyResourceTargetsForm({
} }
return newMap; return newMap;
}); });
}; }, [api]);
const getDockerStateForSite = (siteId: number): DockerState => { const getDockerStateForSite = useCallback((siteId: number): DockerState => {
return ( return (
dockerStates.get(siteId) || { dockerStates.get(siteId) || {
isEnabled: false, isEnabled: false,
@@ -224,7 +206,7 @@ function ProxyResourceTargetsForm({
containers: [] containers: []
} }
); );
}; }, [dockerStates]);
const [isAdvancedMode, setIsAdvancedMode] = useState(() => { const [isAdvancedMode, setIsAdvancedMode] = useState(() => {
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -234,9 +216,41 @@ function ProxyResourceTargetsForm({
return false; 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> = { const priorityColumn: ColumnDef<LocalTarget> = {
id: "priority", id: "priority",
header: () => ( header: () => (
@@ -419,214 +433,16 @@ function ProxyResourceTargetsForm({
accessorKey: "address", accessorKey: "address",
header: () => <span className="p-3">{t("address")}</span>, header: () => <span className="p-3">{t("address")}</span>,
cell: ({ row }) => { 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 ( return (
<div className="flex items-center w-full"> <ResourceTargetAddressItem
<div className="flex items-center w-full justify-start py-0 space-x-2 px-0 cursor-default border border-input rounded-md"> isHttp={isHttp}
{selectedSite && sites={sites}
selectedSite.type === "newt" && getDockerStateForSite={getDockerStateForSite}
(() => { proxyTarget={row.original}
const dockerState = getDockerStateForSite( refreshContainersForSite={refreshContainersForSite}
selectedSite.siteId updateTarget={updateTarget}
);
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>
);
}, },
size: 400, size: 400,
minSize: 350, minSize: 350,
@@ -765,7 +581,7 @@ function ProxyResourceTargetsForm({
actionsColumn actionsColumn
]; ];
} }
}; }, [isAdvancedMode, isHttp, sites, updateTarget, getDockerStateForSite, refreshContainersForSite, openHealthCheckDialog, removeTarget, t]);
function addNewTarget() { function addNewTarget() {
const isHttp = resource.http; const isHttp = resource.http;
@@ -806,32 +622,6 @@ function ProxyResourceTargetsForm({
setTargets((prev) => [...prev, newTarget]); 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) { function updateTargetHealthCheck(targetId: number, config: any) {
setTargets( setTargets(
targets.map((target) => 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({ const table = useReactTable({
data: targets, data: targets,
columns, columns,
@@ -1,5 +1,14 @@
"use client"; "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 { import {
SettingsContainer, SettingsContainer,
SettingsSection, SettingsSection,
@@ -9,6 +18,10 @@ import {
SettingsSectionHeader, SettingsSectionHeader,
SettingsSectionTitle SettingsSectionTitle
} from "@app/components/Settings"; } 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 { import {
Form, Form,
FormControl, FormControl,
@@ -18,22 +31,7 @@ import {
FormLabel, FormLabel,
FormMessage FormMessage
} from "@app/components/ui/form"; } 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 { 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 { import {
Select, Select,
SelectContent, SelectContent,
@@ -41,48 +39,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue SelectValue
} from "@app/components/ui/select"; } from "@app/components/ui/select";
import { ListDomainsResponse } from "@server/routers/domain"; import { Switch } from "@app/components/ui/switch";
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 { import {
Table, Table,
TableBody, TableBody,
@@ -91,30 +48,49 @@ import {
TableHeader, TableHeader,
TableRow TableRow
} from "@app/components/ui/table"; } 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 { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
TooltipProvider, TooltipProvider,
TooltipTrigger TooltipTrigger
} from "@app/components/ui/tooltip"; } 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 { import {
PathMatchDisplay, ColumnDef,
PathMatchModal, flexRender,
PathRewriteDisplay, getCoreRowModel,
PathRewriteModal getFilteredRowModel,
} from "@app/components/PathMatchRenameModal"; getPaginationRowModel,
import { Badge } from "@app/components/ui/badge"; getSortedRowModel,
import HealthCheckDialog from "@app/components/HealthCheckDialog"; useReactTable
import { SwitchInput } from "@app/components/SwitchInput"; } 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({ const baseResourceFormSchema = z.object({
name: z.string().min(1).max(255), 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"; type ResourceType = "http" | "raw";
interface ResourceTypeOption { interface ResourceTypeOption {
@@ -217,7 +189,7 @@ interface ResourceTypeOption {
disabled?: boolean; disabled?: boolean;
} }
type LocalTarget = Omit< export type LocalTarget = Omit<
ArrayElement<ListTargetsResponse["targets"]> & { ArrayElement<ListTargetsResponse["targets"]> & {
new?: boolean; new?: boolean;
updated?: boolean; updated?: boolean;
@@ -233,18 +205,16 @@ export default function Page() {
const router = useRouter(); const router = useRouter();
const t = useTranslations(); const t = useTranslations();
const [loadingPage, setLoadingPage] = useState(true); const { data: sites = [], isLoading: loadingPage } = useQuery(
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]); orgQueries.sites({ orgId: orgId as string })
const [baseDomains, setBaseDomains] = useState< );
{ domainId: string; baseDomain: string }[]
>([]);
const [createLoading, setCreateLoading] = useState(false); const [createLoading, setCreateLoading] = useState(false);
const [showSnippets, setShowSnippets] = useState(false); const [showSnippets, setShowSnippets] = useState(false);
const [niceId, setNiceId] = useState<string>(""); const [niceId, setNiceId] = useState<string>("");
// Target management state // Target management state
const [targets, setTargets] = useState<LocalTarget[]>([]); const [targets, setTargets] = useState<LocalTarget[]>([]);
const [targetsToRemove, setTargetsToRemove] = useState<number[]>([]);
const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>( const [dockerStates, setDockerStates] = useState<Map<number, DockerState>>(
new Map() new Map()
); );
@@ -405,7 +375,8 @@ export default function Page() {
setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); 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 dockerManager = new DockerManager(api, siteId);
const containers = await dockerManager.fetchContainers(); const containers = await dockerManager.fetchContainers();
@@ -417,9 +388,12 @@ export default function Page() {
} }
return newMap; return newMap;
}); });
}; },
[api]
);
const getDockerStateForSite = (siteId: number): DockerState => { const getDockerStateForSite = useCallback(
(siteId: number): DockerState => {
return ( return (
dockerStates.get(siteId) || { dockerStates.get(siteId) || {
isEnabled: false, isEnabled: false,
@@ -427,69 +401,21 @@ export default function Page() {
containers: [] containers: []
} }
); );
}; },
[dockerStates]
);
async function addTarget(data: z.infer<typeof addTargetSchema>) { const removeTarget = useCallback((targetId: number) => {
const site = sites.find((site) => site.siteId === data.siteId); setTargets((prevTargets) => {
return prevTargets.filter((target) => target.targetId !== targetId);
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) => { const updateTarget = useCallback(
setTargets([ (targetId: number, data: Partial<LocalTarget>) => {
...targets.filter((target) => target.targetId !== targetId) setTargets((prevTargets) => {
]);
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); const site = sites.find((site) => site.siteId === data.siteId);
setTargets( return prevTargets.map((target) =>
targets.map((target) =>
target.targetId === targetId target.targetId === targetId
? { ? {
...target, ...target,
@@ -498,9 +424,11 @@ export default function Page() {
siteType: site ? site.type : target.siteType siteType: site ? site.type : target.siteType
} }
: target : target
)
); );
} });
},
[sites]
);
async function onSubmit() { async function onSubmit() {
setCreateLoading(true); setCreateLoading(true);
@@ -638,82 +566,18 @@ export default function Page() {
} }
useEffect(() => { useEffect(() => {
const load = async () => {
setLoadingPage(true);
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 // Initialize Docker for newt sites
for (const site of res.data.data.sites) { for (const site of sites) {
if (site.type === "newt") { if (site.type === "newt") {
initializeDockerForSite(site.siteId); initializeDockerForSite(site.siteId);
} }
} }
// If there's only one site, set it as the default in the form // If there's at least one site, set it as the default in the form
if (res.data.data.sites.length) { if (sites.length > 0) {
addTargetForm.setValue( addTargetForm.setValue("siteId", sites[0].siteId);
"siteId",
res.data.data.sites[0].siteId
);
} }
} }, [sites]);
};
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();
}, []);
function TargetHealthCheck(targetId: number, config: any) { function TargetHealthCheck(targetId: number, config: any) {
setTargets( setTargets(
@@ -729,16 +593,15 @@ export default function Page() {
); );
} }
const openHealthCheckDialog = (target: LocalTarget) => { const openHealthCheckDialog = useCallback((target: LocalTarget) => {
console.log(target); console.log(target);
setSelectedTargetForHealthCheck(target); setSelectedTargetForHealthCheck(target);
setHealthCheckDialogOpen(true); 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> = { const priorityColumn: ColumnDef<LocalTarget> = {
id: "priority", id: "priority",
header: () => ( header: () => (
@@ -875,7 +738,7 @@ export default function Page() {
trigger={ trigger={
<Button <Button
variant="outline" 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 <PathMatchDisplay
value={{ value={{
@@ -899,7 +762,7 @@ export default function Page() {
trigger={ trigger={
<Button <Button
variant="outline" variant="outline"
className="w-full max-w-[200px]" className="w-full max-w-50"
> >
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
{t("matchPath")} {t("matchPath")}
@@ -918,216 +781,16 @@ export default function Page() {
const addressColumn: ColumnDef<LocalTarget> = { const addressColumn: ColumnDef<LocalTarget> = {
accessorKey: "address", accessorKey: "address",
header: () => <span className="p-3">{t("address")}</span>, header: () => <span className="p-3">{t("address")}</span>,
cell: ({ row }) => { cell: ({ row }) => (
const selectedSite = sites.find( <ResourceTargetAddressItem
(site) => site.siteId === row.original.siteId isHttp={isHttp}
); sites={sites}
getDockerStateForSite={getDockerStateForSite}
const handleContainerSelectForTarget = ( proxyTarget={row.original}
hostname: string, refreshContainersForSite={refreshContainersForSite}
port?: number updateTarget={updateTarget}
) => {
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>
);
},
size: 400, size: 400,
minSize: 350, minSize: 350,
maxSize: 500 maxSize: 500
@@ -1186,7 +849,7 @@ export default function Page() {
<Button <Button
variant="outline" variant="outline"
disabled={noPathMatch} disabled={noPathMatch}
className="w-full max-w-[200px]" className="w-full max-w-50"
> >
<Plus className="h-4 w-4 mr-2" /> <Plus className="h-4 w-4 mr-2" />
{t("rewritePath")} {t("rewritePath")}
@@ -1265,9 +928,17 @@ export default function Page() {
actionsColumn actionsColumn
]; ];
} }
}; }, [
isAdvancedMode,
const columns = getColumns(); isHttp,
sites,
updateTarget,
getDockerStateForSite,
refreshContainersForSite,
openHealthCheckDialog,
removeTarget,
t
]);
const table = useReactTable({ const table = useReactTable({
data: targets, data: targets,
@@ -1649,9 +1320,6 @@ export default function Page() {
</TableRow> </TableRow>
)} )}
</TableBody> </TableBody>
{/* <TableCaption> */}
{/* {t('targetNoOneDescription')} */}
{/* </TableCaption> */}
</Table> </Table>
</div> </div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
@@ -39,6 +39,7 @@ import {
} from "@app/lib/wireguard"; } from "@app/lib/wireguard";
import { QRCodeCanvas } from "qrcode.react"; import { QRCodeCanvas } from "qrcode.react";
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert"; import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
export default function CredentialsPage() { export default function CredentialsPage() {
const { env } = useEnvContext(); const { env } = useEnvContext();
@@ -186,13 +187,14 @@ export default function CredentialsPage() {
return site?.name || site?.niceId || "My site"; return site?.name || site?.niceId || "My site";
}; };
const displayNewtId = currentNewtId || siteDefaults?.newtId || null; const displayNewtId = currentNewtId ?? siteDefaults?.newtId ?? null;
const displaySecret = regeneratedSecret || null; const displaySecret = regeneratedSecret ?? null;
return ( return (
<> <>
<SettingsContainer> <SettingsContainer>
{site?.type === "newt" && ( {site?.type === "newt" && (
<>
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
<SettingsSectionTitle> <SettingsSectionTitle>
@@ -227,7 +229,9 @@ export default function CredentialsPage() {
text={displayNewtId} text={displayNewtId}
/> />
) : ( ) : (
<span>{"••••••••••••••••"}</span> <span>
{"••••••••••••••••"}
</span>
)} )}
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
@@ -258,7 +262,9 @@ export default function CredentialsPage() {
{t("siteCredentialsSave")} {t("siteCredentialsSave")}
</AlertTitle> </AlertTitle>
<AlertDescription> <AlertDescription>
{t("siteCredentialsSaveDescription")} {t(
"siteCredentialsSaveDescription"
)}
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
@@ -287,6 +293,13 @@ export default function CredentialsPage() {
</SettingsSectionFooter> </SettingsSectionFooter>
)} )}
</SettingsSection> </SettingsSection>
<NewtSiteInstallCommands
id={displayNewtId ?? "**********"}
secret={displaySecret ?? "**************"}
endpoint={env.app.dashboardUrl}
/>
</>
)} )}
{site?.type === "wireguard" && ( {site?.type === "wireguard" && (
@@ -306,7 +319,7 @@ export default function CredentialsPage() {
{!loadingDefaults && ( {!loadingDefaults && (
<> <>
{wgConfig ? ( {wgConfig ? (
<div className="flex items-center gap-4"> <div className="flex flex-col sm:flex-row items-center gap-4">
<CopyTextBox <CopyTextBox
text={wgConfig} text={wgConfig}
outline={true} outline={true}
+10 -406
View File
@@ -62,6 +62,8 @@ import { QRCodeCanvas } from "qrcode.react";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import { build } from "@server/build"; import { build } from "@server/build";
import { NewtSiteInstallCommands } from "@app/components/newt-install-commands";
import { id } from "date-fns/locale";
type SiteType = "newt" | "wireguard" | "local"; type SiteType = "newt" | "wireguard" | "local";
@@ -189,10 +191,6 @@ export default function Page() {
const [loadingPage, setLoadingPage] = useState(true); 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 [newtId, setNewtId] = useState("");
const [newtSecret, setNewtSecret] = useState(""); const [newtSecret, setNewtSecret] = useState("");
const [newtEndpoint, setNewtEndpoint] = useState(""); const [newtEndpoint, setNewtEndpoint] = useState("");
@@ -202,7 +200,6 @@ export default function Page() {
const [wgConfig, setWgConfig] = useState(""); const [wgConfig, setWgConfig] = useState("");
const [createLoading, setCreateLoading] = useState(false); const [createLoading, setCreateLoading] = useState(false);
const [acceptClients, setAcceptClients] = useState(true);
const [newtVersion, setNewtVersion] = useState("latest"); const [newtVersion, setNewtVersion] = useState("latest");
const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false);
@@ -216,187 +213,6 @@ export default function Page() {
string | undefined 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({ const form = useForm({
resolver: zodResolver(createSiteFormSchema), resolver: zodResolver(createSiteFormSchema),
defaultValues: { defaultValues: {
@@ -414,7 +230,7 @@ WantedBy=default.target`
let payload: CreateSiteBody = { let payload: CreateSiteBody = {
name: data.name, name: data.name,
type: data.method as "newt" | "wireguard" | "local" type: data.method
}; };
if (data.method == "wireguard") { if (data.method == "wireguard") {
@@ -568,14 +384,6 @@ WantedBy=default.target`
setNewtEndpoint(newtEndpoint); setNewtEndpoint(newtEndpoint);
setClientAddress(clientAddress); setClientAddress(clientAddress);
hydrateCommands(
newtId,
newtSecret,
env.app.dashboardUrl,
currentNewtVersion,
acceptClients
);
const wgConfig = generateWireGuardConfig( const wgConfig = generateWireGuardConfig(
privateKey, privateKey,
data.publicKey, data.publicKey,
@@ -631,11 +439,6 @@ WantedBy=default.target`
load(); load();
}, []); }, []);
// Sync form acceptClients value with local state
useEffect(() => {
form.setValue("acceptClients", acceptClients);
}, [acceptClients, form]);
// Sync form exitNodeId value with local state // Sync form exitNodeId value with local state
useEffect(() => { useEffect(() => {
if (build !== "saas") { if (build !== "saas") {
@@ -847,214 +650,15 @@ WantedBy=default.target`
</InfoSectionContent> </InfoSectionContent>
</InfoSection> </InfoSection>
</InfoSections> </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> </SettingsSectionBody>
</SettingsSection> </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> <NewtSiteInstallCommands
<p className="font-bold mb-3"> id={newtId}
{["docker", "podman"].includes( secret={newtSecret}
platform endpoint={env.app.dashboardUrl}
) version={newtVersion}
? 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>
</> </>
)} )}
@@ -1069,7 +673,7 @@ WantedBy=default.target`
</SettingsSectionDescription> </SettingsSectionDescription>
</SettingsSectionHeader> </SettingsSectionHeader>
<SettingsSectionBody> <SettingsSectionBody>
<div className="flex items-center gap-4"> <div className="flex flex-col sm:flex-row items-center gap-4">
<CopyTextBox text={wgConfig} /> <CopyTextBox text={wgConfig} />
<div <div
className={`relative w-fit border rounded-md`} className={`relative w-fit border rounded-md`}
@@ -1087,7 +691,7 @@ WantedBy=default.target`
</SettingsSection> </SettingsSection>
)} )}
{build == "saas" && {build === "saas" &&
form.watch("method") === "local" && ( form.watch("method") === "local" && (
<SettingsSection> <SettingsSection>
<SettingsSectionHeader> <SettingsSectionHeader>
+1 -1
View File
@@ -19,7 +19,7 @@ export const ApprovalsBanner = () => {
description={t("approvalsBannerDescription")} description={t("approvalsBannerDescription")}
> >
<Link <Link
href="https://docs.pangolin.net/manage/access/approvals" href="https://docs.pangolin.net/manage/access-control/approvals"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
+9
View File
@@ -110,6 +110,15 @@ export default function BlueprintDetailsForm({
Dashboard Dashboard
</Badge> </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> </InfoSectionContent>
</InfoSection> </InfoSection>
<InfoSection> <InfoSection>
+13
View File
@@ -128,6 +128,19 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
</Badge> </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 { Button } from "@app/components/ui/button";
import { Download } from "lucide-react"; import { Download } from "lucide-react";
import { FaApple, FaWindows, FaLinux } from "react-icons/fa"; import { FaApple, FaWindows, FaLinux } from "react-icons/fa";
import { SiAndroid } from "react-icons/si";
import { useTranslations } from "next-intl"; import { useTranslations } from "next-intl";
import Link from "next/link"; import Link from "next/link";
import DismissableBanner from "./DismissableBanner"; import DismissableBanner from "./DismissableBanner";
@@ -61,6 +62,34 @@ export const ClientDownloadBanner = () => {
Linux Linux
</Button> </Button>
</Link> </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> </DismissableBanner>
); );
}; };
+27 -1
View File
@@ -11,6 +11,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger DropdownMenuTrigger
} from "@app/components/ui/dropdown-menu"; } from "@app/components/ui/dropdown-menu";
import { InfoPopup } from "@app/components/ui/info-popup";
import { useEnvContext } from "@app/hooks/useEnvContext"; import { useEnvContext } from "@app/hooks/useEnvContext";
import { toast } from "@app/hooks/useToast"; import { toast } from "@app/hooks/useToast";
import { createApiClient, formatAxiosError } from "@app/lib/api"; import { createApiClient, formatAxiosError } from "@app/lib/api";
@@ -40,6 +41,7 @@ export type InternalResourceRow = {
destination: string; destination: string;
// destinationPort: number | null; // destinationPort: number | null;
alias: string | null; alias: string | null;
aliasAddress: string | null;
niceId: string; niceId: string;
tcpPortRangeString: string | null; tcpPortRangeString: string | null;
udpPortRangeString: 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", id: "actions",
enableHiding: false, enableHiding: false,
@@ -316,7 +341,8 @@ export default function ClientResourcesTable({
enableColumnVisibility={true} enableColumnVisibility={true}
persistColumnVisibility="internal-resources" persistColumnVisibility="internal-resources"
columnVisibility={{ columnVisibility={{
niceId: false niceId: false,
aliasAddress: false
}} }}
stickyLeftColumn="name" stickyLeftColumn="name"
stickyRightColumn="actions" stickyRightColumn="actions"
-12
View File
@@ -94,12 +94,6 @@ export default function DomainPicker({
const api = createApiClient({ env }); const api = createApiClient({ env });
const t = useTranslations(); const t = useTranslations();
console.log({
defaultFullDomain,
defaultSubdomain,
defaultDomainId
});
const { data = [], isLoading: loadingDomains } = useQuery( const { data = [], isLoading: loadingDomains } = useQuery(
orgQueries.domains({ orgId }) orgQueries.domains({ orgId })
); );
@@ -369,9 +363,6 @@ export default function DomainPicker({
setSelectedProvidedDomain(null); setSelectedProvidedDomain(null);
} }
console.log({
setSelectedBaseDomain: option
});
setSelectedBaseDomain(option); setSelectedBaseDomain(option);
setOpen(false); setOpen(false);
@@ -442,9 +433,6 @@ export default function DomainPicker({
0, 0,
providedDomainsShown providedDomainsShown
); );
console.log({
displayedProvidedOptions
});
const selectedDomainNamespaceId = const selectedDomainNamespaceId =
selectedProvidedDomain?.domainNamespaceId ?? selectedProvidedDomain?.domainNamespaceId ??
+12
View File
@@ -60,6 +60,18 @@ export default function LocaleSwitcher() {
{ {
value: "zh-TW", value: "zh-TW",
label: "繁體中文" 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(); const router = useRouter();
console.log({ filters });
const dateRange = { const dateRange = {
startDate: filters.timeStart startDate: filters.timeStart
? new Date(filters.timeStart) ? new Date(filters.timeStart)
-1
View File
@@ -143,7 +143,6 @@ export default function LoginOrgSelector({
<IdpLoginButtons <IdpLoginButtons
idps={idps} idps={idps}
redirect={redirect} redirect={redirect}
orgId={org.orgId}
/> />
</div> </div>
</div> </div>
+2 -2
View File
@@ -548,7 +548,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent align="end"> <DropdownMenuContent align="end">
{clientRow.approvalState === "pending" && ( {clientRow.approvalState === "pending" && build !== "oss" && (
<> <>
<DropdownMenuItem <DropdownMenuItem
onClick={() => approveDevice(clientRow)} onClick={() => approveDevice(clientRow)}
@@ -653,7 +653,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
]; ];
if (build === "oss") { if (build === "oss") {
return allOptions.filter((option) => option.value !== "pending"); return allOptions.filter((option) => option.value !== "pending" && option.value !== "denied");
} }
return allOptions; 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" data-slot="input"
className={cn( 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", "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", "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 className
)} )}
ref={ref} ref={ref}
+5 -3
View File
@@ -36,7 +36,9 @@ function SelectTrigger({
data-slot="select-trigger" data-slot="select-trigger"
data-size={size} data-size={size}
className={cn( 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 className
)} )}
{...props} {...props}
@@ -60,7 +62,7 @@ function SelectContent({
<SelectPrimitive.Content <SelectPrimitive.Content
data-slot="select-content" data-slot="select-content"
className={cn( 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" && 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", "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 className
@@ -73,7 +75,7 @@ function SelectContent({
className={cn( className={cn(
"p-1", "p-1",
position === "popper" && 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} {children}